From 80b8b1556cb3ea92f70b4e20382037815abbc304 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Mon, 14 Sep 2020 23:45:21 +0100 Subject: [PATCH 01/21] WIP: add typing info to plistlib --- Lib/fontTools/misc/plistlib.py | 166 ++++++++++++++++++++++----------- 1 file changed, 110 insertions(+), 56 deletions(-) diff --git a/Lib/fontTools/misc/plistlib.py b/Lib/fontTools/misc/plistlib.py index 0ee1e6f72..978016725 100644 --- a/Lib/fontTools/misc/plistlib.py +++ b/Lib/fontTools/misc/plistlib.py @@ -1,5 +1,6 @@ import sys import re +from typing import Any, Callable, Dict, List, NoReturn, Optional, Sequence, Tuple, Union import warnings from io import BytesIO from datetime import datetime @@ -52,9 +53,12 @@ _date_parser = re.compile( ) -def _date_from_string(s): +def _date_from_string(s: str) -> datetime: order = ("year", "month", "day", "hour", "minute", "second") - gd = _date_parser.match(s).groupdict() + m = _date_parser.match(s) + if m is None: + raise ValueError(f"Expected ISO 8601 date string, but got '{s:r}'.") + gd = m.groupdict() lst = [] for key in order: val = gd[key] @@ -64,7 +68,7 @@ def _date_from_string(s): return datetime(*lst) -def _date_to_string(d): +def _date_to_string(d: datetime) -> str: return "%04d-%02d-%02dT%02d:%02d:%02dZ" % ( d.year, d.month, @@ -75,21 +79,6 @@ def _date_to_string(d): ) -def _encode_base64(data, maxlinelength=76, indent_level=1): - data = b64encode(data) - if data and maxlinelength: - # split into multiple lines right-justified to 'maxlinelength' chars - indent = b"\n" + b" " * indent_level - max_length = max(16, maxlinelength - len(indent)) - chunks = [] - for i in range(0, len(data), max_length): - chunks.append(indent) - chunks.append(data[i : i + max_length]) - chunks.append(indent) - data = b"".join(chunks) - return data - - class Data: """Represents binary data when ``use_builtin_types=False.`` @@ -100,21 +89,21 @@ class Data: The actual binary data is retrieved using the ``data`` attribute. """ - def __init__(self, data): + def __init__(self, data: Any) -> None: if not isinstance(data, bytes): raise TypeError("Expected bytes, found %s" % type(data).__name__) self.data = data @classmethod - def fromBase64(cls, data): + def fromBase64(cls, data: Union[bytes, str]) -> "Data": return cls(b64decode(data)) - def asBase64(self, maxlinelength=76, indent_level=1): + def asBase64(self, maxlinelength: int = 76, indent_level: int = 1) -> bytes: return _encode_base64( self.data, maxlinelength=maxlinelength, indent_level=indent_level ) - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: if isinstance(other, self.__class__): return self.data == other.data elif isinstance(other, bytes): @@ -126,6 +115,23 @@ class Data: return "%s(%s)" % (self.__class__.__name__, repr(self.data)) +def _encode_base64( + data: bytes, maxlinelength: int = 76, indent_level: int = 1 +) -> bytes: + data = b64encode(data) + if data and maxlinelength: + # split into multiple lines right-justified to 'maxlinelength' chars + indent = b"\n" + b" " * indent_level + max_length = max(16, maxlinelength - len(indent)) + chunks = [] + for i in range(0, len(data), max_length): + chunks.append(indent) + chunks.append(data[i : i + max_length]) + chunks.append(indent) + data = b"".join(chunks) + return data + + class PlistTarget: """ Event handler using the ElementTree Target API that can be passed to a XMLParser to produce property list objects from XML. @@ -265,9 +271,12 @@ def end_date(self): self.add_object(_date_from_string(self.get_data())) -_TARGET_START_HANDLERS = {"dict": start_dict, "array": start_array} +_TARGET_START_HANDLERS: Dict[str, Callable[[PlistTarget], None]] = { + "dict": start_dict, + "array": start_array +} -_TARGET_END_HANDLERS = { +_TARGET_END_HANDLERS: Dict[str, Callable[[PlistTarget], None]] = { "dict": end_dict, "array": end_array, "key": end_key, @@ -284,35 +293,33 @@ _TARGET_END_HANDLERS = { # functions to build element tree from plist data -def _string_element(value, ctx): +def _string_element(value: str, ctx: SimpleNamespace) -> etree.Element: el = etree.Element("string") el.text = value return el -def _bool_element(value, ctx): +def _bool_element(value: bool, ctx: SimpleNamespace) -> etree.Element: if value: return etree.Element("true") - else: - return etree.Element("false") + return etree.Element("false") -def _integer_element(value, ctx): +def _integer_element(value: Integral, ctx: SimpleNamespace) -> etree.Element: if -1 << 63 <= value < 1 << 64: el = etree.Element("integer") el.text = "%d" % value return el - else: - raise OverflowError(value) + raise OverflowError(value) -def _real_element(value, ctx): +def _real_element(value: float, ctx: SimpleNamespace) -> etree.Element: el = etree.Element("real") el.text = repr(value) return el -def _dict_element(d, ctx): +def _dict_element(d: Dict[str, Any], ctx: SimpleNamespace) -> etree.Element: el = etree.Element("dict") items = d.items() if ctx.sort_keys: @@ -330,7 +337,7 @@ def _dict_element(d, ctx): return el -def _array_element(array, ctx): +def _array_element(array: Sequence[Any], ctx: SimpleNamespace) -> etree.Element: el = etree.Element("array") if len(array) == 0: return el @@ -341,13 +348,15 @@ def _array_element(array, ctx): return el -def _date_element(date, ctx): +def _date_element(date: datetime, ctx: SimpleNamespace) -> etree.Element: el = etree.Element("date") el.text = _date_to_string(date) return el -def _data_element(data, ctx): +def _data_element( + data: Union[bytes, Data], ctx: SimpleNamespace +) -> etree.Element: el = etree.Element("data") el.text = _encode_base64( data, @@ -357,7 +366,9 @@ def _data_element(data, ctx): return el -def _string_or_data_element(raw_bytes, ctx): +def _string_or_data_element( + raw_bytes: Union[bytes, bytearray, Data], ctx: SimpleNamespace +) -> etree.Element: if ctx.use_builtin_types: return _data_element(raw_bytes, ctx) else: @@ -372,20 +383,63 @@ def _string_or_data_element(raw_bytes, ctx): @singledispatch -def _make_element(value, ctx): +def _make_element(value: Any, ctx: SimpleNamespace) -> NoReturn: raise TypeError("unsupported type: %s" % type(value)) -_make_element.register(str)(_string_element) -_make_element.register(bool)(_bool_element) -_make_element.register(Integral)(_integer_element) -_make_element.register(float)(_real_element) -_make_element.register(Mapping)(_dict_element) -_make_element.register(list)(_array_element) -_make_element.register(tuple)(_array_element) -_make_element.register(datetime)(_date_element) -_make_element.register(bytes)(_string_or_data_element) -_make_element.register(bytearray)(_data_element) -_make_element.register(Data)(lambda v, ctx: _data_element(v.data, ctx)) + +@_make_element.register +def _(value: bool, ctx: SimpleNamespace) -> etree.Element: + return _bool_element(value, ctx) + + +@_make_element.register +def _(value: bytearray, ctx: SimpleNamespace) -> etree.Element: + return _data_element(bytes(value), ctx) + + +@_make_element.register +def _(value: bytes, ctx: SimpleNamespace) -> etree.Element: + return _string_or_data_element(value, ctx) + + +@_make_element.register +def _(value: Data, ctx: SimpleNamespace) -> etree.Element: + return _data_element(value.data, ctx) + + +@_make_element.register +def _(value: datetime, ctx: SimpleNamespace) -> etree.Element: + return _date_element(value, ctx) + + +@_make_element.register +def _(value: float, ctx: SimpleNamespace) -> etree.Element: + return _real_element(value, ctx) + + +@_make_element.register +def _(value: Integral, ctx: SimpleNamespace) -> etree.Element: + return _integer_element(value, ctx) + + +@_make_element.register(list) +def _(value: List[Any], ctx: SimpleNamespace) -> etree.Element: + return _array_element(value, ctx) + + +@_make_element.register(Mapping) +def _(value: Dict[str, Any], ctx: SimpleNamespace) -> etree.Element: + return _dict_element(value, ctx) + + +@_make_element.register +def _(value: str, ctx: SimpleNamespace) -> etree.Element: + return _string_element(value, ctx) + + +@_make_element.register(tuple) +def _(value: Tuple[Any, ...], ctx: SimpleNamespace) -> etree.Element: + return _array_element(value, ctx) # Public functions to create element tree from plist-compatible python @@ -393,13 +447,13 @@ _make_element.register(Data)(lambda v, ctx: _data_element(v.data, ctx)) def totree( - value, - sort_keys=True, - skipkeys=False, - use_builtin_types=None, - pretty_print=True, - indent_level=1, -): + value: Any, + sort_keys: bool = True, + skipkeys: bool = False, + use_builtin_types: Optional[bool] = None, + pretty_print: bool = True, + indent_level: int = 1, +) -> etree.Element: """Convert a value derived from a plist into an XML tree. Args: From 7c02ab3d3c9d663f01eca9aa804bc5820a3710af Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 15 Sep 2020 00:02:54 +0100 Subject: [PATCH 02/21] woopsie --- Lib/fontTools/misc/plistlib.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Lib/fontTools/misc/plistlib.py b/Lib/fontTools/misc/plistlib.py index 978016725..bea0de769 100644 --- a/Lib/fontTools/misc/plistlib.py +++ b/Lib/fontTools/misc/plistlib.py @@ -116,7 +116,7 @@ class Data: def _encode_base64( - data: bytes, maxlinelength: int = 76, indent_level: int = 1 + data: bytes, maxlinelength: Optional[int] = 76, indent_level: int = 1 ) -> bytes: data = b64encode(data) if data and maxlinelength: @@ -354,9 +354,7 @@ def _date_element(date: datetime, ctx: SimpleNamespace) -> etree.Element: return el -def _data_element( - data: Union[bytes, Data], ctx: SimpleNamespace -) -> etree.Element: +def _data_element(data: bytes, ctx: SimpleNamespace) -> etree.Element: el = etree.Element("data") el.text = _encode_base64( data, @@ -366,9 +364,7 @@ def _data_element( return el -def _string_or_data_element( - raw_bytes: Union[bytes, bytearray, Data], ctx: SimpleNamespace -) -> etree.Element: +def _string_or_data_element(raw_bytes: bytes, ctx: SimpleNamespace) -> etree.Element: if ctx.use_builtin_types: return _data_element(raw_bytes, ctx) else: From ff0e0028f42b6c51b8f0e4e2113d39c0b6951d21 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 15 Sep 2020 13:00:36 +0100 Subject: [PATCH 03/21] Use overload instead of exploding singledispatch --- Lib/fontTools/misc/plistlib.py | 85 +++++++++++++--------------------- 1 file changed, 32 insertions(+), 53 deletions(-) diff --git a/Lib/fontTools/misc/plistlib.py b/Lib/fontTools/misc/plistlib.py index bea0de769..88c5564d1 100644 --- a/Lib/fontTools/misc/plistlib.py +++ b/Lib/fontTools/misc/plistlib.py @@ -1,6 +1,6 @@ import sys import re -from typing import Any, Callable, Dict, List, NoReturn, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, List, NoReturn, Optional, Sequence, Tuple, Union, overload import warnings from io import BytesIO from datetime import datetime @@ -383,59 +383,38 @@ def _make_element(value: Any, ctx: SimpleNamespace) -> NoReturn: raise TypeError("unsupported type: %s" % type(value)) -@_make_element.register -def _(value: bool, ctx: SimpleNamespace) -> etree.Element: - return _bool_element(value, ctx) +PlistEncodable = Union[ + str, + bool, + Integral, + float, + Mapping[str, Any], + List[Any], + Tuple[Any, ...], + datetime, + bytes, + bytearray, + Data, +] +_make_element.register(str)(_string_element) # type: ignore +_make_element.register(bool)(_bool_element) # type: ignore +_make_element.register(Integral)(_integer_element) # type: ignore +_make_element.register(float)(_real_element) # type: ignore +_make_element.register(Mapping)(_dict_element) # type: ignore +_make_element.register(list)(_array_element) # type: ignore +_make_element.register(tuple)(_array_element) # type: ignore +_make_element.register(datetime)(_date_element) # type: ignore +_make_element.register(bytes)(_string_or_data_element) # type: ignore +_make_element.register(bytearray)(_data_element) # type: ignore +_make_element.register(Data)(lambda v, ctx: _data_element(v.data, ctx)) # type: ignore -@_make_element.register -def _(value: bytearray, ctx: SimpleNamespace) -> etree.Element: - return _data_element(bytes(value), ctx) - - -@_make_element.register -def _(value: bytes, ctx: SimpleNamespace) -> etree.Element: - return _string_or_data_element(value, ctx) - - -@_make_element.register -def _(value: Data, ctx: SimpleNamespace) -> etree.Element: - return _data_element(value.data, ctx) - - -@_make_element.register -def _(value: datetime, ctx: SimpleNamespace) -> etree.Element: - return _date_element(value, ctx) - - -@_make_element.register -def _(value: float, ctx: SimpleNamespace) -> etree.Element: - return _real_element(value, ctx) - - -@_make_element.register -def _(value: Integral, ctx: SimpleNamespace) -> etree.Element: - return _integer_element(value, ctx) - - -@_make_element.register(list) -def _(value: List[Any], ctx: SimpleNamespace) -> etree.Element: - return _array_element(value, ctx) - - -@_make_element.register(Mapping) -def _(value: Dict[str, Any], ctx: SimpleNamespace) -> etree.Element: - return _dict_element(value, ctx) - - -@_make_element.register -def _(value: str, ctx: SimpleNamespace) -> etree.Element: - return _string_element(value, ctx) - - -@_make_element.register(tuple) -def _(value: Tuple[Any, ...], ctx: SimpleNamespace) -> etree.Element: - return _array_element(value, ctx) +# The following is to pacify type checkers. At the time of this writing, neither +# mypy nor Pyright can deal with singledispatch properly. Mypy additionally +# complains about a single overload when there should be more (?) so silence it. +@overload # type: ignore +def _make_element(value: PlistEncodable, ctx: SimpleNamespace) -> etree.Element: + ... # Public functions to create element tree from plist-compatible python @@ -443,7 +422,7 @@ def _(value: Tuple[Any, ...], ctx: SimpleNamespace) -> etree.Element: def totree( - value: Any, + value: PlistEncodable, sort_keys: bool = True, skipkeys: bool = False, use_builtin_types: Optional[bool] = None, From bbfc7b9ff56d4f93b7fd57f88fa0abed0b2bbcb1 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 15 Sep 2020 14:27:22 +0100 Subject: [PATCH 04/21] Fix type mixup --- Lib/fontTools/misc/plistlib.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Lib/fontTools/misc/plistlib.py b/Lib/fontTools/misc/plistlib.py index 88c5564d1..634ce63e6 100644 --- a/Lib/fontTools/misc/plistlib.py +++ b/Lib/fontTools/misc/plistlib.py @@ -1,14 +1,14 @@ +import collections.abc import sys import re from typing import Any, Callable, Dict, List, NoReturn, Optional, Sequence, Tuple, Union, overload +from typing import Any, Callable, Dict, List, Mapping, NoReturn, Optional, Sequence, Tuple, Type, TypeVar, Union, overload, IO import warnings from io import BytesIO from datetime import datetime from base64 import b64encode, b64decode from numbers import Integral - from types import SimpleNamespace -from collections.abc import Mapping from functools import singledispatch from fontTools.misc import etree @@ -393,7 +393,6 @@ PlistEncodable = Union[ Tuple[Any, ...], datetime, bytes, - bytearray, Data, ] @@ -401,7 +400,7 @@ _make_element.register(str)(_string_element) # type: ignore _make_element.register(bool)(_bool_element) # type: ignore _make_element.register(Integral)(_integer_element) # type: ignore _make_element.register(float)(_real_element) # type: ignore -_make_element.register(Mapping)(_dict_element) # type: ignore +_make_element.register(collections.abc.Mapping)(_dict_element) # type: ignore _make_element.register(list)(_array_element) # type: ignore _make_element.register(tuple)(_array_element) # type: ignore _make_element.register(datetime)(_date_element) # type: ignore From 9d13fe11503b8b35b9089f2c4cb981fb8e672e89 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 15 Sep 2020 15:01:53 +0100 Subject: [PATCH 05/21] Simplify typing for singledispatch --- Lib/fontTools/misc/plistlib.py | 61 ++++++++++++++++------------------ 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/Lib/fontTools/misc/plistlib.py b/Lib/fontTools/misc/plistlib.py index 634ce63e6..982685c5a 100644 --- a/Lib/fontTools/misc/plistlib.py +++ b/Lib/fontTools/misc/plistlib.py @@ -378,42 +378,39 @@ def _string_or_data_element(raw_bytes: bytes, ctx: SimpleNamespace) -> etree.Ele return _string_element(string, ctx) -@singledispatch -def _make_element(value: Any, ctx: SimpleNamespace) -> NoReturn: - raise TypeError("unsupported type: %s" % type(value)) - - -PlistEncodable = Union[ - str, - bool, - Integral, - float, - Mapping[str, Any], - List[Any], - Tuple[Any, ...], - datetime, - bytes, - Data, -] - -_make_element.register(str)(_string_element) # type: ignore -_make_element.register(bool)(_bool_element) # type: ignore -_make_element.register(Integral)(_integer_element) # type: ignore -_make_element.register(float)(_real_element) # type: ignore -_make_element.register(collections.abc.Mapping)(_dict_element) # type: ignore -_make_element.register(list)(_array_element) # type: ignore -_make_element.register(tuple)(_array_element) # type: ignore -_make_element.register(datetime)(_date_element) # type: ignore -_make_element.register(bytes)(_string_or_data_element) # type: ignore -_make_element.register(bytearray)(_data_element) # type: ignore -_make_element.register(Data)(lambda v, ctx: _data_element(v.data, ctx)) # type: ignore - # The following is to pacify type checkers. At the time of this writing, neither # mypy nor Pyright can deal with singledispatch properly. Mypy additionally # complains about a single overload when there should be more (?) so silence it. -@overload # type: ignore +PlistEncodable = Union[ + bool, + bytes, + Data, + datetime, + float, + Integral, + List[Any], + Mapping[str, Any], + str, + Tuple[Any, ...], +] + + +@singledispatch def _make_element(value: PlistEncodable, ctx: SimpleNamespace) -> etree.Element: - ... + raise TypeError("unsupported type: %s" % type(value)) + + +_make_element.register(str)(_string_element) +_make_element.register(bool)(_bool_element) +_make_element.register(Integral)(_integer_element) +_make_element.register(float)(_real_element) +_make_element.register(collections.abc.Mapping)(_dict_element) +_make_element.register(list)(_array_element) +_make_element.register(tuple)(_array_element) +_make_element.register(datetime)(_date_element) +_make_element.register(bytes)(_string_or_data_element) +_make_element.register(bytearray)(_data_element) +_make_element.register(Data)(lambda v, ctx: _data_element(v.data, ctx)) # Public functions to create element tree from plist-compatible python From 4f51a5da1082f8a139668f57bada1e034f95e84e Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 15 Sep 2020 17:20:03 +0100 Subject: [PATCH 06/21] More typing --- Lib/fontTools/misc/plistlib.py | 120 ++++++++++++++++++++++++++------- 1 file changed, 97 insertions(+), 23 deletions(-) diff --git a/Lib/fontTools/misc/plistlib.py b/Lib/fontTools/misc/plistlib.py index 982685c5a..d74465127 100644 --- a/Lib/fontTools/misc/plistlib.py +++ b/Lib/fontTools/misc/plistlib.py @@ -1,8 +1,22 @@ import collections.abc import sys import re -from typing import Any, Callable, Dict, List, NoReturn, Optional, Sequence, Tuple, Union, overload -from typing import Any, Callable, Dict, List, Mapping, NoReturn, Optional, Sequence, Tuple, Type, TypeVar, Union, overload, IO +from typing import ( + Any, + Callable, + Dict, + List, + Mapping, + MutableMapping, + Optional, + Sequence, + Tuple, + Type, + TypeVar, + Union, + overload, + IO, +) import warnings from io import BytesIO from datetime import datetime @@ -39,6 +53,12 @@ PLIST_DOCTYPE = ( b'"http://www.apple.com/DTDs/PropertyList-1.0.dtd">' ) + +# Copied from the typeshed module. +mm = MutableMapping[str, Any] +_D = TypeVar("_D", bound=mm) + + # Date should conform to a subset of ISO 8601: # YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z' _date_parser = re.compile( @@ -65,7 +85,7 @@ def _date_from_string(s: str) -> datetime: if val is None: break lst.append(int(val)) - return datetime(*lst) + return datetime(*lst) # type: ignore def _date_to_string(d: datetime) -> str: @@ -89,7 +109,7 @@ class Data: The actual binary data is retrieved using the ``data`` attribute. """ - def __init__(self, data: Any) -> None: + def __init__(self, data: bytes) -> None: if not isinstance(data, bytes): raise TypeError("Expected bytes, found %s" % type(data).__name__) self.data = data @@ -111,7 +131,7 @@ class Data: else: return NotImplemented - def __repr__(self): + def __repr__(self) -> str: return "%s(%s)" % (self.__class__.__name__, repr(self.data)) @@ -154,7 +174,11 @@ class PlistTarget: http://lxml.de/parsing.html#the-target-parser-interface """ - def __init__(self, use_builtin_types=None, dict_type=dict): + def __init__( + self, + use_builtin_types: Optional[bool] = None, + dict_type: Type[_D] = dict, # type: ignore + ) -> None: self.stack = [] self.current_key = None self.root = None @@ -464,7 +488,26 @@ def totree( return _make_element(value, context) -def fromtree(tree, use_builtin_types=None, dict_type=dict): +# NOTE: Due to https://github.com/python/mypy/issues/3737, one needs overrides for dict_type. +@overload +def fromtree( + tree: etree.Element, *, use_builtin_types: Optional[bool] = ... +) -> Dict[str, Any]: + ... + + +@overload +def fromtree( + tree: etree.Element, *, use_builtin_types: Optional[bool] = ..., dict_type: Type[_D] +) -> _D: + ... + + +def fromtree( + tree: etree.Element, + use_builtin_types: Optional[bool] = None, + dict_type: Type[_D] = dict, # type: ignore +) -> _D: """Convert an XML tree to a plist structure. Args: @@ -492,9 +535,24 @@ def fromtree(tree, use_builtin_types=None, dict_type=dict): # python3 plistlib API +# Typing stubs copied from typeshed. +@overload +def load(fp: IO[bytes], *, use_builtin_types: Optional[bool] = ...) -> Dict[str, Any]: + ... -def load(fp, use_builtin_types=None, dict_type=dict): +@overload +def load( + fp: IO[bytes], *, use_builtin_types: Optional[bool] = ..., dict_type: Type[_D] +) -> _D: + ... + + +def load( + fp: IO[bytes], + use_builtin_types: Optional[bool] = None, + dict_type: Type[_D] = dict, # type: ignore +) -> _D: """Load a plist file into an object. Args: @@ -526,11 +584,27 @@ def load(fp, use_builtin_types=None, dict_type=dict): return result -def loads(value, use_builtin_types=None, dict_type=dict): +@overload +def loads(value: bytes, *, use_builtin_types: Optional[bool] = ...) -> Dict[str, Any]: + ... + + +@overload +def loads( + value: bytes, *, use_builtin_types: Optional[bool] = ..., dict_type: Type[_D] +) -> _D: + ... + + +def loads( + value: bytes, + use_builtin_types: Optional[bool] = None, + dict_type: Type[_D] = dict, # type: ignore +) -> _D: """Load a plist file from a string into an object. Args: - value: A string containing a plist. + value: A bytes string containing a plist. use_builtin_types: If True, binary data is deserialized to bytes strings. If False, it is wrapped in :py:class:`Data` objects. Defaults to True if not provided. Deprecated. @@ -546,13 +620,13 @@ def loads(value, use_builtin_types=None, dict_type=dict): def dump( - value, - fp, - sort_keys=True, - skipkeys=False, - use_builtin_types=None, - pretty_print=True, -): + value: PlistEncodable, + fp: IO[Any], + sort_keys: bool = True, + skipkeys: bool = False, + use_builtin_types: Optional[bool] = None, + pretty_print: bool = True, +) -> None: """Write a Python object to a plist file. Args: @@ -605,12 +679,12 @@ def dump( def dumps( - value, - sort_keys=True, - skipkeys=False, - use_builtin_types=None, - pretty_print=True, -): + value: PlistEncodable, + sort_keys: bool = True, + skipkeys: bool = False, + use_builtin_types: Optional[bool] = None, + pretty_print: bool = True, +) -> bytes: """Write a Python object to a string in plist format. Args: From d70ca8224ee08449b1e894e1af06b8c6f276de34 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 15 Sep 2020 18:31:07 +0100 Subject: [PATCH 07/21] More typing --- Lib/fontTools/misc/plistlib.py | 71 ++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/Lib/fontTools/misc/plistlib.py b/Lib/fontTools/misc/plistlib.py index d74465127..42b75edd6 100644 --- a/Lib/fontTools/misc/plistlib.py +++ b/Lib/fontTools/misc/plistlib.py @@ -152,6 +152,23 @@ def _encode_base64( return data +# Mypy does not support recursive type aliases as of 0.782, Pylance does. +# https://github.com/python/mypy/issues/731 +# https://devblogs.microsoft.com/python/pylance-introduces-five-new-features-that-enable-type-magic-for-python-developers/#1-support-for-recursive-type-aliases +PlistEncodable = Union[ + bool, + bytes, + Data, + datetime, + float, + Integral, + List[Any], + Mapping[str, Any], + str, + Tuple[Any, ...], +] + + class PlistTarget: """ Event handler using the ElementTree Target API that can be passed to a XMLParser to produce property list objects from XML. @@ -181,7 +198,7 @@ class PlistTarget: ) -> None: self.stack = [] self.current_key = None - self.root = None + self.root: Optional[PlistEncodable] = None if use_builtin_types is None: self._use_builtin_types = USE_BUILTIN_TYPES else: @@ -194,26 +211,26 @@ class PlistTarget: self._use_builtin_types = use_builtin_types self._dict_type = dict_type - def start(self, tag, attrib): + def start(self, tag: str, attrib: Mapping[str, str]) -> None: self._data = [] handler = _TARGET_START_HANDLERS.get(tag) if handler is not None: handler(self) - def end(self, tag): + def end(self, tag: str) -> None: handler = _TARGET_END_HANDLERS.get(tag) if handler is not None: handler(self) - def data(self, data): + def data(self, data: str) -> None: self._data.append(data) - def close(self): + def close(self) -> Optional[PlistEncodable]: return self.root # helpers - def add_object(self, value): + def add_object(self, value: PlistEncodable) -> None: if self.current_key is not None: if not isinstance(self.stack[-1], type({})): raise ValueError("unexpected element: %r" % self.stack[-1]) @@ -227,7 +244,7 @@ class PlistTarget: raise ValueError("unexpected element: %r" % self.stack[-1]) self.stack[-1].append(value) - def get_data(self): + def get_data(self) -> str: data = "".join(self._data) self._data = [] return data @@ -236,68 +253,68 @@ class PlistTarget: # event handlers -def start_dict(self): +def start_dict(self: PlistTarget) -> None: d = self._dict_type() self.add_object(d) self.stack.append(d) -def end_dict(self): +def end_dict(self: PlistTarget) -> None: if self.current_key: raise ValueError("missing value for key '%s'" % self.current_key) self.stack.pop() -def end_key(self): +def end_key(self: PlistTarget) -> None: if self.current_key or not isinstance(self.stack[-1], type({})): raise ValueError("unexpected key") self.current_key = self.get_data() -def start_array(self): +def start_array(self: PlistTarget) -> None: a = [] self.add_object(a) self.stack.append(a) -def end_array(self): +def end_array(self: PlistTarget) -> None: self.stack.pop() -def end_true(self): +def end_true(self: PlistTarget) -> None: self.add_object(True) -def end_false(self): +def end_false(self: PlistTarget) -> None: self.add_object(False) -def end_integer(self): +def end_integer(self: PlistTarget) -> None: self.add_object(int(self.get_data())) -def end_real(self): +def end_real(self: PlistTarget) -> None: self.add_object(float(self.get_data())) -def end_string(self): +def end_string(self: PlistTarget) -> None: self.add_object(self.get_data()) -def end_data(self): +def end_data(self: PlistTarget) -> None: if self._use_builtin_types: self.add_object(b64decode(self.get_data())) else: self.add_object(Data.fromBase64(self.get_data())) -def end_date(self): +def end_date(self: PlistTarget) -> None: self.add_object(_date_from_string(self.get_data())) _TARGET_START_HANDLERS: Dict[str, Callable[[PlistTarget], None]] = { "dict": start_dict, - "array": start_array + "array": start_array, } _TARGET_END_HANDLERS: Dict[str, Callable[[PlistTarget], None]] = { @@ -405,20 +422,6 @@ def _string_or_data_element(raw_bytes: bytes, ctx: SimpleNamespace) -> etree.Ele # The following is to pacify type checkers. At the time of this writing, neither # mypy nor Pyright can deal with singledispatch properly. Mypy additionally # complains about a single overload when there should be more (?) so silence it. -PlistEncodable = Union[ - bool, - bytes, - Data, - datetime, - float, - Integral, - List[Any], - Mapping[str, Any], - str, - Tuple[Any, ...], -] - - @singledispatch def _make_element(value: PlistEncodable, ctx: SimpleNamespace) -> etree.Element: raise TypeError("unsupported type: %s" % type(value)) From 8a5baa0aa6df6d6d1ea6bfe66e705e8fbf56a78d Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 15 Sep 2020 23:27:39 +0100 Subject: [PATCH 08/21] Return Any from load* https://github.com/python/typeshed/pull/4543 --- Lib/fontTools/misc/plistlib.py | 65 +++++++--------------------------- 1 file changed, 13 insertions(+), 52 deletions(-) diff --git a/Lib/fontTools/misc/plistlib.py b/Lib/fontTools/misc/plistlib.py index 42b75edd6..a224f5b4a 100644 --- a/Lib/fontTools/misc/plistlib.py +++ b/Lib/fontTools/misc/plistlib.py @@ -161,11 +161,10 @@ PlistEncodable = Union[ Data, datetime, float, - Integral, - List[Any], + int, Mapping[str, Any], + Sequence[Any], str, - Tuple[Any, ...], ] @@ -225,7 +224,9 @@ class PlistTarget: def data(self, data: str) -> None: self._data.append(data) - def close(self) -> Optional[PlistEncodable]: + def close(self) -> PlistEncodable: + if self.root is None: + raise ValueError("No root set.") return self.root # helpers @@ -346,7 +347,7 @@ def _bool_element(value: bool, ctx: SimpleNamespace) -> etree.Element: return etree.Element("false") -def _integer_element(value: Integral, ctx: SimpleNamespace) -> etree.Element: +def _integer_element(value: int, ctx: SimpleNamespace) -> etree.Element: if -1 << 63 <= value < 1 << 64: el = etree.Element("integer") el.text = "%d" % value @@ -360,11 +361,11 @@ def _real_element(value: float, ctx: SimpleNamespace) -> etree.Element: return el -def _dict_element(d: Dict[str, Any], ctx: SimpleNamespace) -> etree.Element: +def _dict_element(d: Mapping[str, Any], ctx: SimpleNamespace) -> etree.Element: el = etree.Element("dict") items = d.items() if ctx.sort_keys: - items = sorted(items) + items = sorted(items) # type: ignore ctx.indent_level += 1 for key, value in items: if not isinstance(key, str): @@ -397,7 +398,7 @@ def _date_element(date: datetime, ctx: SimpleNamespace) -> etree.Element: def _data_element(data: bytes, ctx: SimpleNamespace) -> etree.Element: el = etree.Element("data") - el.text = _encode_base64( + el.text = _encode_base64( # type: ignore data, maxlinelength=(76 if ctx.pretty_print else None), indent_level=ctx.indent_level, @@ -491,26 +492,11 @@ def totree( return _make_element(value, context) -# NOTE: Due to https://github.com/python/mypy/issues/3737, one needs overrides for dict_type. -@overload -def fromtree( - tree: etree.Element, *, use_builtin_types: Optional[bool] = ... -) -> Dict[str, Any]: - ... - - -@overload -def fromtree( - tree: etree.Element, *, use_builtin_types: Optional[bool] = ..., dict_type: Type[_D] -) -> _D: - ... - - def fromtree( tree: etree.Element, use_builtin_types: Optional[bool] = None, dict_type: Type[_D] = dict, # type: ignore -) -> _D: +) -> Any: """Convert an XML tree to a plist structure. Args: @@ -522,9 +508,7 @@ def fromtree( Returns: An object (usually a dictionary). """ - target = PlistTarget( - use_builtin_types=use_builtin_types, dict_type=dict_type - ) + target = PlistTarget(use_builtin_types=use_builtin_types, dict_type=dict_type) for action, element in etree.iterwalk(tree, events=("start", "end")): if action == "start": target.start(element.tag, element.attrib) @@ -538,24 +522,13 @@ def fromtree( # python3 plistlib API -# Typing stubs copied from typeshed. -@overload -def load(fp: IO[bytes], *, use_builtin_types: Optional[bool] = ...) -> Dict[str, Any]: - ... - - -@overload -def load( - fp: IO[bytes], *, use_builtin_types: Optional[bool] = ..., dict_type: Type[_D] -) -> _D: - ... def load( fp: IO[bytes], use_builtin_types: Optional[bool] = None, dict_type: Type[_D] = dict, # type: ignore -) -> _D: +) -> Any: """Load a plist file into an object. Args: @@ -587,23 +560,11 @@ def load( return result -@overload -def loads(value: bytes, *, use_builtin_types: Optional[bool] = ...) -> Dict[str, Any]: - ... - - -@overload -def loads( - value: bytes, *, use_builtin_types: Optional[bool] = ..., dict_type: Type[_D] -) -> _D: - ... - - def loads( value: bytes, use_builtin_types: Optional[bool] = None, dict_type: Type[_D] = dict, # type: ignore -) -> _D: +) -> Any: """Load a plist file from a string into an object. Args: From be773359938474ef203c582c5f4c27c179beab6f Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 15 Sep 2020 23:31:03 +0100 Subject: [PATCH 09/21] Remove unused typing imports --- Lib/fontTools/misc/plistlib.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Lib/fontTools/misc/plistlib.py b/Lib/fontTools/misc/plistlib.py index a224f5b4a..de6a9be7f 100644 --- a/Lib/fontTools/misc/plistlib.py +++ b/Lib/fontTools/misc/plistlib.py @@ -5,16 +5,13 @@ from typing import ( Any, Callable, Dict, - List, Mapping, MutableMapping, Optional, Sequence, - Tuple, Type, TypeVar, Union, - overload, IO, ) import warnings From a364cff13f778d332681e3e343777a4dd3e46ae5 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 15 Sep 2020 23:53:58 +0100 Subject: [PATCH 10/21] No need for _D anymore --- Lib/fontTools/misc/plistlib.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Lib/fontTools/misc/plistlib.py b/Lib/fontTools/misc/plistlib.py index de6a9be7f..8553d1f88 100644 --- a/Lib/fontTools/misc/plistlib.py +++ b/Lib/fontTools/misc/plistlib.py @@ -51,11 +51,6 @@ PLIST_DOCTYPE = ( ) -# Copied from the typeshed module. -mm = MutableMapping[str, Any] -_D = TypeVar("_D", bound=mm) - - # Date should conform to a subset of ISO 8601: # YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z' _date_parser = re.compile( @@ -190,7 +185,7 @@ class PlistTarget: def __init__( self, use_builtin_types: Optional[bool] = None, - dict_type: Type[_D] = dict, # type: ignore + dict_type: Type[MutableMapping[str, Any]] = dict, # type: ignore ) -> None: self.stack = [] self.current_key = None @@ -492,7 +487,7 @@ def totree( def fromtree( tree: etree.Element, use_builtin_types: Optional[bool] = None, - dict_type: Type[_D] = dict, # type: ignore + dict_type: Type[MutableMapping[str, Any]] = dict, ) -> Any: """Convert an XML tree to a plist structure. @@ -524,7 +519,7 @@ def fromtree( def load( fp: IO[bytes], use_builtin_types: Optional[bool] = None, - dict_type: Type[_D] = dict, # type: ignore + dict_type: Type[MutableMapping[str, Any]] = dict, ) -> Any: """Load a plist file into an object. @@ -560,7 +555,7 @@ def load( def loads( value: bytes, use_builtin_types: Optional[bool] = None, - dict_type: Type[_D] = dict, # type: ignore + dict_type: Type[MutableMapping[str, Any]] = dict, ) -> Any: """Load a plist file from a string into an object. From fa32cf2fed558b537a1b2be5f36fe67e144906e9 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Wed, 16 Sep 2020 09:31:05 +0100 Subject: [PATCH 11/21] Remove unused type comment --- Lib/fontTools/misc/plistlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/fontTools/misc/plistlib.py b/Lib/fontTools/misc/plistlib.py index 8553d1f88..624910c09 100644 --- a/Lib/fontTools/misc/plistlib.py +++ b/Lib/fontTools/misc/plistlib.py @@ -185,7 +185,7 @@ class PlistTarget: def __init__( self, use_builtin_types: Optional[bool] = None, - dict_type: Type[MutableMapping[str, Any]] = dict, # type: ignore + dict_type: Type[MutableMapping[str, Any]] = dict, ) -> None: self.stack = [] self.current_key = None From e1c97102729c2ab76d6d78b7423551c96602a2ff Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Wed, 16 Sep 2020 09:52:46 +0100 Subject: [PATCH 12/21] More types, correct stale comment --- Lib/fontTools/misc/plistlib.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Lib/fontTools/misc/plistlib.py b/Lib/fontTools/misc/plistlib.py index 624910c09..e9396e90d 100644 --- a/Lib/fontTools/misc/plistlib.py +++ b/Lib/fontTools/misc/plistlib.py @@ -5,6 +5,7 @@ from typing import ( Any, Callable, Dict, + List, Mapping, MutableMapping, Optional, @@ -187,7 +188,7 @@ class PlistTarget: use_builtin_types: Optional[bool] = None, dict_type: Type[MutableMapping[str, Any]] = dict, ) -> None: - self.stack = [] + self.stack: List[PlistEncodable] = [] self.current_key = None self.root: Optional[PlistEncodable] = None if use_builtin_types is None: @@ -203,7 +204,7 @@ class PlistTarget: self._dict_type = dict_type def start(self, tag: str, attrib: Mapping[str, str]) -> None: - self._data = [] + self._data: List[str] = [] handler = _TARGET_START_HANDLERS.get(tag) if handler is not None: handler(self) @@ -412,9 +413,11 @@ def _string_or_data_element(raw_bytes: bytes, ctx: SimpleNamespace) -> etree.Ele return _string_element(string, ctx) -# The following is to pacify type checkers. At the time of this writing, neither -# mypy nor Pyright can deal with singledispatch properly. Mypy additionally -# complains about a single overload when there should be more (?) so silence it. +# The following is probably not entirely correct. The signature should take `Any` +# and return `NoReturn`. At the time of this writing, neither mypy nor Pyright +# can deal with singledispatch properly and will apply the signature of the base +# function to all others. Being slightly dishonest makes it type-check and return +# usable typing information for the optimistic case. @singledispatch def _make_element(value: PlistEncodable, ctx: SimpleNamespace) -> etree.Element: raise TypeError("unsupported type: %s" % type(value)) From 863d9fd3c81e7851d5ae0c0c4041997b350fbe33 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Wed, 16 Sep 2020 12:03:52 +0100 Subject: [PATCH 13/21] Add py.typed file --- .../{plistlib.py => plistlib/__init__.py} | 27 +++++++------------ Lib/fontTools/misc/plistlib/py.typed | 0 MANIFEST.in | 2 ++ setup.py | 1 + 4 files changed, 12 insertions(+), 18 deletions(-) rename Lib/fontTools/misc/{plistlib.py => plistlib/__init__.py} (97%) create mode 100644 Lib/fontTools/misc/plistlib/py.typed diff --git a/Lib/fontTools/misc/plistlib.py b/Lib/fontTools/misc/plistlib/__init__.py similarity index 97% rename from Lib/fontTools/misc/plistlib.py rename to Lib/fontTools/misc/plistlib/__init__.py index e9396e90d..c77f8af56 100644 --- a/Lib/fontTools/misc/plistlib.py +++ b/Lib/fontTools/misc/plistlib/__init__.py @@ -30,7 +30,7 @@ from fontTools.misc.py23 import ( tobytes, ) -# By default, we +# By default, we # - deserialize elements as bytes and # - serialize bytes as elements. # Before, on Python 2, we @@ -62,7 +62,7 @@ _date_parser = re.compile( r"(?::(?P\d\d)" r"(?::(?P\d\d))" r"?)?)?)?)?Z", - re.ASCII + re.ASCII, ) @@ -162,7 +162,7 @@ PlistEncodable = Union[ class PlistTarget: - """ Event handler using the ElementTree Target API that can be + """Event handler using the ElementTree Target API that can be passed to a XMLParser to produce property list objects from XML. It is based on the CPython plistlib module's _PlistParser class, but does not use the expat parser. @@ -407,8 +407,7 @@ def _string_or_data_element(raw_bytes: bytes, ctx: SimpleNamespace) -> etree.Ele string = raw_bytes.decode(encoding="ascii", errors="strict") except UnicodeDecodeError: raise ValueError( - "invalid non-ASCII bytes; use unicode string instead: %r" - % raw_bytes + "invalid non-ASCII bytes; use unicode string instead: %r" % raw_bytes ) return _string_element(string, ctx) @@ -539,12 +538,8 @@ def load( """ if not hasattr(fp, "read"): - raise AttributeError( - "'%s' object has no attribute 'read'" % type(fp).__name__ - ) - target = PlistTarget( - use_builtin_types=use_builtin_types, dict_type=dict_type - ) + raise AttributeError("'%s' object has no attribute 'read'" % type(fp).__name__) + target = PlistTarget(use_builtin_types=use_builtin_types, dict_type=dict_type) parser = etree.XMLParser(target=target) result = etree.parse(fp, parser=parser) # lxml returns the target object directly, while ElementTree wraps @@ -608,12 +603,10 @@ def dump( ``ValueError`` if non-representable binary data is present and `use_builtin_types` is false. - """ + """ if not hasattr(fp, "write"): - raise AttributeError( - "'%s' object has no attribute 'write'" % type(fp).__name__ - ) + raise AttributeError("'%s' object has no attribute 'write'" % type(fp).__name__) root = etree.Element("plist", version="1.0") el = totree( value, @@ -632,9 +625,7 @@ def dump( else: header = XML_DECLARATION + PLIST_DOCTYPE fp.write(header) - tree.write( - fp, encoding="utf-8", pretty_print=pretty_print, xml_declaration=False - ) + tree.write(fp, encoding="utf-8", pretty_print=pretty_print, xml_declaration=False) def dumps( diff --git a/Lib/fontTools/misc/plistlib/py.typed b/Lib/fontTools/misc/plistlib/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/MANIFEST.in b/MANIFEST.in index 5a55dfeb2..5c4d1274b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -13,6 +13,8 @@ include *requirements.txt include tox.ini include run-tests.sh +recursive-include Lib/fontTools py.typed + include .appveyor.yml include .codecov.yml include .coveragerc diff --git a/setup.py b/setup.py index 220814fa0..205a59034 100755 --- a/setup.py +++ b/setup.py @@ -452,6 +452,7 @@ setup_params = dict( packages=find_packages("Lib"), include_package_data=True, data_files=find_data_files(), + zip_safe=False, # So mypy can find typing information. ext_modules=ext_modules, setup_requires=setup_requires, extras_require=extras_require, From 7a5138d91bcfb18a892ef19091c20c5c7cfb2832 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Wed, 16 Sep 2020 15:13:50 +0100 Subject: [PATCH 14/21] Add mypy CI job --- .travis.yml | 3 +++ dev-requirements.txt | 1 + mypy.ini | 21 +++++++++++++++++++++ tox.ini | 9 ++++++++- 4 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 mypy.ini diff --git a/.travis.yml b/.travis.yml index 7ff7e3160..389d3372c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,9 @@ branches: matrix: fast_finish: true include: + - python: 3.6 + env: + - TOXENV=mypy - python: 3.6 env: - TOXENV=py36-cov,package_readme diff --git a/dev-requirements.txt b/dev-requirements.txt index a34deb2e9..6ebb76bf5 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -2,3 +2,4 @@ pytest>=3.0 tox>=2.5 bump2version>=0.5.6 sphinx>=1.5.5 +mypy diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 000000000..7e37b03fb --- /dev/null +++ b/mypy.ini @@ -0,0 +1,21 @@ +[mypy] +python_version = 3.6 +files = Lib/fontTools/misc/plistlib +follow_imports = silent +ignore_missing_imports = True +warn_redundant_casts = True +warn_unused_configs = True +warn_unused_ignores = True + +[mypy-fontTools.misc.plistlib] +check_untyped_defs = True +disallow_any_generics = True +disallow_incomplete_defs = True +disallow_subclassing_any = True +disallow_untyped_decorators = True +disallow_untyped_calls = False +disallow_untyped_defs = True +no_implicit_optional = True +no_implicit_reexport = True +strict_equality = True +warn_return_any = True diff --git a/tox.ini b/tox.ini index df6358c2e..5a8d9f209 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.0 -envlist = py3{6,7,8}-cov, htmlcov +envlist = mypy, py3{6,7,8}-cov, htmlcov skip_missing_interpreters=true [testenv] @@ -33,6 +33,13 @@ commands = coverage combine coverage html +[testenv:mypy] +deps = + -r dev-requirements.txt +skip_install = true +commands = + mypy + [testenv:codecov] passenv = * deps = From 10864be26fb3b033ed69068dc27d70b5258ebca6 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Wed, 16 Sep 2020 17:42:46 +0100 Subject: [PATCH 15/21] Fix one typing warning --- Lib/fontTools/misc/plistlib/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Lib/fontTools/misc/plistlib/__init__.py b/Lib/fontTools/misc/plistlib/__init__.py index c77f8af56..2884895bb 100644 --- a/Lib/fontTools/misc/plistlib/__init__.py +++ b/Lib/fontTools/misc/plistlib/__init__.py @@ -234,9 +234,10 @@ class PlistTarget: # this is the root object self.root = value else: - if not isinstance(self.stack[-1], type([])): - raise ValueError("unexpected element: %r" % self.stack[-1]) - self.stack[-1].append(value) + stack_top = self.stack[-1] + if not isinstance(stack_top, list): + raise ValueError("unexpected element: %r" % stack_top) + stack_top.append(value) def get_data(self) -> str: data = "".join(self._data) From a501c0cbe7d0164253001a3a1ddd91358d4958a6 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Wed, 16 Sep 2020 19:59:50 +0100 Subject: [PATCH 16/21] More typing and ignores --- Lib/fontTools/misc/plistlib/__init__.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Lib/fontTools/misc/plistlib/__init__.py b/Lib/fontTools/misc/plistlib/__init__.py index 2884895bb..3671517e2 100644 --- a/Lib/fontTools/misc/plistlib/__init__.py +++ b/Lib/fontTools/misc/plistlib/__init__.py @@ -189,7 +189,7 @@ class PlistTarget: dict_type: Type[MutableMapping[str, Any]] = dict, ) -> None: self.stack: List[PlistEncodable] = [] - self.current_key = None + self.current_key: Optional[str] = None self.root: Optional[PlistEncodable] = None if use_builtin_types is None: self._use_builtin_types = USE_BUILTIN_TYPES @@ -226,9 +226,10 @@ class PlistTarget: def add_object(self, value: PlistEncodable) -> None: if self.current_key is not None: - if not isinstance(self.stack[-1], type({})): - raise ValueError("unexpected element: %r" % self.stack[-1]) - self.stack[-1][self.current_key] = value + stack_top = self.stack[-1] + if not isinstance(stack_top, collections.abc.MutableMapping): + raise ValueError("unexpected element: %r" % stack_top) + stack_top[self.current_key] = value self.current_key = None elif not self.stack: # this is the root object @@ -261,13 +262,13 @@ def end_dict(self: PlistTarget) -> None: def end_key(self: PlistTarget) -> None: - if self.current_key or not isinstance(self.stack[-1], type({})): + if self.current_key or not isinstance(self.stack[-1], collections.abc.Mapping): raise ValueError("unexpected key") self.current_key = self.get_data() def start_array(self: PlistTarget) -> None: - a = [] + a: List[PlistEncodable] = [] self.add_object(a) self.stack.append(a) @@ -541,7 +542,7 @@ def load( if not hasattr(fp, "read"): raise AttributeError("'%s' object has no attribute 'read'" % type(fp).__name__) target = PlistTarget(use_builtin_types=use_builtin_types, dict_type=dict_type) - parser = etree.XMLParser(target=target) + parser = etree.XMLParser(target=target) # type: ignore result = etree.parse(fp, parser=parser) # lxml returns the target object directly, while ElementTree wraps # it as the root of an ElementTree object @@ -626,7 +627,12 @@ def dump( else: header = XML_DECLARATION + PLIST_DOCTYPE fp.write(header) - tree.write(fp, encoding="utf-8", pretty_print=pretty_print, xml_declaration=False) + tree.write( + fp, + encoding="utf-8", + pretty_print=pretty_print, # type: ignore + xml_declaration=False, + ) def dumps( From 09a64418f027feb26caa732a2b753b9960cd9662 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Wed, 16 Sep 2020 20:08:45 +0100 Subject: [PATCH 17/21] Misplaced ignore --- Lib/fontTools/misc/plistlib/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/misc/plistlib/__init__.py b/Lib/fontTools/misc/plistlib/__init__.py index 3671517e2..f5985560e 100644 --- a/Lib/fontTools/misc/plistlib/__init__.py +++ b/Lib/fontTools/misc/plistlib/__init__.py @@ -627,10 +627,10 @@ def dump( else: header = XML_DECLARATION + PLIST_DOCTYPE fp.write(header) - tree.write( + tree.write( # type: ignore fp, encoding="utf-8", - pretty_print=pretty_print, # type: ignore + pretty_print=pretty_print, xml_declaration=False, ) From 2906ac2908d2197065e7251031be2991bb0bf3b9 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Wed, 16 Sep 2020 20:28:33 +0100 Subject: [PATCH 18/21] One less type-ignore --- Lib/fontTools/misc/plistlib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/fontTools/misc/plistlib/__init__.py b/Lib/fontTools/misc/plistlib/__init__.py index f5985560e..ce9cf9157 100644 --- a/Lib/fontTools/misc/plistlib/__init__.py +++ b/Lib/fontTools/misc/plistlib/__init__.py @@ -78,7 +78,7 @@ def _date_from_string(s: str) -> datetime: if val is None: break lst.append(int(val)) - return datetime(*lst) # type: ignore + return datetime(lst[0], lst[1], lst[2], lst[3], lst[4], lst[5]) def _date_to_string(d: datetime) -> str: From 4efc06e62f13082860d8d3366e0168051cea67bd Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Mon, 21 Sep 2020 16:53:46 +0100 Subject: [PATCH 19/21] Update Lib/fontTools/misc/plistlib/__init__.py Co-authored-by: Cosimo Lupo --- Lib/fontTools/misc/plistlib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/fontTools/misc/plistlib/__init__.py b/Lib/fontTools/misc/plistlib/__init__.py index ce9cf9157..85fd3c128 100644 --- a/Lib/fontTools/misc/plistlib/__init__.py +++ b/Lib/fontTools/misc/plistlib/__init__.py @@ -356,7 +356,7 @@ def _real_element(value: float, ctx: SimpleNamespace) -> etree.Element: return el -def _dict_element(d: Mapping[str, Any], ctx: SimpleNamespace) -> etree.Element: +def _dict_element(d: Mapping[str, PlistEncodable], ctx: SimpleNamespace) -> etree.Element: el = etree.Element("dict") items = d.items() if ctx.sort_keys: From a1df979335abf35fdf11d9b82549dbbae4bb9fe6 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Mon, 21 Sep 2020 16:53:55 +0100 Subject: [PATCH 20/21] Update Lib/fontTools/misc/plistlib/__init__.py Co-authored-by: Cosimo Lupo --- Lib/fontTools/misc/plistlib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/fontTools/misc/plistlib/__init__.py b/Lib/fontTools/misc/plistlib/__init__.py index 85fd3c128..92c80b450 100644 --- a/Lib/fontTools/misc/plistlib/__init__.py +++ b/Lib/fontTools/misc/plistlib/__init__.py @@ -374,7 +374,7 @@ def _dict_element(d: Mapping[str, PlistEncodable], ctx: SimpleNamespace) -> etre return el -def _array_element(array: Sequence[Any], ctx: SimpleNamespace) -> etree.Element: +def _array_element(array: Sequence[PlistEncodable], ctx: SimpleNamespace) -> etree.Element: el = etree.Element("array") if len(array) == 0: return el From 0742a9bff33d5c80f87f3faa57b50dd6d4eb0d55 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Mon, 21 Sep 2020 17:01:22 +0100 Subject: [PATCH 21/21] Implement suggestions --- Lib/fontTools/misc/plistlib/__init__.py | 7 ++++--- dev-requirements.txt | 2 +- setup.py | 1 - 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/fontTools/misc/plistlib/__init__.py b/Lib/fontTools/misc/plistlib/__init__.py index 92c80b450..1335e8cbe 100644 --- a/Lib/fontTools/misc/plistlib/__init__.py +++ b/Lib/fontTools/misc/plistlib/__init__.py @@ -11,7 +11,6 @@ from typing import ( Optional, Sequence, Type, - TypeVar, Union, IO, ) @@ -78,7 +77,8 @@ def _date_from_string(s: str) -> datetime: if val is None: break lst.append(int(val)) - return datetime(lst[0], lst[1], lst[2], lst[3], lst[4], lst[5]) + # NOTE: mypy doesn't know that lst is 6 elements long. + return datetime(*lst) # type:ignore def _date_to_string(d: datetime) -> str: @@ -393,6 +393,7 @@ def _date_element(date: datetime, ctx: SimpleNamespace) -> etree.Element: def _data_element(data: bytes, ctx: SimpleNamespace) -> etree.Element: el = etree.Element("data") + # NOTE: mypy is confused about whether el.text should be str or bytes. el.text = _encode_base64( # type: ignore data, maxlinelength=(76 if ctx.pretty_print else None), @@ -577,7 +578,7 @@ def loads( def dump( value: PlistEncodable, - fp: IO[Any], + fp: IO[bytes], sort_keys: bool = True, skipkeys: bool = False, use_builtin_types: Optional[bool] = None, diff --git a/dev-requirements.txt b/dev-requirements.txt index 6ebb76bf5..73eae6803 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -2,4 +2,4 @@ pytest>=3.0 tox>=2.5 bump2version>=0.5.6 sphinx>=1.5.5 -mypy +mypy>=0.782 diff --git a/setup.py b/setup.py index 205a59034..220814fa0 100755 --- a/setup.py +++ b/setup.py @@ -452,7 +452,6 @@ setup_params = dict( packages=find_packages("Lib"), include_package_data=True, data_files=find_data_files(), - zip_safe=False, # So mypy can find typing information. ext_modules=ext_modules, setup_requires=setup_requires, extras_require=extras_require,