Source code for rebasehelper.tags

# -*- coding: utf-8 -*-
#
# This tool helps you rebase your package to the latest version
# Copyright (C) 2013-2019 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Authors: Petr Hráček <phracek@redhat.com>
#          Tomáš Hozza <thozza@redhat.com>
#          Nikola Forró <nforro@redhat.com>
#          František Nečas <fifinecas@seznam.cz>

import collections.abc
import fnmatch
import re
from typing import Dict, Iterator, List, Optional, Tuple, cast

from rebasehelper.spec_content import SpecContent
from rebasehelper.helpers.macro_helper import MacroHelper


[docs]class Tag: def __init__(self, section_index: int, section_name: str, line: int, name: str, value_span: Tuple[int, int], valid: bool, index: Optional[int] = None) -> None: self.section_index: int = section_index self.section_name: str = section_name self.line: int = line self.name: str = name self.value_span: Tuple[int, int] = value_span self.valid: bool = valid self.index: Optional[int] = index def __eq__(self, other): return (self.section_index == other.section_index and self.section_name == other.section_name and self.line == other.line and self.name == other.name and self.value_span == other.value_span and self.valid == other.valid and self.index == other.index) def __str__(self) -> str: return ( "Tag(" f"section_index='{self.section_index}', " f"section_name='{self.section_name}', " f"line='{self.line}', " f"name='{self.name}', " f"value_span='{self.value_span}', " f"valid='{self.valid}', " f"index='{self.index}')" )
[docs]class Tags(collections.abc.Sequence): def __init__(self, raw_content: SpecContent, parsed_content: SpecContent) -> None: self.items: Tuple[Tag] = self._parse(raw_content, parsed_content) def __getitem__(self, index): return self.items[index] def __len__(self): return len(self.items) @classmethod def _map_sections_to_parsed(cls, raw_content: SpecContent, parsed_content: SpecContent) -> Dict[int, int]: """Creates a mapping of raw sections to parsed sections. Returns: A dict where the keys are indexes of sections in raw content and values are their counterparts in parsed content. Value is set to -1 if the section is not in parsed SpecContent. """ result: Dict[int, int] = {} parsed = 0 for index, (section, _) in enumerate(raw_content.sections): if parsed < len(parsed_content.sections) and parsed_content.sections[parsed][0].lower() == section.lower(): result[index] = parsed parsed += 1 else: result[index] = -1 return result @classmethod def _parse(cls, raw_content: SpecContent, parsed_content: SpecContent) -> Tuple[Tag]: result: List[Tag] = [] parsed_mapping = cls._map_sections_to_parsed(raw_content, parsed_content) next_source_index = 0 next_patch_index = 0 # counts number of occurrences of one particular section for section_index, (section, section_content) in enumerate(raw_content.sections): section = section.lower() parsed_section_index = parsed_mapping[section_index] parsed: List[str] = [] if parsed_section_index == -1 else parsed_content.sections[parsed_section_index][1] if section.startswith('%package'): tags, next_source_index, next_patch_index = cls._parse_package_tags(section, section_content, parsed, section_index, next_source_index, next_patch_index) result.extend(tags) elif section.startswith('%sourcelist') or section.startswith('%patchlist'): tags, next_source_index, next_patch_index = cls._parse_list_tags(section, section_content, parsed, section_index, next_source_index, next_patch_index) result.extend(tags) return cast(Tuple[Tag], tuple(result)) @classmethod def _sanitize_tag(cls, name: str, next_source_index: int, next_patch_index: int) -> Tuple[str, Optional[int], int, int]: """Sanitizes a tag name. Capitalizes the tag. If the tag is numbered, strips padded zeroes. Implements automatic numbering of Patches and Sources (adds the number to their name). """ name, *rest = re.split(r'(\d+)', name.lower(), 1) sanitized_name = name.capitalize() next_index = next_source_index if name == 'source' else next_patch_index index = int(next(iter(rest), next_index)) if name == 'source': return '{}{}'.format(sanitized_name, index), index, index + 1, next_patch_index if name == 'patch': return '{}{}'.format(sanitized_name, index), index, next_source_index, index + 1 return sanitized_name, None, next_source_index, next_patch_index @classmethod def _parse_package_tags(cls, section: str, section_content: List[str], parsed: List[str], section_index: int, next_source_index: int, next_patch_index: int) -> Tuple[List[Tag], int, int]: """Parses all tags in a %package section and determines if they are valid. A tag is considered valid if it is still present after evaluating all conditions. Note that this is not perfect - if the same tag appears in both %if and %else blocks, and has the same value in both, it's impossible to tell them apart, so only the latter is considered valid, disregarding the actual condition. Returns: A tuple containing: a tuple of all Tags object, new next source index, new next patch index. Indexed tag names are sanitized, for example 'Source' is replaced with 'Source0' and 'Patch007' with 'Patch7'. Tag names are capitalized, section names are lowercase. """ result = [] tag_re = re.compile(r'^(?P<prefix>(?P<name>\w+)\s*:\s*)(?P<value>.+)$') for line_index, line in enumerate(section_content): expanded = MacroHelper.expand(line, suppress_errors=True) if not line or not expanded: continue valid = bool(parsed and [p for p in parsed if p == expanded.rstrip()]) m = tag_re.match(line) if m: tag_name, tag_index, next_source_index, next_patch_index = cls._sanitize_tag(m.group('name'), next_source_index, next_patch_index) result.append(Tag(section_index, section, line_index, tag_name, m.span('value'), valid, tag_index)) continue m = tag_re.match(expanded) if m: start = line.find(m.group('prefix')) if start < 0: # tag is probably defined by a macro, just ignore it continue # conditionalized tag line = line[start:].rstrip('}') # FIXME: removing trailing braces is not very robust m = tag_re.match(line) if m: span = cast(Tuple[int, int], tuple(x + start for x in m.span('value'))) tag_name, tag_index, next_source_index, next_patch_index = cls._sanitize_tag(m.group('name'), next_source_index, next_patch_index) result.append(Tag(section_index, section, line_index, tag_name, span, valid, tag_index)) return result, next_source_index, next_patch_index @classmethod def _parse_list_tags(cls, section: str, section_content: List[str], parsed: List[str], section_index: int, next_source_index: int, next_patch_index: int) -> Tuple[List[Tag], int, int]: """Parses all tags in a %sourcelist or %patchlist section. Only parses tags that are valid (that is - are in parsed), nothing more can consistently be detected. Follows how rpm works, the new Source/Patch tags are indexed starting from the last parsed Source/Patch tag. """ tag = 'Source' if section == '%sourcelist' else 'Patch' result = [] for i, line in enumerate(section_content): expanded = MacroHelper.expand(line, suppress_errors=True) is_comment = SpecContent.get_comment_span(line, section)[0] != len(line) if not expanded or not line or is_comment or not [p for p in parsed if p == expanded.rstrip()]: continue tag_name, tag_index, next_source_index, next_patch_index = cls._sanitize_tag(tag, next_source_index, next_patch_index) result.append(Tag(section_index, section, i, tag_name, (0, len(line)), True, tag_index)) return result, next_source_index, next_patch_index @classmethod def _filter(cls, tags: Tuple[Tag], section_index: Optional[int] = None, section_name: Optional[str] = None, name: Optional[str] = None, valid: Optional[bool] = True) -> Iterator[Tag]: result = iter(tags) if section_index is not None: result = filter(lambda t: t.section_index == section_index, result) if section_name is not None: result = filter(lambda t: t.section_name == section_name.lower(), result) # type: ignore if name is not None: result = filter(lambda t: fnmatch.fnmatchcase(t.name, name.capitalize()), result) # type: ignore if valid is not None: result = filter(lambda t: t.valid == valid, result) return result
[docs] def filter(self, section_index: Optional[int] = None, section_name: Optional[str] = None, name: Optional[str] = None, valid: Optional[bool] = True) -> Iterator[Tag]: """Filters tags based on section, name or validity. Defaults to all valid tags in all sections. Args: section_index: If specified, includes tags only from section of this index. section_name: If specified, includes tags only from sections of this name. name: If specified, includes tags matching this name. Wildcards are supported. valid: If specified, includes tags of this validity. Returns: Iterator of matching Tag objects. """ return self._filter(self.items, section_index, section_name, name, valid)