Skip to content

Commit ac1113f

Browse files
authored
Merge pull request #69 from packit/nforro-misc
Various fixes and improvements
2 parents 85bf2fe + ae65ce3 commit ac1113f

File tree

19 files changed

+485
-45
lines changed

19 files changed

+485
-45
lines changed

.pre-commit-config.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@
44

55
repos:
66
- repo: https://github.com/asottile/pyupgrade
7-
rev: v2.32.1
7+
rev: v2.37.1
88
hooks:
99
- id: pyupgrade
1010
- repo: https://github.com/psf/black
11-
rev: 22.3.0
11+
rev: 22.6.0
1212
hooks:
1313
- id: black
1414
- repo: https://github.com/pre-commit/mirrors-prettier
15-
rev: v2.6.2
15+
rev: v2.7.1
1616
hooks:
1717
- id: prettier
1818
- repo: https://github.com/pre-commit/pre-commit-hooks
19-
rev: v4.2.0
19+
rev: v4.3.0
2020
hooks:
2121
- id: check-added-large-files
2222
- id: check-ast
@@ -44,7 +44,7 @@ repos:
4444
- id: isort
4545
args: [--profile, black]
4646
- repo: https://github.com/pre-commit/mirrors-mypy
47-
rev: v0.960
47+
rev: v0.961
4848
hooks:
4949
- id: mypy
5050
args: [--show-error-codes, --ignore-missing-imports]

specfile/exceptions.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
class SpecfileException(Exception):
88
"""Something went wrong during our execution."""
99

10-
pass
11-
1210

1311
class RPMException(SpecfileException):
1412
"""Exception related to RPM."""
@@ -31,3 +29,11 @@ class MacroRemovalException(SpecfileException):
3129

3230
class MacroOptionsException(SpecfileException):
3331
"""Exception related to processing macro options."""
32+
33+
34+
class DuplicateSourceException(SpecfileException):
35+
"""Exception related to adding a duplicate source."""
36+
37+
38+
class SourceNumberException(SpecfileException):
39+
"""Exception related to source numbers."""

specfile/sourcelist.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ def __init__(self, location: str, comments: Comments) -> None:
2323
self.location = location
2424
self.comments = comments.copy()
2525

26+
def __eq__(self, other: object) -> bool:
27+
if not isinstance(other, SourcelistEntry):
28+
return NotImplemented
29+
return self.location == other.location and self.comments == other.comments
30+
2631
def __repr__(self) -> str:
2732
comments = repr(self.comments)
2833
return f"SourcelistEntry('{self.location}', {comments})"

specfile/sources.py

Lines changed: 64 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pathlib import Path
99
from typing import Iterable, List, Optional, Tuple, Union, cast, overload
1010

11-
from specfile.exceptions import SpecfileException
11+
from specfile.exceptions import DuplicateSourceException
1212
from specfile.rpm import Macros
1313
from specfile.sourcelist import Sourcelist, SourcelistEntry
1414
from specfile.tags import Comments, Tag, Tags
@@ -100,14 +100,25 @@ def _extract_number(self) -> Optional[str]:
100100
@property
101101
def number(self) -> int:
102102
"""Source number."""
103-
return self._number or int(self._extract_number() or 0)
103+
if self._number is not None:
104+
return self._number
105+
return int(self._extract_number() or 0)
104106

105107
@property
106108
def number_digits(self) -> int:
107-
"""Number of digits in the source number."""
108-
if self._number:
109+
"""
110+
Gets number of digits in the source number.
111+
112+
Returns 0 if the source has no number, 1 if the source number
113+
has no leading zeros and the actual number of digits if there are
114+
any leading zeros.
115+
"""
116+
if self._number is not None:
109117
return 0
110-
return len(self._extract_number() or "")
118+
number = self._extract_number()
119+
if not number:
120+
return 0
121+
return len(number) if number.startswith("0") else 1
111122

