From a2e8d1519a8a64f92dbc10b33160670915832bd2 Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 12 Sep 2023 15:10:43 -0400 Subject: [PATCH 1/6] Type usages of PIL and zipfile --- stubs/openpyxl/openpyxl/drawing/image.pyi | 21 ++++++++++++++----- .../openpyxl/packaging/relationship.pyi | 16 +++++++++++--- stubs/openpyxl/openpyxl/reader/drawings.pyi | 7 ++++++- stubs/openpyxl/openpyxl/reader/excel.pyi | 7 ++++--- stubs/openpyxl/openpyxl/reader/workbook.pyi | 5 +++-- stubs/openpyxl/openpyxl/styles/stylesheet.pyi | 8 +++++-- .../workbook/external_link/external.pyi | 3 ++- stubs/openpyxl/openpyxl/workbook/workbook.pyi | 3 ++- stubs/openpyxl/openpyxl/writer/excel.pyi | 13 +++++++----- 9 files changed, 60 insertions(+), 23 deletions(-) diff --git a/stubs/openpyxl/openpyxl/drawing/image.pyi b/stubs/openpyxl/openpyxl/drawing/image.pyi index 3864d30df85b..3bd545df3254 100644 --- a/stubs/openpyxl/openpyxl/drawing/image.pyi +++ b/stubs/openpyxl/openpyxl/drawing/image.pyi @@ -1,9 +1,20 @@ -from _typeshed import Incomplete +from _typeshed import SupportsRead +from pathlib import Path +from types import ModuleType +from typing import Any +from typing_extensions import Literal, TypeAlias + +# Is actually PIL.Image.Image +_PILImageImage: TypeAlias = Any +# same as first parameter of PIL.Image.open +_PILImageFilePath: TypeAlias = str | bytes | Path | SupportsRead[bytes] + +PILImage: ModuleType | Literal[False] class Image: anchor: str - ref: Incomplete - format: Incomplete - def __init__(self, img) -> None: ... + ref: _PILImageImage | _PILImageFilePath + format: str + def __init__(self, img: _PILImageImage | _PILImageFilePath) -> None: ... @property - def path(self): ... + def path(self) -> str: ... diff --git a/stubs/openpyxl/openpyxl/packaging/relationship.pyi b/stubs/openpyxl/openpyxl/packaging/relationship.pyi index 09165e7bb179..f3cbb359e4cf 100644 --- a/stubs/openpyxl/openpyxl/packaging/relationship.pyi +++ b/stubs/openpyxl/openpyxl/packaging/relationship.pyi @@ -1,11 +1,14 @@ from _typeshed import Incomplete, Unused from collections.abc import Generator -from typing import ClassVar, overload +from typing import ClassVar, TypeVar, overload from typing_extensions import Literal +from zipfile import ZipFile from openpyxl.descriptors.base import Alias, String from openpyxl.descriptors.serialisable import Serialisable +_SerialisableT = TypeVar("_SerialisableT", bound=Serialisable) + class Relationship(Serialisable): tagname: ClassVar[str] Type: String[Literal[False]] @@ -37,5 +40,12 @@ class RelationshipList(Serialisable): def to_tree(self): ... def get_rels_path(path): ... -def get_dependents(archive, filename): ... -def get_rel(archive, deps, id: Incomplete | None = None, cls: Incomplete | None = None): ... +def get_dependents(archive: ZipFile, filename: str) -> RelationshipList: ... +@overload +def get_rel( + archive: ZipFile, deps: RelationshipList, id: str, cls: type[_SerialisableT] +) -> _SerialisableT: ... # incomplete: this could be restricted further from "Serialisable" +@overload +def get_rel( + archive: ZipFile, deps: RelationshipList, id: str | None = None, *, cls: type[_SerialisableT] +) -> _SerialisableT: ... # incomplete: this could be restricted further from "Serialisable" diff --git a/stubs/openpyxl/openpyxl/reader/drawings.pyi b/stubs/openpyxl/openpyxl/reader/drawings.pyi index 4334877fc2b0..f2bf8cd0863f 100644 --- a/stubs/openpyxl/openpyxl/reader/drawings.pyi +++ b/stubs/openpyxl/openpyxl/reader/drawings.pyi @@ -1 +1,6 @@ -def find_images(archive, path): ... +from zipfile import ZipFile + +from openpyxl.chart._chart import ChartBase +from openpyxl.drawing.image import Image + +def find_images(archive: ZipFile, path: str) -> tuple[list[ChartBase], list[Image]]: ... diff --git a/stubs/openpyxl/openpyxl/reader/excel.pyi b/stubs/openpyxl/openpyxl/reader/excel.pyi index 7464059aa03a..894b397a972e 100644 --- a/stubs/openpyxl/openpyxl/reader/excel.pyi +++ b/stubs/openpyxl/openpyxl/reader/excel.pyi @@ -1,4 +1,5 @@ -from _typeshed import Incomplete, StrPath, SupportsRead +from _typeshed import Incomplete, StrPath +from typing import IO from typing_extensions import Final, Literal, TypeAlias from zipfile import ZipFile @@ -26,7 +27,7 @@ class ExcelReader: def __init__( self, - fn: SupportsRead[bytes] | str, + fn: StrPath | IO[bytes], read_only: bool = False, keep_vba: bool = False, data_only: bool = False, @@ -44,7 +45,7 @@ class ExcelReader: def read(self) -> None: ... def load_workbook( - filename: SupportsRead[bytes] | StrPath, + filename: StrPath | IO[bytes], read_only: bool = False, keep_vba: bool = False, data_only: bool = False, diff --git a/stubs/openpyxl/openpyxl/reader/workbook.pyi b/stubs/openpyxl/openpyxl/reader/workbook.pyi index 7ef1b85e4c09..8e6e5c3d8d5e 100644 --- a/stubs/openpyxl/openpyxl/reader/workbook.pyi +++ b/stubs/openpyxl/openpyxl/reader/workbook.pyi @@ -1,15 +1,16 @@ from _typeshed import Incomplete from collections.abc import Generator +from zipfile import ZipFile from openpyxl.workbook import Workbook class WorkbookParser: - archive: Incomplete + archive: ZipFile workbook_part_name: Incomplete wb: Workbook keep_links: Incomplete sheets: Incomplete - def __init__(self, archive, workbook_part_name, keep_links: bool = True) -> None: ... + def __init__(self, archive: ZipFile, workbook_part_name, keep_links: bool = True) -> None: ... @property def rels(self): ... caches: Incomplete diff --git a/stubs/openpyxl/openpyxl/styles/stylesheet.pyi b/stubs/openpyxl/openpyxl/styles/stylesheet.pyi index b3c1465ad6ad..3141a3c2b944 100644 --- a/stubs/openpyxl/openpyxl/styles/stylesheet.pyi +++ b/stubs/openpyxl/openpyxl/styles/stylesheet.pyi @@ -1,6 +1,7 @@ from _typeshed import Incomplete, Unused -from typing import ClassVar +from typing import ClassVar, TypeVar from typing_extensions import Literal, Self +from zipfile import ZipFile from openpyxl.descriptors.base import Typed from openpyxl.descriptors.excel import ExtensionList @@ -10,6 +11,9 @@ from openpyxl.styles.colors import ColorList from openpyxl.styles.named_styles import _NamedCellStyleList from openpyxl.styles.numbers import NumberFormatList from openpyxl.styles.table import TableStyleList +from openpyxl.workbook.workbook import Workbook + +_WorkbookT = TypeVar("_WorkbookT", bound=Workbook) class Stylesheet(Serialisable): tagname: ClassVar[str] @@ -50,5 +54,5 @@ class Stylesheet(Serialisable): def custom_formats(self): ... def to_tree(self, tagname: str | None = None, idx: Incomplete | None = None, namespace: str | None = None): ... -def apply_stylesheet(archive, wb): ... +def apply_stylesheet(archive: ZipFile, wb: _WorkbookT) -> _WorkbookT | None: ... def write_stylesheet(wb): ... diff --git a/stubs/openpyxl/openpyxl/workbook/external_link/external.pyi b/stubs/openpyxl/openpyxl/workbook/external_link/external.pyi index fcd66e68bdff..ad7cdcca4def 100644 --- a/stubs/openpyxl/openpyxl/workbook/external_link/external.pyi +++ b/stubs/openpyxl/openpyxl/workbook/external_link/external.pyi @@ -1,6 +1,7 @@ from _typeshed import Incomplete, Unused from typing import ClassVar from typing_extensions import Literal, TypeAlias +from zipfile import ZipFile from openpyxl.descriptors.base import Bool, Integer, NoneSet, String, Typed, _ConvertibleToBool, _ConvertibleToInt from openpyxl.descriptors.nested import NestedText @@ -76,4 +77,4 @@ class ExternalLink(Serialisable): @property def path(self): ... -def read_external_link(archive, book_path): ... +def read_external_link(archive: ZipFile, book_path: str) -> ExternalLink: ... diff --git a/stubs/openpyxl/openpyxl/workbook/workbook.pyi b/stubs/openpyxl/openpyxl/workbook/workbook.pyi index 831e57d106a4..cf953d48f346 100644 --- a/stubs/openpyxl/openpyxl/workbook/workbook.pyi +++ b/stubs/openpyxl/openpyxl/workbook/workbook.pyi @@ -3,6 +3,7 @@ from collections.abc import Iterator from datetime import datetime from typing import IO from typing_extensions import Final +from zipfile import ZipFile from openpyxl import _Decodable from openpyxl.chartsheet.chartsheet import Chartsheet @@ -21,7 +22,7 @@ class Workbook: security: Incomplete shared_strings: Incomplete loaded_theme: Incomplete - vba_archive: Incomplete + vba_archive: ZipFile | None is_template: bool code_name: Incomplete encoding: str diff --git a/stubs/openpyxl/openpyxl/writer/excel.pyi b/stubs/openpyxl/openpyxl/writer/excel.pyi index 624a3c4c05bd..0565c778e89b 100644 --- a/stubs/openpyxl/openpyxl/writer/excel.pyi +++ b/stubs/openpyxl/openpyxl/writer/excel.pyi @@ -1,10 +1,13 @@ -from _typeshed import Incomplete +from zipfile import ZipFile + +from openpyxl.packaging.manifest import Manifest +from openpyxl.workbook.workbook import Workbook class ExcelWriter: - workbook: Incomplete - manifest: Incomplete - vba_modified: Incomplete - def __init__(self, workbook, archive) -> None: ... + workbook: Workbook + manifest: Manifest + vba_modified: set[str | None] + def __init__(self, workbook: Workbook, archive: ZipFile) -> None: ... def write_data(self) -> None: ... def write_worksheet(self, ws) -> None: ... def save(self) -> None: ... From f08c6f1ace14ab82ef61855677553f9322307ab1 Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 12 Sep 2023 15:38:16 -0400 Subject: [PATCH 2/6] Update stubtest_allowlist --- stubs/openpyxl/@tests/stubtest_allowlist.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/stubs/openpyxl/@tests/stubtest_allowlist.txt b/stubs/openpyxl/@tests/stubtest_allowlist.txt index f40d00628006..714149bfd477 100644 --- a/stubs/openpyxl/@tests/stubtest_allowlist.txt +++ b/stubs/openpyxl/@tests/stubtest_allowlist.txt @@ -41,8 +41,9 @@ openpyxl\.descriptors\.(base\.)?MinMax\.allow_none openpyxl\.descriptors\.(base\.)?String\.allow_none openpyxl\.descriptors\.(base\.)?Typed\.allow_none -# "has a default value but stub argument does not" -# Runtime has default arguments that would fail +# Inconsistent methods because +# - using the default value results in an error because of the runtime type-guards +# - or, keyword arguments are explicitly specified openpyxl.cell.text.PhoneticProperties.__init__ openpyxl.cell.text.PhoneticText.__init__ openpyxl.chart.axis._BaseAxis.__init__ @@ -109,6 +110,7 @@ openpyxl.drawing.text.GeomGuide.__init__ openpyxl.drawing.text.PresetTextShape.__init__ openpyxl.drawing.text.TextField.__init__ openpyxl.drawing.text.TextNormalAutofit.__init__ +openpyxl.packaging.relationship.get_rel openpyxl.packaging.relationship.Relationship.__init__ openpyxl.packaging.workbook.ChildSheet.__init__ openpyxl.packaging.workbook.PivotCache.__init__ From 29c92189da99bb8c18c8c02727fdd78c0b4e9bb6 Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 12 Sep 2023 15:59:58 -0400 Subject: [PATCH 3/6] Update `get_rel` --- .../openpyxl/packaging/relationship.pyi | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/stubs/openpyxl/openpyxl/packaging/relationship.pyi b/stubs/openpyxl/openpyxl/packaging/relationship.pyi index f3cbb359e4cf..8a0aea2f71e3 100644 --- a/stubs/openpyxl/openpyxl/packaging/relationship.pyi +++ b/stubs/openpyxl/openpyxl/packaging/relationship.pyi @@ -6,8 +6,12 @@ from zipfile import ZipFile from openpyxl.descriptors.base import Alias, String from openpyxl.descriptors.serialisable import Serialisable +from openpyxl.pivot.cache import CacheDefinition +from openpyxl.pivot.record import RecordList +from openpyxl.pivot.table import TableDefinition _SerialisableT = TypeVar("_SerialisableT", bound=Serialisable) +_SerialisableRelTypeT = TypeVar("_SerialisableRelTypeT", bound=CacheDefinition | RecordList | TableDefinition) class Relationship(Serialisable): tagname: ClassVar[str] @@ -41,11 +45,19 @@ class RelationshipList(Serialisable): def get_rels_path(path): ... def get_dependents(archive: ZipFile, filename: str) -> RelationshipList: ... + +# If `id` is None, `cls` needs to have ClassVar `rel_type`. +# The `deps` attribute used at runtime is for internal use immediatly after the return. +# `cls` cannot be None @overload def get_rel( - archive: ZipFile, deps: RelationshipList, id: str, cls: type[_SerialisableT] -) -> _SerialisableT: ... # incomplete: this could be restricted further from "Serialisable" + archive: ZipFile, deps: RelationshipList, id: None = None, *, cls: type[_SerialisableRelTypeT] +) -> _SerialisableRelTypeT | None: ... @overload def get_rel( - archive: ZipFile, deps: RelationshipList, id: str | None = None, *, cls: type[_SerialisableT] -) -> _SerialisableT: ... # incomplete: this could be restricted further from "Serialisable" + archive: ZipFile, deps: RelationshipList, id: None, cls: type[_SerialisableRelTypeT] +) -> _SerialisableRelTypeT | None: ... +@overload +def get_rel(archive: ZipFile, deps: RelationshipList, id: str, *, cls: type[_SerialisableT]) -> _SerialisableT: ... +@overload +def get_rel(archive: ZipFile, deps: RelationshipList, id: str, cls: type[_SerialisableT]) -> _SerialisableT: ... From 52d38ffc94e5e46ccba0318ee8cc30d80d099daa Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 19 Sep 2023 16:55:16 -0400 Subject: [PATCH 4/6] Missed writer.excel.save_workbook --- stubs/openpyxl/openpyxl/writer/excel.pyi | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/stubs/openpyxl/openpyxl/writer/excel.pyi b/stubs/openpyxl/openpyxl/writer/excel.pyi index 0565c778e89b..e3c04910fb04 100644 --- a/stubs/openpyxl/openpyxl/writer/excel.pyi +++ b/stubs/openpyxl/openpyxl/writer/excel.pyi @@ -1,3 +1,6 @@ +from _typeshed import StrPath +from typing import IO +from typing_extensions import Literal from zipfile import ZipFile from openpyxl.packaging.manifest import Manifest @@ -12,4 +15,4 @@ class ExcelWriter: def write_worksheet(self, ws) -> None: ... def save(self) -> None: ... -def save_workbook(workbook, filename): ... +def save_workbook(workbook: Workbook, filename: StrPath | IO[bytes]) -> Literal[True]: ... From 2b7175963b4969191bc4d704b1cd6dceb6948b64 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 23 Sep 2023 00:30:53 -0400 Subject: [PATCH 5/6] Remove redundant get_rel overload --- stubs/openpyxl/openpyxl/packaging/relationship.pyi | 2 -- 1 file changed, 2 deletions(-) diff --git a/stubs/openpyxl/openpyxl/packaging/relationship.pyi b/stubs/openpyxl/openpyxl/packaging/relationship.pyi index 8a0aea2f71e3..4c56bfb4d3eb 100644 --- a/stubs/openpyxl/openpyxl/packaging/relationship.pyi +++ b/stubs/openpyxl/openpyxl/packaging/relationship.pyi @@ -58,6 +58,4 @@ def get_rel( archive: ZipFile, deps: RelationshipList, id: None, cls: type[_SerialisableRelTypeT] ) -> _SerialisableRelTypeT | None: ... @overload -def get_rel(archive: ZipFile, deps: RelationshipList, id: str, *, cls: type[_SerialisableT]) -> _SerialisableT: ... -@overload def get_rel(archive: ZipFile, deps: RelationshipList, id: str, cls: type[_SerialisableT]) -> _SerialisableT: ... From 0e5c5624543e49b564a85c1d195b24b733704399 Mon Sep 17 00:00:00 2001 From: Avasam Date: Fri, 13 Oct 2023 15:46:22 -0400 Subject: [PATCH 6/6] Alias to minimize false-positives --- stubs/openpyxl/openpyxl/__init__.pyi | 7 ++++++- stubs/openpyxl/openpyxl/reader/excel.pyi | 8 ++++---- stubs/openpyxl/openpyxl/workbook/workbook.pyi | 7 +++---- stubs/openpyxl/openpyxl/writer/excel.pyi | 5 ++--- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/stubs/openpyxl/openpyxl/__init__.pyi b/stubs/openpyxl/openpyxl/__init__.pyi index fa930c9cf276..2b1c6fc90be5 100644 --- a/stubs/openpyxl/openpyxl/__init__.pyi +++ b/stubs/openpyxl/openpyxl/__init__.pyi @@ -1,4 +1,5 @@ -from typing import Protocol +from _typeshed import StrPath, SupportsRead +from typing import IO, Protocol from typing_extensions import Literal, TypeAlias from openpyxl.compat.numbers import NUMPY as NUMPY @@ -21,5 +22,9 @@ open = load_workbook # Utility types reused elsewhere _VisibilityType: TypeAlias = Literal["visible", "hidden", "veryHidden"] # noqa: Y047 +# TODO: Use a proper protocol from ZipFile. See: #10880 +# This alias is to minimize false-positives +_ZipFileFileProtocol: TypeAlias = StrPath | IO[bytes] | SupportsRead[bytes] # noqa: Y047 + class _Decodable(Protocol): # noqa: Y046 def decode(self, __encoding: str) -> str: ... diff --git a/stubs/openpyxl/openpyxl/reader/excel.pyi b/stubs/openpyxl/openpyxl/reader/excel.pyi index 894b397a972e..4d707269683b 100644 --- a/stubs/openpyxl/openpyxl/reader/excel.pyi +++ b/stubs/openpyxl/openpyxl/reader/excel.pyi @@ -1,8 +1,8 @@ -from _typeshed import Incomplete, StrPath -from typing import IO +from _typeshed import Incomplete from typing_extensions import Final, Literal, TypeAlias from zipfile import ZipFile +from openpyxl import _ZipFileFileProtocol from openpyxl.chartsheet.chartsheet import Chartsheet from openpyxl.packaging.manifest import Manifest from openpyxl.packaging.relationship import Relationship @@ -27,7 +27,7 @@ class ExcelReader: def __init__( self, - fn: StrPath | IO[bytes], + fn: _ZipFileFileProtocol, read_only: bool = False, keep_vba: bool = False, data_only: bool = False, @@ -45,7 +45,7 @@ class ExcelReader: def read(self) -> None: ... def load_workbook( - filename: StrPath | IO[bytes], + filename: _ZipFileFileProtocol, read_only: bool = False, keep_vba: bool = False, data_only: bool = False, diff --git a/stubs/openpyxl/openpyxl/workbook/workbook.pyi b/stubs/openpyxl/openpyxl/workbook/workbook.pyi index cf953d48f346..d418f46a5f66 100644 --- a/stubs/openpyxl/openpyxl/workbook/workbook.pyi +++ b/stubs/openpyxl/openpyxl/workbook/workbook.pyi @@ -1,11 +1,10 @@ -from _typeshed import Incomplete, StrPath +from _typeshed import Incomplete from collections.abc import Iterator from datetime import datetime -from typing import IO from typing_extensions import Final from zipfile import ZipFile -from openpyxl import _Decodable +from openpyxl import _Decodable, _ZipFileFileProtocol from openpyxl.chartsheet.chartsheet import Chartsheet from openpyxl.styles.named_styles import NamedStyle from openpyxl.workbook.child import _WorkbookChild @@ -78,7 +77,7 @@ class Workbook: def named_styles(self) -> list[str]: ... @property def mime_type(self) -> str: ... - def save(self, filename: StrPath | IO[bytes]) -> None: ... + def save(self, filename: _ZipFileFileProtocol) -> None: ... @property def style_names(self) -> list[str]: ... def copy_worksheet(self, from_worksheet: Worksheet) -> Worksheet | WriteOnlyWorksheet: ... diff --git a/stubs/openpyxl/openpyxl/writer/excel.pyi b/stubs/openpyxl/openpyxl/writer/excel.pyi index e3c04910fb04..059baeb022e7 100644 --- a/stubs/openpyxl/openpyxl/writer/excel.pyi +++ b/stubs/openpyxl/openpyxl/writer/excel.pyi @@ -1,8 +1,7 @@ -from _typeshed import StrPath -from typing import IO from typing_extensions import Literal from zipfile import ZipFile +from openpyxl import _ZipFileFileProtocol from openpyxl.packaging.manifest import Manifest from openpyxl.workbook.workbook import Workbook @@ -15,4 +14,4 @@ class ExcelWriter: def write_worksheet(self, ws) -> None: ... def save(self) -> None: ... -def save_workbook(workbook: Workbook, filename: StrPath | IO[bytes]) -> Literal[True]: ... +def save_workbook(workbook: Workbook, filename: _ZipFileFileProtocol) -> Literal[True]: ...