From 32216b8dba2e64e3b6d3627fcc8b1512d5e052e5 Mon Sep 17 00:00:00 2001 From: Maxwell G Date: Fri, 3 Nov 2023 22:27:34 +0000 Subject: [PATCH] Add type arguments to UserList subclasses By having these classes inherit from the generics in the typing module, type checkers will be able to determine the return types of the dunder methods inherited from UserList. --- specfile/changelog.py | 7 +++---- specfile/conditions.py | 7 ++++--- specfile/macro_definitions.py | 6 +++--- specfile/prep.py | 6 +++--- specfile/sections.py | 3 ++- specfile/sourcelist.py | 4 ++-- specfile/sources.py | 2 +- specfile/tags.py | 7 +++---- specfile/utils.py | 13 ++++++++++++- 9 files changed, 33 insertions(+), 22 deletions(-) diff --git a/specfile/changelog.py b/specfile/changelog.py index 4728ac6..990374d 100644 --- a/specfile/changelog.py +++ b/specfile/changelog.py @@ -1,7 +1,6 @@ # Copyright Contributors to the Packit project. # SPDX-License-Identifier: MIT -import collections import copy import datetime import getpass @@ -19,7 +18,7 @@ from specfile.macros import Macros from specfile.sections import Section from specfile.types import SupportsIndex -from specfile.utils import EVR +from specfile.utils import EVR, UserList WEEKDAYS = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") MONTHS = ( @@ -199,7 +198,7 @@ def assemble( return cls(header, content, [""] if append_newline else None) -class Changelog(collections.UserList): +class Changelog(UserList[ChangelogEntry]): """ Class that represents a changelog. @@ -326,7 +325,7 @@ def parse(cls, section: Section) -> "Changelog": Constructed instance of `Changelog` class. """ - def extract_following_lines(content): + def extract_following_lines(content: List[str]) -> List[str]: following_lines: List[str] = [] while content and not content[-1].strip(): following_lines.insert(0, content.pop()) diff --git a/specfile/conditions.py b/specfile/conditions.py index 3063c5f..2e42da3 100644 --- a/specfile/conditions.py +++ b/specfile/conditions.py @@ -79,9 +79,10 @@ def process_conditions( List of tuples in the form of (line, validity). """ excluded_lines = [] - for md in macro_definitions or []: - position = md.get_position(macro_definitions) - excluded_lines.append(range(position, position + len(md.body.splitlines()))) + if macro_definitions: + for md in macro_definitions: + position = md.get_position(macro_definitions) + excluded_lines.append(range(position, position + len(md.body.splitlines()))) condition_regex = re.compile( r""" ^ diff --git a/specfile/macro_definitions.py b/specfile/macro_definitions.py index 0d70dc5..df3db0f 100644 --- a/specfile/macro_definitions.py +++ b/specfile/macro_definitions.py @@ -1,7 +1,6 @@ # Copyright Contributors to the Packit project. # SPDX-License-Identifier: MIT -import collections import copy import re from enum import Enum, auto @@ -10,6 +9,7 @@ from specfile.conditions import process_conditions from specfile.formatter import formatted from specfile.types import SupportsIndex +from specfile.utils import UserList if TYPE_CHECKING: from specfile.specfile import Specfile @@ -114,7 +114,7 @@ def get_raw_data(self) -> List[str]: return result -class MacroDefinitions(collections.UserList): +class MacroDefinitions(UserList[MacroDefinition]): """ Class that represents all macro definitions. @@ -162,7 +162,7 @@ def __getattr__(self, name: str) -> MacroDefinition: except ValueError: raise AttributeError(name) - def __setattr__(self, name: str, value: Union[MacroDefinition, List[str]]) -> None: + def __setattr__(self, name: str, value: Union[MacroDefinition, str]) -> None: if name not in self: return super().__setattr__(name, value) try: diff --git a/specfile/prep.py b/specfile/prep.py index 0eb30f6..2299aee 100644 --- a/specfile/prep.py +++ b/specfile/prep.py @@ -11,7 +11,7 @@ from specfile.options import Options from specfile.sections import Section from specfile.types import SupportsIndex -from specfile.utils import split_conditional_macro_expansion +from specfile.utils import UserList, split_conditional_macro_expansion def valid_prep_macro(name: str) -> bool: @@ -149,7 +149,7 @@ class AutopatchMacro(PrepMacro): DEFAULTS: Dict[str, Union[bool, int, str]] = {} -class PrepMacros(collections.UserList): +class PrepMacros(UserList[PrepMacro]): """ Class that represents a list of %prep macros. @@ -185,7 +185,7 @@ def __contains__(self, item: object) -> bool: if isinstance(item, type): return any(isinstance(m, item) for m in self.data) return any( - m.name.startswith(item) if item == "%patch" else m.name == item + m.name.startswith(cast(str, item)) if item == "%patch" else m.name == item for m in self.data ) diff --git a/specfile/sections.py b/specfile/sections.py index d8020cc..ab40c21 100644 --- a/specfile/sections.py +++ b/specfile/sections.py @@ -18,6 +18,7 @@ from specfile.macros import Macros from specfile.options import Options from specfile.types import SupportsIndex +from specfile.utils import UserList if TYPE_CHECKING: from specfile.specfile import Specfile @@ -139,7 +140,7 @@ def get_raw_data(self) -> List[str]: return str(self).splitlines() -class Sections(collections.UserList): +class Sections(UserList[Section]): """ Class that represents all spec file sections, hence the entire spec file. diff --git a/specfile/sourcelist.py b/specfile/sourcelist.py index 97b8113..254dbbf 100644 --- a/specfile/sourcelist.py +++ b/specfile/sourcelist.py @@ -1,7 +1,6 @@ # Copyright Contributors to the Packit project. # SPDX-License-Identifier: MIT -import collections import copy from typing import TYPE_CHECKING, Any, Dict, List, Optional, overload @@ -12,6 +11,7 @@ from specfile.sections import Section from specfile.tags import Comments from specfile.types import SupportsIndex +from specfile.utils import UserList if TYPE_CHECKING: from specfile.specfile import Specfile @@ -81,7 +81,7 @@ def expanded_location(self) -> str: return Macros.expand(self.location) -class Sourcelist(collections.UserList): +class Sourcelist(UserList[SourcelistEntry]): """ Class that represents entries in a %sourcelist/%patchlist section. diff --git a/specfile/sources.py b/specfile/sources.py index 34b6675..11b5091 100644 --- a/specfile/sources.py +++ b/specfile/sources.py @@ -539,7 +539,7 @@ def insert(self, i: int, location: str) -> None: valid = self._get_tag_validity(cast(TagSource, source)) container.insert( index, - Tag( + Tag( # type: ignore[arg-type] name, location, separator, diff --git a/specfile/tags.py b/specfile/tags.py index 0b2ba27..5c2f54b 100644 --- a/specfile/tags.py +++ b/specfile/tags.py @@ -1,7 +1,6 @@ # Copyright Contributors to the Packit project. # SPDX-License-Identifier: MIT -import collections import copy import itertools import re @@ -24,7 +23,7 @@ from specfile.macros import Macros from specfile.sections import Section from specfile.types import SupportsIndex -from specfile.utils import split_conditional_macro_expansion +from specfile.utils import UserList, split_conditional_macro_expansion if TYPE_CHECKING: from specfile.specfile import Specfile @@ -66,7 +65,7 @@ def __repr__(self) -> str: return f"Comment({self.text!r}, {self.prefix!r})" -class Comments(collections.UserList): +class Comments(UserList[Comment]): """ Class that represents comments associated with a tag, that is consecutive comment lines located directly above a tag definition. @@ -312,7 +311,7 @@ def get_position(self, container: "Tags") -> int: ) + len(self.comments.get_raw_data()) -class Tags(collections.UserList): +class Tags(UserList[Tag]): """ Class that represents all tags in a certain %package section. diff --git a/specfile/utils.py b/specfile/utils.py index a42e95b..e6ae0db 100644 --- a/specfile/utils.py +++ b/specfile/utils.py @@ -3,7 +3,8 @@ import collections import re -from typing import Tuple +import sys +from typing import TYPE_CHECKING, Tuple from specfile.constants import ARCH_NAMES from specfile.exceptions import SpecfileException, UnterminatedMacroException @@ -160,3 +161,13 @@ def split_conditional_macro_expansion(value: str) -> Tuple[str, str, str]: if not isinstance(node, ConditionalMacroExpansion): return value, "", "" return "".join(str(n) for n in node.body), f"%{{{node.prefix}{node.name}:", "}" + + +# Python 3.6-3.8 do not allow creating a generic UserList at runtime. +# This hack allows type checkers to determine the UserList dunder method return +# types while still working at runtime. +if TYPE_CHECKING or sys.version_info >= (3, 9): + UserList = collections.UserList +else: + # UserList[...] always returns a UserList + UserList = collections.defaultdict(lambda: collections.UserList)