112123
@property
113124
def location(self) -> str:
@@ -378,13 +389,14 @@ def _get_tag_format(
378389
Tuple in the form of (name, separator).
379390
"""
380391
prefix = self.PREFIX.capitalize()
381-
if self._detect_implicit_numbering():
392+
if number_digits_override is not None:
393+
number_digits = number_digits_override
394+
else:
395+
number_digits = reference.number_digits
396+
if self._detect_implicit_numbering() or number_digits == 0:
382397
suffix = ""
383398
else:
384-
if number_digits_override is not None:
385-
suffix = f"{number:0{number_digits_override}}"
386-
else:
387-
suffix = f"{number:0{reference.number_digits}}"
399+
suffix = f"{number:0{number_digits}}"
388400
name = f"{prefix}{suffix}"
389401
diff = len(reference._tag.name) - len(name)
390402
if diff >= 0:
@@ -404,23 +416,34 @@ def _get_initial_tag_setup(self, number: int = 0) -> Tuple[int, str, str]:
404416
Tuple in the form of (index, name, separator).
405417
"""
406418
prefix = self.PREFIX.capitalize()
407-
if self._default_to_implicit_numbering:
419+
if (
420+
self._default_to_implicit_numbering
421+
or self._default_source_number_digits == 0
422+
):
408423
suffix = ""
409424
else:
410425
suffix = f"{number:0{self._default_source_number_digits}}"
411426
return len(self._tags) if self._tags else 0, f"{prefix}{suffix}", ": "
412427

413-
def _deduplicate_tag_names(self) -> None:
414-
"""Eliminates duplicate numbers in source tag names."""
428+
def _deduplicate_tag_names(self, start: int = 0) -> None:
429+
"""
430+
Eliminates duplicate numbers in source tag names.
431+
432+
Args:
433+
start: Starting index, defaults to the first source tag.
434+
"""
415435
tags = self._get_tags()
416436
if not tags:
417437
return
418-
tag_sources = sorted(list(zip(*tags))[0], key=lambda ts: ts.number)
438+
tag_sources = list(zip(*tags[start:]))[0]
419439
for ts0, ts1 in zip(tag_sources, tag_sources[1:]):
420-
if ts1.number <= ts0.number:
421-
ts1._tag.name, ts1._tag._separator = self._get_tag_format(
422-
ts0, ts0.number + 1
423-
)
440+
if ts1.number == ts0.number:
441+
if ts1._number is not None:
442+
ts1._number = ts0.number + 1
443+
else:
444+
ts1._tag.name, ts1._tag._separator = self._get_tag_format(
445+
ts1, ts0.number + 1
446+
)
424447

425448
def insert(self, i: int, location: str) -> None:
426449
"""
@@ -431,11 +454,11 @@ def insert(self, i: int, location: str) -> None:
431454
location: Location of the new source.
432455
433456
Raises:
434-
SpecfileException if duplicates are disallowed and there
435-
already is a source with the same location.
457+
DuplicateSourceException if duplicates are disallowed and there
458+
already is a source with the same location.
436459
"""
437460
if not self._allow_duplicates and location in self:
438-
raise SpecfileException(f"Source '{location}' already exists")
461+
raise DuplicateSourceException(f"Source '{location}' already exists")
439462
items = self._get_items()
440463
if i > len(items):
441464
i = len(items)
@@ -453,7 +476,7 @@ def insert(self, i: int, location: str) -> None:
453476
index,
454477
Tag(name, location, Macros.expand(location), separator, Comments()),
455478
)
456-
self._deduplicate_tag_names()
479+
self._deduplicate_tag_names(i)
457480
else:
458481
container.insert(
459482
index,
@@ -480,11 +503,11 @@ def insert_numbered(self, number: int, location: str) -> int:
480503
Index of the newly inserted source.
481504
482505
Raises:
483-
SpecfileException if duplicates are disallowed and there
484-
already is a source with the same location.
506+
DuplicateSourceException if duplicates are disallowed and there
507+
already is a source with the same location.
485508
"""
486509
if not self._allow_duplicates and location in self:
487-
raise SpecfileException(f"Source '{location}' already exists")
510+
raise DuplicateSourceException(f"Source '{location}' already exists")
488511
tags = self._get_tags()
489512
if tags:
490513
# find the nearest source tag
@@ -501,7 +524,7 @@ def insert_numbered(self, number: int, location: str) -> int:
501524
self._tags.insert(
502525
index, Tag(name, location, Macros.expand(location), separator, Comments())
503526
)
504-
self._deduplicate_tag_names()
527+
self._deduplicate_tag_names(i)
505528
return i
506529

507530
def remove(self, location: str) -> None:
@@ -515,6 +538,21 @@ def remove(self, location: str) -> None:
515538
if source.location == location:
516539
del container[index]
517540

541+
def remove_numbered(self, number: int) -> None:
542+
"""
543+
Removes a source by number.
544+
545+
Args:
546+
number: Number of the source to be removed.
547+
"""
548+
items = self._get_items()
549+
try:
550+
container, index = next((c, i) for s, c, i in items if s.number == number)
551+
except StopIteration:
552+
pass
553+
else:
554+
del container[index]
555+
518556
def count(self, location: str) -> int:
519557
"""
520558
Counts sources by location.

specfile/specfile.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import arrow
1313

1414
from specfile.changelog import Changelog, ChangelogEntry
15-
from specfile.exceptions import SpecfileException
15+
from specfile.exceptions import SourceNumberException, SpecfileException
1616
from specfile.prep import Prep
1717
from specfile.rpm import RPM, Macros
1818
from specfile.sections import Sections
@@ -416,3 +416,39 @@ def set_version_and_release(self, version: str, release: str = "1") -> None:
416416
with self.tags() as tags:
417417
tags.version.value = version
418418
tags.release.value = self._get_updated_release(tags.release.value, release)
419+
420+
def add_patch(
421+
self,
422+
location: str,
423+
number: Optional[int] = None,
424+
comment: Optional[str] = None,
425+
initial_number: int = 0,
426+
number_digits: int = 4,
427+
) -> None:
428+
"""
429+
Adds a patch.
430+
431+
Args:
432+
location: Patch location (filename or URL).
433+
number: Patch number. It will be auto-assigned if not specified.
434+
If specified, it must be higher than any existing patch number.
435+
comment: Associated comment.
436+
initial_number: Auto-assigned number to start with if there are no patches.
437+
number_digits: Number of digits in the patch number.
438+
439+
Raises:
440+
SourceNumberException when the specified patch number is not higher
441+
than any existing patch number.
442+
"""
443+
with self.patches(default_source_number_digits=number_digits) as patches:
444+
highest_number = max((p.number for p in patches), default=-1)
445+
if number is not None:
446+
if number <= highest_number:
447+
raise SourceNumberException(
448+
"Patch number must be higher than any existing patch number"
449+
)
450+
else:
451+
number = max(highest_number + 1, initial_number)
452+
index = patches.insert_numbered(number, location)
453+
if comment:
454+
patches[index].comments.extend(comment.splitlines())

specfile/tags.py

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

44
import collections
5+
import itertools
56
import re
67
from typing import Any, Iterable, List, Optional, Union, overload
78

@@ -124,6 +125,11 @@ def __contains__(self, item: object) -> bool:
124125
return item in [c.text for c in self.data]
125126
return item in self.data
126127

128+
@property
129+
def raw(self) -> List[str]:
130+
"""List of comment texts"""
131+
return [c.text for c in self.data]
132+
127133
@overload
128134
def __getitem__(self, i: SupportsIndex) -> Comment:
129135
pass
@@ -340,14 +346,29 @@ def __getitem__(self, i):
340346

341347
def __delitem__(self, i: Union[SupportsIndex, slice]) -> None:
342348
def delete(index):
343-
preceding_lines = self.data[index].comments._preceding_lines.copy()
349+
preceding_lines = self.data[index].comments._preceding_lines[:]
344350
del self.data[index]
351+
# preserve preceding lines of the deleted tag but compress empty lines
345352
if index < len(self.data):
346-
self.data[index].comments._preceding_lines = (
347-
preceding_lines + self.data[index].comments._preceding_lines
348-
)
353+
lines = self.data[index].comments._preceding_lines
349354
else:
350-
self._remainder = preceding_lines + self._remainder
355+
lines = self._remainder
356+
delimiter = []
357+
if preceding_lines and not preceding_lines[-1] or lines and not lines[0]:
358+
delimiter.append("")
359+
lines[:] = (
360+
list(
361+
reversed(
362+
list(
363+
itertools.dropwhile(
364+
lambda l: not l, reversed(preceding_lines)
365+
)
366+
)
367+
)
368+
)
369+
+ delimiter
370+
+ list(itertools.dropwhile(lambda l: not l, lines))
371+
)
351372

352373
if isinstance(i, slice):
353374
for index in reversed(range(len(self.data))[i]):

tests/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@
1212
SPEC_AUTOPATCH = DATA_DIR / "spec_autopatch"
1313
SPEC_PATCHLIST = DATA_DIR / "spec_patchlist"
1414
SPEC_MULTIPLE_SOURCES = DATA_DIR / "spec_multiple_sources"
15+
SPEC_COMMENTED_PATCHES = DATA_DIR / "spec_commented_patches"
1516

1617
SPECFILE = "test.spec"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
From e5e63915ae9cfb5a40bccbd53514f211b5c8e63b Mon Sep 17 00:00:00 2001
2+
From: =?UTF-8?q?Nikola=20Forr=C3=B3?= <[email protected]>
3+
Date: Wed, 16 Mar 2022 10:29:59 +0100
4+
Subject: [PATCH 1/7] patch0
5+
6+
---
7+
test.txt | 1 +
8+
1 file changed, 1 insertion(+)
9+
10+
diff --git a/test.txt b/test.txt
11+
index 9daeafb..dec2cbe 100644
12+
--- a/test.txt
13+
+++ b/test.txt
14+
@@ -1 +1,2 @@
15+
test
16+
+test
17+
--
18+
2.35.1
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
From 10cd6fe9acec1233c3456871f26b122df901d160 Mon Sep 17 00:00:00 2001
2+
From: =?UTF-8?q?Nikola=20Forr=C3=B3?= <[email protected]>
3+
Date: Wed, 16 Mar 2022 10:30:15 +0100
4+
Subject: [PATCH 2/7] patch1
5+
6+
---
7+
test.txt | 1 +
8+
1 file changed, 1 insertion(+)
9+
10+
diff --git a/test.txt b/test.txt
11+
index dec2cbe..0867e73 100644
12+
--- a/test.txt
13+
+++ b/test.txt
14+
@@ -1,2 +1,3 @@
15+
test
16+
test
17+
+test
18+
--
19+
2.35.1

0 commit comments

Comments
 (0)