Skip to content

Commit 13897cd

Browse files
committed
Provide context for macro expansions
When using multiple instances of Specfile, macros have to be expanded in the right context (because RPM uses a single global macro context, so this global context has to be emptied and repopulated on every context switch). Signed-off-by: Nikola Forró <[email protected]>
1 parent 726c9ec commit 13897cd

File tree

5 files changed

+81
-17
lines changed

5 files changed

+81
-17
lines changed

specfile/sourcelist.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
# SPDX-License-Identifier: MIT
33

44
import collections
5-
from typing import List, Optional, SupportsIndex, overload
5+
from typing import TYPE_CHECKING, List, Optional, SupportsIndex, overload
66

77
from specfile.rpm import Macros
88
from specfile.sections import Section
99
from specfile.tags import Comments
1010

11+
if TYPE_CHECKING:
12+
from specfile.specfile import Specfile
13+
1114

1215
class SourcelistEntry:
1316
"""
@@ -18,9 +21,23 @@ class SourcelistEntry:
1821
comments: List of comments associated with the source/patch.
1922
"""
2023

21-
def __init__(self, location: str, comments: Comments) -> None:
24+
def __init__(
25+
self, location: str, comments: Comments, context: Optional["Specfile"] = None
26+
) -> None:
27+
"""
28+
Constructs a `SourceListEntry` object.
29+
30+
Args:
31+
location: Literal location of the source/patch as stored in the spec file.
32+
comments: List of comments associated with the source/patch.
33+
context: `Specfile` instance that defines the context for macro expansions.
34+
35+
Returns:
36+
Constructed instance of `SourceListEntry` class.
37+
"""
2238
self.location = location
2339
self.comments = comments.copy()
40+
self._context = context
2441

2542
def __eq__(self, other: object) -> bool:
2643
if not isinstance(other, SourcelistEntry):
@@ -34,6 +51,8 @@ def __repr__(self) -> str:
3451
@property
3552
def expanded_location(self) -> str:
3653
"""URL of the source/patch after expanding macros."""
54+
if self._context:
55+
return self._context.expand(self.location)
3756
return Macros.expand(self.location)
3857

3958

@@ -88,12 +107,15 @@ def copy(self) -> "Sourcelist":
88107
return Sourcelist(self.data, self._remainder)
89108

