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 2023-07-19 19:10:44
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-specfile (Old)
 and      /work/SRC/openSUSE:Factory/.python-specfile.new.5570 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-specfile"

Wed Jul 19 19:10:44 2023 rev:13 rq:1099363 version:0.20.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-specfile/python-specfile.changes  
2023-05-30 22:03:21.047331514 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-specfile.new.5570/python-specfile.changes    
    2023-07-19 19:10:46.380614922 +0200
@@ -1,0 +2,16 @@
+Wed Jul 19 04:53:41 UTC 2023 - Steve Kowalik <steven.kowa...@suse.com>
+
+- Update to 0.20.0:
+  * Fixed infinite loop when removing macros with `%` in the name. (#244)
+  * Added a possibility to undefine system macros by setting a macro value
+    to `None` in the `macros` argument of the `Specfile` constructor. (#244)
+  * Fixed a bug in processing options of `%prep` macros. For instance, when
+    a quoted string appeared inside an expression expansion, it could lead
+    to improper parsing, rendering the spec file invalid after accessing
+    the options. (#253)
+  * Parsing has been optimized so that even spec files with hundreds of
+    thousands of lines can be processed in reasonable time. (#240)
+- Drop setuptools_scm_git_archive BuildRequires.
+- Don't need to skip Python 3.8.
+
+-------------------------------------------------------------------

Old:
----
  specfile-0.18.0.tar.gz

New:
----
  specfile-0.20.0.tar.gz

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

Other differences:
------------------
++++++ python-specfile.spec ++++++
--- /var/tmp/diff_new_pack.bY4P3j/_old  2023-07-19 19:10:47.044618806 +0200
+++ /var/tmp/diff_new_pack.bY4P3j/_new  2023-07-19 19:10:47.052618852 +0200
@@ -16,16 +16,14 @@
 #
 
 
-%define skip_python38 1
 Name:           python-specfile
-Version:        0.18.0
+Version:        0.20.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_module pip}
-BuildRequires:  %{python_module setuptools_scm_git_archive}
 BuildRequires:  %{python_module setuptools_scm}
 BuildRequires:  %{python_module setuptools}
 BuildRequires:  %{python_module wheel}

++++++ specfile-0.18.0.tar.gz -> specfile-0.20.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/specfile-0.18.0/.github/workflows/prepare-release.yml 
new/specfile-0.20.0/.github/workflows/prepare-release.yml
--- old/specfile-0.18.0/.github/workflows/prepare-release.yml   2023-05-26 
11:50:44.000000000 +0200
+++ new/specfile-0.20.0/.github/workflows/prepare-release.yml   2023-07-13 
17:26:17.000000000 +0200
@@ -11,6 +11,9 @@
     # To not run in forks
     if: github.repository_owner == 'packit'
     runs-on: ubuntu-latest
+    permissions:
+      pull-requests: write
+      contents: write
 
     steps:
       - uses: actions/checkout@v3
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.18.0/.pre-commit-config.yaml 
new/specfile-0.20.0/.pre-commit-config.yaml
--- old/specfile-0.18.0/.pre-commit-config.yaml 2023-05-26 11:50:44.000000000 
+0200
+++ new/specfile-0.20.0/.pre-commit-config.yaml 2023-07-13 17:26:17.000000000 
+0200
@@ -4,15 +4,15 @@
 
 repos:
   - repo: https://github.com/asottile/pyupgrade
-    rev: v3.4.0
+    rev: v3.9.0
     hooks:
       - id: pyupgrade
   - repo: https://github.com/psf/black
-    rev: 23.3.0
+    rev: 23.7.0
     hooks:
       - id: black
   - repo: https://github.com/pre-commit/mirrors-prettier
-    rev: v3.0.0-alpha.9-for-vscode
+    rev: v3.0.0
     hooks:
       - id: prettier
   - repo: https://github.com/pre-commit/pre-commit-hooks
@@ -44,7 +44,7 @@
       - id: isort
         args: [--profile, black]
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v1.3.0
+    rev: v1.4.1
     hooks:
       - id: mypy
         args: [--show-error-codes, --ignore-missing-imports]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.18.0/CHANGELOG.md 
new/specfile-0.20.0/CHANGELOG.md
--- old/specfile-0.18.0/CHANGELOG.md    2023-05-26 11:50:44.000000000 +0200
+++ new/specfile-0.20.0/CHANGELOG.md    2023-07-13 17:26:17.000000000 +0200
@@ -1,3 +1,13 @@
+# 0.20.0
+
+- Fixed infinite loop when removing macros with `%` in the name. (#244)
+- Added a possibility to undefine system macros by setting a macro value to 
`None` in the `macros` argument of the `Specfile` constructor. (#244)
+- Fixed a bug in processing options of `%prep` macros. For instance, when a 
quoted string appeared inside an expression expansion, it could lead to 
improper parsing, rendering the spec file invalid after accessing the options. 
(#253)
+
+# 0.19.0
+
+- Parsing has been optimized so that even spec files with hundreds of 
thousands of lines can be processed in reasonable time. (#240)
+
 # 0.18.0
 
 - Specfile library now handles multiple `%changelog` sections. (#230)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.18.0/Containerfile.tests 
new/specfile-0.20.0/Containerfile.tests
--- old/specfile-0.18.0/Containerfile.tests     2023-05-26 11:50:44.000000000 
+0200
+++ new/specfile-0.20.0/Containerfile.tests     2023-07-13 17:26:17.000000000 
+0200
@@ -1,4 +1,4 @@
-FROM quay.io/packit/base
+FROM quay.io/packit/base:c9s
 
 COPY files/tasks/*.yaml /files/tasks/
 COPY files/*.yaml /files/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.18.0/PKG-INFO new/specfile-0.20.0/PKG-INFO
--- old/specfile-0.18.0/PKG-INFO        2023-05-26 11:50:53.421457300 +0200
+++ new/specfile-0.20.0/PKG-INFO        2023-07-13 17:26:27.064338700 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: specfile
-Version: 0.18.0
+Version: 0.20.0
 Summary: A library for parsing and manipulating RPM spec files.
 Home-page: https://github.com/packit/specfile
 Author: Red Hat
@@ -105,6 +105,16 @@
     ...
 ```
 
+### Defining and undefining macros
+
+```python
+# override macros loaded from system macro files
+specfile = Specfile('test.spec', macros=[('fedora', '38'), ('dist', '.fc38')])
+
+# undefine a system macro (in case it's defined)
+specfile = Specfile('test.spec', macros=[('rhel', None)])
+```
+
 ### Low-level manipulation
 
 ```python
@@ -243,8 +253,40 @@
 specfile.url = 'https://example.com'
 ```
 
+Note that if you want to access multiple tag values, it may be noticeably 
faster to do it using the `tags` context manager:
+
+```python
+# same as above, but roughly 4x times faster (parsing/saving happens only once)
+with specfile.tags() as tags:
+    print(tags.name.value)
+    print(tags.license.value)
+    print(tags.summary.value)
+    tags.url.value = 'https://example.com'
+```
+
+### Read-only access
+
+If you don't need write access, you can use the `content` property of context 
managers and avoid the `with` statement:
+
+```python
+# no changes done to the tags object will be saved
+tags = specfile.tags().content
+
+print(tags.version.expanded_value)
+print(tags.release.expanded_value)
+
+# number of sources
+print(len(specfile.sources().content))
+```
+
 ## Caveats
 
 ### RPM macros
 
 specfile uses RPM for parsing spec files and macro expansion. Unfortunately, 
macros are always stored in a global context, which poses a problem for 
multiple instances of Specfile.
+
+## Videos
+
+Here is a demo showcasing the `Specfile.update_tag()` method and its use cases:
+
+[![Demo of Specfile.update_tag() 
functionality](https://img.youtube.com/vi/yzMfBPdFXZY/0.jpg)](https://www.youtube.com/watch?v=yzMfBPdFXZY)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.18.0/README.md 
new/specfile-0.20.0/README.md
--- old/specfile-0.18.0/README.md       2023-05-26 11:50:44.000000000 +0200
+++ new/specfile-0.20.0/README.md       2023-07-13 17:26:17.000000000 +0200
@@ -78,6 +78,16 @@
     ...
 ```
 
+### Defining and undefining macros
+
+```python
+# override macros loaded from system macro files
+specfile = Specfile('test.spec', macros=[('fedora', '38'), ('dist', '.fc38')])
+
+# undefine a system macro (in case it's defined)
+specfile = Specfile('test.spec', macros=[('rhel', None)])
+```
+
 ### Low-level manipulation
 
 ```python
@@ -216,8 +226,40 @@
 specfile.url = 'https://example.com'
 ```
 
+Note that if you want to access multiple tag values, it may be noticeably 
faster to do it using the `tags` context manager:
+
+```python
+# same as above, but roughly 4x times faster (parsing/saving happens only once)
+with specfile.tags() as tags:
+    print(tags.name.value)
+    print(tags.license.value)
+    print(tags.summary.value)
+    tags.url.value = 'https://example.com'
+```
+
+### Read-only access
+
+If you don't need write access, you can use the `content` property of context 
managers and avoid the `with` statement:
+
+```python
+# no changes done to the tags object will be saved
+tags = specfile.tags().content
+
+print(tags.version.expanded_value)
+print(tags.release.expanded_value)
+
+# number of sources
+print(len(specfile.sources().content))
+```
+
 ## Caveats
 
 ### RPM macros
 
 specfile uses RPM for parsing spec files and macro expansion. Unfortunately, 
macros are always stored in a global context, which poses a problem for 
multiple instances of Specfile.
+
+## Videos
+
+Here is a demo showcasing the `Specfile.update_tag()` method and its use cases:
+
+[![Demo of Specfile.update_tag() 
functionality](https://img.youtube.com/vi/yzMfBPdFXZY/0.jpg)](https://www.youtube.com/watch?v=yzMfBPdFXZY)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.18.0/fedora/python-specfile.spec 
new/specfile-0.20.0/fedora/python-specfile.spec
--- old/specfile-0.18.0/fedora/python-specfile.spec     2023-05-26 
11:50:44.000000000 +0200
+++ new/specfile-0.20.0/fedora/python-specfile.spec     2023-07-13 
17:26:17.000000000 +0200
@@ -13,7 +13,7 @@
 
 
 Name:           python-specfile
-Version:        0.18.0
+Version:        0.20.0
 Release:        1%{?dist}
 
 Summary:        A library for parsing and manipulating RPM spec files
@@ -24,7 +24,7 @@
 
 BuildArch:      noarch
 
-BuildRequires:  python%{python3_pkgversion}-devel
+BuildRequires:  python3-devel
 %if %{with tests}
 # tests/unit/test_guess_packager.py
 BuildRequires:  git-core
@@ -71,6 +71,12 @@
 
 
 %changelog
+* Thu Jul 13 2023 Packit Team <he...@packit.dev> - 0.20.0-1
+- New upstream release 0.20.0
+
+* Thu Jun 22 2023 Packit Team <he...@packit.dev> - 0.19.0-1
+- New upstream release 0.19.0
+
 * Fri May 26 2023 Packit Team <he...@packit.dev> - 0.18.0-1
 - New upstream release 0.18.0
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.18.0/files/tasks/rpm-test-deps.yaml 
new/specfile-0.20.0/files/tasks/rpm-test-deps.yaml
--- old/specfile-0.18.0/files/tasks/rpm-test-deps.yaml  2023-05-26 
11:50:44.000000000 +0200
+++ new/specfile-0.20.0/files/tasks/rpm-test-deps.yaml  2023-07-13 
17:26:17.000000000 +0200
@@ -2,7 +2,10 @@
 - name: Install test RPM dependencies
   dnf:
     name:
-      - python3-flexmock
       - python3-pytest
       - python3-pytest-cov
   become: true
+- name: Pip install test dependencies
+  ansible.builtin.pip:
+    name:
+      - flexmock # RHBZ#2120251
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.18.0/specfile/macros.py 
new/specfile-0.20.0/specfile/macros.py
--- old/specfile-0.18.0/specfile/macros.py      2023-05-26 11:50:44.000000000 
+0200
+++ new/specfile-0.20.0/specfile/macros.py      2023-07-13 17:26:17.000000000 
+0200
@@ -169,7 +169,7 @@
         while retry < MAX_REMOVAL_RETRIES:
             rpm.delMacro(macro)
             try:
-                if cls.expand(f"%{macro}") == f"%{macro}":
+                if cls.expand(f"%{{{macro}}}") == f"%{{{macro.replace('%%', 
'%')}}}":
                     break
             except RPMException:
                 # the macro can't be expanded, but it still exists
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.18.0/specfile/options.py 
new/specfile-0.20.0/specfile/options.py
--- old/specfile-0.18.0/specfile/options.py     2023-05-26 11:50:44.000000000 
+0200
+++ new/specfile-0.20.0/specfile/options.py     2023-07-13 17:26:17.000000000 
+0200
@@ -9,6 +9,7 @@
 
 from specfile.exceptions import OptionsException
 from specfile.formatter import formatted
+from specfile.value_parser import Node, StringLiteral, ValueParser
 
 
 class TokenType(Enum):
@@ -474,63 +475,80 @@
         Raises:
             OptionsException if the option string is untokenizable.
         """
-        result = []
-        token = ""
-        quote = None
-        inp = list(option_string)
-        while inp:
-            c = inp.pop(0)
-            if c == quote:
-                if token:
-                    result.append(
-                        Token(
-                            TokenType.QUOTED
-                            if quote == "'"
-                            else TokenType.DOUBLE_QUOTED,
-                            token,
-                        )
-                    )
-                    token = ""
+        result: List[Token] = []
+
+        def append_default(s):
+            if result and result[-1].type == TokenType.DEFAULT:
+                result[-1].value += s
+            else:
+                result.append(Token(TokenType.DEFAULT, s))
+
+        token_nodes: List[Node] = []
+        for node in ValueParser.parse(option_string):
+            if isinstance(node, StringLiteral):
+                if token_nodes:
+                    append_default("".join(str(n) for n in token_nodes))
+                    token_nodes = []
+                token = ""
                 quote = None
-                continue
-            if quote:
-                if c == "\\":
-                    if not inp:
-                        raise OptionsException("No escaped character")
-                    c = inp.pop(0)
-                    if c != quote:
-                        token += "\\"
-                token += c
-                continue
-            if c.isspace():
-                if token:
-                    result.append(Token(TokenType.DEFAULT, token))
-                    token = ""
-                whitespace = c
+                inp = list(str(node))
                 while inp:
                     c = inp.pop(0)
-                    if not c.isspace():
-                        break
-                    whitespace += c
-                else:
-                    result.append(Token(TokenType.WHITESPACE, whitespace))
-                    break
-                inp.insert(0, c)
-                result.append(Token(TokenType.WHITESPACE, whitespace))
-                continue
-            if c in ('"', "'"):
+                    if c == quote:
+                        if token:
+                            result.append(
+                                Token(
+                                    TokenType.QUOTED
+                                    if quote == "'"
+                                    else TokenType.DOUBLE_QUOTED,
+                                    token,
+                                )
+                            )
+                            token = ""
+                        quote = None
+                        continue
+                    if quote:
+                        if c == "\\":
+                            if not inp:
+                                raise OptionsException("No escaped character")
+                            c = inp.pop(0)
+                            if c != quote:
+                                token += "\\"
+                        token += c
+                        continue
+                    if c.isspace():
+                        if token:
+                            append_default(token)
+                            token = ""
+                        whitespace = c
+                        while inp:
+                            c = inp.pop(0)
+                            if not c.isspace():
+                                break
+                            whitespace += c
+                        else:
+                            result.append(Token(TokenType.WHITESPACE, 
whitespace))
+                            break
+                        inp.insert(0, c)
+                        result.append(Token(TokenType.WHITESPACE, whitespace))
+                        continue
+                    if c in ('"', "'"):
+                        if token:
+                            append_default(token)
+                            token = ""
+                        quote = c
+                        continue
+                    if c == "\\":
+                        if not inp:
+                            raise OptionsException("No escaped character")
+                        c = inp.pop(0)
+                    token += c
+                if quote:
+                    raise OptionsException("No closing quotation")
                 if token:
-                    result.append(Token(TokenType.DEFAULT, token))
-                    token = ""
-                quote = c
-                continue
-            if c == "\\":
-                if not inp:
-                    raise OptionsException("No escaped character")
-                c = inp.pop(0)
-            token += c
-        if quote:
-            raise OptionsException("No closing quotation")
-        if token:
-            result.append(Token(TokenType.DEFAULT, token))
+                    append_default(token)
+            else:
+                token_nodes.append(node)
+        if token_nodes:
+            append_default("".join(str(n) for n in token_nodes))
         return result
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.18.0/specfile/sections.py 
new/specfile-0.20.0/specfile/sections.py
--- old/specfile-0.18.0/specfile/sections.py    2023-05-26 11:50:44.000000000 
+0200
+++ new/specfile-0.20.0/specfile/sections.py    2023-07-13 17:26:17.000000000 
+0200
@@ -12,6 +12,7 @@
     SECTION_OPTIONS,
     SIMPLE_SCRIPT_SECTIONS,
 )
+from specfile.exceptions import RPMException
 from specfile.formatter import formatted
 from specfile.macro_definitions import MacroDefinitions
 from specfile.macros import Macros
@@ -231,19 +232,29 @@
 
         def expand(s):
             if context:
-                return context.expand(s)
+                result = context.expand(
+                    s, skip_parsing=getattr(expand, "skip_parsing", False)
+                )
+                # parse only once
+                expand.skip_parsing = True
+                return result
             return Macros.expand(s)
 
         def split_id(line):
             content = []
             separator = "\n"
             tokens = re.split(r"(\s+)", line)
-            if len(tokens) > 2:
+            if len(tokens) > 2 and tokens[-1].startswith("%"):
                 # if the last token after macro expansion starts with a 
newline,
                 # consider it part of section content
-                if expand(tokens[-1]).startswith("\n"):
-                    content = [tokens.pop()]
-                    separator = tokens.pop()
+                try:
+                    expanded = expand(tokens[-1])
+                except RPMException:
+                    pass
+                else:
+                    if expanded.startswith("\n"):
+                        content = [tokens.pop()]
+                        separator = tokens.pop()
             if len(tokens) > 2:
                 name = tokens[0]
                 delimiter = tokens[1]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.18.0/specfile/spec_parser.py 
new/specfile-0.20.0/specfile/spec_parser.py
--- old/specfile-0.18.0/specfile/spec_parser.py 2023-05-26 11:50:44.000000000 
+0200
+++ new/specfile-0.20.0/specfile/spec_parser.py 2023-07-13 17:26:17.000000000 
+0200
@@ -49,7 +49,7 @@
     def __init__(
         self,
         sourcedir: Path,
-        macros: Optional[List[Tuple[str, str]]] = None,
+        macros: Optional[List[Tuple[str, Optional[str]]]] = None,
         force_parse: bool = False,
     ) -> None:
         self.sourcedir = sourcedir
@@ -170,7 +170,9 @@
                 restore(key)
 
     def _do_parse(
-        self, content: str, extra_macros: Optional[List[Tuple[str, str]]] = 
None
+        self,
+        content: str,
+        extra_macros: Optional[List[Tuple[str, Optional[str]]]] = None,
     ) -> Tuple[rpm.spec, bool]:
         """
         Parses the content of a spec file.
@@ -190,7 +192,10 @@
         def get_rpm_spec(content, flags):
             Macros.reinit()
             for name, value in self.macros + (extra_macros or []):
-                Macros.define(name, value)
+                if value is None:
+                    Macros.remove(name)
+                else:
+                    Macros.define(name, value)
             Macros.define("_sourcedir", str(self.sourcedir))
             with tempfile.NamedTemporaryFile() as tmp:
                 tmp.write(content.encode())
@@ -318,7 +323,9 @@
             return get_rpm_spec(content, rpm.RPMSPEC_ANYARCH), tainted
 
     def parse(
-        self, content: str, extra_macros: Optional[List[Tuple[str, str]]] = 
None
+        self,
+        content: str,
+        extra_macros: Optional[List[Tuple[str, Optional[str]]]] = None,
     ) -> None:
         """
         Parses the content of a spec file and updates the `spec` and `tainted` 
attributes.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.18.0/specfile/specfile.py 
new/specfile-0.20.0/specfile/specfile.py
--- old/specfile-0.18.0/specfile/specfile.py    2023-05-26 11:50:44.000000000 
+0200
+++ new/specfile-0.20.0/specfile/specfile.py    2023-07-13 17:26:17.000000000 
+0200
@@ -43,7 +43,7 @@
         path: Union[Path, str],
         sourcedir: Optional[Union[Path, str]] = None,
         autosave: bool = False,
-        macros: Optional[List[Tuple[str, str]]] = None,
+        macros: Optional[List[Tuple[str, Optional[str]]]] = None,
         force_parse: bool = False,
     ) -> None:
         """
@@ -64,7 +64,7 @@
         """
         self.autosave = autosave
         self._path = Path(path)
-        self._lines = self.path.read_text().splitlines()
+        self._lines = self._read_lines(self._path)
         self._parser = SpecParser(
             Path(sourcedir or self.path.parent), macros, force_parse
         )
@@ -101,6 +101,10 @@
     ) -> None:
         self.save()
 
+    @staticmethod
+    def _read_lines(path: Path) -> List[str]:
+        return path.read_text(encoding="utf8", 
errors="surrogateescape").splitlines()
+
     @property
     def path(self) -> Path:
         """Path to the spec file."""
@@ -120,7 +124,7 @@
         self._parser.sourcedir = Path(value)
 
     @property
-    def macros(self) -> List[Tuple[str, str]]:
+    def macros(self) -> List[Tuple[str, Optional[str]]]:
         """List of extra macro definitions."""
         return self._parser.macros
 
@@ -154,16 +158,17 @@
 
     def reload(self) -> None:
         """Reload the spec file content."""
-        self._lines = self.path.read_text().splitlines()
+        self._lines = self._read_lines(self.path)
 
     def save(self) -> None:
         """Save the spec file content."""
-        self.path.write_text(str(self))
+        self.path.write_text(str(self), encoding="utf8", 
errors="surrogateescape")
 
     def expand(
         self,
         expression: str,
-        extra_macros: Optional[List[Tuple[str, str]]] = None,
+        extra_macros: Optional[List[Tuple[str, Optional[str]]]] = None,
+        skip_parsing: bool = False,
     ) -> str:
         """
         Expands an expression in the context of the spec file.
@@ -171,11 +176,15 @@
         Args:
             expression: Expression to expand.
             extra_macros: Extra macros to be defined before expansion is 
performed.
+            skip_parsing: Do not parse the spec file before expansion is 
performed.
+              Defaults to False. Mutually exclusive with extra_macros. Set 
this to True
+              only if you are certain that the global macro context is 
up-to-date.
 
         Returns:
             Expanded expression.
         """
-        self._parser.parse(str(self), extra_macros)
+        if not skip_parsing or extra_macros is not None:
+            self._parser.parse(str(self), extra_macros)
         return Macros.expand(expression)
 
     def get_active_macros(self) -> List[Macro]:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.18.0/specfile/value_parser.py 
new/specfile-0.20.0/specfile/value_parser.py
--- old/specfile-0.18.0/specfile/value_parser.py        2023-05-26 
11:50:44.000000000 +0200
+++ new/specfile-0.20.0/specfile/value_parser.py        2023-07-13 
17:26:17.000000000 +0200
@@ -312,7 +312,12 @@
 
         def expand(s):
             if context:
-                return context.expand(s)
+                result = context.expand(
+                    s, skip_parsing=getattr(expand, "skip_parsing", False)
+                )
+                # parse only once
+                expand.skip_parsing = True
+                return result
             return Macros.expand(s)
 
         def flatten(nodes):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.18.0/specfile.egg-info/PKG-INFO 
new/specfile-0.20.0/specfile.egg-info/PKG-INFO
--- old/specfile-0.18.0/specfile.egg-info/PKG-INFO      2023-05-26 
11:50:53.000000000 +0200
+++ new/specfile-0.20.0/specfile.egg-info/PKG-INFO      2023-07-13 
17:26:26.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: specfile
-Version: 0.18.0
+Version: 0.20.0
 Summary: A library for parsing and manipulating RPM spec files.
 Home-page: https://github.com/packit/specfile
 Author: Red Hat
@@ -105,6 +105,16 @@
     ...
 ```
 
+### Defining and undefining macros
+
+```python
+# override macros loaded from system macro files
+specfile = Specfile('test.spec', macros=[('fedora', '38'), ('dist', '.fc38')])
+
+# undefine a system macro (in case it's defined)
+specfile = Specfile('test.spec', macros=[('rhel', None)])
+```
+
 ### Low-level manipulation
 
 ```python
@@ -243,8 +253,40 @@
 specfile.url = 'https://example.com'
 ```
 
+Note that if you want to access multiple tag values, it may be noticeably 
faster to do it using the `tags` context manager:
+
+```python
+# same as above, but roughly 4x times faster (parsing/saving happens only once)
+with specfile.tags() as tags:
+    print(tags.name.value)
+    print(tags.license.value)
+    print(tags.summary.value)
+    tags.url.value = 'https://example.com'
+```
+
+### Read-only access
+
+If you don't need write access, you can use the `content` property of context 
managers and avoid the `with` statement:
+
+```python
+# no changes done to the tags object will be saved
+tags = specfile.tags().content
+
+print(tags.version.expanded_value)
+print(tags.release.expanded_value)
+
+# number of sources
+print(len(specfile.sources().content))
+```
+
 ## Caveats
 
 ### RPM macros
 
 specfile uses RPM for parsing spec files and macro expansion. Unfortunately, 
macros are always stored in a global context, which poses a problem for 
multiple instances of Specfile.
+
+## Videos
+
+Here is a demo showcasing the `Specfile.update_tag()` method and its use cases:
+
+[![Demo of Specfile.update_tag() 
functionality](https://img.youtube.com/vi/yzMfBPdFXZY/0.jpg)](https://www.youtube.com/watch?v=yzMfBPdFXZY)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.18.0/tests/unit/test_macros.py 
new/specfile-0.20.0/tests/unit/test_macros.py
--- old/specfile-0.18.0/tests/unit/test_macros.py       2023-05-26 
11:50:44.000000000 +0200
+++ new/specfile-0.20.0/tests/unit/test_macros.py       2023-07-13 
17:26:17.000000000 +0200
@@ -81,6 +81,11 @@
     Macros.remove("test")
     Macros.remove("non_existent_macro")
     assert Macros.dump() == macros
+    rpm.addMacro("%test", "1")
+    rpm.addMacro("te%st%%", "2")
+    Macros.remove("%test")
+    Macros.remove("te%st%%")
+    assert Macros.dump() == macros
 
 
 def test_macros_remove_failure():
@@ -88,7 +93,7 @@
     # ensure that we are not stuck in an infinite loop
     rpm.reloadConfig()
     rpm.addMacro("foo", "bar")
-    
flexmock(rpm).should_receive("expandMacro").with_args("%foo").and_raise(rpm.error)
+    
flexmock(rpm).should_receive("expandMacro").with_args("%{foo}").and_raise(rpm.error)
     with pytest.raises(MacroRemovalException):
         Macros.remove("foo")
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.18.0/tests/unit/test_options.py 
new/specfile-0.20.0/tests/unit/test_options.py
--- old/specfile-0.18.0/tests/unit/test_options.py      2023-05-26 
11:50:44.000000000 +0200
+++ new/specfile-0.20.0/tests/unit/test_options.py      2023-07-13 
17:26:17.000000000 +0200
@@ -248,6 +248,16 @@
                 Token(TokenType.WHITESPACE, "  "),
             ],
         ),
+        (
+            '-q -n %{name}-%{version}%[%{rc}?"-rc":""]',
+            [
+                Token(TokenType.DEFAULT, "-q"),
+                Token(TokenType.WHITESPACE, " "),
+                Token(TokenType.DEFAULT, "-n"),
+                Token(TokenType.WHITESPACE, " "),
+                Token(TokenType.DEFAULT, 
'%{name}-%{version}%[%{rc}?"-rc":""]'),
+            ],
+        ),
     ],
 )
 def test_options_tokenize(option_string, result):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/specfile-0.18.0/tests/unit/test_spec_parser.py 
new/specfile-0.20.0/tests/unit/test_spec_parser.py
--- old/specfile-0.18.0/tests/unit/test_spec_parser.py  2023-05-26 
11:50:44.000000000 +0200
+++ new/specfile-0.20.0/tests/unit/test_spec_parser.py  2023-07-13 
17:26:17.000000000 +0200
@@ -5,6 +5,7 @@
 from pathlib import Path
 
 import rpm
+from flexmock import flexmock
 
 from specfile.spec_parser import SpecParser
 
@@ -39,3 +40,27 @@
     deep_copy = copy.deepcopy(parser)
     assert deep_copy == parser
     assert deep_copy is not parser
+
+
+def test_spec_parser_macros():
+    flexmock(rpm).should_call("delMacro").with_args(
+        "fedora"
+    ).at_least().once().ordered()
+    
flexmock(rpm).should_call("delMacro").with_args("rhel").at_least().once().ordered()
+    flexmock(rpm).should_call("addMacro").with_args("rhel", 
"9").once().ordered()
+    # we don't care about the rest
+    flexmock(rpm).should_call("addMacro")
+    flexmock(rpm).should_call("delMacro")
+    parser = SpecParser(Path("."), macros=[("fedora", None), ("rhel", "9")])
+    spec, _ = parser._do_parse(
+        (
+            "Name:           test\n"
+            "Version:        0.1\n"
+            "Release:        1%{?dist}\n"
+            "Summary:        Test package\n"
+            "License:        MIT\n"
+            "\n"
+            "%description\n"
+            "Test package\n"
+        ),
+    )

Reply via email to