diff options
author | robot-piglet <[email protected]> | 2025-07-31 11:14:11 +0300 |
---|---|---|
committer | robot-piglet <[email protected]> | 2025-07-31 12:10:37 +0300 |
commit | e177928be72df9669dbb830824b4233a33c8723f (patch) | |
tree | a91d4ec6bbe7dc221c049475a91255c2996fd84e /contrib/python | |
parent | a1700abf3c749b43117e757deb259d2a7bcdf46a (diff) |
Intermediate changes
commit_hash:60aaacde4a6a0fb68b6435d7f100365d0c77d64d
Diffstat (limited to 'contrib/python')
45 files changed, 1231 insertions, 159 deletions
diff --git a/contrib/python/fonttools/.dist-info/METADATA b/contrib/python/fonttools/.dist-info/METADATA index 164ca311dc3..19786640848 100644 --- a/contrib/python/fonttools/.dist-info/METADATA +++ b/contrib/python/fonttools/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: fonttools -Version: 4.58.5 +Version: 4.59.0 Summary: Tools to manipulate font files Home-page: http://github.com/fonttools/fonttools Author: Just van Rossum @@ -31,7 +31,6 @@ Description-Content-Type: text/x-rst License-File: LICENSE License-File: LICENSE.external Provides-Extra: ufo -Requires-Dist: fs<3,>=2.2.0; extra == "ufo" Provides-Extra: lxml Requires-Dist: lxml>=4.0; extra == "lxml" Provides-Extra: woff @@ -57,7 +56,6 @@ Requires-Dist: skia-pathops>=0.5.0; extra == "pathops" Provides-Extra: repacker Requires-Dist: uharfbuzz>=0.23.0; extra == "repacker" Provides-Extra: all -Requires-Dist: fs<3,>=2.2.0; extra == "all" Requires-Dist: lxml>=4.0; extra == "all" Requires-Dist: brotli>=1.0.1; platform_python_implementation == "CPython" and extra == "all" Requires-Dist: brotlicffi>=0.8.0; platform_python_implementation != "CPython" and extra == "all" @@ -388,6 +386,24 @@ Have fun! Changelog ~~~~~~~~~ +4.59.0 (released 2025-07-16) +---------------------------- + +- Removed hard-dependency on pyfilesystem2 (``fs`` package) from ``fonttools[ufo]`` extra. + This is replaced by the `fontTools.misc.filesystem` package, a stdlib-only, drop-in + replacement for the subset of the pyfilesystem2's API used by ``fontTools.ufoLib``. + The latter should continue to work with the upstream ``fs`` (we even test with/without). + Clients who wish to continue using ``fs`` can do so by depending on it directly instead + of via the ``fonttools[ufo]`` extra (#3885, #3620). +- [xmlWriter] Replace illegal XML characters (e.g. control or non-characters) with "?" + when dumping to ttx (#3868, #71). +- [varLib.hvar] Fixed vertical metrics fields copy/pasta error (#3884). +- Micro optimizations in ttLib and sstruct modules (#3878, #3879). +- [unicodedata] Add Garay script to RTL_SCRIPTS (#3882). +- [roundingPen] Remove unreliable kwarg usage. Argument names aren’t consistent among + point pens’ ``.addComponent()`` implementations, in particular ``baseGlyphName`` + vs ``glyphName`` (#3880). + 4.58.5 (released 2025-07-03) ---------------------------- diff --git a/contrib/python/fonttools/LICENSE.external b/contrib/python/fonttools/LICENSE.external index 2bc4dab3eb5..5c45052f870 100644 --- a/contrib/python/fonttools/LICENSE.external +++ b/contrib/python/fonttools/LICENSE.external @@ -357,3 +357,32 @@ Licensed under the Apache License, Version 2.0, a copy of which is reproduced be WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +===== + +FontTools includes code in `fontTools.misc.filesystem` which is derived from: + +PyFilesystem2 (i.e. the `fs` package) by Will McGugan +Licensed under the MIT License +https://github.com/PyFilesystem/pyfilesystem2 + +Copyright (c) 2017-2021 The PyFilesystem2 contributors +Copyright (c) 2016-2019 Will McGugan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/contrib/python/fonttools/fontTools/__init__.py b/contrib/python/fonttools/fontTools/__init__.py index 306cd68a6be..d2ff98f6a68 100644 --- a/contrib/python/fonttools/fontTools/__init__.py +++ b/contrib/python/fonttools/fontTools/__init__.py @@ -3,6 +3,6 @@ from fontTools.misc.loggingTools import configLogger log = logging.getLogger(__name__) -version = __version__ = "4.58.5" +version = __version__ = "4.59.0" __all__ = ["version", "log", "configLogger"] diff --git a/contrib/python/fonttools/fontTools/merge/__init__.py b/contrib/python/fonttools/fontTools/merge/__init__.py index 3f5875c5868..c1f4b65de07 100644 --- a/contrib/python/fonttools/fontTools/merge/__init__.py +++ b/contrib/python/fonttools/fontTools/merge/__init__.py @@ -196,7 +196,7 @@ def main(args=None): if len(fontfiles) < 1: print( - "usage: pyftmerge [font1 ... fontN] [--input-file=filelist.txt] [--output-file=merged.ttf] [--import-file=tables.ttx]", + "usage: fonttools merge [font1 ... fontN] [--input-file=filelist.txt] [--output-file=merged.ttf] [--import-file=tables.ttx]", file=sys.stderr, ) print( diff --git a/contrib/python/fonttools/fontTools/misc/filesystem/__init__.py b/contrib/python/fonttools/fontTools/misc/filesystem/__init__.py new file mode 100644 index 00000000000..9f9ae249b32 --- /dev/null +++ b/contrib/python/fonttools/fontTools/misc/filesystem/__init__.py @@ -0,0 +1,68 @@ +"""Minimal, stdlib-only replacement for [`pyfilesystem2`][1] API for use by `fontTools.ufoLib`. + +This package is a partial reimplementation of the `fs` package by Will McGugan, used under the +MIT license. See LICENSE.external for details. + +Note this only exports a **subset** of the `pyfilesystem2` API, in particular the modules, +classes and functions that are currently used directly by `fontTools.ufoLib`. + +It opportunistically tries to import the relevant modules from the upstream `fs` package +when this is available. Otherwise it falls back to the replacement modules within this package. + +As of version 4.59.0, the `fonttools[ufo]` extra no longer requires the `fs` package, thus +this `fontTools.misc.filesystem` package is used by default. + +Client code can either replace `import fs` with `from fontTools.misc import filesystem as fs` +if that happens to work (no guarantee), or they can continue to use `fs` but they will have +to specify it as an explicit dependency of their project. + +[1]: https://github.com/PyFilesystem/pyfilesystem2 +""" + +from __future__ import annotations + +try: + __import__("fs") +except ImportError: + from . import _base as base + from . import _copy as copy + from . import _errors as errors + from . import _info as info + from . import _osfs as osfs + from . import _path as path + from . import _subfs as subfs + from . import _tempfs as tempfs + from . import _tools as tools + from . import _walk as walk + from . import _zipfs as zipfs + + _haveFS = False +else: + import fs.base as base + import fs.copy as copy + import fs.errors as errors + import fs.info as info + import fs.osfs as osfs + import fs.path as path + import fs.subfs as subfs + import fs.tempfs as tempfs + import fs.tools as tools + import fs.walk as walk + import fs.zipfs as zipfs + + _haveFS = True + + +__all__ = [ + "base", + "copy", + "errors", + "info", + "osfs", + "path", + "subfs", + "tempfs", + "tools", + "walk", + "zipfs", +] diff --git a/contrib/python/fonttools/fontTools/misc/filesystem/_base.py b/contrib/python/fonttools/fontTools/misc/filesystem/_base.py new file mode 100644 index 00000000000..14603d5b265 --- /dev/null +++ b/contrib/python/fonttools/fontTools/misc/filesystem/_base.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +import typing +from abc import ABC, abstractmethod + +from ._copy import copy_dir, copy_file +from ._errors import ( + DestinationExists, + DirectoryExpected, + FileExpected, + FilesystemClosed, + NoSysPath, + ResourceNotFound, +) +from ._path import dirname +from ._walk import BoundWalker + +if typing.TYPE_CHECKING: + from typing import IO, Any, Collection, Iterator, Self, Type + + from ._info import Info + from ._subfs import SubFS + + +class FS(ABC): + """Abstract base class for custom filesystems.""" + + _closed: bool = False + + @abstractmethod + def open(self, path: str, mode: str = "rb", **kwargs) -> IO[Any]: ... + + @abstractmethod + def exists(self, path: str) -> bool: ... + + @abstractmethod + def isdir(self, path: str) -> bool: ... + + @abstractmethod + def isfile(self, path: str) -> bool: ... + + @abstractmethod + def listdir(self, path: str) -> list[str]: ... + + @abstractmethod + def makedir(self, path: str, recreate: bool = False) -> SubFS: ... + + @abstractmethod + def makedirs(self, path: str, recreate: bool = False) -> SubFS: ... + + @abstractmethod + def getinfo(self, path: str, namespaces: Collection[str] | None = None) -> Info: ... + + @abstractmethod + def remove(self, path: str) -> None: ... + + @abstractmethod + def removedir(self, path: str) -> None: ... + + @abstractmethod + def removetree(self, path: str) -> None: ... + + @abstractmethod + def movedir(self, src: str, dst: str, create: bool = False) -> None: ... + + def getsyspath(self, path: str) -> str: + raise NoSysPath(f"the filesystem {self!r} has no system path") + + def close(self): + self._closed = True + + def isclosed(self) -> bool: + return self._closed + + def __enter__(self) -> Self: + return self + + def __exit__(self, exc_type, exc, tb): + self.close() + return False # never swallow exceptions + + def check(self): + if self._closed: + raise FilesystemClosed(f"the filesystem {self!r} is closed") + + def opendir(self, path: str, *, factory: Type[SubFS] | None = None) -> SubFS: + """Return a sub‑filesystem rooted at `path`.""" + if factory is None: + from ._subfs import SubFS + + factory = SubFS + return factory(self, path) + + def scandir( + self, path: str, namespaces: Collection[str] | None = None + ) -> Iterator[Info]: + return (self.getinfo(f"{path}/{p}", namespaces) for p in self.listdir(path)) + + @property + def walk(self) -> BoundWalker: + return BoundWalker(self) + + def readbytes(self, path: str) -> bytes: + with self.open(path, "rb") as f: + return f.read() + + def writebytes(self, path: str, data: bytes): + with self.open(path, "wb") as f: + f.write(data) + + def create(self, path: str, wipe: bool = False): + if not wipe and self.exists(path): + return False + with self.open(path, "wb"): + pass # 'touch' empty file + return True + + def copy(self, src_path: str, dst_path: str, overwrite=False): + if not self.exists(src_path): + raise ResourceNotFound(f"{src_path!r} does not exist") + elif not self.isfile(src_path): + raise FileExpected(f"path {src_path!r} should be a file") + if not overwrite and self.exists(dst_path): + raise DestinationExists(f"destination {dst_path!r} already exists") + if not self.isdir(dirname(dst_path)): + raise DirectoryExpected(f"path {dirname(dst_path)!r} should be a directory") + copy_file(self, src_path, self, dst_path) + + def copydir(self, src_path: str, dst_path: str, create=False): + if not create and not self.exists(dst_path): + raise ResourceNotFound(f"{dst_path!r} does not exist") + if not self.isdir(src_path): + raise DirectoryExpected(f"path {src_path!r} should be a directory") + copy_dir(self, src_path, self, dst_path) diff --git a/contrib/python/fonttools/fontTools/misc/filesystem/_copy.py b/contrib/python/fonttools/fontTools/misc/filesystem/_copy.py new file mode 100644 index 00000000000..194f9ffbb45 --- /dev/null +++ b/contrib/python/fonttools/fontTools/misc/filesystem/_copy.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import typing + +from ._errors import IllegalDestination +from ._path import combine, frombase, isbase +from ._tools import copy_file_data + +if typing.TYPE_CHECKING: + from ._base import FS + + +def copy_file(src_fs: FS, src_path: str, dst_fs: FS, dst_path: str): + if src_fs is dst_fs and src_path == dst_path: + raise IllegalDestination(f"cannot copy {src_path!r} to itself") + + with src_fs.open(src_path, "rb") as src_file: + with dst_fs.open(dst_path, "wb") as dst_file: + copy_file_data(src_file, dst_file) + + +def copy_structure( + src_fs: FS, + dst_fs: FS, + src_root: str = "/", + dst_root: str = "/", +): + if src_fs is dst_fs and isbase(src_root, dst_root): + raise IllegalDestination(f"cannot copy {src_fs!r} to itself") + + dst_fs.makedirs(dst_root, recreate=True) + for dir_path in src_fs.walk.dirs(src_root): + dst_fs.makedir(combine(dst_root, frombase(src_root, dir_path)), recreate=True) + + +def copy_dir(src_fs: FS, src_path: str, dst_fs: FS, dst_path: str): + copy_structure(src_fs, dst_fs, src_path, dst_path) + + for file_path in src_fs.walk.files(src_path): + copy_path = combine(dst_path, frombase(src_path, file_path)) + copy_file(src_fs, file_path, dst_fs, copy_path) + + +def copy_fs(src_fs: FS, dst_fs: FS): + copy_dir(src_fs, "/", dst_fs, "/") diff --git a/contrib/python/fonttools/fontTools/misc/filesystem/_errors.py b/contrib/python/fonttools/fontTools/misc/filesystem/_errors.py new file mode 100644 index 00000000000..5017d563377 --- /dev/null +++ b/contrib/python/fonttools/fontTools/misc/filesystem/_errors.py @@ -0,0 +1,54 @@ +class FSError(Exception): + pass + + +class CreateFailed(FSError): + pass + + +class FilesystemClosed(FSError): + pass + + +class MissingInfoNamespace(FSError): + pass + + +class NoSysPath(FSError): + pass + + +class OperationFailed(FSError): + pass + + +class IllegalDestination(OperationFailed): + pass + + +class ResourceError(FSError): + pass + + +class ResourceNotFound(ResourceError): + pass + + +class DirectoryExpected(ResourceError): + pass + + +class DirectoryNotEmpty(ResourceError): + pass + + +class FileExpected(ResourceError): + pass + + +class DestinationExists(ResourceError): + pass + + +class ResourceReadOnly(ResourceError): + pass diff --git a/contrib/python/fonttools/fontTools/misc/filesystem/_info.py b/contrib/python/fonttools/fontTools/misc/filesystem/_info.py new file mode 100644 index 00000000000..7c204c83c47 --- /dev/null +++ b/contrib/python/fonttools/fontTools/misc/filesystem/_info.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import typing +from datetime import datetime, timezone + +from ._errors import MissingInfoNamespace + +if typing.TYPE_CHECKING: + from collections.abc import Mapping + from typing import Any + + +def epoch_to_datetime(t: int | None) -> datetime | None: + """Convert epoch time to a UTC datetime.""" + if t is None: + return None + return datetime.fromtimestamp(t, tz=timezone.utc) + + +class Info: + __slots__ = ["raw", "namespaces"] + + def __init__(self, raw_info: Mapping[str, Any]): + self.raw = raw_info + self.namespaces = frozenset(raw_info.keys()) + + def get(self, namespace: str, key: str, default: Any | None = None) -> Any | None: + try: + return self.raw[namespace].get(key, default) + except KeyError: + raise MissingInfoNamespace(f"Namespace {namespace!r} does not exist") + + @property + def name(self) -> str: + return self.get("basic", "name") + + @property + def is_dir(self) -> bool: + return self.get("basic", "is_dir") + + @property + def is_file(self) -> bool: + return not self.is_dir + + @property + def accessed(self) -> datetime | None: + return epoch_to_datetime(self.get("details", "accessed")) + + @property + def modified(self) -> datetime | None: + return epoch_to_datetime(self.get("details", "modified")) + + @property + def size(self) -> int | None: + return self.get("details", "size") + + @property + def type(self) -> int | None: + return self.get("details", "type") + + @property + def created(self) -> datetime | None: + return epoch_to_datetime(self.get("details", "created")) + + @property + def metadata_changed(self) -> datetime | None: + return epoch_to_datetime(self.get("details", "metadata_changed")) + + def __str__(self) -> str: + if self.is_dir: + return "<dir '{}'>".format(self.name) + else: + return "<file '{}'>".format(self.name) + + __repr__ = __str__ diff --git a/contrib/python/fonttools/fontTools/misc/filesystem/_osfs.py b/contrib/python/fonttools/fontTools/misc/filesystem/_osfs.py new file mode 100644 index 00000000000..3f533bb8da4 --- /dev/null +++ b/contrib/python/fonttools/fontTools/misc/filesystem/_osfs.py @@ -0,0 +1,164 @@ +from __future__ import annotations + +import errno +import platform +import shutil +import stat +import typing +from os import PathLike +from pathlib import Path + +from ._base import FS +from ._errors import ( + CreateFailed, + DirectoryExpected, + DirectoryNotEmpty, + FileExpected, + IllegalDestination, + ResourceError, + ResourceNotFound, +) +from ._info import Info +from ._path import isbase + +if typing.TYPE_CHECKING: + from collections.abc import Collection + from typing import IO, Any + + from ._subfs import SubFS + + +_WINDOWS_PLATFORM = platform.system() == "Windows" + + +class OSFS(FS): + """Filesystem for a directory on the local disk. + + A thin layer on top of `pathlib.Path`. + """ + + def __init__(self, root: str | PathLike, create: bool = False): + super().__init__() + self._root = Path(root).resolve() + if create: + self._root.mkdir(parents=True, exist_ok=True) + else: + if not self._root.is_dir(): + raise CreateFailed( + f"unable to create OSFS: {root!r} does not exist or is not a directory" + ) + + def _abs(self, rel_path: str) -> Path: + self.check() + return (self._root / rel_path.strip("/")).resolve() + + def open(self, path: str, mode: str = "rb", **kwargs) -> IO[Any]: + try: + return self._abs(path).open(mode, **kwargs) + except FileNotFoundError: + raise ResourceNotFound(f"No such file or directory: {path!r}") + + def exists(self, path: str) -> bool: + return self._abs(path).exists() + + def isdir(self, path: str) -> bool: + return self._abs(path).is_dir() + + def isfile(self, path: str) -> bool: + return self._abs(path).is_file() + + def listdir(self, path: str) -> list[str]: + return [p.name for p in self._abs(path).iterdir()] + + def _mkdir(self, path: str, parents: bool = False, exist_ok: bool = False) -> SubFS: + self._abs(path).mkdir(parents=parents, exist_ok=exist_ok) + return self.opendir(path) + + def makedir(self, path: str, recreate: bool = False) -> SubFS: + return self._mkdir(path, parents=False, exist_ok=recreate) + + def makedirs(self, path: str, recreate: bool = False) -> SubFS: + return self._mkdir(path, parents=True, exist_ok=recreate) + + def getinfo(self, path: str, namespaces: Collection[str] | None = None) -> Info: + path = self._abs(path) + if not path.exists(): + raise ResourceNotFound(f"No such file or directory: {str(path)!r}") + info = { + "basic": { + "name": path.name, + "is_dir": path.is_dir(), + } + } + namespaces = namespaces or () + if "details" in namespaces: + stat_result = path.stat() + details = info["details"] = { + "accessed": stat_result.st_atime, + "modified": stat_result.st_mtime, + "size": stat_result.st_size, + "type": stat.S_IFMT(stat_result.st_mode), + "created": getattr(stat_result, "st_birthtime", None), + } + ctime_key = "created" if _WINDOWS_PLATFORM else "metadata_changed" + details[ctime_key] = stat_result.st_ctime + return Info(info) + + def remove(self, path: str): + path = self._abs(path) + try: + path.unlink() + except FileNotFoundError: + raise ResourceNotFound(f"No such file or directory: {str(path)!r}") + except OSError as e: + if path.is_dir(): + raise FileExpected(f"path {str(path)!r} should be a file") + else: + raise ResourceError(f"unable to remove {str(path)!r}: {e}") + + def removedir(self, path: str): + try: + self._abs(path).rmdir() + except NotADirectoryError: + raise DirectoryExpected(f"path {path!r} should be a directory") + except OSError as e: + if e.errno == errno.ENOTEMPTY: + raise DirectoryNotEmpty(f"Directory not empty: {path!r}") + else: + raise ResourceError(f"unable to remove {path!r}: {e}") + + def removetree(self, path: str): + shutil.rmtree(self._abs(path)) + + def movedir(self, src_dir: str, dst_dir: str, create: bool = False): + if isbase(src_dir, dst_dir): + raise IllegalDestination(f"cannot move {src_dir!r} to {dst_dir!r}") + src_path = self._abs(src_dir) + if not src_path.exists(): + raise ResourceNotFound(f"Source {src_dir!r} does not exist") + elif not src_path.is_dir(): + raise DirectoryExpected(f"Source {src_dir!r} should be a directory") + dst_path = self._abs(dst_dir) + if not create and not dst_path.exists(): + raise ResourceNotFound(f"Destination {dst_dir!r} does not exist") + if dst_path.is_file(): + raise DirectoryExpected(f"Destination {dst_dir!r} should be a directory") + if create: + dst_path.parent.mkdir(parents=True, exist_ok=True) + if dst_path.exists(): + if list(dst_path.iterdir()): + raise DirectoryNotEmpty(f"Destination {dst_dir!r} is not empty") + elif _WINDOWS_PLATFORM: + # on Unix os.rename silently replaces an empty dst_dir whereas on + # Windows it always raises FileExistsError, empty or not. + dst_path.rmdir() + src_path.rename(dst_path) + + def getsyspath(self, path: str) -> str: + return str(self._abs(path)) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({str(self._root)!r})" + + def __str__(self) -> str: + return f"<{self.__class__.__name__.lower()} '{self._root}'>" diff --git a/contrib/python/fonttools/fontTools/misc/filesystem/_path.py b/contrib/python/fonttools/fontTools/misc/filesystem/_path.py new file mode 100644 index 00000000000..e89c00b3d97 --- /dev/null +++ b/contrib/python/fonttools/fontTools/misc/filesystem/_path.py @@ -0,0 +1,67 @@ +import os +import platform + +_WINDOWS_PLATFORM = platform.system() == "Windows" + + +def combine(path1: str, path2) -> str: + if not path1: + return path2 + return "{}/{}".format(path1.rstrip("/"), path2.lstrip("/")) + + +def split(path: str) -> tuple[str, str]: + if "/" not in path: + return ("", path) + split = path.rsplit("/", 1) + return (split[0] or "/", split[1]) + + +def dirname(path: str) -> str: + return split(path)[0] + + +def basename(path: str) -> str: + return split(path)[1] + + +def forcedir(path: str) -> str: + # Ensure the path ends with a trailing forward slash. + if not path.endswith("/"): + return path + "/" + return path + + +def abspath(path: str) -> str: + # FS objects have no concept of a *current directory*. This simply + # ensures the path starts with a forward slash. + if not path.startswith("/"): + return "/" + path + return path + + +def isbase(path1: str, path2: str) -> bool: + # Check if `path1` is a base or prefix of `path2`. + _path1 = forcedir(abspath(path1)) + _path2 = forcedir(abspath(path2)) + return _path2.startswith(_path1) + + +def frombase(path1: str, path2: str) -> str: + # Get the final path of `path2` that isn't in `path1`. + if not isbase(path1, path2): + raise ValueError(f"path1 must be a prefix of path2: {path1!r} vs {path2!r}") + return path2[len(path1) :] + + +def relpath(path: str) -> str: + return path.lstrip("/") + + +def normpath(path: str) -> str: + normalized = os.path.normpath(path) + if _WINDOWS_PLATFORM: + # os.path.normpath converts backslashes to forward slashes on Windows + # but we want forward slashes, so we convert them back + normalized = normalized.replace("\\", "/") + return normalized diff --git a/contrib/python/fonttools/fontTools/misc/filesystem/_subfs.py b/contrib/python/fonttools/fontTools/misc/filesystem/_subfs.py new file mode 100644 index 00000000000..bc7d8825af1 --- /dev/null +++ b/contrib/python/fonttools/fontTools/misc/filesystem/_subfs.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import typing +from pathlib import PurePosixPath + +from ._base import FS +from ._errors import DirectoryExpected, ResourceNotFound + +if typing.TYPE_CHECKING: + from collections.abc import Collection + from typing import IO, Any + + from ._info import Info + + +class SubFS(FS): + """Maps a sub-directory of another filesystem.""" + + def __init__(self, parent: FS, sub_path: str): + super().__init__() + self._parent = parent + self._prefix = PurePosixPath(sub_path).as_posix().rstrip("/") + if not parent.exists(self._prefix): + raise ResourceNotFound(f"No such file or directory: {sub_path!r}") + elif not parent.isdir(self._prefix): + raise DirectoryExpected(f"{sub_path!r} is not a directory") + + def delegate_fs(self): + return self._parent + + def _full(self, rel: str) -> str: + self.check() + return f"{self._prefix}/{PurePosixPath(rel).as_posix()}".lstrip("/") + + def open(self, path: str, mode: str = "rb", **kwargs) -> IO[Any]: + return self._parent.open(self._full(path), mode, **kwargs) + + def exists(self, path: str) -> bool: + return self._parent.exists(self._full(path)) + + def isdir(self, path: str) -> bool: + return self._parent.isdir(self._full(path)) + + def isfile(self, path: str) -> bool: + return self._parent.isfile(self._full(path)) + + def listdir(self, path: str) -> list[str]: + return self._parent.listdir(self._full(path)) + + def makedir(self, path: str, recreate: bool = False): + return self._parent.makedir(self._full(path), recreate=recreate) + + def makedirs(self, path: str, recreate: bool = False): + return self._parent.makedirs(self._full(path), recreate=recreate) + + def getinfo(self, path: str, namespaces: Collection[str] | None = None) -> Info: + return self._parent.getinfo(self._full(path), namespaces=namespaces) + + def remove(self, path: str): + return self._parent.remove(self._full(path)) + + def removedir(self, path: str): + return self._parent.removedir(self._full(path)) + + def removetree(self, path: str): + return self._parent.removetree(self._full(path)) + + def movedir(self, src: str, dst: str, create: bool = False): + self._parent.movedir(self._full(src), self._full(dst), create=create) + + def getsyspath(self, path: str) -> str: + return self._parent.getsyspath(self._full(path)) + + def readbytes(self, path: str) -> bytes: + return self._parent.readbytes(self._full(path)) + + def writebytes(self, path: str, data: bytes): + self._parent.writebytes(self._full(path), data) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self._parent!r}, {self._prefix!r})" + + def __str__(self) -> str: + return f"{self._parent}/{self._prefix}" + + +class ClosingSubFS(SubFS): + """Like SubFS, but auto-closes the parent filesystem when closed.""" + + def close(self): + super().close() + self._parent.close() diff --git a/contrib/python/fonttools/fontTools/misc/filesystem/_tempfs.py b/contrib/python/fonttools/fontTools/misc/filesystem/_tempfs.py new file mode 100644 index 00000000000..9f968c45f02 --- /dev/null +++ b/contrib/python/fonttools/fontTools/misc/filesystem/_tempfs.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import shutil +import tempfile + +from ._errors import OperationFailed +from ._osfs import OSFS + + +class TempFS(OSFS): + def __init__(self, auto_clean: bool = True, ignore_clean_errors: bool = True): + self.auto_clean = auto_clean + self.ignore_clean_errors = ignore_clean_errors + self._temp_dir = tempfile.mkdtemp("__temp_fs__") + self._cleaned = False + super().__init__(self._temp_dir) + + def close(self): + if self.auto_clean: + self.clean() + super().close() + + def clean(self): + if self._cleaned: + return + + try: + shutil.rmtree(self._temp_dir) + except Exception as e: + if not self.ignore_clean_errors: + raise OperationFailed( + f"failed to remove temporary directory: {self._temp_dir!r}" + ) from e + self._cleaned = True diff --git a/contrib/python/fonttools/fontTools/misc/filesystem/_tools.py b/contrib/python/fonttools/fontTools/misc/filesystem/_tools.py new file mode 100644 index 00000000000..4b02ac0d253 --- /dev/null +++ b/contrib/python/fonttools/fontTools/misc/filesystem/_tools.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import typing +from pathlib import PurePosixPath + +from ._errors import DirectoryNotEmpty + +if typing.TYPE_CHECKING: + from typing import IO + + from ._base import FS + + +def remove_empty(fs: FS, path: str): + """Remove all empty parents.""" + path = PurePosixPath(path) + root = PurePosixPath("/") + try: + while path != root: + fs.removedir(path.as_posix()) + path = path.parent + except DirectoryNotEmpty: + pass + + +def copy_file_data(src_file: IO, dst_file: IO, chunk_size: int | None = None): + """Copy data from one file object to another.""" + _chunk_size = 1024 * 1024 if chunk_size is None else chunk_size + read = src_file.read + write = dst_file.write + # in iter(callable, sentilel), callable is called until it returns the sentinel; + # this allows to copy `chunk_size` bytes at a time. + for chunk in iter(lambda: read(_chunk_size) or None, None): + write(chunk) diff --git a/contrib/python/fonttools/fontTools/misc/filesystem/_walk.py b/contrib/python/fonttools/fontTools/misc/filesystem/_walk.py new file mode 100644 index 00000000000..e372e618a4b --- /dev/null +++ b/contrib/python/fonttools/fontTools/misc/filesystem/_walk.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import typing +from collections import deque +from collections.abc import Collection, Iterator + +from ._path import combine + +if typing.TYPE_CHECKING: + from typing import Callable + + from ._base import FS + from ._info import Info + + +class BoundWalker: + def __init__(self, fs: FS): + self._fs = fs + + def _iter_walk( + self, path: str, namespaces: Collection[str] | None = None + ) -> Iterator[tuple[str, Info | None]]: + """Walk files using a *breadth first* search.""" + queue = deque([path]) + push = queue.appendleft + pop = queue.pop + _scan = self._fs.scandir + _combine = combine + + while queue: + dir_path = pop() + for info in _scan(dir_path, namespaces=namespaces): + if info.is_dir: + yield dir_path, info + push(_combine(dir_path, info.name)) + else: + yield dir_path, info + yield path, None + + def _filter( + self, + include: Callable[[str, Info], bool] = lambda path, info: True, + path: str = "/", + namespaces: Collection[str] | None = None, + ) -> Iterator[str]: + _combine = combine + for path, info in self._iter_walk(path, namespaces): + if info is not None and include(path, info): + yield _combine(path, info.name) + + def files(self, path: str = "/") -> Iterator[str]: + yield from self._filter(lambda _, info: info.is_file, path) + + def dirs(self, path: str = "/") -> Iterator[str]: + yield from self._filter(lambda _, info: info.is_dir, path) diff --git a/contrib/python/fonttools/fontTools/misc/filesystem/_zipfs.py b/contrib/python/fonttools/fontTools/misc/filesystem/_zipfs.py new file mode 100644 index 00000000000..1635a6d0abd --- /dev/null +++ b/contrib/python/fonttools/fontTools/misc/filesystem/_zipfs.py @@ -0,0 +1,204 @@ +from __future__ import annotations + +import io +import os +import shutil +import stat +import typing +import zipfile +from datetime import datetime + +from ._base import FS +from ._errors import FileExpected, ResourceNotFound, ResourceReadOnly +from ._info import Info +from ._path import dirname, forcedir, normpath, relpath +from ._tempfs import TempFS + +if typing.TYPE_CHECKING: + from collections.abc import Collection + from typing import IO, Any + + from ._subfs import SubFS + + +class ZipFS(FS): + """Read and write zip files.""" + + def __new__( + cls, file: str | os.PathLike, write: bool = False, encoding: str = "utf-8" + ): + if write: + return WriteZipFS(file, encoding) + else: + return ReadZipFS(file, encoding) + + if typing.TYPE_CHECKING: + + def __init__( + self, file: str | os.PathLike, write: bool = False, encoding: str = "utf-8" + ): + pass + + +class ReadZipFS(FS): + """A readable zip file.""" + + def __init__(self, file: str | os.PathLike, encoding: str = "utf-8"): + super().__init__() + self._file = os.fspath(file) + self.encoding = encoding # unused + self._zip = zipfile.ZipFile(file, "r") + self._directory_fs = None + + def __repr__(self) -> str: + return f"ReadZipFS({self._file!r})" + + def __str__(self) -> str: + return f"<zipfs '{self._file}'>" + + def _path_to_zip_name(self, path: str) -> str: + """Convert a path to a zip file name.""" + path = relpath(normpath(path)) + if self._directory.isdir(path): + path = forcedir(path) + return path + + @property + def _directory(self) -> TempFS: + if self._directory_fs is None: + self._directory_fs = _fs = TempFS() + for zip_name in self._zip.namelist(): + resource_name = zip_name + if resource_name.endswith("/"): + _fs.makedirs(resource_name, recreate=True) + else: + _fs.makedirs(dirname(resource_name), recreate=True) + _fs.create(resource_name) + return self._directory_fs + + def close(self): + super(ReadZipFS, self).close() + self._zip.close() + if self._directory_fs is not None: + self._directory_fs.close() + + def getinfo(self, path: str, namespaces: Collection[str] | None = None) -> Info: + namespaces = namespaces or () + raw_info = {} + + if path == "/": + raw_info["basic"] = {"name": "", "is_dir": True} + if "details" in namespaces: + raw_info["details"] = {"type": stat.S_IFDIR} + else: + basic_info = self._directory.getinfo(path) + raw_info["basic"] = {"name": basic_info.name, "is_dir": basic_info.is_dir} + + if "details" in namespaces: + zip_name = self._path_to_zip_name(path) + try: + zip_info = self._zip.getinfo(zip_name) + except KeyError: + pass + else: + if "details" in namespaces: + raw_info["details"] = { + "size": zip_info.file_size, + "type": int( + stat.S_IFDIR if basic_info.is_dir else stat.S_IFREG + ), + "modified": datetime(*zip_info.date_time).timestamp(), + } + + return Info(raw_info) + + def exists(self, path: str) -> bool: + self.check() + return self._directory.exists(path) + + def isdir(self, path: str) -> bool: + self.check() + return self._directory.isdir(path) + + def isfile(self, path: str) -> bool: + self.check() + return self._directory.isfile(path) + + def listdir(self, path: str) -> str: + self.check() + return self._directory.listdir(path) + + def makedir(self, path: str, recreate: bool = False) -> SubFS: + self.check() + raise ResourceReadOnly(path) + + def makedirs(self, path: str, recreate: bool = False) -> SubFS: + self.check() + raise ResourceReadOnly(path) + + def remove(self, path: str): + self.check() + raise ResourceReadOnly(path) + + def removedir(self, path: str): + self.check() + raise ResourceReadOnly(path) + + def removetree(self, path: str): + self.check() + raise ResourceReadOnly(path) + + def movedir(self, src: str, dst: str, create: bool = False): + self.check() + raise ResourceReadOnly(src) + + def readbytes(self, path: str) -> bytes: + self.check() + if not self._directory.isfile(path): + raise ResourceNotFound(path) + zip_name = self._path_to_zip_name(path) + zip_bytes = self._zip.read(zip_name) + return zip_bytes + + def open(self, path: str, mode: str = "rb", **kwargs) -> IO[Any]: + self.check() + if self._directory.isdir(path): + raise FileExpected(f"{path!r} is a directory") + + zip_mode = mode[0] + if zip_mode == "r" and not self._directory.exists(path): + raise ResourceNotFound(f"No such file or directory: {path!r}") + + if any(m in mode for m in "wax+"): + raise ResourceReadOnly(path) + + zip_name = self._path_to_zip_name(path) + stream = self._zip.open(zip_name, zip_mode) + if "b" in mode: + if kwargs: + raise ValueError("encoding args invalid for binary operation") + return stream + # Text mode + return io.TextIOWrapper(stream, **kwargs) + + +class WriteZipFS(TempFS): + """A writable zip file.""" + + def __init__(self, file: str | os.PathLike, encoding: str = "utf-8"): + super().__init__() + self._file = os.fspath(file) + self.encoding = encoding # unused + + def __repr__(self) -> str: + return f"WriteZipFS({self._file!r})" + + def __str__(self) -> str: + return f"<zipfs-write '{self._file}'>" + + def close(self): + base_name = os.path.splitext(self._file)[0] + shutil.make_archive(base_name, format="zip", root_dir=self._temp_dir) + if self._file != base_name + ".zip": + shutil.move(base_name + ".zip", self._file) + super().close() diff --git a/contrib/python/fonttools/fontTools/misc/sstruct.py b/contrib/python/fonttools/fontTools/misc/sstruct.py index 92be275b89f..23227d8a683 100644 --- a/contrib/python/fonttools/fontTools/misc/sstruct.py +++ b/contrib/python/fonttools/fontTools/misc/sstruct.py @@ -64,10 +64,7 @@ def pack(fmt, obj): elements = [] if not isinstance(obj, dict): obj = obj.__dict__ - string_index = formatstring - if formatstring.startswith(">"): - string_index = formatstring[1:] - for ix, name in enumerate(names.keys()): + for name in names.keys(): value = obj[name] if name in fixes: # fixed point conversion @@ -96,8 +93,7 @@ def unpack(fmt, data, obj=None): else: d = obj.__dict__ elements = struct.unpack(formatstring, data) - for i in range(len(names)): - name = list(names.keys())[i] + for i, name in enumerate(names.keys()): value = elements[i] if name in fixes: # fixed point conversion diff --git a/contrib/python/fonttools/fontTools/misc/xmlWriter.py b/contrib/python/fonttools/fontTools/misc/xmlWriter.py index 9a8dc3e3b7f..240d762aec2 100644 --- a/contrib/python/fonttools/fontTools/misc/xmlWriter.py +++ b/contrib/python/fonttools/fontTools/misc/xmlWriter.py @@ -4,8 +4,22 @@ from fontTools.misc.textTools import byteord, strjoin, tobytes, tostr import sys import os import string +import logging +import itertools INDENT = " " +TTX_LOG = logging.getLogger("fontTools.ttx") +REPLACEMENT = "?" +ILLEGAL_XML_CHARS = dict.fromkeys( + itertools.chain( + range(0x00, 0x09), + (0x0B, 0x0C), + range(0x0E, 0x20), + range(0xD800, 0xE000), + (0xFFFE, 0xFFFF), + ), + REPLACEMENT, +) class XMLWriter(object): @@ -168,12 +182,25 @@ class XMLWriter(object): def escape(data): + """Escape characters not allowed in `XML 1.0 <https://www.w3.org/TR/xml/#NT-Char>`_.""" data = tostr(data, "utf_8") data = data.replace("&", "&") data = data.replace("<", "<") data = data.replace(">", ">") data = data.replace("\r", " ") - return data + + newData = data.translate(ILLEGAL_XML_CHARS) + if newData != data: + maxLen = 10 + preview = repr(data) + if len(data) > maxLen: + preview = repr(data[:maxLen])[1:-1] + "..." + TTX_LOG.warning( + "Illegal XML character(s) found; replacing offending " "string %r with %r", + preview, + REPLACEMENT, + ) + return newData def escapeattr(data): diff --git a/contrib/python/fonttools/fontTools/pens/roundingPen.py b/contrib/python/fonttools/fontTools/pens/roundingPen.py index 176bcc7a55b..a47a3d3df74 100644 --- a/contrib/python/fonttools/fontTools/pens/roundingPen.py +++ b/contrib/python/fonttools/fontTools/pens/roundingPen.py @@ -116,8 +116,8 @@ class RoundingPointPen(FilterPointPen): def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs): xx, xy, yx, yy, dx, dy = transformation self._outPen.addComponent( - baseGlyphName=baseGlyphName, - transformation=Transform( + baseGlyphName, + Transform( self.transformRoundFunc(xx), self.transformRoundFunc(xy), self.transformRoundFunc(yx), diff --git a/contrib/python/fonttools/fontTools/subset/__init__.py b/contrib/python/fonttools/fontTools/subset/__init__.py index baad419a11e..77642e6440b 100644 --- a/contrib/python/fonttools/fontTools/subset/__init__.py +++ b/contrib/python/fonttools/fontTools/subset/__init__.py @@ -27,16 +27,16 @@ from collections import Counter, defaultdict from functools import reduce from types import MethodType -__usage__ = "pyftsubset font-file [glyph...] [--option=value]..." +__usage__ = "fonttools subset font-file [glyph...] [--option=value]..." __doc__ = ( """\ -pyftsubset -- OpenType font subsetter and optimizer +fonttools subset -- OpenType font subsetter and optimizer -pyftsubset is an OpenType font subsetter and optimizer, based on fontTools. -It accepts any TT- or CFF-flavored OpenType (.otf or .ttf) or WOFF (.woff) -font file. The subsetted glyph set is based on the specified glyphs -or characters, and specified OpenType layout features. +fonttools subset is an OpenType font subsetter and optimizer, based on +fontTools. It accepts any TT- or CFF-flavored OpenType (.otf or .ttf) +or WOFF (.woff) font file. The subsetted glyph set is based on the +specified glyphs or characters, and specified OpenType layout features. The tool also performs some size-reducing optimizations, aimed for using subset fonts as webfonts. Individual optimizations can be enabled or @@ -130,11 +130,11 @@ you might need to escape the question mark, like this: '--glyph-names\\?'. Examples:: - $ pyftsubset --glyph-names? + $ fonttools subset --glyph-names? Current setting for 'glyph-names' is: False - $ pyftsubset --name-IDs=? + $ fonttools subset --name-IDs=? Current setting for 'name-IDs' is: [0, 1, 2, 3, 4, 5, 6] - $ pyftsubset --hinting? --no-hinting --hinting? + $ fonttools subset --hinting? --no-hinting --hinting? Current setting for 'hinting' is: True Current setting for 'hinting' is: False @@ -445,7 +445,7 @@ Example Produce a subset containing the characters ' !"#$%' without performing size-reducing optimizations:: - $ pyftsubset font.ttf --unicodes="U+0020-0025" \\ + $ fonttools subset font.ttf --unicodes="U+0020-0025" \\ --layout-features=* --glyph-names --symbol-cmap --legacy-cmap \\ --notdef-glyph --notdef-outline --recommended-glyphs \\ --name-IDs=* --name-legacy --name-languages=* @@ -3768,7 +3768,7 @@ def parse_glyphs(s): def usage(): print("usage:", __usage__, file=sys.stderr) - print("Try pyftsubset --help for more information.\n", file=sys.stderr) + print("Try fonttools subset --help for more information.\n", file=sys.stderr) @timer("make one with everything (TOTAL TIME)") diff --git a/contrib/python/fonttools/fontTools/ttLib/sfnt.py b/contrib/python/fonttools/fontTools/ttLib/sfnt.py index 6cc867a4d7c..a9d07e615de 100644 --- a/contrib/python/fonttools/fontTools/ttLib/sfnt.py +++ b/contrib/python/fonttools/fontTools/ttLib/sfnt.py @@ -375,10 +375,9 @@ class SFNTWriter(object): def _calcMasterChecksum(self, directory): # calculate checkSumAdjustment - tags = list(self.tables.keys()) checksums = [] - for i in range(len(tags)): - checksums.append(self.tables[tags[i]].checkSum) + for tag in self.tables.keys(): + checksums.append(self.tables[tag].checkSum) if self.DirectoryEntry != SFNTDirectoryEntry: # Create a SFNT directory for checksum calculation purposes diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/S__i_l_f.py b/contrib/python/fonttools/fontTools/ttLib/tables/S__i_l_f.py index 876fef3cbaa..e8090af1e52 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/S__i_l_f.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/S__i_l_f.py @@ -948,7 +948,7 @@ class Pass(object): writer.newline() writer.begintag("rules") writer.newline() - for i in range(len(self.actions)): + for i, action in enumerate(self.actions): writer.begintag( "rule", index=i, @@ -958,7 +958,7 @@ class Pass(object): writer.newline() if len(self.ruleConstraints[i]): writecode("constraint", writer, self.ruleConstraints[i]) - writecode("action", writer, self.actions[i]) + writecode("action", writer, action) writer.endtag("rule") writer.newline() writer.endtag("rules") diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__1.py b/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__1.py index b0b851cc788..19537dac9c5 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__1.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__1.py @@ -91,12 +91,11 @@ class table_T_S_I__1(LogMixin, DefaultTable.DefaultTable): glyphNames = ttFont.getGlyphOrder() indices = [] - for i in range(len(glyphNames)): + for i, name in enumerate(glyphNames): if len(data) % 2: data = ( data + b"\015" ) # align on 2-byte boundaries, fill with return chars. Yum. - name = glyphNames[i] if name in self.glyphPrograms: text = tobytes(self.glyphPrograms[name], encoding="utf-8") else: @@ -108,13 +107,11 @@ class table_T_S_I__1(LogMixin, DefaultTable.DefaultTable): data = data + text extra_indices = [] - codes = sorted(self.extras.items()) - for i in range(len(codes)): + for code, name in sorted(self.extras.items()): if len(data) % 2: data = ( data + b"\015" ) # align on 2-byte boundaries, fill with return chars. - code, name = codes[i] if name in self.extraPrograms: text = tobytes(self.extraPrograms[name], encoding="utf-8") else: diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__5.py b/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__5.py index 6afd76832fe..8aa382e43ca 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__5.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/T_S_I__5.py @@ -38,8 +38,8 @@ class table_T_S_I__5(DefaultTable.DefaultTable): def compile(self, ttFont): glyphNames = ttFont.getGlyphOrder() a = array.array("H") - for i in range(len(glyphNames)): - a.append(self.glyphGrouping.get(glyphNames[i], 0)) + for glyphName in glyphNames: + a.append(self.glyphGrouping.get(glyphName, 0)) if sys.byteorder != "big": a.byteswap() return a.tobytes() diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_c_m_a_p.py b/contrib/python/fonttools/fontTools/ttLib/tables/_c_m_a_p.py index 7fad1a2d852..e935313a18c 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_c_m_a_p.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_c_m_a_p.py @@ -398,7 +398,7 @@ class cmap_format_0(CmapSubtable): assert 262 == self.length, "Format 0 cmap subtable not 262 bytes" gids = array.array("B") gids.frombytes(self.data) - charCodes = list(range(len(gids))) + charCodes = range(len(gids)) self.cmap = _make_map(self.ttFont, charCodes, gids) def compile(self, ttFont): diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_c_v_t.py b/contrib/python/fonttools/fontTools/ttLib/tables/_c_v_t.py index 51e2f78df8d..c89fe2c239a 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_c_v_t.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_c_v_t.py @@ -29,8 +29,7 @@ class table__c_v_t(DefaultTable.DefaultTable): return values.tobytes() def toXML(self, writer, ttFont): - for i in range(len(self.values)): - value = self.values[i] + for i, value in enumerate(self.values): writer.simpletag("cv", value=value, index=i) writer.newline() diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py b/contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py index ea46c9f7971..3dea653baa0 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py @@ -974,11 +974,10 @@ class Glyph(object): lastcomponent = len(self.components) - 1 more = 1 haveInstructions = 0 - for i in range(len(self.components)): + for i, compo in enumerate(self.components): if i == lastcomponent: haveInstructions = hasattr(self, "program") more = 0 - compo = self.components[i] data = data + compo.compile(more, haveInstructions, glyfTable) if haveInstructions: instructions = self.program.getBytecode() @@ -2037,8 +2036,8 @@ class GlyphCoordinates(object): if round is noRound: return a = self._a - for i in range(len(a)): - a[i] = round(a[i]) + for i, value in enumerate(a): + a[i] = round(value) def calcBounds(self): a = self._a @@ -2168,8 +2167,8 @@ class GlyphCoordinates(object): """ r = self.copy() a = r._a - for i in range(len(a)): - a[i] = -a[i] + for i, value in enumerate(a): + a[i] = -value return r def __round__(self, *, round=otRound): @@ -2214,8 +2213,8 @@ class GlyphCoordinates(object): other = other._a a = self._a assert len(a) == len(other) - for i in range(len(a)): - a[i] += other[i] + for i, value in enumerate(other): + a[i] += value return self return NotImplemented @@ -2238,8 +2237,8 @@ class GlyphCoordinates(object): other = other._a a = self._a assert len(a) == len(other) - for i in range(len(a)): - a[i] -= other[i] + for i, value in enumerate(other): + a[i] -= value return self return NotImplemented diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_h_d_m_x.py b/contrib/python/fonttools/fontTools/ttLib/tables/_h_d_m_x.py index 1ec913de152..07c4566a98d 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_h_d_m_x.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_h_d_m_x.py @@ -65,8 +65,8 @@ class table__h_d_m_x(DefaultTable.DefaultTable): items = sorted(self.hdmx.items()) for ppem, widths in items: data = data + bytechr(ppem) + bytechr(max(widths.values())) - for glyphID in range(len(glyphOrder)): - width = widths[glyphOrder[glyphID]] + for glyphName in glyphOrder: + width = widths[glyphName] data = data + bytechr(width) data = data + pad return data @@ -123,5 +123,5 @@ class table__h_d_m_x(DefaultTable.DefaultTable): glyphName = safeEval('"""' + glyphName + '"""') line = list(map(int, line[1:])) assert len(line) == len(ppems), "illegal hdmx format" - for i in range(len(ppems)): - hdmx[ppems[i]][glyphName] = line[i] + for i, ppem in enumerate(ppems): + hdmx[ppem][glyphName] = line[i] diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_l_o_c_a.py b/contrib/python/fonttools/fontTools/ttLib/tables/_l_o_c_a.py index 713a6eaffc2..f0b12fe57ee 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_l_o_c_a.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_l_o_c_a.py @@ -46,8 +46,8 @@ class table__l_o_c_a(DefaultTable.DefaultTable): max_location = 0 if max_location < 0x20000 and all(l % 2 == 0 for l in self.locations): locations = array.array("H") - for i in range(len(self.locations)): - locations.append(self.locations[i] // 2) + for location in self.locations: + locations.append(location // 2) ttFont["head"].indexToLocFormat = 0 else: locations = array.array("I", self.locations) diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_p_o_s_t.py b/contrib/python/fonttools/fontTools/ttLib/tables/_p_o_s_t.py index c449e5f0c03..8aa37b6a034 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/_p_o_s_t.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/_p_o_s_t.py @@ -174,10 +174,9 @@ class table__p_o_s_t(DefaultTable.DefaultTable): extraNames = self.extraNames = [ n for n in self.extraNames if n not in standardGlyphOrder ] - for i in range(len(extraNames)): - extraDict[extraNames[i]] = i - for glyphID in range(numGlyphs): - glyphName = glyphOrder[glyphID] + for i, name in enumerate(extraNames): + extraDict[name] = i + for glyphName in glyphOrder: if glyphName in self.mapping: psName = self.mapping[glyphName] else: diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/otBase.py b/contrib/python/fonttools/fontTools/ttLib/tables/otBase.py index 582b02024b3..46828178182 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/otBase.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/otBase.py @@ -500,8 +500,7 @@ class OTTableWriter(object): internedTables = {} items = self.items - for i in range(len(items)): - item = items[i] + for i, item in enumerate(items): if hasattr(item, "getCountData"): items[i] = item.getCountData() elif hasattr(item, "subWriter"): @@ -1130,8 +1129,7 @@ class BaseTable(object): for conv in self.getConverters(): if conv.repeat: value = getattr(self, conv.name, []) - for i in range(len(value)): - item = value[i] + for i, item in enumerate(value): conv.xmlWrite(xmlWriter, font, item, conv.name, [("index", i)]) else: if conv.aux and not eval(conv.aux, None, vars(self)): diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/otTables.py b/contrib/python/fonttools/fontTools/ttLib/tables/otTables.py index 0a998319063..ab5dace7265 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/otTables.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/otTables.py @@ -990,8 +990,7 @@ class Coverage(FormatSwitchingBaseTable): if brokenOrder or len(ranges) * 3 < len(glyphs): # 3 words vs. 1 word # Format 2 is more compact index = 0 - for i in range(len(ranges)): - start, end = ranges[i] + for i, (start, end) in enumerate(ranges): r = RangeRecord() r.StartID = start r.Start = font.getGlyphName(start) @@ -1404,8 +1403,7 @@ class ClassDef(FormatSwitchingBaseTable): glyphCount = endGlyph - startGlyph + 1 if len(ranges) * 3 < glyphCount + 1: # Format 2 is more compact - for i in range(len(ranges)): - cls, start, startName, end, endName = ranges[i] + for i, (cls, start, startName, end, endName) in enumerate(ranges): rec = ClassRangeRecord() rec.Start = startName rec.End = endName @@ -1463,8 +1461,7 @@ class AlternateSubst(FormatSwitchingBaseTable): if alternates is None: alternates = self.alternates = {} items = list(alternates.items()) - for i in range(len(items)): - glyphName, set = items[i] + for i, (glyphName, set) in enumerate(items): items[i] = font.getGlyphID(glyphName), glyphName, set items.sort() cov = Coverage() @@ -1520,8 +1517,8 @@ class LigatureSubst(FormatSwitchingBaseTable): input = _getGlyphsFromCoverageTable(rawTable["Coverage"]) ligSets = rawTable["LigatureSet"] assert len(input) == len(ligSets) - for i in range(len(input)): - ligatures[input[i]] = ligSets[i].Ligature + for i, inp in enumerate(input): + ligatures[inp] = ligSets[i].Ligature else: assert 0, "unknown format: %s" % self.Format self.ligatures = ligatures @@ -1577,8 +1574,7 @@ class LigatureSubst(FormatSwitchingBaseTable): ligatures = newLigatures items = list(ligatures.items()) - for i in range(len(items)): - glyphName, set = items[i] + for i, (glyphName, set) in enumerate(items): items[i] = font.getGlyphID(glyphName), glyphName, set items.sort() cov = Coverage() @@ -2279,8 +2275,7 @@ def fixLookupOverFlows(ttf, overflowRecord): lookup = lookups[lookupIndex] if lookup.LookupType != extType: lookup.LookupType = extType - for si in range(len(lookup.SubTable)): - subTable = lookup.SubTable[si] + for si, subTable in enumerate(lookup.SubTable): extSubTableClass = lookupTypes[overflowRecord.tableType][extType] extSubTable = extSubTableClass() extSubTable.Format = 1 diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/sbixStrike.py b/contrib/python/fonttools/fontTools/ttLib/tables/sbixStrike.py index 7614af4c7b3..4dfba2e76e8 100644 --- a/contrib/python/fonttools/fontTools/ttLib/tables/sbixStrike.py +++ b/contrib/python/fonttools/fontTools/ttLib/tables/sbixStrike.py @@ -128,9 +128,9 @@ class Strike(object): xmlWriter.simpletag("resolution", value=self.resolution) xmlWriter.newline() glyphOrder = ttFont.getGlyphOrder() - for i in range(len(glyphOrder)): - if glyphOrder[i] in self.glyphs: - self.glyphs[glyphOrder[i]].toXML(xmlWriter, ttFont) + for glyphName in glyphOrder: + if glyphName in self.glyphs: + self.glyphs[glyphName].toXML(xmlWriter, ttFont) # TODO: what if there are more glyph data records than (glyf table) glyphs? xmlWriter.endtag("strike") xmlWriter.newline() diff --git a/contrib/python/fonttools/fontTools/ttLib/ttFont.py b/contrib/python/fonttools/fontTools/ttLib/ttFont.py index 8228ab654e7..2b3338e5784 100644 --- a/contrib/python/fonttools/fontTools/ttLib/ttFont.py +++ b/contrib/python/fonttools/fontTools/ttLib/ttFont.py @@ -259,9 +259,8 @@ class TTFont(object): "head" ] # make sure 'head' is loaded so the recalculation is actually done - tags = list(self.keys()) - if "GlyphOrder" in tags: - tags.remove("GlyphOrder") + tags = self.keys() + tags.pop(0) # skip GlyphOrder tag numTables = len(tags) # write to a temporary stream to allow saving to unseekable streams writer = SFNTWriter( @@ -307,14 +306,9 @@ class TTFont(object): self.disassembleInstructions = disassembleInstructions self.bitmapGlyphDataFormat = bitmapGlyphDataFormat if not tables: - tables = list(self.keys()) - if "GlyphOrder" not in tables: - tables = ["GlyphOrder"] + tables + tables = self.keys() if skipTables: - for tag in skipTables: - if tag in tables: - tables.remove(tag) - numTables = len(tables) + tables = [tag for tag in tables if tag not in skipTables] if writeVersion: from fontTools import version @@ -337,8 +331,7 @@ class TTFont(object): else: path, ext = os.path.splitext(writer.filename) - for i in range(numTables): - tag = tables[i] + for tag in tables: if splitTables: tablePath = path + "." + tagToIdentifier(tag) + ext tableWriter = xmlWriter.XMLWriter( @@ -608,8 +601,7 @@ class TTFont(object): else: reversecmap = {} useCount = {} - for i in range(numGlyphs): - tempName = glyphOrder[i] + for i, tempName in enumerate(glyphOrder): if tempName in reversecmap: # If a font maps both U+0041 LATIN CAPITAL LETTER A and # U+0391 GREEK CAPITAL LETTER ALPHA to the same glyph, @@ -866,8 +858,7 @@ class GlyphOrder(object): "The 'id' attribute is only for humans; " "it is ignored when parsed." ) writer.newline() - for i in range(len(glyphOrder)): - glyphName = glyphOrder[i] + for i, glyphName in enumerate(glyphOrder): writer.simpletag("GlyphID", id=i, name=glyphName) writer.newline() diff --git a/contrib/python/fonttools/fontTools/ttLib/woff2.py b/contrib/python/fonttools/fontTools/ttLib/woff2.py index 03667e834b7..c11aeb2e543 100644 --- a/contrib/python/fonttools/fontTools/ttLib/woff2.py +++ b/contrib/python/fonttools/fontTools/ttLib/woff2.py @@ -394,10 +394,9 @@ class WOFF2Writer(SFNTWriter): def _calcMasterChecksum(self): """Calculate checkSumAdjustment.""" - tags = list(self.tables.keys()) checksums = [] - for i in range(len(tags)): - checksums.append(self.tables[tags[i]].checkSum) + for tag in self.tables.keys(): + checksums.append(self.tables[tag].checkSum) # Create a SFNT directory for checksum calculation purposes self.searchRange, self.entrySelector, self.rangeShift = getSearchRange( @@ -642,10 +641,10 @@ woff2OverlapSimpleBitmapFlag = 0x0001 def getKnownTagIndex(tag): """Return index of 'tag' in woff2KnownTags list. Return 63 if not found.""" - for i in range(len(woff2KnownTags)): - if tag == woff2KnownTags[i]: - return i - return woff2UnknownTagIndex + try: + return woff2KnownTags.index(tag) + except ValueError: + return woff2UnknownTagIndex class WOFF2DirectoryEntry(DirectoryEntry): @@ -747,8 +746,8 @@ class WOFF2LocaTable(getTableClass("loca")): "indexFormat is 0 but local offsets not multiples of 2" ) locations = array.array("H") - for i in range(len(self.locations)): - locations.append(self.locations[i] // 2) + for location in self.locations: + locations.append(location // 2) else: locations = array.array("I", self.locations) if sys.byteorder != "big": @@ -1026,11 +1025,10 @@ class WOFF2GlyfTable(getTableClass("glyf")): lastcomponent = len(glyph.components) - 1 more = 1 haveInstructions = 0 - for i in range(len(glyph.components)): + for i, component in enumerate(glyph.components): if i == lastcomponent: haveInstructions = hasattr(glyph, "program") more = 0 - component = glyph.components[i] self.compositeStream += component.compile(more, haveInstructions, self) if haveInstructions: self._encodeInstructions(glyph) @@ -1078,9 +1076,8 @@ class WOFF2GlyfTable(getTableClass("glyf")): flags = array.array("B") triplets = array.array("B") - for i in range(len(coordinates)): + for i, (x, y) in enumerate(coordinates): onCurve = glyph.flags[i] & _g_l_y_f.flagOnCurve - x, y = coordinates[i] absX = abs(x) absY = abs(y) onCurveBit = 0 if onCurve else 128 diff --git a/contrib/python/fonttools/fontTools/ufoLib/__init__.py b/contrib/python/fonttools/fontTools/ufoLib/__init__.py index 2c5c51d61bd..a6ce1434d9d 100644 --- a/contrib/python/fonttools/fontTools/ufoLib/__init__.py +++ b/contrib/python/fonttools/fontTools/ufoLib/__init__.py @@ -32,30 +32,27 @@ Value conversion functions are available for converting - :func:`.convertFontInfoValueForAttributeFromVersion3ToVersion2` """ -import os -from copy import deepcopy -from os import fsdecode +import enum import logging +import os import zipfile -import enum from collections import OrderedDict -import fs -import fs.base -import fs.subfs -import fs.errors -import fs.copy -import fs.osfs -import fs.zipfs -import fs.tempfs -import fs.tools +from copy import deepcopy +from os import fsdecode + +from fontTools.misc import filesystem as fs from fontTools.misc import plistlib -from fontTools.ufoLib.validators import * -from fontTools.ufoLib.filenames import userNameToFileName from fontTools.ufoLib.converters import convertUFO1OrUFO2KerningToUFO3Kerning from fontTools.ufoLib.errors import UFOLibError -from fontTools.ufoLib.utils import numberTypes, _VersionTupleEnumMixin +from fontTools.ufoLib.filenames import userNameToFileName +from fontTools.ufoLib.utils import _VersionTupleEnumMixin, numberTypes +from fontTools.ufoLib.validators import * + +# client code can check this to see if the upstream `fs` package is being used +haveFS = fs._haveFS __all__ = [ + "haveFS", "makeUFOPath", "UFOLibError", "UFOReader", @@ -184,7 +181,7 @@ class _UFOBaseIO: return self.fs.writebytes(fileName, data) else: - with self.fs.openbin(fileName, mode="w") as fp: + with self.fs.open(fileName, mode="wb") as fp: try: plistlib.dump(obj, fp) except Exception as e: @@ -412,7 +409,7 @@ class UFOReader(_UFOBaseIO): path = fsdecode(path) try: if encoding is None: - return self.fs.openbin(path) + return self.fs.open(path, mode="rb") else: return self.fs.open(path, mode="r", encoding=encoding) except fs.errors.ResourceNotFound: @@ -818,7 +815,7 @@ class UFOReader(_UFOBaseIO): # systems often have hidden directories continue if validate: - with imagesFS.openbin(path.name) as fp: + with imagesFS.open(path.name, "rb") as fp: valid, error = pngValidator(fileObj=fp) if valid: result.append(path.name) @@ -984,18 +981,16 @@ class UFOWriter(UFOReader): % len(rootDirs) ) else: - # 'ClosingSubFS' ensures that the parent filesystem is closed - # when its root subdirectory is closed - self.fs = parentFS.opendir( - rootDirs[0], factory=fs.subfs.ClosingSubFS - ) + rootDir = rootDirs[0] else: # if the output zip file didn't exist, we create the root folder; # we name it the same as input 'path', but with '.ufo' extension rootDir = os.path.splitext(os.path.basename(path))[0] + ".ufo" parentFS = fs.zipfs.ZipFS(path, write=True, encoding="utf-8") parentFS.makedir(rootDir) - self.fs = parentFS.opendir(rootDir, factory=fs.subfs.ClosingSubFS) + # 'ClosingSubFS' ensures that the parent filesystem is closed + # when its root subdirectory is closed + self.fs = parentFS.opendir(rootDir, factory=fs.subfs.ClosingSubFS) else: self.fs = fs.osfs.OSFS(path, create=True) self._fileStructure = structure diff --git a/contrib/python/fonttools/fontTools/ufoLib/glifLib.py b/contrib/python/fonttools/fontTools/ufoLib/glifLib.py index a5a05003ee8..028d38c36b6 100644 --- a/contrib/python/fonttools/fontTools/ufoLib/glifLib.py +++ b/contrib/python/fonttools/fontTools/ufoLib/glifLib.py @@ -3,7 +3,7 @@ Generic module for reading and writing the .glif format. More info about the .glif format (GLyphInterchangeFormat) can be found here: - http://unifiedfontobject.org + http://unifiedfontobject.org The main class in this module is :class:`GlyphSet`. It manages a set of .glif files in a folder. It offers two ways to read glyph data, and one way to write @@ -12,33 +12,28 @@ glyph data. See the class doc string for details. from __future__ import annotations -import logging import enum -from warnings import warn +import logging from collections import OrderedDict -import fs -import fs.base -import fs.errors -import fs.osfs -import fs.path +from warnings import warn + +import fontTools.misc.filesystem as fs +from fontTools.misc import etree, plistlib from fontTools.misc.textTools import tobytes -from fontTools.misc import plistlib from fontTools.pens.pointPen import AbstractPointPen, PointToSegmentPen +from fontTools.ufoLib import UFOFormatVersion, _UFOBaseIO from fontTools.ufoLib.errors import GlifLibError from fontTools.ufoLib.filenames import userNameToFileName +from fontTools.ufoLib.utils import _VersionTupleEnumMixin, numberTypes from fontTools.ufoLib.validators import ( - genericTypeValidator, + anchorsValidator, colorValidator, + genericTypeValidator, + glyphLibValidator, guidelinesValidator, - anchorsValidator, identifierValidator, imageValidator, - glyphLibValidator, ) -from fontTools.misc import etree -from fontTools.ufoLib import _UFOBaseIO, UFOFormatVersion -from fontTools.ufoLib.utils import numberTypes, _VersionTupleEnumMixin - __all__ = [ "GlyphSet", @@ -206,7 +201,7 @@ class GlyphSet(_UFOBaseIO): # 'dirName' is kept for backward compatibility only, but it's DEPRECATED # as it's not guaranteed that it maps to an existing OSFS directory. # Client could use the FS api via the `self.fs` attribute instead. - self.dirName = fs.path.parts(path)[-1] + self.dirName = fs.path.basename(path) self.fs = filesystem # if glyphSet contains no 'contents.plist', we consider it empty self._havePreviousFile = filesystem.exists(CONTENTS_FILENAME) diff --git a/contrib/python/fonttools/fontTools/ufoLib/validators.py b/contrib/python/fonttools/fontTools/ufoLib/validators.py index 01e3124fd38..7d87cc93063 100644 --- a/contrib/python/fonttools/fontTools/ufoLib/validators.py +++ b/contrib/python/fonttools/fontTools/ufoLib/validators.py @@ -1,14 +1,12 @@ """Various low level data validators.""" import calendar +from collections.abc import Mapping from io import open -import fs.base -import fs.osfs -from collections.abc import Mapping +import fontTools.misc.filesystem as fs from fontTools.ufoLib.utils import numberTypes - # ------- # Generic # ------- diff --git a/contrib/python/fonttools/fontTools/unicodedata/__init__.py b/contrib/python/fonttools/fontTools/unicodedata/__init__.py index 1adb07d2896..fb95af0a2f0 100644 --- a/contrib/python/fonttools/fontTools/unicodedata/__init__.py +++ b/contrib/python/fonttools/fontTools/unicodedata/__init__.py @@ -197,6 +197,8 @@ RTL_SCRIPTS = { "Yezi", # Yezidi # Unicode-14.0 additions "Ougr", # Old Uyghur + # Unicode-16.0 additions + "Gara", # Garay } diff --git a/contrib/python/fonttools/fontTools/varLib/hvar.py b/contrib/python/fonttools/fontTools/varLib/hvar.py index 0bdb16c62b8..14b662ebf72 100644 --- a/contrib/python/fonttools/fontTools/varLib/hvar.py +++ b/contrib/python/fonttools/fontTools/varLib/hvar.py @@ -56,7 +56,7 @@ def add_HVAR(font): def add_VVAR(font): if "VVAR" in font: del font["VVAR"] - getAdvanceMetrics = partial(_get_advance_metrics, font, axisTags, HVAR_FIELDS) + getAdvanceMetrics = partial(_get_advance_metrics, font, axisTags, VVAR_FIELDS) axisTags = [axis.axisTag for axis in font["fvar"].axes] _add_VHVAR(font, axisTags, VVAR_FIELDS, getAdvanceMetrics) diff --git a/contrib/python/fonttools/ya.make b/contrib/python/fonttools/ya.make index e114871a154..0632f1b026f 100644 --- a/contrib/python/fonttools/ya.make +++ b/contrib/python/fonttools/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(4.58.5) +VERSION(4.59.0) LICENSE(MIT) @@ -85,6 +85,18 @@ PY_SRCS( fontTools/misc/encodingTools.py fontTools/misc/etree.py fontTools/misc/filenames.py + fontTools/misc/filesystem/__init__.py + fontTools/misc/filesystem/_base.py + fontTools/misc/filesystem/_copy.py + fontTools/misc/filesystem/_errors.py + fontTools/misc/filesystem/_info.py + fontTools/misc/filesystem/_osfs.py + fontTools/misc/filesystem/_path.py + fontTools/misc/filesystem/_subfs.py + fontTools/misc/filesystem/_tempfs.py + fontTools/misc/filesystem/_tools.py + fontTools/misc/filesystem/_walk.py + fontTools/misc/filesystem/_zipfs.py fontTools/misc/fixedTools.py fontTools/misc/intTools.py fontTools/misc/iterTools.py diff --git a/contrib/python/pytest-lazy-fixtures/.dist-info/METADATA b/contrib/python/pytest-lazy-fixtures/.dist-info/METADATA index 94b8a038466..5186092087b 100644 --- a/contrib/python/pytest-lazy-fixtures/.dist-info/METADATA +++ b/contrib/python/pytest-lazy-fixtures/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: pytest-lazy-fixtures -Version: 1.3.0 +Version: 1.3.1 Summary: Allows you to use fixtures in @pytest.mark.parametrize. Project-URL: Homepage, https://github.com/dev-petrov/pytest-lazy-fixtures Project-URL: Repository, https://github.com/dev-petrov/pytest-lazy-fixtures @@ -28,6 +28,7 @@ Improvements that have been made in this project: 1. You can use fixtures in any data structures 2. You can access the attributes of fixtures 3. You can use functions in fixtures +4. It is compatible with [pytest-deadfixtures](https://github.com/jllorencetti/pytest-deadfixtures) ## Installation diff --git a/contrib/python/pytest-lazy-fixtures/README.md b/contrib/python/pytest-lazy-fixtures/README.md index 956c5335754..e77941bd699 100644 --- a/contrib/python/pytest-lazy-fixtures/README.md +++ b/contrib/python/pytest-lazy-fixtures/README.md @@ -14,6 +14,7 @@ Improvements that have been made in this project: 1. You can use fixtures in any data structures 2. You can access the attributes of fixtures 3. You can use functions in fixtures +4. It is compatible with [pytest-deadfixtures](https://github.com/jllorencetti/pytest-deadfixtures) ## Installation diff --git a/contrib/python/pytest-lazy-fixtures/pytest_lazy_fixtures/fixture_collector.py b/contrib/python/pytest-lazy-fixtures/pytest_lazy_fixtures/fixture_collector.py index 47e1d474d99..982176c02db 100644 --- a/contrib/python/pytest-lazy-fixtures/pytest_lazy_fixtures/fixture_collector.py +++ b/contrib/python/pytest-lazy-fixtures/pytest_lazy_fixtures/fixture_collector.py @@ -12,7 +12,7 @@ def collect_fixtures(config: pytest.Config, items: list[pytest.Item]): for marker in item.own_markers: if marker.name != "parametrize": continue - params = marker.args[1] + params = marker.args[1] if len(marker.args) > 1 else marker.kwargs["argvalues"] arg2fixturedefs = {} for param in params: _, _arg2fixturedefs = get_fixturenames_closure_and_arg2fixturedefs(fm, item.parent, param) diff --git a/contrib/python/pytest-lazy-fixtures/ya.make b/contrib/python/pytest-lazy-fixtures/ya.make index cfc750fa27c..bba355ab0f1 100644 --- a/contrib/python/pytest-lazy-fixtures/ya.make +++ b/contrib/python/pytest-lazy-fixtures/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(1.3.0) +VERSION(1.3.1) LICENSE(MIT) |