90109
@classmethod
91-
def parse(cls, section: Section) -> "Sourcelist":
110+
def parse(
111+
cls, section: Section, context: Optional["Specfile"] = None
112+
) -> "Sourcelist":
92113
"""
93114
Parses a section into sources/patches.
94115
95116
Args:
96117
section: %sourcelist/%patchlist section.
118+
context: `Specfile` instance that defines the context for macro expansions.
97119
98120
Returns:
99121
Constructed instance of `Sourcelist` class.
@@ -102,7 +124,7 @@ def parse(cls, section: Section) -> "Sourcelist":
102124
buffer: List[str] = []
103125
for line in section:
104126
if line and not line.lstrip().startswith("#"):
105-
data.append(SourcelistEntry(line, Comments.parse(buffer)))
127+
data.append(SourcelistEntry(line, Comments.parse(buffer), context))
106128
buffer = []
107129
else:
108130
buffer.append(line)

specfile/sources.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@
55
import re
66
import urllib.parse
77
from abc import ABC, abstractmethod
8-
from typing import Iterable, List, Optional, Tuple, Union, cast, overload
8+
from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple, Union, cast, overload
99

1010
from specfile.exceptions import DuplicateSourceException
1111
from specfile.rpm import Macros
1212
from specfile.sourcelist import Sourcelist, SourcelistEntry
1313
from specfile.tags import Comments, Tag, Tags
1414
from specfile.utils import get_filename_from_location
1515

16+
if TYPE_CHECKING:
17+
from specfile.specfile import Specfile
18+
1619

1720
class Source(ABC):
1821
"""Class that represents a source."""
@@ -218,6 +221,7 @@ def __init__(
218221
allow_duplicates: bool = False,
219222
default_to_implicit_numbering: bool = False,
220223
default_source_number_digits: int = 1,
224+
context: Optional["Specfile"] = None,
221225
) -> None:
222226
"""
223227
Constructs a `Sources` object.
@@ -228,6 +232,7 @@ def __init__(
228232
allow_duplicates: Whether to allow duplicate entries when adding new sources.
229233
default_to_implicit_numbering: Use implicit numbering (no source numbers) by default.
230234
default_source_number_digits: Default number of digits in a source number.
235+
context: `Specfile` instance that defines the context for macro expansions.
231236
232237
Returns:
233238
Constructed instance of `Sources` class.
@@ -237,6 +242,7 @@ def __init__(
237242
self._allow_duplicates = allow_duplicates
238243
self._default_to_implicit_numbering = default_to_implicit_numbering
239244
self._default_source_number_digits = default_source_number_digits
245+
self._context = context
240246

241247
def __repr__(self) -> str:
242248
tags = repr(self._tags)
@@ -306,6 +312,11 @@ def __delitem__(self, i: Union[int, slice]) -> None:
306312
_, container, index = items[i]
307313
del container[index]
308314

315+
def _expand(self, s: str) -> str:
316+
if self._context:
317+
return self._context.expand(s)
318+
return Macros.expand(s)
319+
309320
def _get_tags(self) -> List[Tuple[TagSource, Tags, int]]:
310321
"""
311322
Gets all tag sources.
@@ -474,7 +485,7 @@ def insert(self, i: int, location: str) -> None:
474485
name, separator = self._get_tag_format(source, number)
475486
container.insert(
476487
index,
477-
Tag(name, location, Macros.expand(location), separator, Comments()),
488+
Tag(name, location, self._expand(location), separator, Comments()),
478489
)
479490
self._deduplicate_tag_names(i)
480491
else:
@@ -488,7 +499,7 @@ def insert(self, i: int, location: str) -> None:
488499
index, name, separator = self._get_initial_tag_setup()
489500
self._tags.insert(
490501
index,
491-
Tag(name, location, Macros.expand(location), separator, Comments()),
502+
Tag(name, location, self._expand(location), separator, Comments()),
492503
)
493504

494505
def insert_numbered(self, number: int, location: str) -> int:
@@ -522,7 +533,7 @@ def insert_numbered(self, number: int, location: str) -> int:
522533
i = 0
523534
index, name, separator = self._get_initial_tag_setup(number)
524535
self._tags.insert(
525-
index, Tag(name, location, Macros.expand(location), separator, Comments())
536+
index, Tag(name, location, self._expand(location), separator, Comments())
526537
)
527538
self._deduplicate_tag_names(i)
528539
return i

specfile/specfile.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,9 @@ def sources(
233233
"""
234234
with self.sections() as sections, self.tags(sections.package) as tags:
235235
sourcelists = [
236-
(s, Sourcelist.parse(s)) for s in sections if s.name == "sourcelist"
236+
(s, Sourcelist.parse(s, context=self))
237+
for s in sections
238+
if s.name == "sourcelist"
237239
]
238240
try:
239241
yield Sources(
@@ -242,6 +244,7 @@ def sources(
242244
allow_duplicates,
243245
default_to_implicit_numbering,
244246
default_source_number_digits,
247+
context=self,
245248
)
246249
finally:
247250
for section, sourcelist in sourcelists:
@@ -267,7 +270,9 @@ def patches(
267270
"""
268271
with self.sections() as sections, self.tags(sections.package) as tags:
269272
patchlists = [
270-
(s, Sourcelist.parse(s)) for s in sections if s.name == "patchlist"
273+
(s, Sourcelist.parse(s, context=self))
274+
for s in sections
275+
if s.name == "patchlist"
271276
]
272277
try:
273278
yield Patches(
@@ -276,6 +281,7 @@ def patches(
276281
allow_duplicates,
277282
default_to_implicit_numbering,
278283
default_source_number_digits,
284+
context=self,
279285
)
280286
finally:
281287
for section, patchlist in patchlists:
@@ -572,7 +578,9 @@ class Entity:
572578
entities.update({k.upper(): v for k, v in entities.items() if v.type == Tag})
573579

574580
def update(value, requested_value):
575-
regex, template = ValueParser.construct_regex(value, entities.keys())
581+
regex, template = ValueParser.construct_regex(
582+
value, entities.keys(), context=self
583+
)
576584
m = regex.match(requested_value)
577585
if m:
578586
d = m.groupdict()

specfile/value_parser.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
import re
66
from abc import ABC
77
from string import Template
8-
from typing import List, Tuple
8+
from typing import TYPE_CHECKING, List, Optional, Tuple
99
from typing.re import Pattern
1010

1111
from specfile.exceptions import UnterminatedMacroException
1212
from specfile.rpm import Macros
1313

14+
if TYPE_CHECKING:
15+
from specfile.specfile import Specfile
16+
1417
SUBSTITUTION_GROUP_PREFIX = "sub_"
1518

1619

@@ -227,7 +230,10 @@ def find_macro_end(index):
227230

228231
@classmethod
229232
def construct_regex(
230-
cls, value: str, modifiable_entities: List[str]
233+
cls,
234+
value: str,
235+
modifiable_entities: List[str],
236+
context: Optional["Specfile"] = None,
231237
) -> Tuple[Pattern, Template]:
232238
"""
233239
Parses the given value and constructs a regex that allows matching
@@ -250,18 +256,24 @@ def construct_regex(
250256
value: Value string to parse.
251257
modifiable_entities: Names of modifiable entities, i.e. local macro definitions
252258
and tags.
259+
context: `Specfile` instance that defines the context for macro expansions.
253260
254261
Returns:
255262
Tuple in the form of (constructed regex, corresponding template).
256263
"""
257264

265+
def expand(s):
266+
if context:
267+
return context.expand(s)
268+
return Macros.expand(s)
269+
258270
def flatten(nodes):
259271
# get rid of conditional macro expansions
260272
result = []
261273
for node in nodes:
262274
if isinstance(node, ConditionalMacroExpansion):
263275
# evaluate the condition
264-
if Macros.expand(f"%{node.prefix}{node.name}"):
276+
if expand(f"%{node.prefix}{node.name}"):
265277
result.append(f"%{{{node.prefix}{node.name}:")
266278
result.extend(flatten(node.body))
267279
result.append("}")
@@ -281,13 +293,13 @@ def flatten(nodes):
281293
elif isinstance(node, StringLiteral):
282294
tokens.append(("v", node.value, ""))
283295
elif isinstance(node, (ShellExpansion, ExpressionExpansion)):
284-
const = Macros.expand(str(node))
296+
const = expand(str(node))
285297
tokens.append(("c", const, str(node)))
286298
elif isinstance(node, MacroSubstitution):
287299
if node.prefix.count("!") % 2 == 0 and node.name in modifiable_entities:
288300
tokens.append(("g", node.name, str(node)))
289301
else:
290-
const = Macros.expand(str(node))
302+
const = expand(str(node))
291303
tokens.append(("c", const, str(node)))
292304
elif isinstance(node, EnclosedMacroSubstitution):
293305
if (
@@ -297,7 +309,7 @@ def flatten(nodes):
297309
):
298310
tokens.append(("g", node.name, str(node)))
299311
else:
300-
const = Macros.expand(str(node))
312+
const = expand(str(node))
301313
tokens.append(("c", const, str(node)))
302314

303315
def escape(s):

tests/integration/test_specfile.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,3 +333,14 @@ def test_update_tag(spec_macros):
333333
assert md.minorver.body == "2"
334334
with spec.sources() as sources:
335335
assert sources[1].location == "tests-86.tar.xz"
336+
337+
338+
def test_multiple_instances(spec_minimal, spec_autosetup):
339+
spec1 = Specfile(spec_minimal)
340+
spec2 = Specfile(spec_autosetup)
341+
spec1.version = "14.2"
342+
assert spec2.expanded_version == "0.1"
343+
with spec2.sources() as sources:
344+
assert sources[0].expanded_location == "test-0.1.tar.xz"
345+
sources.append("tests-%{version}.tar.xz")
346+
assert sources[1].expanded_location == "tests-0.1.tar.xz"

0 commit comments

Comments
 (0)