Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-specfile for openSUSE:Factory 
checked in at 2022-11-12 17:40:56
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-specfile (Old)
 and      /work/SRC/openSUSE:Factory/.python-specfile.new.1597 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-specfile"

Sat Nov 12 17:40:56 2022 rev:2 rq:1035243 version:0.9.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-specfile/python-specfile.changes  
2022-10-26 12:31:31.432278077 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-specfile.new.1597/python-specfile.changes    
    2022-11-12 17:41:13.062185332 +0100
@@ -1,0 +2,7 @@
+Wed Nov  9 19:02:12 UTC 2022 - Yogalakshmi Arunachalam <yarunacha...@suse.com>
+
+- Update to version 0.9.0 
+  * Added utility classes for working with (N)EVR. (#113)
+  * Fixed an issue with multiple instances of Specfile not expanding macros in 
the right context. (#117)
+
+-------------------------------------------------------------------

Old:
----
  specfile-0.8.0.tar.gz

New:
----
  specfile-0.9.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-specfile.spec ++++++
--- /var/tmp/diff_new_pack.iQkmU7/_old  2022-11-12 17:41:13.858190071 +0100
+++ /var/tmp/diff_new_pack.iQkmU7/_new  2022-11-12 17:41:13.866190119 +0100
@@ -15,23 +15,24 @@
 # Please submit bugfixes or comments via https://bugs.opensuse.org/
 #
 
+
 %define skip_python38 1
 Name:           python-specfile
-Version:        0.8.0
+Version:        0.9.0
 Release:        0
 Summary:        A library for parsing and manipulating RPM spec files
 License:        MIT
 URL:            https://github.com/packit/specfile
 Source:         
https://files.pythonhosted.org/packages/source/s/specfile/specfile-%{version}.tar.gz
-BuildRequires:  python-rpm-macros
-BuildRequires:  %{python_module setuptools}
-BuildRequires:  %{python_module setuptools_scm}
 BuildRequires:  %{python_module setuptools_scm_git_archive}
+BuildRequires:  %{python_module setuptools_scm}
+BuildRequires:  %{python_module setuptools}
+BuildRequires:  python-rpm-macros
 # SECTION test requirements
 BuildRequires:  %{python_module rpm}
-BuildRequires:  %{python_module typing-extensions}
-BuildRequires:  %{python_module pytest}
 BuildRequires:  %{python_module flexmock}
+BuildRequires:  %{python_module pytest}
+BuildRequires:  %{python_module typing-extensions}
 # /SECTION
 BuildRequires:  fdupes
 Requires:       python-rpm

++++++ specfile-0.8.0.tar.gz -> specfile-0.9.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.8.0/.packit.yaml 
new/specfile-0.9.0/.packit.yaml
--- old/specfile-0.8.0/.packit.yaml     2022-10-14 13:31:08.000000000 +0200
+++ new/specfile-0.9.0/.packit.yaml     2022-10-25 19:06:38.000000000 +0200
@@ -54,6 +54,12 @@
       - fedora-all
       - epel-9
 
+  - job: tests
+    trigger: pull_request
+    targets:
+      - fedora-all
+      - epel-9
+
   - job: copr_build
     trigger: commit
     branch: main
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.8.0/.pre-commit-config.yaml 
new/specfile-0.9.0/.pre-commit-config.yaml
--- old/specfile-0.8.0/.pre-commit-config.yaml  2022-10-14 13:31:08.000000000 
+0200
+++ new/specfile-0.9.0/.pre-commit-config.yaml  2022-10-25 19:06:38.000000000 
+0200
@@ -12,7 +12,7 @@
     hooks:
       - id: black
   - repo: https://github.com/pre-commit/mirrors-prettier
-    rev: v3.0.0-alpha.1
+    rev: v3.0.0-alpha.3
     hooks:
       - id: prettier
   - repo: https://github.com/pre-commit/pre-commit-hooks
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.8.0/CHANGELOG.md 
new/specfile-0.9.0/CHANGELOG.md
--- old/specfile-0.8.0/CHANGELOG.md     2022-10-14 13:31:08.000000000 +0200
+++ new/specfile-0.9.0/CHANGELOG.md     2022-10-25 19:06:38.000000000 +0200
@@ -1,3 +1,8 @@
+# 0.9.0
+
+- Added utility classes for working with (N)EVR. (#113)
+- Fixed an issue with multiple instances of `Specfile` not expanding macros in 
the right context. (#117)
+
 # 0.8.0
 
 - Added `Specfile.update_tag()` method that allows updating tag values while 
trying to preserve macro expansions. You can watch a demo on 
[YouTube](https://youtu.be/yzMfBPdFXZY). (#101)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.8.0/PKG-INFO new/specfile-0.9.0/PKG-INFO
--- old/specfile-0.8.0/PKG-INFO 2022-10-14 13:31:18.927929400 +0200
+++ new/specfile-0.9.0/PKG-INFO 2022-10-25 19:06:48.976166000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: specfile
-Version: 0.8.0
+Version: 0.9.0
 Summary: A library for parsing and manipulating RPM spec files.
 Home-page: https://github.com/packit/specfile
 Author: Red Hat
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.8.0/fedora/python-specfile.spec 
new/specfile-0.9.0/fedora/python-specfile.spec
--- old/specfile-0.8.0/fedora/python-specfile.spec      2022-10-14 
13:31:08.000000000 +0200
+++ new/specfile-0.9.0/fedora/python-specfile.spec      2022-10-25 
19:06:38.000000000 +0200
@@ -13,7 +13,7 @@
 
 
 Name:           python-specfile
-Version:        0.8.0
+Version:        0.9.0
 Release:        1%{?dist}
 
 Summary:        A library for parsing and manipulating RPM spec files
@@ -69,6 +69,9 @@
 
 
 %changelog
+* Tue Oct 25 2022 Packit Team <he...@packit.dev> - 0.9.0-1
+- New upstream release 0.9.0
+
 * Fri Oct 14 2022 Packit Team <he...@packit.dev> - 0.8.0-1
 - New upstream release 0.8.0
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.8.0/specfile/changelog.py 
new/specfile-0.9.0/specfile/changelog.py
--- old/specfile-0.8.0/specfile/changelog.py    2022-10-14 13:31:08.000000000 
+0200
+++ new/specfile-0.9.0/specfile/changelog.py    2022-10-25 19:06:38.000000000 
+0200
@@ -8,7 +8,9 @@
 
 import rpm
 
+from specfile.exceptions import SpecfileException
 from specfile.sections import Section
+from specfile.utils import EVR
 
 WEEKDAYS = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
 MONTHS = (
@@ -137,8 +139,9 @@
             return ""
         return m.group("wsp") + (m.group("zp") or "")
 
-    @staticmethod
+    @classmethod
     def assemble(
+        cls,
         timestamp: Union[datetime.date, datetime.datetime],
         author: str,
         content: List[str],
@@ -176,7 +179,7 @@
         header += f" {timestamp:%Y} {author}"
         if evr is not None:
             header += f" - {evr}"
-        return ChangelogEntry(header, content, [""] if append_newline else 
None)
+        return cls(header, content, [""] if append_newline else None)
 
 
 class Changelog(collections.UserList):
@@ -262,12 +265,11 @@
         """
 
         def parse_evr(s):
-            if not s:
+            try:
+                evr = EVR.from_string(s)
+            except SpecfileException:
                 return "0", "0", ""
-            m = re.match(r"^(?:(\d+):)?(.*?)(?:-([^-]*))?$", s)
-            if not m:
-                return "0", "0", ""
-            return m.group(1) or "0", m.group(2), m.group(3) or ""
+            return str(evr.epoch), evr.version or "0", evr.release
 
         if since is None:
             start_index = 0
@@ -293,8 +295,8 @@
             )
         return self[start_index:end_index]
 
-    @staticmethod
-    def parse(section: Section) -> "Changelog":
+    @classmethod
+    def parse(cls, section: Section) -> "Changelog":
         """
         Parses a %changelog section.
 
@@ -325,7 +327,7 @@
                 predecessor.append(line)
         if header:
             data.insert(0, ChangelogEntry(header, content, following_lines))
-        return Changelog(data, predecessor)
+        return cls(data, predecessor)
 
     def get_raw_section_data(self) -> List[str]:
         """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.8.0/specfile/macro_definitions.py 
new/specfile-0.9.0/specfile/macro_definitions.py
--- old/specfile-0.8.0/specfile/macro_definitions.py    2022-10-14 
13:31:08.000000000 +0200
+++ new/specfile-0.9.0/specfile/macro_definitions.py    2022-10-25 
19:06:38.000000000 +0200
@@ -162,8 +162,8 @@
                 return i
         raise ValueError
 
-    @staticmethod
-    def parse(lines: List[str]) -> "MacroDefinitions":
+    @classmethod
+    def parse(cls, lines: List[str]) -> "MacroDefinitions":
         """
         Parses given lines into macro defintions.
 
@@ -213,7 +213,7 @@
                 buffer = []
             else:
                 buffer.append(line)
-        return MacroDefinitions(data, buffer)
+        return cls(data, buffer)
 
     def get_raw_data(self) -> List[str]:
         result = []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.8.0/specfile/prep.py 
new/specfile-0.9.0/specfile/prep.py
--- old/specfile-0.8.0/specfile/prep.py 2022-10-14 13:31:08.000000000 +0200
+++ new/specfile-0.9.0/specfile/prep.py 2022-10-25 19:06:38.000000000 +0200
@@ -319,8 +319,8 @@
         if index:
             del self.macros[index]
 
-    @staticmethod
-    def parse(section: Section) -> "Prep":
+    @classmethod
+    def parse(cls, section: Section) -> "Prep":
         """
         Parses a section into a `Prep` object.
 
@@ -350,25 +350,27 @@
                     m.group("o"),
                 )
                 prefix, suffix = line[: m.start("m")], line[m.end("o") :]
-                cls = next(
+                klass = next(
                     (
-                        cls
-                        for cls in PrepMacro.__subclasses__()
-                        if name.startswith(cls.CANONICAL_NAME)
+                        klass
+                        for klass in PrepMacro.__subclasses__()
+                        if name.startswith(klass.CANONICAL_NAME)
                     ),
                     None,
                 )
-                if not cls:
+                if not klass:
                     buffer.append(line)
                     continue
                 options = MacroOptions(
-                    MacroOptions.tokenize(option_string), cls.OPTSTRING, 
cls.DEFAULTS
+                    MacroOptions.tokenize(option_string),
+                    klass.OPTSTRING,
+                    klass.DEFAULTS,
                 )
-                data.append(cls(name, options, delimiter, prefix, suffix, 
buffer))
+                data.append(klass(name, options, delimiter, prefix, suffix, 
buffer))
                 buffer = []
             else:
                 buffer.append(line)
-        return Prep(PrepMacros(data, buffer))
+        return cls(PrepMacros(data, buffer))
 
     def get_raw_section_data(self) -> List[str]:
         """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.8.0/specfile/sections.py 
new/specfile-0.9.0/specfile/sections.py
--- old/specfile-0.8.0/specfile/sections.py     2022-10-14 13:31:08.000000000 
+0200
+++ new/specfile-0.9.0/specfile/sections.py     2022-10-25 19:06:38.000000000 
+0200
@@ -172,8 +172,8 @@
                 return i
         raise ValueError
 
-    @staticmethod
-    def parse(lines: List[str]) -> "Sections":
+    @classmethod
+    def parse(cls, lines: List[str]) -> "Sections":
         """
         Parses given lines into sections.
 
@@ -197,7 +197,7 @@
         data = [Section(PREAMBLE, lines[: section_starts[0]])]
         for start, end in zip(section_starts, section_starts[1:]):
             data.append(Section(lines[start][1:], lines[start + 1 : end]))
-        return Sections(data)
+        return cls(data)
 
     def get_raw_data(self) -> List[str]:
         result = []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.8.0/specfile/sourcelist.py 
new/specfile-0.9.0/specfile/sourcelist.py
--- old/specfile-0.8.0/specfile/sourcelist.py   2022-10-14 13:31:08.000000000 
+0200
+++ new/specfile-0.9.0/specfile/sourcelist.py   2022-10-25 19:06:38.000000000 
+0200
@@ -2,12 +2,15 @@
 # SPDX-License-Identifier: MIT
 
 import collections
-from typing import List, Optional, SupportsIndex, overload
+from typing import TYPE_CHECKING, List, Optional, SupportsIndex, overload
 
 from specfile.rpm import Macros
 from specfile.sections import Section
 from specfile.tags import Comments
 
+if TYPE_CHECKING:
+    from specfile.specfile import Specfile
+
 
 class SourcelistEntry:
     """
@@ -18,9 +21,23 @@
         comments: List of comments associated with the source/patch.
     """
 
-    def __init__(self, location: str, comments: Comments) -> None:
+    def __init__(
+        self, location: str, comments: Comments, context: Optional["Specfile"] 
= None
+    ) -> None:
+        """
+        Constructs a `SourceListEntry` object.
+
+        Args:
+            location: Literal location of the source/patch as stored in the 
spec file.
+            comments: List of comments associated with the source/patch.
+            context: `Specfile` instance that defines the context for macro 
expansions.
+
+        Returns:
+            Constructed instance of `SourceListEntry` class.
+        """
         self.location = location
         self.comments = comments.copy()
+        self._context = context
 
     def __eq__(self, other: object) -> bool:
         if not isinstance(other, SourcelistEntry):
@@ -34,6 +51,8 @@
     @property
     def expanded_location(self) -> str:
         """URL of the source/patch after expanding macros."""
+        if self._context:
+            return self._context.expand(self.location)
         return Macros.expand(self.location)
 
 
@@ -87,13 +106,16 @@
     def copy(self) -> "Sourcelist":
         return Sourcelist(self.data, self._remainder)
 
-    @staticmethod
-    def parse(section: Section) -> "Sourcelist":
+    @classmethod
+    def parse(
+        cls, section: Section, context: Optional["Specfile"] = None
+    ) -> "Sourcelist":
         """
         Parses a section into sources/patches.
 
         Args:
             section: %sourcelist/%patchlist section.
+            context: `Specfile` instance that defines the context for macro 
expansions.
 
         Returns:
             Constructed instance of `Sourcelist` class.
@@ -102,11 +124,11 @@
         buffer: List[str] = []
         for line in section:
             if line and not line.lstrip().startswith("#"):
-                data.append(SourcelistEntry(line, Comments.parse(buffer)))
+                data.append(SourcelistEntry(line, Comments.parse(buffer), 
context))
                 buffer = []
             else:
                 buffer.append(line)
-        return Sourcelist(data, buffer)
+        return cls(data, buffer)
 
     def get_raw_section_data(self) -> List[str]:
         """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.8.0/specfile/sources.py 
new/specfile-0.9.0/specfile/sources.py
--- old/specfile-0.8.0/specfile/sources.py      2022-10-14 13:31:08.000000000 
+0200
+++ new/specfile-0.9.0/specfile/sources.py      2022-10-25 19:06:38.000000000 
+0200
@@ -5,7 +5,7 @@
 import re
 import urllib.parse
 from abc import ABC, abstractmethod
-from typing import Iterable, List, Optional, Tuple, Union, cast, overload
+from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple, Union, 
cast, overload
 
 from specfile.exceptions import DuplicateSourceException
 from specfile.rpm import Macros
@@ -13,6 +13,9 @@
 from specfile.tags import Comments, Tag, Tags
 from specfile.utils import get_filename_from_location
 
+if TYPE_CHECKING:
+    from specfile.specfile import Specfile
+
 
 class Source(ABC):
     """Class that represents a source."""
@@ -218,6 +221,7 @@
         allow_duplicates: bool = False,
         default_to_implicit_numbering: bool = False,
         default_source_number_digits: int = 1,
+        context: Optional["Specfile"] = None,
     ) -> None:
         """
         Constructs a `Sources` object.
@@ -228,6 +232,7 @@
             allow_duplicates: Whether to allow duplicate entries when adding 
new sources.
             default_to_implicit_numbering: Use implicit numbering (no source 
numbers) by default.
             default_source_number_digits: Default number of digits in a source 
number.
+            context: `Specfile` instance that defines the context for macro 
expansions.
 
         Returns:
             Constructed instance of `Sources` class.
@@ -237,6 +242,7 @@
         self._allow_duplicates = allow_duplicates
         self._default_to_implicit_numbering = default_to_implicit_numbering
         self._default_source_number_digits = default_source_number_digits
+        self._context = context
 
     def __repr__(self) -> str:
         tags = repr(self._tags)
@@ -306,6 +312,11 @@
             _, container, index = items[i]
             del container[index]
 
+    def _expand(self, s: str) -> str:
+        if self._context:
+            return self._context.expand(s)
+        return Macros.expand(s)
+
     def _get_tags(self) -> List[Tuple[TagSource, Tags, int]]:
         """
         Gets all tag sources.
@@ -474,7 +485,7 @@
                 name, separator = self._get_tag_format(source, number)
                 container.insert(
                     index,
-                    Tag(name, location, Macros.expand(location), separator, 
Comments()),
+                    Tag(name, location, self._expand(location), separator, 
Comments()),
                 )
                 self._deduplicate_tag_names(i)
             else:
@@ -488,7 +499,7 @@
             index, name, separator = self._get_initial_tag_setup()
             self._tags.insert(
                 index,
-                Tag(name, location, Macros.expand(location), separator, 
Comments()),
+                Tag(name, location, self._expand(location), separator, 
Comments()),
             )
 
     def insert_numbered(self, number: int, location: str) -> int:
@@ -522,7 +533,7 @@
             i = 0
             index, name, separator = self._get_initial_tag_setup(number)
         self._tags.insert(
-            index, Tag(name, location, Macros.expand(location), separator, 
Comments())
+            index, Tag(name, location, self._expand(location), separator, 
Comments())
         )
         self._deduplicate_tag_names(i)
         return i
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.8.0/specfile/specfile.py 
new/specfile-0.9.0/specfile/specfile.py
--- old/specfile-0.8.0/specfile/specfile.py     2022-10-14 13:31:08.000000000 
+0200
+++ new/specfile-0.9.0/specfile/specfile.py     2022-10-25 19:06:38.000000000 
+0200
@@ -233,7 +233,9 @@
         """
         with self.sections() as sections, self.tags(sections.package) as tags:
             sourcelists = [
-                (s, Sourcelist.parse(s)) for s in sections if s.name == 
"sourcelist"
+                (s, Sourcelist.parse(s, context=self))
+                for s in sections
+                if s.name == "sourcelist"
             ]
             try:
                 yield Sources(
@@ -242,6 +244,7 @@
                     allow_duplicates,
                     default_to_implicit_numbering,
                     default_source_number_digits,
+                    context=self,
                 )
             finally:
                 for section, sourcelist in sourcelists:
@@ -267,7 +270,9 @@
         """
         with self.sections() as sections, self.tags(sections.package) as tags:
             patchlists = [
-                (s, Sourcelist.parse(s)) for s in sections if s.name == 
"patchlist"
+                (s, Sourcelist.parse(s, context=self))
+                for s in sections
+                if s.name == "patchlist"
             ]
             try:
                 yield Patches(
@@ -276,6 +281,7 @@
                     allow_duplicates,
                     default_to_implicit_numbering,
                     default_source_number_digits,
+                    context=self,
                 )
             finally:
                 for section, patchlist in patchlists:
@@ -572,7 +578,9 @@
         entities.update({k.upper(): v for k, v in entities.items() if v.type 
== Tag})
 
         def update(value, requested_value):
-            regex, template = ValueParser.construct_regex(value, 
entities.keys())
+            regex, template = ValueParser.construct_regex(
+                value, entities.keys(), context=self
+            )
             m = regex.match(requested_value)
             if m:
                 d = m.groupdict()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.8.0/specfile/tags.py 
new/specfile-0.9.0/specfile/tags.py
--- old/specfile-0.8.0/specfile/tags.py 2022-10-14 13:31:08.000000000 +0200
+++ new/specfile-0.9.0/specfile/tags.py 2022-10-25 19:06:38.000000000 +0200
@@ -190,8 +190,8 @@
                 item = Comment(item)
             self.data.append(item)
 
-    @staticmethod
-    def parse(lines: List[str]) -> "Comments":
+    @classmethod
+    def parse(cls, lines: List[str]) -> "Comments":
         """
         Parses list of lines into comments.
 
@@ -210,7 +210,7 @@
                 preceding_lines.insert(0, line)
                 continue
             comments.insert(0, Comment(*reversed(m.groups())))
-        return Comments(comments, preceding_lines)
+        return cls(comments, preceding_lines)
 
     def get_raw_data(self) -> List[str]:
         return self._preceding_lines + [str(i) for i in self.data]
@@ -427,8 +427,10 @@
             item.comments._preceding_lines[0:0] = lines[: index + 1]
             del lines[: index + 1]
 
-    @staticmethod
-    def parse(raw_section: Section, parsed_section: Optional[Section] = None) 
-> "Tags":
+    @classmethod
+    def parse(
+        cls, raw_section: Section, parsed_section: Optional[Section] = None
+    ) -> "Tags":
         """
         Parses a section into tags.
 
@@ -475,7 +477,7 @@
                 buffer = []
             else:
                 buffer.append(line)
-        return Tags(data, buffer)
+        return cls(data, buffer)
 
     def get_raw_section_data(self) -> List[str]:
         """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.8.0/specfile/utils.py 
new/specfile-0.9.0/specfile/utils.py
--- old/specfile-0.8.0/specfile/utils.py        2022-10-14 13:31:08.000000000 
+0200
+++ new/specfile-0.9.0/specfile/utils.py        2022-10-25 19:06:38.000000000 
+0200
@@ -1,9 +1,79 @@
 # Copyright Contributors to the Packit project.
 # SPDX-License-Identifier: MIT
 
+import collections
+import re
 import urllib.parse
 from pathlib import Path
 
+from specfile.exceptions import SpecfileException
+
+
+class EVR(collections.abc.Hashable):
+    """Class representing Epoch-Version-Release combination."""
+
+    def __init__(self, *, version: str, release: str = "", epoch: int = 0) -> 
None:
+        self.epoch = epoch
+        self.version = version
+        self.release = release
+
+    def _key(self) -> tuple:
+        return self.epoch, self.version, self.release
+
+    def __hash__(self) -> int:
+        return hash(self._key())
+
+    def __eq__(self, other: object) -> bool:
+        if type(other) != self.__class__:
+            return NotImplemented
+        return self._key() == other._key()
+
+    def __repr__(self) -> str:
+        return f"EVR(epoch={self.epoch}, version='{self.version}', 
release='{self.release}')"
+
+    def __str__(self) -> str:
+        epoch = f"{self.epoch}:" if self.epoch > 0 else ""
+        release = f"-{self.release}" if self.release else ""
+        return f"{epoch}{self.version}{release}"
+
+    @classmethod
+    def from_string(cls, evr: str) -> "EVR":
+        m = re.match(r"^(?:(\d+):)?([^-]+?)(?:-([^-]+))?$", evr)
+        if not m:
+            raise SpecfileException("Invalid EVR string.")
+        e, v, r = m.groups()
+        return cls(epoch=int(e) if e else 0, version=v, release=r or "")
+
+
+class NEVR(EVR):
+    """Class representing Name-Epoch-Version-Release combination."""
+
+    def __init__(
+        self, *, name: str, version: str, release: str = "", epoch: int = 0
+    ) -> None:
+        self.name = name
+        super().__init__(epoch=epoch, version=version, release=release)
+
+    def _key(self) -> tuple:
+        return self.name, self.epoch, self.version, self.release
+
+    def __repr__(self) -> str:
+        return (
+            f"NEVR(name='{self.name}', epoch={self.epoch}, "
+            f"version='{self.version}', release='{self.release}')"
+        )
+
+    def __str__(self) -> str:
+        return f"{self.name}-" + super().__str__()
+
+    @classmethod
+    def from_string(cls, nevr: str) -> "NEVR":
+        m = re.match(r"^(.+?)-(?:(\d+):)?([^-]+?)(?:-([^-]+))?$", nevr)
+        if not m:
+            raise SpecfileException("Invalid NEVR string.")
+        n, e, v, r = m.groups()
+        return cls(name=n, epoch=int(e) if e else 0, version=v, release=r or 
"")
+
 
 def get_filename_from_location(location: str) -> str:
     """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.8.0/specfile/value_parser.py 
new/specfile-0.9.0/specfile/value_parser.py
--- old/specfile-0.8.0/specfile/value_parser.py 2022-10-14 13:31:08.000000000 
+0200
+++ new/specfile-0.9.0/specfile/value_parser.py 2022-10-25 19:06:38.000000000 
+0200
@@ -5,12 +5,15 @@
 import re
 from abc import ABC
 from string import Template
-from typing import List, Tuple
+from typing import TYPE_CHECKING, List, Optional, Tuple
 from typing.re import Pattern
 
 from specfile.exceptions import UnterminatedMacroException
 from specfile.rpm import Macros
 
+if TYPE_CHECKING:
+    from specfile.specfile import Specfile
+
 SUBSTITUTION_GROUP_PREFIX = "sub_"
 
 
@@ -227,7 +230,10 @@
 
     @classmethod
     def construct_regex(
-        cls, value: str, modifiable_entities: List[str]
+        cls,
+        value: str,
+        modifiable_entities: List[str],
+        context: Optional["Specfile"] = None,
     ) -> Tuple[Pattern, Template]:
         """
         Parses the given value and constructs a regex that allows matching
@@ -250,18 +256,24 @@
             value: Value string to parse.
             modifiable_entities: Names of modifiable entities, i.e. local 
macro definitions
               and tags.
+            context: `Specfile` instance that defines the context for macro 
expansions.
 
         Returns:
             Tuple in the form of (constructed regex, corresponding template).
         """
 
+        def expand(s):
+            if context:
+                return context.expand(s)
+            return Macros.expand(s)
+
         def flatten(nodes):
             # get rid of conditional macro expansions
             result = []
             for node in nodes:
                 if isinstance(node, ConditionalMacroExpansion):
                     # evaluate the condition
-                    if Macros.expand(f"%{node.prefix}{node.name}"):
+                    if expand(f"%{node.prefix}{node.name}"):
                         result.append(f"%{{{node.prefix}{node.name}:")
                         result.extend(flatten(node.body))
                         result.append("}")
@@ -281,13 +293,13 @@
             elif isinstance(node, StringLiteral):
                 tokens.append(("v", node.value, ""))
             elif isinstance(node, (ShellExpansion, ExpressionExpansion)):
-                const = Macros.expand(str(node))
+                const = expand(str(node))
                 tokens.append(("c", const, str(node)))
             elif isinstance(node, MacroSubstitution):
                 if node.prefix.count("!") % 2 == 0 and node.name in 
modifiable_entities:
                     tokens.append(("g", node.name, str(node)))
                 else:
-                    const = Macros.expand(str(node))
+                    const = expand(str(node))
                     tokens.append(("c", const, str(node)))
             elif isinstance(node, EnclosedMacroSubstitution):
                 if (
@@ -297,7 +309,7 @@
                 ):
                     tokens.append(("g", node.name, str(node)))
                 else:
-                    const = Macros.expand(str(node))
+                    const = expand(str(node))
                     tokens.append(("c", const, str(node)))
 
         def escape(s):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.8.0/specfile.egg-info/PKG-INFO 
new/specfile-0.9.0/specfile.egg-info/PKG-INFO
--- old/specfile-0.8.0/specfile.egg-info/PKG-INFO       2022-10-14 
13:31:18.000000000 +0200
+++ new/specfile-0.9.0/specfile.egg-info/PKG-INFO       2022-10-25 
19:06:48.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: specfile
-Version: 0.8.0
+Version: 0.9.0
 Summary: A library for parsing and manipulating RPM spec files.
 Home-page: https://github.com/packit/specfile
 Author: Red Hat
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.8.0/tests/integration/test_specfile.py 
new/specfile-0.9.0/tests/integration/test_specfile.py
--- old/specfile-0.8.0/tests/integration/test_specfile.py       2022-10-14 
13:31:08.000000000 +0200
+++ new/specfile-0.9.0/tests/integration/test_specfile.py       2022-10-25 
19:06:38.000000000 +0200
@@ -333,3 +333,14 @@
         assert md.minorver.body == "2"
     with spec.sources() as sources:
         assert sources[1].location == "tests-86.tar.xz"
+
+
+def test_multiple_instances(spec_minimal, spec_autosetup):
+    spec1 = Specfile(spec_minimal)
+    spec2 = Specfile(spec_autosetup)
+    spec1.version = "14.2"
+    assert spec2.expanded_version == "0.1"
+    with spec2.sources() as sources:
+        assert sources[0].expanded_location == "test-0.1.tar.xz"
+        sources.append("tests-%{version}.tar.xz")
+        assert sources[1].expanded_location == "tests-0.1.tar.xz"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.8.0/tests/unit/test_utils.py 
new/specfile-0.9.0/tests/unit/test_utils.py
--- old/specfile-0.8.0/tests/unit/test_utils.py 2022-10-14 13:31:08.000000000 
+0200
+++ new/specfile-0.9.0/tests/unit/test_utils.py 2022-10-25 19:06:38.000000000 
+0200
@@ -3,7 +3,7 @@
 
 import pytest
 
-from specfile.utils import get_filename_from_location
+from specfile.utils import EVR, NEVR, get_filename_from_location
 
 
 @pytest.mark.parametrize(
@@ -29,3 +29,55 @@
 )
 def test_get_filename_from_location(location, filename):
     assert get_filename_from_location(location) == filename
+
+
+@pytest.mark.parametrize(
+    "evr, result",
+    [
+        ("0", EVR(version="0")),
+        ("12.0-1", EVR(version="12.0", release="1")),
+        ("2:56.8-5", EVR(epoch=2, version="56.8", release="5")),
+        ("0.8.0-1.fc37", EVR(version="0.8.0", release="1.fc37")),
+        ("0.5.0~rc2-1.el9", EVR(version="0.5.0~rc2", release="1.el9")),
+        ("7.3-0.2.rc1.fc38", EVR(version="7.3", release="0.2.rc1.fc38")),
+        (
+            "7.3~rc1^20200701gdeadf00f-12.fc38",
+            EVR(version="7.3~rc1^20200701gdeadf00f", release="12.fc38"),
+        ),
+    ],
+)
+def test_EVR_from_string(evr, result):
+    assert EVR.from_string(evr) == result
+
+
+@pytest.mark.parametrize(
+    "nevr, result",
+    [
+        ("package-0", NEVR(name="package", version="0")),
+        ("package-12.0-1", NEVR(name="package", version="12.0", release="1")),
+        (
+            "package-2:56.8-5",
+            NEVR(name="package", epoch=2, version="56.8", release="5"),
+        ),
+        (
+            "package-0.8.0-1.fc37",
+            NEVR(name="package", version="0.8.0", release="1.fc37"),
+        ),
+        (
+            "package-0.5.0~rc2-1.el9",
+            NEVR(name="package", version="0.5.0~rc2", release="1.el9"),
+        ),
+        (
+            "package-devel-7.3-0.2.rc1.fc38",
+            NEVR(name="package-devel", version="7.3", release="0.2.rc1.fc38"),
+        ),
+        (
+            "package-7.3~rc1^20200701gdeadf00f-12.fc38",
+            NEVR(
+                name="package", version="7.3~rc1^20200701gdeadf00f", 
release="12.fc38"
+            ),
+        ),
+    ],
+)
+def test_NEVR_from_string(nevr, result):
+    assert NEVR.from_string(nevr) == result

Reply via email to