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: + +[](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: + +[](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: + +[](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" + ), + )