Skip to content

Commit 6c19c9e

Browse files
fix: [BUG] # character accumulation in markdown frontmatter tags prop (#79)
1 parent 9bff1f7 commit 6c19c9e

File tree

3 files changed

+62
-3
lines changed

3 files changed

+62
-3
lines changed

src/basic_memory/services/entity_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ async def update_entity_relations(
315315
except IntegrityError:
316316
# Unique constraint violation - relation already exists
317317
logger.debug(
318-
f"Skipping duplicate relation {rel.type} from {db_entity.permalink} target: {rel.target}, type: {rel.type}"
318+
f"Skipping duplicate relation {rel.type} from {db_entity.permalink} target: {rel.target}"
319319
)
320320
continue
321321

src/basic_memory/utils.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,15 +138,23 @@ def parse_tags(tags: Union[List[str], str, None]) -> List[str]:
138138
139139
Returns:
140140
A list of tag strings, or an empty list if no tags
141+
142+
Note:
143+
This function strips leading '#' characters from tags to prevent
144+
their accumulation when tags are processed multiple times.
141145
"""
142146
if tags is None:
143147
return []
144148

149+
# Process list of tags
145150
if isinstance(tags, list):
146-
return tags
151+
# First strip whitespace, then strip leading '#' characters to prevent accumulation
152+
return [tag.strip().lstrip('#') for tag in tags if tag and tag.strip()]
147153

154+
# Process comma-separated string of tags
148155
if isinstance(tags, str):
149-
return [tag.strip() for tag in tags.split(",") if tag.strip()]
156+
# Split by comma, strip whitespace, then strip leading '#' characters
157+
return [tag.strip().lstrip('#') for tag in tags.split(",") if tag and tag.strip()]
150158

151159
# For any other type, try to convert to string and parse
152160
try: # pragma: no cover

tests/utils/test_parse_tags.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Tests for parse_tags utility function."""
2+
3+
from typing import List, Union
4+
5+
import pytest
6+
7+
from basic_memory.utils import parse_tags
8+
9+
10+
@pytest.mark.parametrize(
11+
"input_tags,expected",
12+
[
13+
# None input
14+
(None, []),
15+
# List inputs
16+
([], []),
17+
(["tag1", "tag2"], ["tag1", "tag2"]),
18+
(["tag1", "", "tag2"], ["tag1", "tag2"]), # Empty tags are filtered
19+
([" tag1 ", " tag2 "], ["tag1", "tag2"]), # Whitespace is stripped
20+
# String inputs
21+
("", []),
22+
("tag1", ["tag1"]),
23+
("tag1,tag2", ["tag1", "tag2"]),
24+
("tag1, tag2", ["tag1", "tag2"]), # Whitespace after comma is stripped
25+
("tag1,,tag2", ["tag1", "tag2"]), # Empty tags are filtered
26+
# Tags with leading '#' characters - these should be stripped
27+
(["#tag1", "##tag2"], ["tag1", "tag2"]),
28+
("#tag1,##tag2", ["tag1", "tag2"]),
29+
(["tag1", "#tag2", "##tag3"], ["tag1", "tag2", "tag3"]),
30+
# Mixed whitespace and '#' characters
31+
([" #tag1 ", " ##tag2 "], ["tag1", "tag2"]),
32+
(" #tag1 , ##tag2 ", ["tag1", "tag2"]),
33+
],
34+
)
35+
def test_parse_tags(
36+
input_tags: Union[List[str], str, None], expected: List[str]
37+
) -> None:
38+
"""Test tag parsing with various input formats."""
39+
result = parse_tags(input_tags)
40+
assert result == expected
41+
42+
43+
def test_parse_tags_special_case() -> None:
44+
"""Test parsing from non-string, non-list types."""
45+
# Test with custom object that has __str__ method
46+
class TagObject:
47+
def __str__(self) -> str:
48+
return "tag1,tag2"
49+
50+
result = parse_tags(TagObject()) # pyright: ignore [reportArgumentType]
51+
assert result == ["tag1", "tag2"]

0 commit comments

Comments
 (0)