From 2f89569020983dad890f6967f618b2bee513943b Mon Sep 17 00:00:00 2001 From: Nathaniel Landau Date: Sat, 4 Feb 2023 23:24:08 -0500 Subject: [PATCH 1/2] feat: add new tags --- README.md | 2 +- src/obsidian_metadata/cli.py | 3 +- 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 | 652 ++++++++++---------- tests/notes_test.py | 25 + 10 files changed, 426 insertions(+), 346 deletions(-) diff --git a/README.md b/README.md index d8112fa..b29a44e 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Once installed, run `obsidian-metadata` in your terminal to enter an interactive - 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 to 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. diff --git a/src/obsidian_metadata/cli.py b/src/obsidian_metadata/cli.py index a49c234..8108047 100644 --- a/src/obsidian_metadata/cli.py +++ b/src/obsidian_metadata/cli.py @@ -120,7 +120,8 @@ def main( • 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) - • [dim]Add to inline tag (Not yet implemented)[/] + • Add to 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 @@ def application_add_metadata(self) -> None: 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 @@ def __repr__(self) -> str: # pragma: no cover """ 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 @@ def add(self, key: str, value: str | list[str] = None) -> bool: 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 @@ def _grab_inline_tags(self, file_content: str) -> list[str]: ) ) + 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 @@ def _rename_inline_metadata(self, key: str, value_1: str, value_2: str = None) - def add_metadata( self, area: MetadataType, - key: str, + key: str = None, value: str | list[str] = None, location: InsertLocation = None, ) -> bool: @@ -128,7 +128,7 @@ def add_metadata( 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 @@ def add_metadata( 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 @@ def add_metadata( 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 @@ def ask_new_key(self, question: str = "New key name") -> str: # pragma: no cove 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 @@ def _rebuild_vault_metadata(self) -> None: def add_metadata( self, area: MetadataType, - key: str, + key: str = None, value: str | list[str] = None, location: InsertLocation = None, ) -> int: @@ -186,7 +186,7 @@ def add_metadata( 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,234 +69,6 @@ """ -def test_vault_metadata() -> None: - """Test VaultMetadata class.""" - vm = VaultMetadata() - assert vm.dict == {} - - vm.index_metadata(area=MetadataType.FRONTMATTER, metadata=METADATA) - vm.index_metadata(area=MetadataType.INLINE, metadata=METADATA_2) - vm.index_metadata(area=MetadataType.TAGS, metadata=TAG_LIST) - assert vm.dict == { - "frontmatter_Key1": ["author name"], - "frontmatter_Key2": ["article", "note"], - "intext_key": ["intext_key_value"], - "key1": ["value1"], - "key2": ["value2", "value3"], - "shared_key1": ["shared_key1_value"], - "shared_key2": ["shared_key2_value"], - "tags": ["tag 1", "tag 2", "tag 3"], - "top_key1": ["top_key1_value"], - "top_key2": ["top_key2_value"], - "top_key3": ["top_key3_value"], - } - assert vm.frontmatter == { - "frontmatter_Key1": ["author name"], - "frontmatter_Key2": ["article", "note"], - "intext_key": ["intext_key_value"], - "shared_key1": ["shared_key1_value"], - "shared_key2": ["shared_key2_value"], - "tags": ["tag 1", "tag 2", "tag 3"], - "top_key1": ["top_key1_value"], - "top_key2": ["top_key2_value"], - "top_key3": ["top_key3_value"], - } - assert vm.inline_metadata == {"key1": ["value1"], "key2": ["value2", "value3"]} - assert vm.tags == ["tag 1", "tag 2", "tag 3"] - - new_metadata = {"added_key": ["added_value"], "frontmatter_Key2": ["new_value"]} - new_tags = ["tag 4", "tag 5"] - vm.index_metadata(area=MetadataType.FRONTMATTER, metadata=new_metadata) - vm.index_metadata(area=MetadataType.TAGS, metadata=new_tags) - assert vm.dict == { - "added_key": ["added_value"], - "frontmatter_Key1": ["author name"], - "frontmatter_Key2": ["article", "new_value", "note"], - "intext_key": ["intext_key_value"], - "key1": ["value1"], - "key2": ["value2", "value3"], - "shared_key1": ["shared_key1_value"], - "shared_key2": ["shared_key2_value"], - "tags": ["tag 1", "tag 2", "tag 3"], - "top_key1": ["top_key1_value"], - "top_key2": ["top_key2_value"], - "top_key3": ["top_key3_value"], - } - assert vm.frontmatter == { - "added_key": ["added_value"], - "frontmatter_Key1": ["author name"], - "frontmatter_Key2": ["article", "new_value", "note"], - "intext_key": ["intext_key_value"], - "shared_key1": ["shared_key1_value"], - "shared_key2": ["shared_key2_value"], - "tags": ["tag 1", "tag 2", "tag 3"], - "top_key1": ["top_key1_value"], - "top_key2": ["top_key2_value"], - "top_key3": ["top_key3_value"], - } - assert vm.inline_metadata == {"key1": ["value1"], "key2": ["value2", "value3"]} - assert vm.tags == ["tag 1", "tag 2", "tag 3", "tag 4", "tag 5"] - - -def test_vault_metadata_print(capsys) -> None: - """Test print_metadata method.""" - vm = VaultMetadata() - vm.index_metadata(area=MetadataType.FRONTMATTER, metadata=METADATA) - vm.index_metadata(area=MetadataType.INLINE, metadata=METADATA_2) - vm.index_metadata(area=MetadataType.TAGS, metadata=TAG_LIST) - - vm.print_metadata(area=MetadataType.ALL) - captured = capsys.readouterr() - assert "All metadata" in captured.out - assert "All inline tags" in captured.out - assert "┃ Keys ┃ Values ┃" in captured.out - assert "│ shared_key1 │ shared_key1_value │" in captured.out - assert captured.out == Regex("#tag 1 +#tag 2") - - vm.print_metadata(area=MetadataType.FRONTMATTER) - captured = capsys.readouterr() - assert "All frontmatter" in captured.out - assert "┃ Keys ┃ Values ┃" in captured.out - assert "│ shared_key1 │ shared_key1_value │" in captured.out - assert "value1" not in captured.out - - vm.print_metadata(area=MetadataType.INLINE) - captured = capsys.readouterr() - assert "All inline" in captured.out - assert "┃ Keys ┃ Values ┃" in captured.out - assert "shared_key1" not in captured.out - assert "│ key1 │ value1 │" in captured.out - - vm.print_metadata(area=MetadataType.TAGS) - captured = capsys.readouterr() - assert "All inline tags " in captured.out - assert "┃ Keys ┃ Values ┃" not in captured.out - assert captured.out == Regex("#tag 1 +#tag 2") - - vm.print_metadata(area=MetadataType.KEYS) - captured = capsys.readouterr() - assert "All Keys " in captured.out - assert "┃ Keys ┃ Values ┃" not in captured.out - assert captured.out != Regex("#tag 1 +#tag 2") - assert captured.out == Regex("frontmatter_Key1 +frontmatter_Key2") - - -def test_vault_metadata_contains() -> None: - """Test contains method.""" - vm = VaultMetadata() - vm.index_metadata(area=MetadataType.FRONTMATTER, metadata=METADATA) - vm.index_metadata(area=MetadataType.INLINE, metadata=METADATA_2) - vm.index_metadata(area=MetadataType.TAGS, metadata=TAG_LIST) - assert vm.dict == { - "frontmatter_Key1": ["author name"], - "frontmatter_Key2": ["article", "note"], - "intext_key": ["intext_key_value"], - "key1": ["value1"], - "key2": ["value2", "value3"], - "shared_key1": ["shared_key1_value"], - "shared_key2": ["shared_key2_value"], - "tags": ["tag 1", "tag 2", "tag 3"], - "top_key1": ["top_key1_value"], - "top_key2": ["top_key2_value"], - "top_key3": ["top_key3_value"], - } - assert vm.frontmatter == { - "frontmatter_Key1": ["author name"], - "frontmatter_Key2": ["article", "note"], - "intext_key": ["intext_key_value"], - "shared_key1": ["shared_key1_value"], - "shared_key2": ["shared_key2_value"], - "tags": ["tag 1", "tag 2", "tag 3"], - "top_key1": ["top_key1_value"], - "top_key2": ["top_key2_value"], - "top_key3": ["top_key3_value"], - } - assert vm.inline_metadata == {"key1": ["value1"], "key2": ["value2", "value3"]} - assert vm.tags == ["tag 1", "tag 2", "tag 3"] - - with pytest.raises(ValueError): - vm.contains(area=MetadataType.ALL, value="key1") - - assert vm.contains(area=MetadataType.ALL, key="no_key") is False - assert vm.contains(area=MetadataType.ALL, key="key1") is True - assert vm.contains(area=MetadataType.ALL, key="frontmatter_Key2", value="article") is True - assert vm.contains(area=MetadataType.ALL, key="frontmatter_Key2", value="none") is False - assert vm.contains(area=MetadataType.ALL, key="1$", is_regex=True) is True - assert vm.contains(area=MetadataType.ALL, key=r"\d\d", is_regex=True) is False - - assert vm.contains(area=MetadataType.FRONTMATTER, key="no_key") is False - assert vm.contains(area=MetadataType.FRONTMATTER, key="frontmatter_Key1") is True - assert ( - vm.contains(area=MetadataType.FRONTMATTER, key="frontmatter_Key2", value="article") is True - ) - assert vm.contains(area=MetadataType.FRONTMATTER, key="frontmatter_Key2", value="none") is False - assert vm.contains(area=MetadataType.FRONTMATTER, key="1$", is_regex=True) is True - assert vm.contains(area=MetadataType.FRONTMATTER, key=r"\d\d", is_regex=True) is False - - assert vm.contains(area=MetadataType.INLINE, key="no_key") is False - assert vm.contains(area=MetadataType.INLINE, key="key1") is True - assert vm.contains(area=MetadataType.INLINE, key="key2", value="value3") is True - assert vm.contains(area=MetadataType.INLINE, key="key2", value="none") is False - assert vm.contains(area=MetadataType.INLINE, key="1$", is_regex=True) is True - assert vm.contains(area=MetadataType.INLINE, key=r"\d\d", is_regex=True) is False - - assert vm.contains(area=MetadataType.TAGS, value="no_tag") is False - assert vm.contains(area=MetadataType.TAGS, value="tag 1") is True - assert vm.contains(area=MetadataType.TAGS, value=r"\w+ \d$", is_regex=True) is True - assert vm.contains(area=MetadataType.TAGS, value=r"\w+ \d\d$", is_regex=True) is False - with pytest.raises(ValueError): - vm.contains(area=MetadataType.TAGS, key="key1") - - -def test_vault_metadata_delete() -> None: - """Test delete method.""" - vm = VaultMetadata() - vm.index_metadata(area=MetadataType.FRONTMATTER, metadata=METADATA) - assert vm.dict == { - "frontmatter_Key1": ["author name"], - "frontmatter_Key2": ["article", "note"], - "intext_key": ["intext_key_value"], - "shared_key1": ["shared_key1_value"], - "shared_key2": ["shared_key2_value"], - "tags": ["tag 1", "tag 2", "tag 3"], - "top_key1": ["top_key1_value"], - "top_key2": ["top_key2_value"], - "top_key3": ["top_key3_value"], - } - - assert vm.delete("no key") is False - assert vm.delete("tags", "no value") is False - assert vm.delete("tags", "tag 2") is True - assert vm.dict["tags"] == ["tag 1", "tag 3"] - assert vm.delete("tags") is True - assert "tags" not in vm.dict - - -def test_vault_metadata_rename() -> None: - """Test rename method.""" - vm = VaultMetadata() - vm.index_metadata(area=MetadataType.FRONTMATTER, metadata=METADATA) - assert vm.dict == { - "frontmatter_Key1": ["author name"], - "frontmatter_Key2": ["article", "note"], - "intext_key": ["intext_key_value"], - "shared_key1": ["shared_key1_value"], - "shared_key2": ["shared_key2_value"], - "tags": ["tag 1", "tag 2", "tag 3"], - "top_key1": ["top_key1_value"], - "top_key2": ["top_key2_value"], - "top_key3": ["top_key3_value"], - } - - assert vm.rename("no key", "new key") is False - assert vm.rename("tags", "no tag", "new key") is False - assert vm.rename("tags", "tag 2", "new tag") is True - assert vm.dict["tags"] == ["new tag", "tag 1", "tag 3"] - 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) @@ -475,12 +247,15 @@ def test_frontmatter_yaml_conversion(): 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 == {} +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"], @@ -490,7 +265,11 @@ def test_inline_metadata_create() -> None: "repeated_key": ["repeated_key_value1", "repeated_key_value2"], "tag_key": ["tag_key_value"], } - assert inline.dict_original == { + + 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"], @@ -501,31 +280,12 @@ def test_inline_metadata_create() -> None: "tag_key": ["tag_key_value"], } + with pytest.raises(ValueError): + assert inline.add("added_key1", "added_value_2") is True -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": [], + "added_key1": ["added_value"], "bold_key1": ["bold_key1_value"], "bold_key2": ["bold_key2_value"], "emoji_📅_key": ["emoji_📅_key_value"], @@ -536,9 +296,9 @@ def test_inline_add() -> None: "tag_key": ["tag_key_value"], } - assert inline.add("added_key1", "added_value") is True + assert inline.add("added_key", "added_value") assert inline.dict == { - "added_key": [], + "added_key": ["added_value"], "added_key1": ["added_value"], "bold_key1": ["bold_key1_value"], "bold_key2": ["bold_key2_value"], @@ -550,42 +310,28 @@ def test_inline_add() -> None: "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"], - } +def test_inline_metadata_contains() -> None: + """Test inline metadata contains method.""" + inline = InlineMetadata(INLINE_CONTENT) - 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.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.add("added_key", "added_value") + 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 == { - "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"], @@ -595,12 +341,7 @@ def test_inline_add() -> None: "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 == { + assert inline.dict_original == { "bold_key1": ["bold_key1_value"], "bold_key2": ["bold_key2_value"], "emoji_📅_key": ["emoji_📅_key_value"], @@ -611,16 +352,6 @@ def test_inline_metadata_rename() -> None: "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.""" @@ -663,25 +394,43 @@ def test_inline_metadata_delete() -> None: } -def test_inline_tags_create() -> None: - """Test inline tags creation.""" - tags = InlineTags(FRONTMATTER_CONTENT) - tags.metadata_key - assert tags.list == [] +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", - "tag_key_value", - ] - assert tags.list_original == [ - "bold_tag", - "in_text_tag", - "inline_tag_top1", - "inline_tag_top2", + "new_tag", "tag_key_value", ] @@ -696,8 +445,12 @@ def test_inline_tags_contains() -> None: assert tags.contains(r"\d_\d", is_regex=True) is False -def test_inline_tags_rename() -> None: - """Test inline tags rename.""" +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", @@ -706,18 +459,13 @@ def test_inline_tags_rename() -> None: "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 == [ + assert tags.list_original == [ + "bold_tag", "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: @@ -744,3 +492,255 @@ def test_inline_tags_delete() -> None: 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() + assert vm.dict == {} + + vm.index_metadata(area=MetadataType.FRONTMATTER, metadata=METADATA) + vm.index_metadata(area=MetadataType.INLINE, metadata=METADATA_2) + vm.index_metadata(area=MetadataType.TAGS, metadata=TAG_LIST) + assert vm.dict == { + "frontmatter_Key1": ["author name"], + "frontmatter_Key2": ["article", "note"], + "intext_key": ["intext_key_value"], + "key1": ["value1"], + "key2": ["value2", "value3"], + "shared_key1": ["shared_key1_value"], + "shared_key2": ["shared_key2_value"], + "tags": ["tag 1", "tag 2", "tag 3"], + "top_key1": ["top_key1_value"], + "top_key2": ["top_key2_value"], + "top_key3": ["top_key3_value"], + } + assert vm.frontmatter == { + "frontmatter_Key1": ["author name"], + "frontmatter_Key2": ["article", "note"], + "intext_key": ["intext_key_value"], + "shared_key1": ["shared_key1_value"], + "shared_key2": ["shared_key2_value"], + "tags": ["tag 1", "tag 2", "tag 3"], + "top_key1": ["top_key1_value"], + "top_key2": ["top_key2_value"], + "top_key3": ["top_key3_value"], + } + assert vm.inline_metadata == {"key1": ["value1"], "key2": ["value2", "value3"]} + assert vm.tags == ["tag 1", "tag 2", "tag 3"] + + new_metadata = {"added_key": ["added_value"], "frontmatter_Key2": ["new_value"]} + new_tags = ["tag 4", "tag 5"] + vm.index_metadata(area=MetadataType.FRONTMATTER, metadata=new_metadata) + vm.index_metadata(area=MetadataType.TAGS, metadata=new_tags) + assert vm.dict == { + "added_key": ["added_value"], + "frontmatter_Key1": ["author name"], + "frontmatter_Key2": ["article", "new_value", "note"], + "intext_key": ["intext_key_value"], + "key1": ["value1"], + "key2": ["value2", "value3"], + "shared_key1": ["shared_key1_value"], + "shared_key2": ["shared_key2_value"], + "tags": ["tag 1", "tag 2", "tag 3"], + "top_key1": ["top_key1_value"], + "top_key2": ["top_key2_value"], + "top_key3": ["top_key3_value"], + } + assert vm.frontmatter == { + "added_key": ["added_value"], + "frontmatter_Key1": ["author name"], + "frontmatter_Key2": ["article", "new_value", "note"], + "intext_key": ["intext_key_value"], + "shared_key1": ["shared_key1_value"], + "shared_key2": ["shared_key2_value"], + "tags": ["tag 1", "tag 2", "tag 3"], + "top_key1": ["top_key1_value"], + "top_key2": ["top_key2_value"], + "top_key3": ["top_key3_value"], + } + assert vm.inline_metadata == {"key1": ["value1"], "key2": ["value2", "value3"]} + assert vm.tags == ["tag 1", "tag 2", "tag 3", "tag 4", "tag 5"] + + +def test_vault_metadata_print(capsys) -> None: + """Test print_metadata method.""" + vm = VaultMetadata() + vm.index_metadata(area=MetadataType.FRONTMATTER, metadata=METADATA) + vm.index_metadata(area=MetadataType.INLINE, metadata=METADATA_2) + vm.index_metadata(area=MetadataType.TAGS, metadata=TAG_LIST) + + vm.print_metadata(area=MetadataType.ALL) + captured = capsys.readouterr() + assert "All metadata" in captured.out + assert "All inline tags" in captured.out + assert "┃ Keys ┃ Values ┃" in captured.out + assert "│ shared_key1 │ shared_key1_value │" in captured.out + assert captured.out == Regex("#tag 1 +#tag 2") + + vm.print_metadata(area=MetadataType.FRONTMATTER) + captured = capsys.readouterr() + assert "All frontmatter" in captured.out + assert "┃ Keys ┃ Values ┃" in captured.out + assert "│ shared_key1 │ shared_key1_value │" in captured.out + assert "value1" not in captured.out + + vm.print_metadata(area=MetadataType.INLINE) + captured = capsys.readouterr() + assert "All inline" in captured.out + assert "┃ Keys ┃ Values ┃" in captured.out + assert "shared_key1" not in captured.out + assert "│ key1 │ value1 │" in captured.out + + vm.print_metadata(area=MetadataType.TAGS) + captured = capsys.readouterr() + assert "All inline tags " in captured.out + assert "┃ Keys ┃ Values ┃" not in captured.out + assert captured.out == Regex("#tag 1 +#tag 2") + + vm.print_metadata(area=MetadataType.KEYS) + captured = capsys.readouterr() + assert "All Keys " in captured.out + assert "┃ Keys ┃ Values ┃" not in captured.out + assert captured.out != Regex("#tag 1 +#tag 2") + assert captured.out == Regex("frontmatter_Key1 +frontmatter_Key2") + + +def test_vault_metadata_contains() -> None: + """Test contains method.""" + vm = VaultMetadata() + vm.index_metadata(area=MetadataType.FRONTMATTER, metadata=METADATA) + vm.index_metadata(area=MetadataType.INLINE, metadata=METADATA_2) + vm.index_metadata(area=MetadataType.TAGS, metadata=TAG_LIST) + assert vm.dict == { + "frontmatter_Key1": ["author name"], + "frontmatter_Key2": ["article", "note"], + "intext_key": ["intext_key_value"], + "key1": ["value1"], + "key2": ["value2", "value3"], + "shared_key1": ["shared_key1_value"], + "shared_key2": ["shared_key2_value"], + "tags": ["tag 1", "tag 2", "tag 3"], + "top_key1": ["top_key1_value"], + "top_key2": ["top_key2_value"], + "top_key3": ["top_key3_value"], + } + assert vm.frontmatter == { + "frontmatter_Key1": ["author name"], + "frontmatter_Key2": ["article", "note"], + "intext_key": ["intext_key_value"], + "shared_key1": ["shared_key1_value"], + "shared_key2": ["shared_key2_value"], + "tags": ["tag 1", "tag 2", "tag 3"], + "top_key1": ["top_key1_value"], + "top_key2": ["top_key2_value"], + "top_key3": ["top_key3_value"], + } + assert vm.inline_metadata == {"key1": ["value1"], "key2": ["value2", "value3"]} + assert vm.tags == ["tag 1", "tag 2", "tag 3"] + + with pytest.raises(ValueError): + vm.contains(area=MetadataType.ALL, value="key1") + + assert vm.contains(area=MetadataType.ALL, key="no_key") is False + assert vm.contains(area=MetadataType.ALL, key="key1") is True + assert vm.contains(area=MetadataType.ALL, key="frontmatter_Key2", value="article") is True + assert vm.contains(area=MetadataType.ALL, key="frontmatter_Key2", value="none") is False + assert vm.contains(area=MetadataType.ALL, key="1$", is_regex=True) is True + assert vm.contains(area=MetadataType.ALL, key=r"\d\d", is_regex=True) is False + + assert vm.contains(area=MetadataType.FRONTMATTER, key="no_key") is False + assert vm.contains(area=MetadataType.FRONTMATTER, key="frontmatter_Key1") is True + assert ( + vm.contains(area=MetadataType.FRONTMATTER, key="frontmatter_Key2", value="article") is True + ) + assert vm.contains(area=MetadataType.FRONTMATTER, key="frontmatter_Key2", value="none") is False + assert vm.contains(area=MetadataType.FRONTMATTER, key="1$", is_regex=True) is True + assert vm.contains(area=MetadataType.FRONTMATTER, key=r"\d\d", is_regex=True) is False + + assert vm.contains(area=MetadataType.INLINE, key="no_key") is False + assert vm.contains(area=MetadataType.INLINE, key="key1") is True + assert vm.contains(area=MetadataType.INLINE, key="key2", value="value3") is True + assert vm.contains(area=MetadataType.INLINE, key="key2", value="none") is False + assert vm.contains(area=MetadataType.INLINE, key="1$", is_regex=True) is True + assert vm.contains(area=MetadataType.INLINE, key=r"\d\d", is_regex=True) is False + + assert vm.contains(area=MetadataType.TAGS, value="no_tag") is False + assert vm.contains(area=MetadataType.TAGS, value="tag 1") is True + assert vm.contains(area=MetadataType.TAGS, value=r"\w+ \d$", is_regex=True) is True + assert vm.contains(area=MetadataType.TAGS, value=r"\w+ \d\d$", is_regex=True) is False + with pytest.raises(ValueError): + vm.contains(area=MetadataType.TAGS, key="key1") + + +def test_vault_metadata_delete() -> None: + """Test delete method.""" + vm = VaultMetadata() + vm.index_metadata(area=MetadataType.FRONTMATTER, metadata=METADATA) + assert vm.dict == { + "frontmatter_Key1": ["author name"], + "frontmatter_Key2": ["article", "note"], + "intext_key": ["intext_key_value"], + "shared_key1": ["shared_key1_value"], + "shared_key2": ["shared_key2_value"], + "tags": ["tag 1", "tag 2", "tag 3"], + "top_key1": ["top_key1_value"], + "top_key2": ["top_key2_value"], + "top_key3": ["top_key3_value"], + } + + assert vm.delete("no key") is False + assert vm.delete("tags", "no value") is False + assert vm.delete("tags", "tag 2") is True + assert vm.dict["tags"] == ["tag 1", "tag 3"] + assert vm.delete("tags") is True + assert "tags" not in vm.dict + + +def test_vault_metadata_rename() -> None: + """Test rename method.""" + vm = VaultMetadata() + vm.index_metadata(area=MetadataType.FRONTMATTER, metadata=METADATA) + assert vm.dict == { + "frontmatter_Key1": ["author name"], + "frontmatter_Key2": ["article", "note"], + "intext_key": ["intext_key_value"], + "shared_key1": ["shared_key1_value"], + "shared_key2": ["shared_key2_value"], + "tags": ["tag 1", "tag 2", "tag 3"], + "top_key1": ["top_key1_value"], + "top_key2": ["top_key2_value"], + "top_key3": ["top_key3_value"], + } + + assert vm.rename("no key", "new key") is False + assert vm.rename("tags", "no tag", "new key") is False + assert vm.rename("tags", "tag 2", "new tag") is True + assert vm.dict["tags"] == ["new tag", "tag 1", "tag 3"] + 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 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) From baa63261b8b1989140fbafd514f6a1461a7631a7 Mon Sep 17 00:00:00 2001 From: Nathaniel Landau Date: Sat, 4 Feb 2023 23:31:16 -0500 Subject: [PATCH 2/2] docs: update readme --- README.md | 43 ++++++++++++++++++------------------ src/obsidian_metadata/cli.py | 12 +++++----- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index b29a44e..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 - Set `insert_location` in the config to control where the new tag is inserted. (Default: Bottom) +- **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 8108047..6e8eb10 100644 --- a/src/obsidian_metadata/cli.py +++ b/src/obsidian_metadata/cli.py @@ -110,17 +110,17 @@ 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) - • Add to inline tag - Set `insert_location` in the config to + • Add new inline tag - Set `insert_location` in the config to control where the new tag is inserted. (Default: Bottom) [bold underline]Rename Metadata[/]