From d94d9f219761431b1226c5033dc3d3000585806f Mon Sep 17 00:00:00 2001 From: Nathaniel Landau Date: Sat, 4 Feb 2023 23:32:55 -0500 Subject: [PATCH] feat: add new tags (#16) --- README.md | 43 +- src/obsidian_metadata/cli.py | 13 +- src/obsidian_metadata/models/application.py | 14 +- src/obsidian_metadata/models/metadata.py | 24 +- src/obsidian_metadata/models/notes.py | 13 +- src/obsidian_metadata/models/questions.py | 8 +- src/obsidian_metadata/models/vault.py | 4 +- tests/application_test.py | 27 +- tests/metadata_test.py | 898 ++++++++++---------- tests/notes_test.py | 25 + 10 files changed, 574 insertions(+), 495 deletions(-) diff --git a/README.md b/README.md index d8112fa..1acd15f 100644 --- a/README.md +++ b/README.md @@ -45,46 +45,45 @@ Once installed, run `obsidian-metadata` in your terminal to enter an interactive **Inspect Metadata** -- View all metadata in the vault -- View all metadata in the vault -- View all frontmatter -- View all inline metadata -- View all inline tags -- Export all metadata to CSV or JSON file +- **View all metadata in the vault** +- View all **frontmatter** +- View all **inline metadata** +- View all **inline tags** +- **Export all metadata to CSV or JSON file** **Filter Notes in Scope**: Limit the scope of notes to be processed with one or more filters. -- Path filter (regex): Limit scope based on the path or filename -- Metadata Filter: Limit scope based on a key or key/value pair -- Tag Filter: Limit scope based on an in-text tag -- List and Clear Filters List all current filters and clear one or all -- List notes in scope: List notes that will be processed. +- **Path filter (regex)**: Limit scope based on the path or filename +- **Metadata filter**: Limit scope based on a key or key/value pair +- **Tag filter**: Limit scope based on an in-text tag +- **List and clear filters**: List all current filters and clear one or all +- **List notes in scope**: List notes that will be processed. **Add Metadata**: Add new metadata to your vault. -- Add metadata to the frontmatter -- Add to inline metadata - Set `insert_location` in the config to control where the new metadata is inserted. (Default: Bottom) -- Add to inline tag (Not yet implemented) +- **Add new metadata to the frontmatter** +- **Add new inline metadata** - Set `insert_location` in the config to control where the new metadata is inserted. (Default: Bottom) +- **Add new inline tag** - Set `insert_location` in the config to control where the new tag is inserted. (Default: Bottom) **Rename Metadata**: Rename either a key and all associated values, a specific value within a key. or an in-text tag. -- Rename a key -- Rename a value -- rename an inline tag +- **Rename a key** +- **Rename a value** +- **Rename an inline tag** **Delete Metadata**: Delete either a key and all associated values, or a specific value. -- Delete a key and associated values -- Delete a value from a key -- Delete an inline tag +- **Delete a key and associated values** +- **Delete a value from a key** +- **Delete an inline tag** **Review Changes**: Prior to committing changes, review all changes that will be made. -- View a diff of the changes that will be made +- **View a diff of the changes** that will be made **Commit Changes**: Write the changes to disk. This step is not undoable. -- Commit changes to the vault +- **Commit changes to the vault** ### Configuration diff --git a/src/obsidian_metadata/cli.py b/src/obsidian_metadata/cli.py index a49c234..6e8eb10 100644 --- a/src/obsidian_metadata/cli.py +++ b/src/obsidian_metadata/cli.py @@ -110,17 +110,18 @@ def main( [bold underline]Filter Notes in Scope[/] Limit the scope of notes to be processed with one or more filters. • Path filter (regex): Limit scope based on the path or filename - • Metadata Filter: Limit scope based on a key or key/value pair - • Tag Filter: Limit scope based on an in-text tag - • List and Clear Filters: List all current filters and clear one or all + • Metadata filter: Limit scope based on a key or key/value pair + • Tag filter: Limit scope based on an in-text tag + • List and clear filters: List all current filters and clear one or all • List notes in scope: List notes that will be processed. [bold underline]Add Metadata[/] Add new metadata to your vault. - • Add metadata to the frontmatter - • Add to inline metadata - Set `insert_location` in the config to + • Add new metadata to the frontmatter + • Add new inline metadata - Set `insert_location` in the config to control where the new metadata is inserted. (Default: Bottom) - • [dim]Add to inline tag (Not yet implemented)[/] + • Add new inline tag - Set `insert_location` in the config to + control where the new tag is inserted. (Default: Bottom) [bold underline]Rename Metadata[/] Rename either a key and all associated values, a specific value within a key. or an in-text tag. diff --git a/src/obsidian_metadata/models/application.py b/src/obsidian_metadata/models/application.py index 0bfe1f5..3201273 100644 --- a/src/obsidian_metadata/models/application.py +++ b/src/obsidian_metadata/models/application.py @@ -104,7 +104,19 @@ class Application: alerts.success(f"Added metadata to {num_changed} notes") case MetadataType.TAGS: - alerts.warning(f"Adding metadata to {area} is not supported yet") + tag = self.questions.ask_new_tag() + if tag is None: # pragma: no cover + return + + num_changed = self.vault.add_metadata( + area=area, value=tag, location=self.vault.insert_location + ) + + if num_changed == 0: # pragma: no cover + alerts.warning(f"No notes were changed") + return + + alerts.success(f"Added metadata to {num_changed} notes") case _: # pragma: no cover return diff --git a/src/obsidian_metadata/models/metadata.py b/src/obsidian_metadata/models/metadata.py index ceb0ed6..e7ae940 100644 --- a/src/obsidian_metadata/models/metadata.py +++ b/src/obsidian_metadata/models/metadata.py @@ -401,7 +401,7 @@ class InlineMetadata: """ return f"InlineMetadata(inline_metadata={self.dict})" - def add(self, key: str, value: str | list[str] = None) -> bool: + def add(self, key: str, value: str = None) -> bool: """Add a key and value to the inline metadata. Args: @@ -411,15 +411,12 @@ class InlineMetadata: Returns: bool: True if the metadata was added """ - if value is None: + if value is None or value == "" or value == "None": if key not in self.dict: self.dict[key] = [] return True return False - if isinstance(value, list): - value = value[0] - if key not in self.dict: self.dict[key] = [value] return True @@ -564,6 +561,23 @@ class InlineTags: ) ) + def add(self, new_tag: str) -> bool: + """Add a new inline tag. + + Args: + new_tag (str): Tag to add. + + Returns: + bool: True if a tag was added. + """ + if new_tag in self.list: + return False + + new_list = self.list.copy() + new_list.append(new_tag) + self.list = sorted(new_list) + return True + def contains(self, tag: str, is_regex: bool = False) -> bool: """Check if a tag exists in the metadata. diff --git a/src/obsidian_metadata/models/notes.py b/src/obsidian_metadata/models/notes.py index 8584ae1..ffc2c45 100644 --- a/src/obsidian_metadata/models/notes.py +++ b/src/obsidian_metadata/models/notes.py @@ -120,7 +120,7 @@ class Note: def add_metadata( self, area: MetadataType, - key: str, + key: str = None, value: str | list[str] = None, location: InsertLocation = None, ) -> bool: @@ -128,7 +128,7 @@ class Note: Args: area (MetadataType): Area to add metadata to. - key (str): Key to add. + key (str, optional): Key to add location (InsertLocation, optional): Location to add inline metadata and tags. value (str, optional): Value to add. @@ -140,7 +140,7 @@ class Note: return True try: - if area is MetadataType.INLINE and self.inline_metadata.add(key, value): + if area is MetadataType.INLINE and self.inline_metadata.add(key, str(value)): line = f"{key}:: " if value is None else f"{key}:: {value}" self.insert(new_string=line, location=location) return True @@ -149,9 +149,10 @@ class Note: log.warning(f"Could not add metadata to {self.note_path}: {e}") return False - if area is MetadataType.TAGS: - # TODO: implement adding to intext tags - pass + if area is MetadataType.TAGS and self.inline_tags.add(str(value)): + line = f"#{value}" + self.insert(new_string=line, location=location) + return True return False diff --git a/src/obsidian_metadata/models/questions.py b/src/obsidian_metadata/models/questions.py index 0c751b2..61574e5 100644 --- a/src/obsidian_metadata/models/questions.py +++ b/src/obsidian_metadata/models/questions.py @@ -436,8 +436,12 @@ class Questions: question, validate=self._validate_new_key, style=self.style, qmark="INPUT |" ).ask() - def ask_new_tag(self, question: str = "New tag name") -> str: # pragma: no cover - """Ask the user for a new inline tag.""" + def ask_new_tag(self, question: str = "Enter a new tag") -> str: # pragma: no cover + """Ask the user for a new tag. + + Args: + question (str, optional): The question to ask. Defaults to "Enter a new tag". + """ return questionary.text( question, validate=self._validate_new_tag, style=self.style, qmark="INPUT |" ).ask() diff --git a/src/obsidian_metadata/models/vault.py b/src/obsidian_metadata/models/vault.py index 55d64d8..748c805 100644 --- a/src/obsidian_metadata/models/vault.py +++ b/src/obsidian_metadata/models/vault.py @@ -165,7 +165,7 @@ class Vault: def add_metadata( self, area: MetadataType, - key: str, + key: str = None, value: str | list[str] = None, location: InsertLocation = None, ) -> int: @@ -186,7 +186,7 @@ class Vault: num_changed = 0 for _note in self.notes_in_scope: - if _note.add_metadata(area, key, value, location): + if _note.add_metadata(area=area, key=key, value=value, location=location): num_changed += 1 if num_changed > 0: diff --git a/tests/application_test.py b/tests/application_test.py index fd7337b..0de04c5 100644 --- a/tests/application_test.py +++ b/tests/application_test.py @@ -42,7 +42,7 @@ def test_abort(test_application, mocker, capsys) -> None: assert "Done!" in captured.out -def test_add_metadata_frontmatter_success(test_application, mocker, capsys) -> None: +def test_add_metadata_frontmatter(test_application, mocker, capsys) -> None: """Test adding new metadata to the vault.""" app = test_application app._load_vault() @@ -69,7 +69,7 @@ def test_add_metadata_frontmatter_success(test_application, mocker, capsys) -> N assert captured.out == Regex(r"SUCCESS +\| Added metadata to.*\d+.*notes", re.DOTALL) -def test_add_metadata_inline_success(test_application, mocker, capsys) -> None: +def test_add_metadata_inline(test_application, mocker, capsys) -> None: """Test adding new metadata to the vault.""" app = test_application app._load_vault() @@ -96,6 +96,29 @@ def test_add_metadata_inline_success(test_application, mocker, capsys) -> None: assert captured.out == Regex(r"SUCCESS +\| Added metadata to.*\d+.*notes", re.DOTALL) +def test_add_metadata_tag(test_application, mocker, capsys) -> None: + """Test adding new metadata to the vault.""" + app = test_application + app._load_vault() + mocker.patch( + "obsidian_metadata.models.application.Questions.ask_application_main", + side_effect=["add_metadata", KeyError], + ) + mocker.patch( + "obsidian_metadata.models.application.Questions.ask_area", + return_value=MetadataType.TAGS, + ) + mocker.patch( + "obsidian_metadata.models.application.Questions.ask_new_tag", + return_value="new_tag", + ) + + with pytest.raises(KeyError): + app.application_main() + captured = capsys.readouterr() + assert captured.out == Regex(r"SUCCESS +\| Added metadata to.*\d+.*notes", re.DOTALL) + + def test_delete_inline_tag(test_application, mocker, capsys) -> None: """Test renaming an inline tag.""" app = test_application diff --git a/tests/metadata_test.py b/tests/metadata_test.py index 0a676de..2b9d699 100644 --- a/tests/metadata_test.py +++ b/tests/metadata_test.py @@ -69,6 +69,455 @@ repeated_key:: repeated_key_value2 """ +def test_frontmatter_create() -> None: + """Test frontmatter creation.""" + frontmatter = Frontmatter(INLINE_CONTENT) + assert frontmatter.dict == {} + + frontmatter = Frontmatter(FRONTMATTER_CONTENT) + assert frontmatter.dict == { + "frontmatter_Key1": ["frontmatter_Key1_value"], + "frontmatter_Key2": ["article", "note"], + "shared_key1": ["shared_key1_value"], + "tags": ["tag_1", "tag_2", "📅/tag_3"], + } + assert frontmatter.dict_original == { + "frontmatter_Key1": ["frontmatter_Key1_value"], + "frontmatter_Key2": ["article", "note"], + "shared_key1": ["shared_key1_value"], + "tags": ["tag_1", "tag_2", "📅/tag_3"], + } + + +def test_frontmatter_contains() -> None: + """Test frontmatter contains.""" + frontmatter = Frontmatter(FRONTMATTER_CONTENT) + + assert frontmatter.contains("frontmatter_Key1") is True + assert frontmatter.contains("frontmatter_Key2", "article") is True + assert frontmatter.contains("frontmatter_Key3") is False + assert frontmatter.contains("frontmatter_Key2", "no value") is False + + assert frontmatter.contains(r"\d$", is_regex=True) is True + assert frontmatter.contains(r"^\d", is_regex=True) is False + assert frontmatter.contains("key", r"_\d", is_regex=True) is False + assert frontmatter.contains("key", r"\w\d_", is_regex=True) is True + + +def test_frontmatter_add() -> None: + """Test frontmatter add.""" + frontmatter = Frontmatter(FRONTMATTER_CONTENT) + + assert frontmatter.add("frontmatter_Key1") is False + assert frontmatter.add("added_key") is True + assert frontmatter.dict == { + "added_key": [], + "frontmatter_Key1": ["frontmatter_Key1_value"], + "frontmatter_Key2": ["article", "note"], + "shared_key1": ["shared_key1_value"], + "tags": ["tag_1", "tag_2", "📅/tag_3"], + } + + assert frontmatter.add("added_key", "added_value") is True + assert frontmatter.dict == { + "added_key": ["added_value"], + "frontmatter_Key1": ["frontmatter_Key1_value"], + "frontmatter_Key2": ["article", "note"], + "shared_key1": ["shared_key1_value"], + "tags": ["tag_1", "tag_2", "📅/tag_3"], + } + + assert frontmatter.add("added_key", "added_value_2") is True + assert frontmatter.dict == { + "added_key": ["added_value", "added_value_2"], + "frontmatter_Key1": ["frontmatter_Key1_value"], + "frontmatter_Key2": ["article", "note"], + "shared_key1": ["shared_key1_value"], + "tags": ["tag_1", "tag_2", "📅/tag_3"], + } + + assert frontmatter.add("added_key", ["added_value_3", "added_value_4"]) is True + assert frontmatter.dict == { + "added_key": ["added_value", "added_value_2", "added_value_3", "added_value_4"], + "frontmatter_Key1": ["frontmatter_Key1_value"], + "frontmatter_Key2": ["article", "note"], + "shared_key1": ["shared_key1_value"], + "tags": ["tag_1", "tag_2", "📅/tag_3"], + } + + assert frontmatter.add("added_key2", ["added_value_1", "added_value_2"]) is True + assert frontmatter.dict == { + "added_key": ["added_value", "added_value_2", "added_value_3", "added_value_4"], + "added_key2": ["added_value_1", "added_value_2"], + "frontmatter_Key1": ["frontmatter_Key1_value"], + "frontmatter_Key2": ["article", "note"], + "shared_key1": ["shared_key1_value"], + "tags": ["tag_1", "tag_2", "📅/tag_3"], + } + + assert frontmatter.add("added_key3", "added_value_1") is True + assert frontmatter.dict == { + "added_key": ["added_value", "added_value_2", "added_value_3", "added_value_4"], + "added_key2": ["added_value_1", "added_value_2"], + "added_key3": ["added_value_1"], + "frontmatter_Key1": ["frontmatter_Key1_value"], + "frontmatter_Key2": ["article", "note"], + "shared_key1": ["shared_key1_value"], + "tags": ["tag_1", "tag_2", "📅/tag_3"], + } + + assert frontmatter.add("added_key3", "added_value_1") is False + + +def test_frontmatter_rename() -> None: + """Test frontmatter rename.""" + frontmatter = Frontmatter(FRONTMATTER_CONTENT) + assert frontmatter.dict == { + "frontmatter_Key1": ["frontmatter_Key1_value"], + "frontmatter_Key2": ["article", "note"], + "shared_key1": ["shared_key1_value"], + "tags": ["tag_1", "tag_2", "📅/tag_3"], + } + + assert frontmatter.rename("no key", "new key") is False + assert frontmatter.rename("tags", "no tag", "new key") is False + + assert frontmatter.has_changes() is False + assert frontmatter.rename("tags", "tag_2", "new tag") is True + + assert frontmatter.dict["tags"] == ["new tag", "tag_1", "📅/tag_3"] + assert frontmatter.rename("tags", "old_tags") is True + assert frontmatter.dict["old_tags"] == ["new tag", "tag_1", "📅/tag_3"] + assert "tags" not in frontmatter.dict + + assert frontmatter.has_changes() is True + + +def test_frontmatter_delete() -> None: + """Test Frontmatter delete method.""" + frontmatter = Frontmatter(FRONTMATTER_CONTENT) + assert frontmatter.dict == { + "frontmatter_Key1": ["frontmatter_Key1_value"], + "frontmatter_Key2": ["article", "note"], + "shared_key1": ["shared_key1_value"], + "tags": ["tag_1", "tag_2", "📅/tag_3"], + } + + assert frontmatter.delete("no key") is False + assert frontmatter.delete("tags", "no value") is False + assert frontmatter.delete(r"\d{3}") is False + assert frontmatter.has_changes() is False + assert frontmatter.delete("tags", "tag_2") is True + assert frontmatter.dict["tags"] == ["tag_1", "📅/tag_3"] + assert frontmatter.delete("tags") is True + assert "tags" not in frontmatter.dict + assert frontmatter.has_changes() is True + assert frontmatter.delete("shared_key1", r"\w+") is True + assert frontmatter.dict["shared_key1"] == [] + assert frontmatter.delete(r"\w.tter") is True + assert frontmatter.dict == {"shared_key1": []} + + +def test_frontmatter_yaml_conversion(): + """Test Frontmatter to_yaml method.""" + new_frontmatter: str = """\ +tags: + - tag_1 + - tag_2 + - 📅/tag_3 +frontmatter_Key1: frontmatter_Key1_value +frontmatter_Key2: + - article + - note +shared_key1: shared_key1_value +""" + new_frontmatter_sorted: str = """\ +frontmatter_Key1: frontmatter_Key1_value +frontmatter_Key2: + - article + - note +shared_key1: shared_key1_value +tags: + - tag_1 + - tag_2 + - 📅/tag_3 +""" + frontmatter = Frontmatter(FRONTMATTER_CONTENT) + assert frontmatter.to_yaml() == new_frontmatter + assert frontmatter.to_yaml(sort_keys=True) == new_frontmatter_sorted + + +def test_inline_metadata_add() -> None: + """Test inline add.""" + inline = InlineMetadata(INLINE_CONTENT) + + assert inline.add("bold_key1") is False + assert inline.add("bold_key1", "bold_key1_value") is False + assert inline.add("added_key") is True + assert inline.dict == { + "added_key": [], + "bold_key1": ["bold_key1_value"], + "bold_key2": ["bold_key2_value"], + "emoji_📅_key": ["emoji_📅_key_value"], + "in_text_key1": ["in_text_key1_value"], + "in_text_key2": ["in_text_key2_value"], + "link_key": ["link_key_value"], + "repeated_key": ["repeated_key_value1", "repeated_key_value2"], + "tag_key": ["tag_key_value"], + } + + assert inline.add("added_key1", "added_value") is True + assert inline.dict == { + "added_key": [], + "added_key1": ["added_value"], + "bold_key1": ["bold_key1_value"], + "bold_key2": ["bold_key2_value"], + "emoji_📅_key": ["emoji_📅_key_value"], + "in_text_key1": ["in_text_key1_value"], + "in_text_key2": ["in_text_key2_value"], + "link_key": ["link_key_value"], + "repeated_key": ["repeated_key_value1", "repeated_key_value2"], + "tag_key": ["tag_key_value"], + } + + with pytest.raises(ValueError): + assert inline.add("added_key1", "added_value_2") is True + + assert inline.dict == { + "added_key": [], + "added_key1": ["added_value"], + "bold_key1": ["bold_key1_value"], + "bold_key2": ["bold_key2_value"], + "emoji_📅_key": ["emoji_📅_key_value"], + "in_text_key1": ["in_text_key1_value"], + "in_text_key2": ["in_text_key2_value"], + "link_key": ["link_key_value"], + "repeated_key": ["repeated_key_value1", "repeated_key_value2"], + "tag_key": ["tag_key_value"], + } + + assert inline.add("added_key", "added_value") + assert inline.dict == { + "added_key": ["added_value"], + "added_key1": ["added_value"], + "bold_key1": ["bold_key1_value"], + "bold_key2": ["bold_key2_value"], + "emoji_📅_key": ["emoji_📅_key_value"], + "in_text_key1": ["in_text_key1_value"], + "in_text_key2": ["in_text_key2_value"], + "link_key": ["link_key_value"], + "repeated_key": ["repeated_key_value1", "repeated_key_value2"], + "tag_key": ["tag_key_value"], + } + + +def test_inline_metadata_contains() -> None: + """Test inline metadata contains method.""" + inline = InlineMetadata(INLINE_CONTENT) + + assert inline.contains("bold_key1") is True + assert inline.contains("bold_key2", "bold_key2_value") is True + assert inline.contains("bold_key3") is False + assert inline.contains("bold_key2", "no value") is False + + assert inline.contains(r"\w{4}_key", is_regex=True) is True + assert inline.contains(r"^\d", is_regex=True) is False + assert inline.contains("1$", r"\d_value", is_regex=True) is True + assert inline.contains("key", r"^\d_value", is_regex=True) is False + + +def test_inline_metadata_create() -> None: + """Test inline metadata creation.""" + inline = InlineMetadata(FRONTMATTER_CONTENT) + assert inline.dict == {} + inline = InlineMetadata(INLINE_CONTENT) + assert inline.dict == { + "bold_key1": ["bold_key1_value"], + "bold_key2": ["bold_key2_value"], + "emoji_📅_key": ["emoji_📅_key_value"], + "in_text_key1": ["in_text_key1_value"], + "in_text_key2": ["in_text_key2_value"], + "link_key": ["link_key_value"], + "repeated_key": ["repeated_key_value1", "repeated_key_value2"], + "tag_key": ["tag_key_value"], + } + assert inline.dict_original == { + "bold_key1": ["bold_key1_value"], + "bold_key2": ["bold_key2_value"], + "emoji_📅_key": ["emoji_📅_key_value"], + "in_text_key1": ["in_text_key1_value"], + "in_text_key2": ["in_text_key2_value"], + "link_key": ["link_key_value"], + "repeated_key": ["repeated_key_value1", "repeated_key_value2"], + "tag_key": ["tag_key_value"], + } + + +def test_inline_metadata_delete() -> None: + """Test inline metadata delete.""" + inline = InlineMetadata(INLINE_CONTENT) + assert inline.dict == { + "bold_key1": ["bold_key1_value"], + "bold_key2": ["bold_key2_value"], + "emoji_📅_key": ["emoji_📅_key_value"], + "in_text_key1": ["in_text_key1_value"], + "in_text_key2": ["in_text_key2_value"], + "link_key": ["link_key_value"], + "repeated_key": ["repeated_key_value1", "repeated_key_value2"], + "tag_key": ["tag_key_value"], + } + + assert inline.delete("no key") is False + assert inline.delete("repeated_key", "no value") is False + assert inline.has_changes() is False + assert inline.delete("repeated_key", "repeated_key_value1") is True + assert inline.dict["repeated_key"] == ["repeated_key_value2"] + assert inline.delete("repeated_key") is True + assert "repeated_key" not in inline.dict + assert inline.has_changes() is True + assert inline.delete(r"\d{3}") is False + assert inline.delete(r"bold_key\d") is True + assert inline.dict == { + "emoji_📅_key": ["emoji_📅_key_value"], + "in_text_key1": ["in_text_key1_value"], + "in_text_key2": ["in_text_key2_value"], + "link_key": ["link_key_value"], + "tag_key": ["tag_key_value"], + } + assert inline.delete("emoji_📅_key", ".*📅.*") is True + assert inline.dict == { + "emoji_📅_key": [], + "in_text_key1": ["in_text_key1_value"], + "in_text_key2": ["in_text_key2_value"], + "link_key": ["link_key_value"], + "tag_key": ["tag_key_value"], + } + + +def test_inline_metadata_rename() -> None: + """Test inline metadata rename.""" + inline = InlineMetadata(INLINE_CONTENT) + assert inline.dict == { + "bold_key1": ["bold_key1_value"], + "bold_key2": ["bold_key2_value"], + "emoji_📅_key": ["emoji_📅_key_value"], + "in_text_key1": ["in_text_key1_value"], + "in_text_key2": ["in_text_key2_value"], + "link_key": ["link_key_value"], + "repeated_key": ["repeated_key_value1", "repeated_key_value2"], + "tag_key": ["tag_key_value"], + } + + assert inline.rename("no key", "new key") is False + assert inline.rename("repeated_key", "no value", "new key") is False + assert inline.has_changes() is False + assert inline.rename("repeated_key", "repeated_key_value1", "new value") is True + assert inline.dict["repeated_key"] == ["new value", "repeated_key_value2"] + assert inline.rename("repeated_key", "old_key") is True + assert inline.dict["old_key"] == ["new value", "repeated_key_value2"] + assert "repeated_key" not in inline.dict + assert inline.has_changes() is True + + +def test_inline_tags_add() -> None: + """Test inline tags add.""" + tags = InlineTags(INLINE_CONTENT) + + assert tags.add("bold_tag") is False + assert tags.add("new_tag") is True + assert tags.list == [ + "bold_tag", + "in_text_tag", + "inline_tag_top1", + "inline_tag_top2", + "new_tag", + "tag_key_value", + ] + + +def test_inline_tags_contains() -> None: + """Test inline tags contains.""" + tags = InlineTags(INLINE_CONTENT) + assert tags.contains("bold_tag") is True + assert tags.contains("no tag") is False + + assert tags.contains(r"\w_\w", is_regex=True) is True + assert tags.contains(r"\d_\d", is_regex=True) is False + + +def test_inline_tags_create() -> None: + """Test inline tags creation.""" + tags = InlineTags(FRONTMATTER_CONTENT) + tags.metadata_key + assert tags.list == [] + + tags = InlineTags(INLINE_CONTENT) + assert tags.list == [ + "bold_tag", + "in_text_tag", + "inline_tag_top1", + "inline_tag_top2", + "tag_key_value", + ] + assert tags.list_original == [ + "bold_tag", + "in_text_tag", + "inline_tag_top1", + "inline_tag_top2", + "tag_key_value", + ] + + +def test_inline_tags_delete() -> None: + """Test inline tags delete.""" + tags = InlineTags(INLINE_CONTENT) + assert tags.list == [ + "bold_tag", + "in_text_tag", + "inline_tag_top1", + "inline_tag_top2", + "tag_key_value", + ] + + assert tags.delete("no tag") is False + assert tags.has_changes() is False + assert tags.delete("bold_tag") is True + assert tags.list == [ + "in_text_tag", + "inline_tag_top1", + "inline_tag_top2", + "tag_key_value", + ] + assert tags.has_changes() is True + assert tags.delete(r"\d{3}") is False + assert tags.delete(r"inline_tag_top\d") is True + assert tags.list == ["in_text_tag", "tag_key_value"] + + +def test_inline_tags_rename() -> None: + """Test inline tags rename.""" + tags = InlineTags(INLINE_CONTENT) + assert tags.list == [ + "bold_tag", + "in_text_tag", + "inline_tag_top1", + "inline_tag_top2", + "tag_key_value", + ] + + assert tags.rename("no tag", "new tag") is False + assert tags.has_changes() is False + assert tags.rename("bold_tag", "new tag") is True + assert tags.list == [ + "in_text_tag", + "inline_tag_top1", + "inline_tag_top2", + "new tag", + "tag_key_value", + ] + assert tags.has_changes() is True + + def test_vault_metadata() -> None: """Test VaultMetadata class.""" vm = VaultMetadata() @@ -295,452 +744,3 @@ def test_vault_metadata_rename() -> None: assert vm.rename("tags", "old_tags") is True assert vm.dict["old_tags"] == ["new tag", "tag 1", "tag 3"] assert "tags" not in vm.dict - - -def test_frontmatter_create() -> None: - """Test frontmatter creation.""" - frontmatter = Frontmatter(INLINE_CONTENT) - assert frontmatter.dict == {} - - frontmatter = Frontmatter(FRONTMATTER_CONTENT) - assert frontmatter.dict == { - "frontmatter_Key1": ["frontmatter_Key1_value"], - "frontmatter_Key2": ["article", "note"], - "shared_key1": ["shared_key1_value"], - "tags": ["tag_1", "tag_2", "📅/tag_3"], - } - assert frontmatter.dict_original == { - "frontmatter_Key1": ["frontmatter_Key1_value"], - "frontmatter_Key2": ["article", "note"], - "shared_key1": ["shared_key1_value"], - "tags": ["tag_1", "tag_2", "📅/tag_3"], - } - - -def test_frontmatter_contains() -> None: - """Test frontmatter contains.""" - frontmatter = Frontmatter(FRONTMATTER_CONTENT) - - assert frontmatter.contains("frontmatter_Key1") is True - assert frontmatter.contains("frontmatter_Key2", "article") is True - assert frontmatter.contains("frontmatter_Key3") is False - assert frontmatter.contains("frontmatter_Key2", "no value") is False - - assert frontmatter.contains(r"\d$", is_regex=True) is True - assert frontmatter.contains(r"^\d", is_regex=True) is False - assert frontmatter.contains("key", r"_\d", is_regex=True) is False - assert frontmatter.contains("key", r"\w\d_", is_regex=True) is True - - -def test_frontmatter_add() -> None: - """Test frontmatter add.""" - frontmatter = Frontmatter(FRONTMATTER_CONTENT) - - assert frontmatter.add("frontmatter_Key1") is False - assert frontmatter.add("added_key") is True - assert frontmatter.dict == { - "added_key": [], - "frontmatter_Key1": ["frontmatter_Key1_value"], - "frontmatter_Key2": ["article", "note"], - "shared_key1": ["shared_key1_value"], - "tags": ["tag_1", "tag_2", "📅/tag_3"], - } - - assert frontmatter.add("added_key", "added_value") is True - assert frontmatter.dict == { - "added_key": ["added_value"], - "frontmatter_Key1": ["frontmatter_Key1_value"], - "frontmatter_Key2": ["article", "note"], - "shared_key1": ["shared_key1_value"], - "tags": ["tag_1", "tag_2", "📅/tag_3"], - } - - assert frontmatter.add("added_key", "added_value_2") is True - assert frontmatter.dict == { - "added_key": ["added_value", "added_value_2"], - "frontmatter_Key1": ["frontmatter_Key1_value"], - "frontmatter_Key2": ["article", "note"], - "shared_key1": ["shared_key1_value"], - "tags": ["tag_1", "tag_2", "📅/tag_3"], - } - - assert frontmatter.add("added_key", ["added_value_3", "added_value_4"]) is True - assert frontmatter.dict == { - "added_key": ["added_value", "added_value_2", "added_value_3", "added_value_4"], - "frontmatter_Key1": ["frontmatter_Key1_value"], - "frontmatter_Key2": ["article", "note"], - "shared_key1": ["shared_key1_value"], - "tags": ["tag_1", "tag_2", "📅/tag_3"], - } - - assert frontmatter.add("added_key2", ["added_value_1", "added_value_2"]) is True - assert frontmatter.dict == { - "added_key": ["added_value", "added_value_2", "added_value_3", "added_value_4"], - "added_key2": ["added_value_1", "added_value_2"], - "frontmatter_Key1": ["frontmatter_Key1_value"], - "frontmatter_Key2": ["article", "note"], - "shared_key1": ["shared_key1_value"], - "tags": ["tag_1", "tag_2", "📅/tag_3"], - } - - assert frontmatter.add("added_key3", "added_value_1") is True - assert frontmatter.dict == { - "added_key": ["added_value", "added_value_2", "added_value_3", "added_value_4"], - "added_key2": ["added_value_1", "added_value_2"], - "added_key3": ["added_value_1"], - "frontmatter_Key1": ["frontmatter_Key1_value"], - "frontmatter_Key2": ["article", "note"], - "shared_key1": ["shared_key1_value"], - "tags": ["tag_1", "tag_2", "📅/tag_3"], - } - - assert frontmatter.add("added_key3", "added_value_1") is False - - -def test_frontmatter_rename() -> None: - """Test frontmatter rename.""" - frontmatter = Frontmatter(FRONTMATTER_CONTENT) - assert frontmatter.dict == { - "frontmatter_Key1": ["frontmatter_Key1_value"], - "frontmatter_Key2": ["article", "note"], - "shared_key1": ["shared_key1_value"], - "tags": ["tag_1", "tag_2", "📅/tag_3"], - } - - assert frontmatter.rename("no key", "new key") is False - assert frontmatter.rename("tags", "no tag", "new key") is False - - assert frontmatter.has_changes() is False - assert frontmatter.rename("tags", "tag_2", "new tag") is True - - assert frontmatter.dict["tags"] == ["new tag", "tag_1", "📅/tag_3"] - assert frontmatter.rename("tags", "old_tags") is True - assert frontmatter.dict["old_tags"] == ["new tag", "tag_1", "📅/tag_3"] - assert "tags" not in frontmatter.dict - - assert frontmatter.has_changes() is True - - -def test_frontmatter_delete() -> None: - """Test Frontmatter delete method.""" - frontmatter = Frontmatter(FRONTMATTER_CONTENT) - assert frontmatter.dict == { - "frontmatter_Key1": ["frontmatter_Key1_value"], - "frontmatter_Key2": ["article", "note"], - "shared_key1": ["shared_key1_value"], - "tags": ["tag_1", "tag_2", "📅/tag_3"], - } - - assert frontmatter.delete("no key") is False - assert frontmatter.delete("tags", "no value") is False - assert frontmatter.delete(r"\d{3}") is False - assert frontmatter.has_changes() is False - assert frontmatter.delete("tags", "tag_2") is True - assert frontmatter.dict["tags"] == ["tag_1", "📅/tag_3"] - assert frontmatter.delete("tags") is True - assert "tags" not in frontmatter.dict - assert frontmatter.has_changes() is True - assert frontmatter.delete("shared_key1", r"\w+") is True - assert frontmatter.dict["shared_key1"] == [] - assert frontmatter.delete(r"\w.tter") is True - assert frontmatter.dict == {"shared_key1": []} - - -def test_frontmatter_yaml_conversion(): - """Test Frontmatter to_yaml method.""" - new_frontmatter: str = """\ -tags: - - tag_1 - - tag_2 - - 📅/tag_3 -frontmatter_Key1: frontmatter_Key1_value -frontmatter_Key2: - - article - - note -shared_key1: shared_key1_value -""" - new_frontmatter_sorted: str = """\ -frontmatter_Key1: frontmatter_Key1_value -frontmatter_Key2: - - article - - note -shared_key1: shared_key1_value -tags: - - tag_1 - - tag_2 - - 📅/tag_3 -""" - frontmatter = Frontmatter(FRONTMATTER_CONTENT) - assert frontmatter.to_yaml() == new_frontmatter - assert frontmatter.to_yaml(sort_keys=True) == new_frontmatter_sorted - - -def test_inline_metadata_create() -> None: - """Test inline metadata creation.""" - inline = InlineMetadata(FRONTMATTER_CONTENT) - assert inline.dict == {} - inline = InlineMetadata(INLINE_CONTENT) - assert inline.dict == { - "bold_key1": ["bold_key1_value"], - "bold_key2": ["bold_key2_value"], - "emoji_📅_key": ["emoji_📅_key_value"], - "in_text_key1": ["in_text_key1_value"], - "in_text_key2": ["in_text_key2_value"], - "link_key": ["link_key_value"], - "repeated_key": ["repeated_key_value1", "repeated_key_value2"], - "tag_key": ["tag_key_value"], - } - assert inline.dict_original == { - "bold_key1": ["bold_key1_value"], - "bold_key2": ["bold_key2_value"], - "emoji_📅_key": ["emoji_📅_key_value"], - "in_text_key1": ["in_text_key1_value"], - "in_text_key2": ["in_text_key2_value"], - "link_key": ["link_key_value"], - "repeated_key": ["repeated_key_value1", "repeated_key_value2"], - "tag_key": ["tag_key_value"], - } - - -def test_inline_contains() -> None: - """Test inline metadata contains method.""" - inline = InlineMetadata(INLINE_CONTENT) - - assert inline.contains("bold_key1") is True - assert inline.contains("bold_key2", "bold_key2_value") is True - assert inline.contains("bold_key3") is False - assert inline.contains("bold_key2", "no value") is False - - assert inline.contains(r"\w{4}_key", is_regex=True) is True - assert inline.contains(r"^\d", is_regex=True) is False - assert inline.contains("1$", r"\d_value", is_regex=True) is True - assert inline.contains("key", r"^\d_value", is_regex=True) is False - - -def test_inline_add() -> None: - """Test inline add.""" - inline = InlineMetadata(INLINE_CONTENT) - - assert inline.add("bold_key1") is False - assert inline.add("bold_key1", "bold_key1_value") is False - assert inline.add("added_key") is True - assert inline.dict == { - "added_key": [], - "bold_key1": ["bold_key1_value"], - "bold_key2": ["bold_key2_value"], - "emoji_📅_key": ["emoji_📅_key_value"], - "in_text_key1": ["in_text_key1_value"], - "in_text_key2": ["in_text_key2_value"], - "link_key": ["link_key_value"], - "repeated_key": ["repeated_key_value1", "repeated_key_value2"], - "tag_key": ["tag_key_value"], - } - - assert inline.add("added_key1", "added_value") is True - assert inline.dict == { - "added_key": [], - "added_key1": ["added_value"], - "bold_key1": ["bold_key1_value"], - "bold_key2": ["bold_key2_value"], - "emoji_📅_key": ["emoji_📅_key_value"], - "in_text_key1": ["in_text_key1_value"], - "in_text_key2": ["in_text_key2_value"], - "link_key": ["link_key_value"], - "repeated_key": ["repeated_key_value1", "repeated_key_value2"], - "tag_key": ["tag_key_value"], - } - - with pytest.raises(ValueError): - assert inline.add("added_key1", "added_value_2") is True - - assert inline.dict == { - "added_key": [], - "added_key1": ["added_value"], - "bold_key1": ["bold_key1_value"], - "bold_key2": ["bold_key2_value"], - "emoji_📅_key": ["emoji_📅_key_value"], - "in_text_key1": ["in_text_key1_value"], - "in_text_key2": ["in_text_key2_value"], - "link_key": ["link_key_value"], - "repeated_key": ["repeated_key_value1", "repeated_key_value2"], - "tag_key": ["tag_key_value"], - } - - assert inline.add("added_key2", ["added_value_1", "added_value_2"]) is True - assert inline.dict == { - "added_key": [], - "added_key1": ["added_value"], - "added_key2": ["added_value_1"], - "bold_key1": ["bold_key1_value"], - "bold_key2": ["bold_key2_value"], - "emoji_📅_key": ["emoji_📅_key_value"], - "in_text_key1": ["in_text_key1_value"], - "in_text_key2": ["in_text_key2_value"], - "link_key": ["link_key_value"], - "repeated_key": ["repeated_key_value1", "repeated_key_value2"], - "tag_key": ["tag_key_value"], - } - - assert inline.add("added_key", "added_value") - assert inline.dict == { - "added_key": ["added_value"], - "added_key1": ["added_value"], - "added_key2": ["added_value_1"], - "bold_key1": ["bold_key1_value"], - "bold_key2": ["bold_key2_value"], - "emoji_📅_key": ["emoji_📅_key_value"], - "in_text_key1": ["in_text_key1_value"], - "in_text_key2": ["in_text_key2_value"], - "link_key": ["link_key_value"], - "repeated_key": ["repeated_key_value1", "repeated_key_value2"], - "tag_key": ["tag_key_value"], - } - - -def test_inline_metadata_rename() -> None: - """Test inline metadata rename.""" - inline = InlineMetadata(INLINE_CONTENT) - assert inline.dict == { - "bold_key1": ["bold_key1_value"], - "bold_key2": ["bold_key2_value"], - "emoji_📅_key": ["emoji_📅_key_value"], - "in_text_key1": ["in_text_key1_value"], - "in_text_key2": ["in_text_key2_value"], - "link_key": ["link_key_value"], - "repeated_key": ["repeated_key_value1", "repeated_key_value2"], - "tag_key": ["tag_key_value"], - } - - assert inline.rename("no key", "new key") is False - assert inline.rename("repeated_key", "no value", "new key") is False - assert inline.has_changes() is False - assert inline.rename("repeated_key", "repeated_key_value1", "new value") is True - assert inline.dict["repeated_key"] == ["new value", "repeated_key_value2"] - assert inline.rename("repeated_key", "old_key") is True - assert inline.dict["old_key"] == ["new value", "repeated_key_value2"] - assert "repeated_key" not in inline.dict - assert inline.has_changes() is True - - -def test_inline_metadata_delete() -> None: - """Test inline metadata delete.""" - inline = InlineMetadata(INLINE_CONTENT) - assert inline.dict == { - "bold_key1": ["bold_key1_value"], - "bold_key2": ["bold_key2_value"], - "emoji_📅_key": ["emoji_📅_key_value"], - "in_text_key1": ["in_text_key1_value"], - "in_text_key2": ["in_text_key2_value"], - "link_key": ["link_key_value"], - "repeated_key": ["repeated_key_value1", "repeated_key_value2"], - "tag_key": ["tag_key_value"], - } - - assert inline.delete("no key") is False - assert inline.delete("repeated_key", "no value") is False - assert inline.has_changes() is False - assert inline.delete("repeated_key", "repeated_key_value1") is True - assert inline.dict["repeated_key"] == ["repeated_key_value2"] - assert inline.delete("repeated_key") is True - assert "repeated_key" not in inline.dict - assert inline.has_changes() is True - assert inline.delete(r"\d{3}") is False - assert inline.delete(r"bold_key\d") is True - assert inline.dict == { - "emoji_📅_key": ["emoji_📅_key_value"], - "in_text_key1": ["in_text_key1_value"], - "in_text_key2": ["in_text_key2_value"], - "link_key": ["link_key_value"], - "tag_key": ["tag_key_value"], - } - assert inline.delete("emoji_📅_key", ".*📅.*") is True - assert inline.dict == { - "emoji_📅_key": [], - "in_text_key1": ["in_text_key1_value"], - "in_text_key2": ["in_text_key2_value"], - "link_key": ["link_key_value"], - "tag_key": ["tag_key_value"], - } - - -def test_inline_tags_create() -> None: - """Test inline tags creation.""" - tags = InlineTags(FRONTMATTER_CONTENT) - tags.metadata_key - assert tags.list == [] - - tags = InlineTags(INLINE_CONTENT) - assert tags.list == [ - "bold_tag", - "in_text_tag", - "inline_tag_top1", - "inline_tag_top2", - "tag_key_value", - ] - assert tags.list_original == [ - "bold_tag", - "in_text_tag", - "inline_tag_top1", - "inline_tag_top2", - "tag_key_value", - ] - - -def test_inline_tags_contains() -> None: - """Test inline tags contains.""" - tags = InlineTags(INLINE_CONTENT) - assert tags.contains("bold_tag") is True - assert tags.contains("no tag") is False - - assert tags.contains(r"\w_\w", is_regex=True) is True - assert tags.contains(r"\d_\d", is_regex=True) is False - - -def test_inline_tags_rename() -> None: - """Test inline tags rename.""" - tags = InlineTags(INLINE_CONTENT) - assert tags.list == [ - "bold_tag", - "in_text_tag", - "inline_tag_top1", - "inline_tag_top2", - "tag_key_value", - ] - - assert tags.rename("no tag", "new tag") is False - assert tags.has_changes() is False - assert tags.rename("bold_tag", "new tag") is True - assert tags.list == [ - "in_text_tag", - "inline_tag_top1", - "inline_tag_top2", - "new tag", - "tag_key_value", - ] - assert tags.has_changes() is True - - -def test_inline_tags_delete() -> None: - """Test inline tags delete.""" - tags = InlineTags(INLINE_CONTENT) - assert tags.list == [ - "bold_tag", - "in_text_tag", - "inline_tag_top1", - "inline_tag_top2", - "tag_key_value", - ] - - assert tags.delete("no tag") is False - assert tags.has_changes() is False - assert tags.delete("bold_tag") is True - assert tags.list == [ - "in_text_tag", - "inline_tag_top1", - "inline_tag_top2", - "tag_key_value", - ] - assert tags.has_changes() is True - assert tags.delete(r"\d{3}") is False - assert tags.delete(r"inline_tag_top\d") is True - assert tags.list == ["in_text_tag", "tag_key_value"] diff --git a/tests/notes_test.py b/tests/notes_test.py index c2c3f6b..2f2a859 100644 --- a/tests/notes_test.py +++ b/tests/notes_test.py @@ -103,6 +103,7 @@ def test_add_metadata_inline(short_note) -> None: def test_add_metadata_frontmatter(sample_note) -> None: """Test adding metadata.""" note = Note(note_path=sample_note) + assert note.add_metadata(MetadataType.FRONTMATTER, "frontmatter_Key1") is False assert note.add_metadata(MetadataType.FRONTMATTER, "shared_key1", "shared_key1_value") is False assert note.add_metadata(MetadataType.FRONTMATTER, "new_key1") is True @@ -159,6 +160,30 @@ def test_add_metadata_frontmatter(sample_note) -> None: } +def test_add_metadata_tag(sample_note) -> None: + """Test adding inline tags.""" + note = Note(note_path=sample_note) + + assert ( + note.add_metadata(MetadataType.TAGS, value="shared_tag", location=InsertLocation.TOP) + is False + ) + assert ( + note.add_metadata(MetadataType.TAGS, value="a_new_tag", location=InsertLocation.TOP) is True + ) + assert note.inline_tags.list == [ + "a_new_tag", + "inline_tag_bottom1", + "inline_tag_bottom2", + "inline_tag_top1", + "inline_tag_top2", + "intext_tag1", + "intext_tag2", + "shared_tag", + ] + assert "#a_new_tag" in note.file_content + + def test_contains_inline_tag(sample_note) -> None: """Test contains inline tag.""" note = Note(note_path=sample_note)