diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 23e19f9..115e86b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,7 +3,7 @@ updates: - package-ecosystem: "pip" directory: "/" schedule: - interval: "weekly" + interval: "monthly" assignees: - "bkircher" ignore: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 779855d..1113418 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,6 +38,19 @@ jobs: run: | mypy . + - name: Run basedpyright + run: | + python -m basedpyright + - name: Run linter run: | pylint --rcfile=.pylintrc pyrpm + + check-spelling: + runs-on: ubuntu-latest + name: Spell check source files + steps: + - uses: actions/checkout@v5 + + - name: Check for spelling mistakes + uses: crate-ci/typos@v1.36.2 diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000..b499caf --- /dev/null +++ b/.typos.toml @@ -0,0 +1,4 @@ +# .typos.toml (https://github.com/crate-ci/typos) + +[files] +extend-exclude = ["tests/**/*.spec"] diff --git a/CHANGELOG.md b/CHANGELOG.md index 6029716..55fdc59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 0.x.y (YYYY-MM-DD) +- Clean up typing; add basedpyright to CI. +- Fix a possible endless loop with recursive macro expansion. - Add support for Python 3.14. - Drop support for Python 3.9 version. diff --git a/pyproject.toml b/pyproject.toml index 2faed26..8d4948d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,3 +14,7 @@ description-file = "README.md" [tool.ruff] target-version = "py313" line-length = 130 + +[tool.basedpyright] +reportExplicitAny = false +reportUnreachable = false diff --git a/pyrpm/spec.py b/pyrpm/spec.py index ace0d4d..9b1ee63 100644 --- a/pyrpm/spec.py +++ b/pyrpm/spec.py @@ -4,13 +4,30 @@ """ +from __future__ import annotations + import os import re +import sys from warnings import warn from abc import ABCMeta, abstractmethod -from typing import Any, AnyStr, Dict, List, Optional, Union, Tuple, Type, cast +from typing import TYPE_CHECKING, Any, Callable, ClassVar, TypeVar, cast + +if TYPE_CHECKING: + from typing_extensions import override +elif sys.version_info >= (3, 12): + from typing import override +else: + try: + from typing_extensions import override + except ImportError: + F = TypeVar("F", bound=Callable[..., Any]) + + def override(func: F, /) -> F: + return func + -__all__ = ["Spec", "replace_macros", "Package", "warnings_enabled"] +__all__: list[str] = ["Spec", "replace_macros", "Package", "warnings_enabled"] # Set this to True if you want the library to issue warnings during parsing. @@ -18,15 +35,21 @@ class _Tag(metaclass=ABCMeta): - def __init__(self, name: str, pattern_obj: re.Pattern, attr_type: Type[Any]) -> None: + name: str + pattern_obj: re.Pattern[str] + attr_type: type[Any] + + def __init__(self, name: str, pattern_obj: re.Pattern[str], attr_type: type[Any]) -> None: self.name = name self.pattern_obj = pattern_obj self.attr_type = attr_type - def test(self, line: str) -> Optional[re.Match]: + def test(self, line: str) -> re.Match[str] | None: return re.search(self.pattern_obj, line) - def update(self, spec_obj: "Spec", context: Dict[str, Any], match_obj: re.Match, line: str) -> Any: + def update( + self, spec_obj: Spec, context: dict[str, Any], match_obj: re.Match[str], line: str + ) -> tuple["Spec", dict[str, Any]]: """Update given spec object and parse context and return them again. :param spec_obj: An instance of Spec class @@ -44,28 +67,34 @@ def update(self, spec_obj: "Spec", context: Dict[str, Any], match_obj: re.Match, return self.update_impl(spec_obj, context, match_obj, line) @abstractmethod - def update_impl(self, spec_obj: "Spec", context: Dict[str, Any], match_obj: re.Match, line: str) -> Tuple["Spec", dict]: + def update_impl( + self, spec_obj: Spec, context: dict[str, Any], match_obj: re.Match[str], line: str + ) -> tuple[Spec, dict[str, Any]]: pass @staticmethod - def current_target(spec_obj: "Spec", context: Dict[str, Any]) -> Union["Spec", "Package"]: - target_obj = spec_obj - if context["current_subpackage"] is not None: - target_obj = context["current_subpackage"] + def current_target(spec_obj: Spec, context: dict[str, Any]) -> Spec | Package: + target_obj: Spec | Package = spec_obj + current_subpackage = context.get("current_subpackage") + if isinstance(current_subpackage, Package): + target_obj = current_subpackage return target_obj class _NameValue(_Tag): """Parse a simple name → value tag.""" - def __init__(self, name: str, pattern_obj: re.Pattern, attr_type: Optional[Type[Any]] = None) -> None: - super().__init__(name, pattern_obj, cast(Type[Any], attr_type if attr_type else str)) + def __init__(self, name: str, pattern_obj: re.Pattern[str], attr_type: type[Any] | None = None) -> None: + super().__init__(name, pattern_obj, cast(type[Any], attr_type if attr_type else str)) - def update_impl(self, spec_obj: "Spec", context: Dict[str, Any], match_obj: re.Match, line: str) -> Tuple["Spec", dict]: + @override + def update_impl( + self, spec_obj: Spec, context: dict[str, Any], match_obj: re.Match[str], line: str + ) -> tuple[Spec, dict[str, Any]]: if self.name == "changelog": context["current_subpackage"] = None - target_obj = _Tag.current_target(spec_obj, context) + target_obj: Spec | Package = _Tag.current_target(spec_obj, context) value = match_obj.group(1) # Sub-packages @@ -81,43 +110,16 @@ def update_impl(self, spec_obj: "Spec", context: Dict[str, Any], match_obj: re.M return spec_obj, context -class _SetterMacroDef(_Tag): - """Parse global macro definitions.""" - - def __init__(self, name: str, pattern_obj: re.Pattern) -> None: - super().__init__(name, pattern_obj, str) - - @abstractmethod - def get_namespace(self, spec_obj: "Spec", context: Dict[str, Any]) -> "Spec": - raise NotImplementedError() - - def update_impl(self, spec_obj: "Spec", context: Dict[str, Any], match_obj: re.Match, line: str) -> Tuple["Spec", dict]: - name, value = match_obj.groups() - setattr(self.get_namespace(spec_obj, context), name, str(value)) - return spec_obj, context - - -class _GlobalMacroDef(_SetterMacroDef): - """Parse global macro definitions.""" - - def get_namespace(self, spec_obj: "Spec", context: Dict[str, Any]) -> "Spec": - return spec_obj - - -class _LocalMacroDef(_SetterMacroDef): - """Parse define macro definitions.""" - - def get_namespace(self, spec_obj: "Spec", context: Dict[str, Any]) -> "Spec": - return context["current_subpackage"] - - class _MacroDef(_Tag): """Parse global macro definitions.""" - def __init__(self, name: str, pattern_obj: re.Pattern) -> None: + def __init__(self, name: str, pattern_obj: re.Pattern[str]) -> None: super().__init__(name, pattern_obj, str) - def update_impl(self, spec_obj: "Spec", context: Dict[str, Any], match_obj: re.Match, line: str) -> Tuple["Spec", dict]: + @override + def update_impl( + self, spec_obj: Spec, context: dict[str, Any], match_obj: re.Match[str], line: str + ) -> tuple[Spec, dict[str, Any]]: name, value = match_obj.groups() raw_value = str(value) stored_value = raw_value @@ -143,11 +145,14 @@ def _macro_references_itself(raw_value: str, macro_name: str) -> bool: class _List(_Tag): """Parse a tag that expands to a list.""" - def __init__(self, name: str, pattern_obj: re.Pattern) -> None: + def __init__(self, name: str, pattern_obj: re.Pattern[str]) -> None: super().__init__(name, pattern_obj, list) - def update_impl(self, spec_obj: "Spec", context: Dict[str, Any], match_obj: re.Match, line: str) -> Tuple["Spec", dict]: - target_obj = _Tag.current_target(spec_obj, context) + @override + def update_impl( + self, spec_obj: Spec, context: dict[str, Any], match_obj: re.Match[str], line: str + ) -> tuple[Spec, dict[str, Any]]: + target_obj: Spec | Package = _Tag.current_target(spec_obj, context) if not hasattr(target_obj, self.name): setattr(target_obj, self.name, []) @@ -162,7 +167,8 @@ def update_impl(self, spec_obj: "Spec", context: Dict[str, Any], match_obj: re.M context["current_subpackage"] = package package.is_subpackage = True spec_obj.packages.append(package) - elif self.name in [ + return spec_obj, context + if self.name in [ "build_requires", "requires", "conflicts", @@ -179,7 +185,7 @@ def update_impl(self, spec_obj: "Spec", context: Dict[str, Any], match_obj: re.M # 1. Tokenize tokens = [val for val in re.split("[\t\n, ]", value) if val] - values: List[str] = [] + values: list[str] = [] # 2. Join add = False @@ -194,9 +200,10 @@ def update_impl(self, spec_obj: "Spec", context: Dict[str, Any], match_obj: re.M for val in values: requirement = Requirement(val) - getattr(target_obj, self.name).append(requirement) - else: - getattr(target_obj, self.name).append(value) + cast("list[Requirement]", getattr(target_obj, self.name)).append(requirement) + return spec_obj, context + target_list = cast("list[str]", getattr(target_obj, self.name)) + target_list.append(value) return spec_obj, context @@ -204,53 +211,66 @@ def update_impl(self, spec_obj: "Spec", context: Dict[str, Any], match_obj: re.M class _ListAndDict(_Tag): """Parse a tag that expands to a list and to a dict.""" - def __init__(self, name: str, pattern_obj: re.Pattern) -> None: + def __init__(self, name: str, pattern_obj: re.Pattern[str]) -> None: super().__init__(name, pattern_obj, list) - def update_impl(self, spec_obj: "Spec", context: Dict[str, Any], match_obj: re.Match, line: str) -> Tuple["Spec", dict]: + @override + def update_impl( + self, spec_obj: Spec, context: dict[str, Any], match_obj: re.Match[str], line: str + ) -> tuple[Spec, dict[str, Any]]: source_name, value = match_obj.groups() - dictionary = getattr(spec_obj, f"{self.name}_dict") - dictionary[source_name] = value - target_obj = _Tag.current_target(spec_obj, context) + spec_dictionary = cast("dict[str, str]", getattr(spec_obj, f"{self.name}_dict")) + spec_dictionary[source_name] = value + target_obj: Spec | Package = _Tag.current_target(spec_obj, context) # If we are in a subpackage, add sources and patches to the subpackage dicts as well - if hasattr(target_obj, "is_subpackage") and target_obj.is_subpackage: - dictionary = getattr(target_obj, f"{self.name}_dict") - dictionary[source_name] = value - getattr(target_obj, self.name).append(value) - getattr(spec_obj, self.name).append(value) + if isinstance(target_obj, Package) and target_obj.is_subpackage: + package_dictionary = cast("dict[str, str]", getattr(target_obj, f"{self.name}_dict")) + package_dictionary[source_name] = value + cast("list[str]", getattr(target_obj, self.name)).append(value) + cast("list[str]", getattr(spec_obj, self.name)).append(value) return spec_obj, context class _SplitValue(_NameValue): """Parse a (name->value) tag, and at the same time split the tag to a list.""" - def __init__(self, name: str, pattern_obj: re.Pattern, sep: Optional[None] = None) -> None: + name_list: str + sep: str | None + + def __init__(self, name: str, pattern_obj: re.Pattern[str], sep: str | None = None) -> None: super().__init__(name, pattern_obj) self.name_list = f"{name}_list" self.sep = sep - def update_impl(self, spec_obj: "Spec", context: Dict[str, Any], match_obj: re.Match, line: str) -> Tuple["Spec", dict]: - super().update_impl(spec_obj, context, match_obj, line) + @override + def update_impl( + self, spec_obj: Spec, context: dict[str, Any], match_obj: re.Match[str], line: str + ) -> tuple[Spec, dict[str, Any]]: + spec_obj, context = super().update_impl(spec_obj, context, match_obj, line) - target_obj = _Tag.current_target(spec_obj, context) - value: str = getattr(target_obj, self.name) + target_obj: Spec | Package = _Tag.current_target(spec_obj, context) + value = cast(str, getattr(target_obj, self.name)) values = value.split(self.sep) setattr(target_obj, self.name_list, values) return spec_obj, context -def re_tag_compile(tag: AnyStr) -> re.Pattern: +def re_tag_compile(tag: str) -> re.Pattern[str]: return re.compile(tag, re.IGNORECASE) class _DummyMacroDef(_Tag): """Parse global macro definitions.""" - def __init__(self, name: str, pattern_obj: re.Pattern) -> None: + def __init__(self, name: str, pattern_obj: re.Pattern[str]) -> None: super().__init__(name, pattern_obj, str) - def update_impl(self, spec_obj: "Spec", context: Dict[str, Any], _: re.Match, line: str) -> Tuple["Spec", dict]: + @override + def update_impl( + self, spec_obj: Spec, context: dict[str, Any], match_obj: re.Match[str], line: str + ) -> tuple[Spec, dict[str, Any]]: + del match_obj context["line_processor"] = None if warnings_enabled: warn("Unknown macro: " + line) @@ -290,19 +310,18 @@ def update_impl(self, spec_obj: "Spec", context: Dict[str, Any], _: re.Match, li _macro_pattern = re.compile(r"%{(\S+?)\}|%(\w+?)\b") -def _parse(spec_obj: "Spec", context: Dict[str, Any], line: str) -> Any: +def _parse(spec_obj: Spec, context: dict[str, Any], line: str) -> tuple[Spec, dict[str, Any]]: for tag in _tags: match = tag.test(line) if match: if "multiline" in context: context.pop("multiline", None) return tag.update(spec_obj, context, match, line) - if "multiline" in context: - target_obj = _Tag.current_target(spec_obj, context) - previous_txt = getattr(target_obj, context["multiline"], "") - if previous_txt is None: - previous_txt = "" - setattr(target_obj, context["multiline"], str(previous_txt) + line + os.linesep) + multiline_key = context.get("multiline") + if isinstance(multiline_key, str): + target_obj: Spec | Package = _Tag.current_target(spec_obj, context) + previous_txt = getattr(target_obj, multiline_key, "") or "" + setattr(target_obj, multiline_key, str(previous_txt) + line + os.linesep) return spec_obj, context @@ -334,11 +353,11 @@ class Requirement: version. """ - expr = re.compile(r"(.*?)\s+([<>]=?|=)\s+(\S+)") + expr: ClassVar[re.Pattern[str]] = re.compile(r"(.*?)\s+([<>]=?|=)\s+(\S+)") def __init__(self, name: str) -> None: assert isinstance(name, str) - self.line = name + self.line: str = name self.name: str self.operator: str | None self.version: str | None @@ -352,6 +371,7 @@ def __init__(self, name: str) -> None: self.operator = None self.version = None + @override def __eq__(self, o: object) -> bool: if isinstance(o, str): return self.line == o @@ -359,6 +379,7 @@ def __eq__(self, o: object) -> bool: return self.name == o.name and self.operator == o.operator and self.version == o.version return False + @override def __repr__(self) -> str: return f"Requirement('{self.line}')" @@ -404,30 +425,75 @@ class Package: """ + # pylint: disable=too-many-instance-attributes + name: str + summary: str | None + description: str | None + changelog: str | None + license: str | None + group: str | None + url: str | None + buildroot: str | None + epoch: str | None + release: str | None + version: str | None + buildarch: str | None + buildarch_list: list[str] + excludearch: str | None + excludearch_list: list[str] + exclusivearch: str | None + exclusivearch_list: list[str] + sources: list[str] + sources_dict: dict[str, str] + patches: list[str] + patches_dict: dict[str, str] + build_requires: list["Requirement"] + requires: list["Requirement"] + conflicts: list[str] + obsoletes: list[str] + provides: list[str] + is_subpackage: bool + def __init__(self, name: str) -> None: assert isinstance(name, str) for tag in _tags: - if tag.attr_type is list and tag.name in [ - "build_requires", - "requires", - "conflicts", - "obsoletes", - "provides", - "sources", - "patches", - ]: + if tag.attr_type is list: + if tag.name == "packages": + continue setattr(self, tag.name, tag.attr_type()) - elif tag.name in [ - "description", - ]: + else: setattr(self, tag.name, None) - self.sources_dict: Dict[str, str] = {} - self.patches_dict: Dict[str, str] = {} + self.summary = None + self.description = None + self.changelog = None + self.license = None + self.group = None + self.url = None + self.buildroot = None + self.version = None + self.epoch = None + self.release = None + self.buildarch = None + self.excludearch = None + self.exclusivearch = None + self.buildarch_list = [] + self.excludearch_list = [] + self.exclusivearch_list = [] + self.sources = [] + self.sources_dict = {} + self.patches = [] + self.patches_dict = {} + self.build_requires = [] + self.requires = [] + self.conflicts = [] + self.obsoletes = [] + self.provides = [] self.name = name self.is_subpackage = False + @override def __repr__(self) -> str: return f"Package('{self.name}')" @@ -435,6 +501,36 @@ def __repr__(self) -> str: class Spec: """Represents a single spec file.""" + # pylint: disable=too-many-instance-attributes + name: str | None + version: str | None + epoch: str | None + release: str | None + summary: str | None + description: str | None + changelog: str | None + license: str | None + group: str | None + url: str | None + buildroot: str | None + buildarch: str | None + buildarch_list: list[str] + excludearch: str | None + excludearch_list: list[str] + exclusivearch: str | None + exclusivearch_list: list[str] + sources: list[str] + sources_dict: dict[str, str] + patches: list[str] + patches_dict: dict[str, str] + build_requires: list["Requirement"] + requires: list["Requirement"] + conflicts: list[str] + obsoletes: list[str] + provides: list[str] + packages: list["Package"] + macros: dict[str, str] + def __init__(self) -> None: for tag in _tags: if tag.attr_type is list: @@ -442,15 +538,38 @@ def __init__(self) -> None: else: setattr(self, tag.name, None) - self.sources_dict: Dict[str, str] = {} - self.patches_dict: Dict[str, str] = {} - self.macros: Dict[str, str] = {"nil": ""} - - self.name: str | None - self.packages: List[Package] = [] + self.name = None + self.version = None + self.epoch = None + self.release = None + self.summary = None + self.description = None + self.changelog = None + self.license = None + self.group = None + self.url = None + self.buildroot = None + self.buildarch = None + self.excludearch = None + self.exclusivearch = None + self.buildarch_list = [] + self.excludearch_list = [] + self.exclusivearch_list = [] + self.sources = [] + self.sources_dict = {} + self.patches = [] + self.patches_dict = {} + self.build_requires = [] + self.requires = [] + self.conflicts = [] + self.obsoletes = [] + self.provides = [] + self.macros = {"nil": ""} + + self.packages = [] @property - def packages_dict(self) -> Dict[str, Package]: + def packages_dict(self) -> dict[str, Package]: """All packages in this RPM spec as a dictionary. You can access the individual packages by their package name, e.g., @@ -462,7 +581,7 @@ def packages_dict(self) -> Dict[str, Package]: return dict(zip([package.name for package in self.packages], self.packages)) @classmethod - def from_file(cls, filename: str) -> "Spec": + def from_file(cls, filename: str) -> Spec: """Creates a new Spec object from a given file. :param filename: The path to the spec file. @@ -477,7 +596,7 @@ def from_file(cls, filename: str) -> "Spec": return spec @classmethod - def from_string(cls, string: str) -> "Spec": + def from_string(cls, string: str) -> Spec: """Creates a new Spec object from a given string. :param string: The contents of a spec file. @@ -508,9 +627,6 @@ def replace_macros(string: str, spec: Spec, max_attempts: int = 1000) -> str: """ assert isinstance(spec, Spec) - def get_first_non_none_value(values: Tuple[Any, ...]) -> Any: - return next((v for v in values if v is not None), None) - def is_conditional_macro(macro: str) -> bool: return macro.startswith(("?", "!")) @@ -520,11 +636,22 @@ def is_optional_macro(macro: str) -> bool: def is_negation_macro(macro: str) -> bool: return macro.startswith("!") - def get_replacement_string(match: re.Match) -> str: + def get_macro_value(macro: str, default: str = "") -> str: + dict_value = spec.macros.get(macro) + if dict_value is not None: + return dict_value + sentinel = object() + attr_value = getattr(spec, macro, sentinel) + if attr_value is sentinel or attr_value is None: + return default + return str(cast(object, attr_value)) + + def get_replacement_string(match: re.Match[str]) -> str: # pylint: disable=too-many-return-statements - groups = match.groups() - macro_name: str = get_first_non_none_value(groups) - assert macro_name, "Expected a non None value" + groups: tuple[str | None, ...] = match.groups() + macro_name = next((group for group in groups if group is not None), None) + if macro_name is None: + return match.group(0) if is_conditional_macro(macro_name) and spec: parts = macro_name[1:].split(sep=":", maxsplit=1) assert parts, "Expected a ':' in macro name'" @@ -538,9 +665,10 @@ def get_replacement_string(match: re.Match) -> str: return spec.macros[macro] if hasattr(spec, macro): - return getattr(spec, macro) + attr_value = cast(object, getattr(spec, macro)) + return "" if attr_value is None else str(attr_value) - assert False, "Unreachable" + raise AssertionError("Unreachable") return "" @@ -548,14 +676,17 @@ def get_replacement_string(match: re.Match) -> str: if len(parts) == 2: return parts[1] - return spec.macros.get(macro, getattr(spec, macro, "")) + return get_macro_value(macro, "") if spec: - value = spec.macros.get(macro_name, getattr(spec, macro_name, None)) - if value is not None: - return str(value) - - return match.string[match.start() : match.end()] + macro_value = spec.macros.get(macro_name) + if macro_value is not None: + return macro_value + attr_value = cast(object, getattr(spec, macro_name, None)) + if attr_value is not None: + return str(attr_value) + + return match.group(0) attempt = 0 ret = "" diff --git a/requirements.txt b/requirements.txt index b4ce203..b0a08ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +basedpyright==1.32.1 black==24.10.0 coverage==7.11.0 flit==3.12.0 @@ -7,3 +8,4 @@ pytest-asyncio==1.2.0 pytest-cov==7.0.0 pytest==8.4.2 rope==1.14.0 +typing-extensions==4.15.0 diff --git a/scripts/fedora_sources.py b/scripts/fedora_sources.py index 3175a50..68cab7c 100644 --- a/scripts/fedora_sources.py +++ b/scripts/fedora_sources.py @@ -5,7 +5,7 @@ # Spec files to skip because of known issues. -skipfiles = () +skipfiles: tuple[str, ...] = () def skip(filename: str) -> bool: @@ -29,4 +29,4 @@ def skip(filename: str) -> bool: for filename in filenames: if not skip(filename): print(filename) - Spec.from_file(os.path.join(rpmspecs, filename)) + _ = Spec.from_file(os.path.join(rpmspecs, filename)) diff --git a/tests/test_regressions.py b/tests/test_regressions.py index c06cf37..c6b4447 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -1,6 +1,8 @@ -import os.path import asyncio import concurrent.futures +import os.path +from collections.abc import Awaitable, Callable +from typing import ParamSpec, TypeVar import pytest @@ -8,10 +10,13 @@ TEST_DATA = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data") +P = ParamSpec("P") +T = TypeVar("T") + -def with_timeout(t): - def wrapper(corofunc): - async def run(*args, **kwargs): +def with_timeout(t: float) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]: + def wrapper(corofunc: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]: + async def run(*args: P.args, **kwargs: P.kwargs) -> T: return await asyncio.wait_for(corofunc(*args, **kwargs), timeout=t) return run @@ -30,4 +35,4 @@ async def test_endless_loop() -> None: loop = asyncio.get_running_loop() with concurrent.futures.ThreadPoolExecutor() as pool: - await loop.run_in_executor(pool, Spec.from_file, specfile) + _ = await loop.run_in_executor(pool, Spec.from_file, specfile) diff --git a/tests/test_spec_file_parser.py b/tests/test_spec_file_parser.py index 05da097..f7a0e39 100644 --- a/tests/test_spec_file_parser.py +++ b/tests/test_spec_file_parser.py @@ -1,5 +1,6 @@ -import re import os.path +import re +from typing import cast import pytest @@ -153,15 +154,16 @@ def test_subpackage_tags(self) -> None: def test_defines(self) -> None: spec = Spec.from_file(os.path.join(TEST_DATA, "attica-qt5.spec")) - # Check if they exist - for define in ("sonum", "_tar_path", "_libname", "rname"): + for define, expected in ( + ("sonum", "5"), + ("rname", "attica"), + ("_libname", "KF5Attica"), + ("_tar_path", "5.31"), + ): assert hasattr(spec, define) - - # Check values - assert spec.sonum == "5" - assert spec.rname == "attica" - assert spec._libname == "KF5Attica" - assert spec._tar_path == "5.31" + attr = cast(object, getattr(spec, define)) + assert isinstance(attr, str) + assert attr == expected def test_replace_macro_that_is_tag_name(self) -> None: """Test that we are able to replace macros which are in the tag list. @@ -175,6 +177,7 @@ def test_replace_macro_that_is_tag_name(self) -> None: Version: %{myversion} """ ) + assert spec.version is not None assert replace_macros(spec.version, spec) == "1.2.3" spec = Spec.from_string( @@ -183,6 +186,7 @@ def test_replace_macro_that_is_tag_name(self) -> None: Version: %{version} """ ) + assert spec.version is not None assert replace_macros(spec.version, spec) == "1.2.3" def test_custom_conditional_macro(self) -> None: @@ -195,6 +199,9 @@ def test_custom_conditional_macro(self) -> None: """ ) spec.macros["dist"] = ".el8" + assert spec.name is not None + assert spec.version is not None + assert spec.release is not None assert replace_macros(f"{spec.name}-{spec.version}-{spec.release}.src.rpm", spec) == "foo-1-1.el8.src.rpm" def test_macro_appends_to_itself(self) -> None: @@ -206,6 +213,7 @@ def test_macro_appends_to_itself(self) -> None: Release: 1%{flagrel} """ ) + assert spec.release is not None assert replace_macros(spec.release, spec) == "1.SAN" def test_macro_conditional_expands_when_inputs_ready(self) -> None: @@ -218,6 +226,7 @@ def test_macro_conditional_expands_when_inputs_ready(self) -> None: """ ) assert spec.macros["extra"] == "%{?debug:.DEBUG}" + assert spec.release is not None assert replace_macros(spec.release, spec) == "1.DEBUG" def test_replace_macro_raises_with_max_attempts_reached(self) -> None: @@ -232,8 +241,9 @@ def test_replace_macro_raises_with_max_attempts_reached(self) -> None: Version: %{version} """ ) + assert spec.version is not None with pytest.raises(RuntimeError): - replace_macros(spec.version, spec, max_attempts=1) + _ = replace_macros(spec.version, spec, max_attempts=1) def test_requirement_parsing(self) -> None: spec = Spec.from_file(os.path.join(TEST_DATA, "attica-qt5.spec")) @@ -331,7 +341,7 @@ def test_replace_macro_without_spec_raises(self) -> None: """Make sure to assert that caller passes a spec file.""" with pytest.raises(AssertionError): - replace_macros("something something", spec=None) + _ = replace_macros("something something", spec=None) # pyright: ignore[reportArgumentType] def test_replace_unknown_section(self) -> None: """Ensure that we can print warnings during parsing.""" @@ -339,7 +349,7 @@ def test_replace_unknown_section(self) -> None: try: pyrpm.spec.warnings_enabled = True with pytest.warns(UserWarning): - Spec.from_file(os.path.join(TEST_DATA, "perl-Array-Compare.spec")) + _ = Spec.from_file(os.path.join(TEST_DATA, "perl-Array-Compare.spec")) finally: pyrpm.spec.warnings_enabled = False