Skip to content

Commit 9fb529c

Browse files
committed
Parse only if necessary
Parsing before every macro expansion etc. is unnecessary and can cause performance issues, because parsing is a really slow operation. Parse only after a context switch (necessary because all `Specfile` instances share a single global macro context) or if something that affects parsing has actually changed. Signed-off-by: Nikola Forró <[email protected]>
1 parent 9ccbd97 commit 9fb529c

File tree

5 files changed

+49
-21
lines changed

5 files changed

+49
-21
lines changed

specfile/sections.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -231,12 +231,7 @@ def parse(
231231

232232
def expand(s):
233233
if context:
234-
result = context.expand(
235-
s, skip_parsing=getattr(expand, "skip_parsing", False)
236-
)
237-
# parse only once
238-
expand.skip_parsing = True
239-
return result
234+
return context.expand(s)
240235
return Macros.expand(s)
241236

242237
def split_id(line):

specfile/spec_parser.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
import contextlib
55
import copy
6+
import hashlib
67
import logging
78
import os
9+
import pickle
810
import re
911
import tempfile
1012
from pathlib import Path
@@ -41,6 +43,9 @@ class SpecParser:
4143
and were replaced with dummy files.
4244
"""
4345

46+
# hash of input parameters to the last parse performed
47+
_last_parse_hash = None
48+
4449
def __init__(
4550
self,
4651
sourcedir: Path,
@@ -325,11 +330,32 @@ def parse(
325330
Raises:
326331
RPMException, if parsing error occurs.
327332
"""
333+
# calculate hash of all input parameters
334+
payload = (
335+
id(self),
336+
self.sourcedir,
337+
self.macros,
338+
self.force_parse,
339+
content,
340+
extra_macros,
341+
)
342+
parse_hash = hashlib.sha256(
343+
pickle.dumps(payload, protocol=pickle.HIGHEST_PROTOCOL)
344+
).digest()
345+
if parse_hash == SpecParser._last_parse_hash:
346+
# none of the input parameters has changed, no need to parse again
347+
return
328348
if self.spec:
329349
# workaround RPM lua tables feature/bug, see above for details
330350
del self.spec
331351
try:
332-
self.spec, self.tainted = self._do_parse(content, extra_macros)
352+
try:
353+
self.spec, self.tainted = self._do_parse(content, extra_macros)
354+
except Exception:
355+
SpecParser._last_parse_hash = None
356+
raise
357+
else:
358+
SpecParser._last_parse_hash = parse_hash
333359
except RPMException:
334360
self.spec = None
335361
self.tainted = False

specfile/specfile.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ def __init__(
6464
self._parser = SpecParser(
6565
Path(sourcedir or self.path.parent), macros, force_parse
6666
)
67-
# parse here to fail early on parsing errors
6867
self._parser.parse(str(self))
6968

7069
def __eq__(self, other: object) -> bool:
@@ -161,23 +160,18 @@ def expand(
161160
self,
162161
expression: str,
163162
extra_macros: Optional[List[Tuple[str, str]]] = None,
164-
skip_parsing: bool = False,
165163
) -> str:
166164
"""
167165
Expands an expression in the context of the spec file.
168166
169167
Args:
170168
expression: Expression to expand.
171169
extra_macros: Extra macros to be defined before expansion is performed.
172-
skip_parsing: Do not parse the spec file before expansion is performed.
173-
Defaults to False. Mutually exclusive with extra_macros. Set this to True
174-
only if you are certain that the global macro context is up-to-date.
175170
176171
Returns:
177172
Expanded expression.
178173
"""
179-
if not skip_parsing:
180-
self._parser.parse(str(self), extra_macros)
174+
self._parser.parse(str(self), extra_macros)
181175
return Macros.expand(expression)
182176

183177
def get_active_macros(self) -> List[Macro]:

specfile/value_parser.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -312,12 +312,7 @@ def construct_regex(
312312

313313
def expand(s):
314314
if context:
315-
result = context.expand(
316-
s, skip_parsing=getattr(expand, "skip_parsing", False)
317-
)
318-
# parse only once
319-
expand.skip_parsing = True
320-
return result
315+
return context.expand(s)
321316
return Macros.expand(s)
322317

323318
def flatten(nodes):

tests/integration/test_specfile.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from specfile.exceptions import RPMException, SpecfileException
1313
from specfile.prep import AutopatchMacro, AutosetupMacro, PatchMacro, SetupMacro
1414
from specfile.sections import Section
15-
from specfile.specfile import Specfile
15+
from specfile.specfile import Specfile, SpecParser
1616

1717

1818
def test_parse(spec_multiple_sources):
@@ -481,3 +481,21 @@ def test_copy(spec_autosetup):
481481
assert deep_copy is not spec
482482
assert deep_copy._lines is not spec._lines
483483
assert deep_copy._parser is not spec._parser
484+
485+
486+
def test_parse_if_necessary(spec_macros):
487+
flexmock(SpecParser).should_call("_do_parse").once()
488+
spec1 = Specfile(spec_macros)
489+
spec2 = copy.deepcopy(spec1)
490+
flexmock(SpecParser).should_call("_do_parse").never()
491+
assert spec1.expanded_name == "test"
492+
flexmock(SpecParser).should_call("_do_parse").once()
493+
assert spec2.expanded_name == "test"
494+
assert spec2.expanded_version == "0.1.2~rc2"
495+
flexmock(SpecParser).should_call("_do_parse").once()
496+
assert spec1.expanded_version == "0.1.2~rc2"
497+
with spec1.macro_definitions() as md:
498+
md[0].body = "28"
499+
flexmock(SpecParser).should_call("_do_parse").once()
500+
assert spec1.expanded_name == "test"
501+
assert spec1.expanded_version == "28.1.2~rc2"

0 commit comments

Comments
 (0)