Skip to content

Commit 9fde844

Browse files
committed
removed unused hamming distance functions
1 parent 7dafc55 commit 9fde844

2 files changed

Lines changed: 60 additions & 160 deletions

File tree

bugwarrior/db.py

Lines changed: 29 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from bugwarrior.notifications import send_notification
1414

1515
if TYPE_CHECKING:
16+
from bugwarrior.config.schema import MainSectionConfig
1617
from bugwarrior.config.validation import Config
1718

1819
log = logging.getLogger(__name__)
@@ -26,29 +27,10 @@ class MultipleMatches(Exception):
2627
pass
2728

2829

29-
def get_normalized_annotation(annotation: str) -> str:
30+
def normalize_annotation(annotation: str) -> str:
3031
return re.sub(r'[\W_]', '', str(annotation))
3132

3233

33-
def get_annotation_hamming_distance(left: str, right: str) -> int:
34-
left = get_normalized_annotation(left)
35-
right = get_normalized_annotation(right)
36-
if len(left) > len(right):
37-
left = left[0 : len(right)]
38-
elif len(right) > len(left):
39-
right = right[0 : len(left)]
40-
return hamdist(left, right)
41-
42-
43-
def hamdist(str1: str, str2: str) -> int:
44-
"""Count the # of differences between equal length strings str1 and str2"""
45-
diffs = 0
46-
for ch1, ch2 in zip(str1, str2):
47-
if ch1 != ch2:
48-
diffs += 1
49-
return diffs
50-
51-
5234
def get_managed_task_uuids(
5335
tw: TaskWarriorShellout, key_list: dict[str, list[str]]
5436
) -> set[str]:
@@ -155,99 +137,36 @@ def find_taskwarrior_uuid(
155137
raise NotFound("No issue was found matching %s" % issue)
156138

157139

158-
def replace_left(
159-
field: str,
160-
local_task: dict[str, Any],
161-
remote_issue: dict[str, Any],
162-
keep_items: list[str] = [],
163-
) -> None:
164-
"""Replace array field from the remote_issue to the local_task
165-
166-
* Local 'left' entries are suppressed, unless those listed in keep_items.
167-
* Remote 'left' are appended to task, if not present in local.
168-
169-
:param `field`: Task field to merge.
170-
:param `local_task`: `taskw.task.Task` object into which to replace
171-
remote changes.
172-
:param `remote_issue`: `dict` instance from which to add into
173-
local task.
174-
:param `keep_items`: list of items to keep into local_task even if not
175-
present in remote_issue
176-
"""
140+
def are_normalized_annotations_equal(left: str, right: str) -> bool:
141+
_left, _right = map(normalize_annotation, (left, right))
142+
min_length = min(len(_left), len(_right))
143+
return _left[:min_length] == _right[:min_length]
177144

178-
# Ensure that empty default are present
179-
local_field = local_task.get(field, []).copy()
180-
remote_field = remote_issue.get(field, [])
181-
182-
# We need to make sure an array exists for this field because
183-
# we will be appending to it in a moment.
184-
if field not in local_task:
185-
local_task[field] = []
186-
187-
# Delete all items in local_task, unless they are in keep_items or in remote_issue
188-
# This ensure that the task is not being updated if there is no changes
189-
for item in local_field:
190-
if keep_items.count(item) == 0 and remote_field.count(item) == 0:
191-
log.debug('found %s to remove' % (item))
192-
local_task[field].remove(item)
193-
elif remote_field.count(item) > 0:
194-
remote_field.remove(item)
195-
196-
if len(remote_field) > 0:
197-
local_task[field] += remote_field
198-
199-
200-
def merge_left(
201-
field: str,
202-
local_task: dict[str, Any],
203-
remote_issue: dict[str, Any],
204-
hamming: bool = False,
205-
) -> None:
206-
"""Merge array field from the remote_issue into local_task
207145

208-
* Local 'left' entries are preserved without modification
209-
* Remote 'left' are appended to task if not present in local.
146+
def merge_annotations(local: dict[str, Any], remote: dict[str, Any]) -> list[str]:
147+
"""
148+
Merge annotations. Order and duplication are preserved.
149+
"""
150+
local_annotations = local.get("annotations", [])
151+
new_annotations = [
152+
annotation
153+
for annotation in remote.get("annotations", [])
154+
if not any(
155+
are_normalized_annotations_equal(annotation, local_annotation)
156+
for local_annotation in local_annotations
157+
)
158+
]
159+
return [*local_annotations, *new_annotations]
210160

211-
:param `field`: Task field to merge.
212-
:param `local_task`: `taskw.task.Task` object into which to merge
213-
remote changes.
214-
:param `remote_issue`: `dict` instance from which to merge into
215-
local task.
216-
:param `hamming`: (default `False`) If `True`, compare entries by
217-
truncating to maximum length, and comparing hamming distances.
218-
Useful generally only for annotations.
219161

220-
"""
162+
def merge_tags(
163+
main_conf: "MainSectionConfig", local: dict[str, Any], remote: dict[str, Any]
164+
) -> list[str]:
165+
task_tags: set[str] = set(local.get("tags", []))
166+
if main_conf.replace_tags:
167+
task_tags &= set(main_conf.static_tags)
221168

222-
# Ensure that empty defaults are present
223-
local_field = local_task.get(field, [])
224-
remote_field = remote_issue.get(field, [])
225-
226-
# We need to make sure an array exists for this field because
227-
# we will be appending to it in a moment.
228-
if field not in local_task:
229-
local_task[field] = []
230-
231-
# If a remote does not appear in local, add it to the local task
232-
new_count = 0
233-
for remote in remote_field:
234-
for local in local_field:
235-
if (
236-
# For annotations, they don't have to match *exactly*.
237-
(hamming and get_annotation_hamming_distance(remote, local) == 0)
238-
# But for everything else, they should.
239-
or (remote == local)
240-
):
241-
break
242-
else:
243-
log.debug("%s not found in %r" % (remote, local_field))
244-
local_task[field].append(remote)
245-
new_count += 1
246-
if new_count > 0:
247-
log.debug(
248-
'Added %s new values to %s (total: %s)'
249-
% (new_count, field, len(local_task[field]))
250-
)
169+
return sorted(task_tags | set(remote.get("tags", [])))
251170

252171

253172
def run_hooks(pre_import: list[str]) -> None:
@@ -357,13 +276,10 @@ def synchronize(
357276

358277
# Merge annotations & tags from online into our task object
359278
if conf.main.merge_annotations:
360-
merge_left('annotations', task, issue, hamming=True)
279+
task["annotations"] = merge_annotations(task, issue)
361280

362281
if conf.main.merge_tags:
363-
if conf.main.replace_tags:
364-
replace_left('tags', task, issue, list(conf.main.static_tags))
365-
else:
366-
merge_left('tags', task, issue)
282+
task["tags"] = merge_tags(conf.main, task, issue)
367283

368284
issue.pop('annotations', None)
369285
issue.pop('tags', None)

tests/test_db.py

Lines changed: 31 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import copy
2-
import unittest
2+
from types import SimpleNamespace
33

44
import taskw.task
55

@@ -8,66 +8,50 @@
88
from .base import ConfigTest
99

1010

11-
class TestMergeLeft(unittest.TestCase):
12-
def setUp(self):
13-
self.issue_dict = {'annotations': ['testing']}
11+
class TestMergeAnnotations:
12+
def test_merges_local_and_remote_annotations(self):
13+
local = {'annotations': ['existing']}
14+
remote = {'annotations': ['new', 'new']}
1415

15-
def assertMerged(self, local, remote, **kwargs):
16-
db.merge_left('annotations', local, remote, **kwargs)
17-
self.assertEqual(local, remote)
16+
assert db.merge_annotations(local, remote) == ['existing', 'new', 'new']
1817

19-
def test_with_dict(self):
20-
self.assertMerged({}, self.issue_dict)
21-
22-
def test_with_taskw(self):
23-
self.assertMerged(taskw.task.Task({}), self.issue_dict)
24-
25-
def test_already_in_sync(self):
26-
self.assertMerged(self.issue_dict, self.issue_dict)
27-
28-
def test_rough_equality_hamming_false(self):
29-
"""When hamming=False, rough equivalents are duplicated."""
18+
def test_skips_normalized_matches(self):
19+
local = {'annotations': ['testing']}
3020
remote = {'annotations': ['\n testing \n']}
3121

32-
db.merge_left('annotations', self.issue_dict, remote, hamming=False)
33-
self.assertEqual(len(self.issue_dict['annotations']), 2)
22+
assert db.merge_annotations(local, remote) == ['testing']
3423

35-
def test_rough_equality_hamming_true(self):
36-
"""When hamming=True, rough equivalents are not duplicated."""
37-
remote = {'annotations': ['\n testing \n']}
24+
def test_skips_matches_up_to_shortest_annotation_length(self):
25+
local = {'annotations': ['testing']}
26+
remote = {'annotations': ['testing with more detail']}
3827

39-
db.merge_left('annotations', self.issue_dict, remote, hamming=True)
40-
self.assertEqual(len(self.issue_dict['annotations']), 1)
28+
assert db.merge_annotations(local, remote) == ['testing']
4129

30+
def test_handles_missing_annotations(self):
31+
assert db.merge_annotations({}, {}) == []
32+
assert db.merge_annotations({}, {'annotations': ['new']}) == ['new']
4233

43-
class TestReplaceLeft(unittest.TestCase):
44-
def setUp(self):
45-
self.issue_dict = {'tags': ['test', 'test2']}
46-
self.remote = {'tags': ['remote_tag1', 'remote_tag2']}
4734

48-
def assertReplaced(self, local, remote, **kwargs):
49-
db.replace_left('tags', local, remote, **kwargs)
50-
self.assertEqual(local, remote)
35+
class TestMergeTags:
36+
def test_merges_and_sorts_unique_tags(self):
37+
main_conf = SimpleNamespace(replace_tags=False, static_tags=[])
38+
local = {'tags': ['existing', 'shared']}
39+
remote = {'tags': ['new', 'shared']}
5140

52-
def test_with_dict(self):
53-
self.assertReplaced({}, self.issue_dict)
41+
assert db.merge_tags(main_conf, local, remote) == ['existing', 'new', 'shared']
5442

55-
def test_with_taskw(self):
56-
self.assertReplaced(taskw.task.Task({}), self.issue_dict)
43+
def test_replaces_non_static_local_tags_when_configured(self):
44+
main_conf = SimpleNamespace(replace_tags=True, static_tags=['keep'])
45+
local = {'tags': ['drop', 'keep']}
46+
remote = {'tags': ['new']}
5747

58-
def test_already_in_sync(self):
59-
self.assertReplaced(self.issue_dict, self.issue_dict)
48+
assert db.merge_tags(main_conf, local, remote) == ['keep', 'new']
6049

61-
def test_replace(self):
62-
self.assertReplaced(self.issue_dict, self.remote)
50+
def test_handles_missing_tags(self):
51+
main_conf = SimpleNamespace(replace_tags=False, static_tags=[])
6352

64-
def test_replace_with_keeped_item(self):
65-
"""When keeped_item is set, all item in this list are keeped"""
66-
result = {'tags': ['test', 'remote_tag1', 'remote_tag2']}
67-
print(self.issue_dict)
68-
keeped_items = ['test']
69-
db.replace_left('tags', self.issue_dict, self.remote, keeped_items)
70-
self.assertEqual(self.issue_dict, result)
53+
assert db.merge_tags(main_conf, {}, {}) == []
54+
assert db.merge_tags(main_conf, {}, {'tags': ['new']}) == ['new']
7155

7256

7357
class TestSynchronize(ConfigTest):

0 commit comments

Comments
 (0)