From 000ac1a16ce0be17ffcd1e844ee2f5454af518b4 Mon Sep 17 00:00:00 2001 From: Nathaniel Landau Date: Sat, 11 Mar 2023 12:04:26 -0500 Subject: [PATCH] feat: transpose metadata between frontmatter and inline --- .pre-commit-config.yaml | 2 +- README.md | 4 +- poetry.lock | 44 +- pyproject.toml | 2 +- src/obsidian_metadata/_config/config.py | 5 +- src/obsidian_metadata/_utils/utilities.py | 2 +- src/obsidian_metadata/models/enums.py | 2 +- src/obsidian_metadata/models/notes.py | 195 +-- src/obsidian_metadata/models/vault.py | 4 +- tests/application_test.py | 2 +- tests/config_test.py | 4 +- tests/conftest.py | 10 +- tests/metadata_test.py | 52 +- tests/notes_test.py | 1464 +++++++++++++-------- 14 files changed, 1069 insertions(+), 723 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 48f72e0..147f8c5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,7 +61,7 @@ repos: entry: yamllint --strict --config-file .yamllint.yml - repo: "https://github.com/charliermarsh/ruff-pre-commit" - rev: "v0.0.253" + rev: "v0.0.254" hooks: - id: ruff args: ["--extend-ignore", "I001,D301,D401"] diff --git a/README.md b/README.md index 397c990..5afe96e 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Once installed, run `obsidian-metadata` in your terminal to enter an interactive - **List notes in scope**: List notes that will be processed. **Add Metadata**: Add new metadata to your vault. +When adding a new key to inline metadata, the `insert location` value in the config file will specify where in the note it will be inserted. - **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) @@ -78,6 +79,7 @@ Once installed, run `obsidian-metadata` in your terminal to enter an interactive - **Delete an inline tag** **Transpose Metadata**: Move metadata from inline to frontmatter or the reverse. +When transposing to inline metadata, the `insert location` value in the config file will specify where in the note it will be inserted. - **Transpose all metadata** - Moves all frontmatter to inline metadata, or the reverse - **Transpose key** - Transposes a specific key and all it's values @@ -110,7 +112,7 @@ Below is an example with two vaults. # Location to add metadata. One of: # TOP: Directly after frontmatter. - # AFTER_TITLE: After a header following frontmatter. + # AFTER_TITLE: After the first header following frontmatter. # BOTTOM: The bottom of the note insert_location = "BOTTOM" diff --git a/poetry.lock b/poetry.lock index b23ffc7..f75cc5e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -616,14 +616,14 @@ dev = ["black", "hypothesis", "mypy", "pygments (>=2.14.0)", "pytest", "pytest-c [[package]] name = "platformdirs" -version = "3.1.0" +version = "3.1.1" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.1.0-py3-none-any.whl", hash = "sha256:13b08a53ed71021350c9e300d4ea8668438fb0046ab3937ac9a29913a1a1350a"}, - {file = "platformdirs-3.1.0.tar.gz", hash = "sha256:accc3665857288317f32c7bebb5a8e482ba717b474f3fc1d18ca7f9214be0cef"}, + {file = "platformdirs-3.1.1-py3-none-any.whl", hash = "sha256:e5986afb596e4bb5bde29a79ac9061aa955b94fca2399b7aaac4090860920dd8"}, + {file = "platformdirs-3.1.1.tar.gz", hash = "sha256:024996549ee88ec1a9aa99ff7f8fc819bb59e2c3477b410d90a16d32d6e707aa"}, ] [package.extras] @@ -1102,29 +1102,29 @@ files = [ [[package]] name = "ruff" -version = "0.0.253" +version = "0.0.254" description = "An extremely fast Python linter, written in Rust." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.0.253-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:69126b80d4da50a394cfe9da947377841cc6c83b0e05cfe9933672ce5c61bfcf"}, - {file = "ruff-0.0.253-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:0f44caf5bbdaeacc3cba4ee3369638e4f6e4e71c9ca773d2f3fc3f65e4bfb434"}, - {file = "ruff-0.0.253-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8144a2fd6533e7a0dbaaf9a3dde44b8414eebf5a86a1fe21e0471d052a3e9c14"}, - {file = "ruff-0.0.253-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07603b362f0dad56e30e7ef2f37bf480732ff8bcf52fe4fd6c9445eb42259f42"}, - {file = "ruff-0.0.253-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68f9a50f48510a443ec57bcf51656bbef47e5972290c450398108ac2a53dfd32"}, - {file = "ruff-0.0.253-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c6ed42010c379d42b81b537957b413cf8531a00d0a6270913e8527d9d73c7e0c"}, - {file = "ruff-0.0.253-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba4b3921fa9c59855b66e1a5ef140d0d872f15a83282bff5b5e3e8db89a45aa2"}, - {file = "ruff-0.0.253-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:60bda6fd99f9d3919df4362b671a12c83ef83279fc7bc1dc0e1aa689dfd91a71"}, - {file = "ruff-0.0.253-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19061d9b5809a0505a233580b48b59b847823ab90e266f8ae40cb31d3708bacf"}, - {file = "ruff-0.0.253-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ee92a7688f327c664891567aa24e4a8cae8635934df95e0dbe65b0e991fcc6e"}, - {file = "ruff-0.0.253-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f0ff811ea61684c6e9284afa701b8388818ab5ef8ebd6144c15c9ba64f459f1e"}, - {file = "ruff-0.0.253-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4548734b2671b80ee4c20aa410d7d2a5b32f087f8759d4f5991c74b8cfa51d7b"}, - {file = "ruff-0.0.253-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e2485f728f04bf3bd6142e55dd2869c769299b73a4bdbe1a795e98332df75561"}, - {file = "ruff-0.0.253-py3-none-win32.whl", hash = "sha256:a66109185382375246d7b0dae2f594801fd8ceb5f8206159c55791aaec9aa4bb"}, - {file = "ruff-0.0.253-py3-none-win_amd64.whl", hash = "sha256:a64e9f97a6b0bfce924e65fa845f669c969d42c30fb61e1e4d87b2c70d835cb9"}, - {file = "ruff-0.0.253-py3-none-win_arm64.whl", hash = "sha256:506987ac3bc212cd74bf1ca032756e67ada93c4add3b7541e3549bbad5e0fc40"}, - {file = "ruff-0.0.253.tar.gz", hash = "sha256:ab746c843a9673d2637bcbcb45da12ed4d44c0c90f0823484d6dcb660118b539"}, + {file = "ruff-0.0.254-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:dd58c500d039fb381af8d861ef456c3e94fd6855c3d267d6c6718c9a9fe07be0"}, + {file = "ruff-0.0.254-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:688379050ae05394a6f9f9c8471587fd5dcf22149bd4304a4ede233cc4ef89a1"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1429be6d8bd3db0bf5becac3a38bd56f8421447790c50599cd90fd53417ec4"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:059a380c08e849b6f312479b18cc63bba2808cff749ad71555f61dd930e3c9a2"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3f15d5d033fd3dcb85d982d6828ddab94134686fac2c02c13a8822aa03e1321"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8deba44fd563361c488dedec90dc330763ee0c01ba54e17df54ef5820079e7e0"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef20bf798ffe634090ad3dc2e8aa6a055f08c448810a2f800ab716cc18b80107"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0deb1d7226ea9da9b18881736d2d96accfa7f328c67b7410478cc064ad1fa6aa"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27d39d697fdd7df1f2a32c1063756ee269ad8d5345c471ee3ca450636d56e8c6"}, + {file = "ruff-0.0.254-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2fc21d060a3197ac463596a97d9b5db2d429395938b270ded61dd60f0e57eb21"}, + {file = "ruff-0.0.254-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f70dc93bc9db15cccf2ed2a831938919e3e630993eeea6aba5c84bc274237885"}, + {file = "ruff-0.0.254-py3-none-musllinux_1_2_i686.whl", hash = "sha256:09c764bc2bd80c974f7ce1f73a46092c286085355a5711126af351b9ae4bea0c"}, + {file = "ruff-0.0.254-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d4385cdd30153b7aa1d8f75dfd1ae30d49c918ead7de07e69b7eadf0d5538a1f"}, + {file = "ruff-0.0.254-py3-none-win32.whl", hash = "sha256:c38291bda4c7b40b659e8952167f386e86ec29053ad2f733968ff1d78b4c7e15"}, + {file = "ruff-0.0.254-py3-none-win_amd64.whl", hash = "sha256:e15742df0f9a3615fbdc1ee9a243467e97e75bf88f86d363eee1ed42cedab1ec"}, + {file = "ruff-0.0.254-py3-none-win_arm64.whl", hash = "sha256:b435afc4d65591399eaf4b2af86e441a71563a2091c386cadf33eaa11064dc09"}, + {file = "ruff-0.0.254.tar.gz", hash = "sha256:0eb66c9520151d3bd950ea43b3a088618a8e4e10a5014a72687881e6f3606312"}, ] [[package]] @@ -1349,4 +1349,4 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "30cbfdd2c49ef85a89a19ff225241872fcbbc3648195075118933f4e8e8d4eb4" +content-hash = "b77f1653a71eca187c8d47f8b27d2677191fe37d932fc9ae8e696c462d2ea999" diff --git a/pyproject.toml b/pyproject.toml index a5b73f5..6586c9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ poethepoet = "^0.18.1" pre-commit = "^3.1.1" pysnooper = "^1.1.1" - ruff = "^0.0.253" + ruff = "^0.0.254" typeguard = "^2.13.3" types-python-dateutil = "^2.8.19.10" vulture = "^2.7" diff --git a/src/obsidian_metadata/_config/config.py b/src/obsidian_metadata/_config/config.py index cff6227..ebb709f 100644 --- a/src/obsidian_metadata/_config/config.py +++ b/src/obsidian_metadata/_config/config.py @@ -122,9 +122,9 @@ class Config: # Folders within the vault to ignore when indexing metadata exclude_paths = [".git", ".obsidian"] - # Location to add metadata. One of: + # Location to add new metadata. One of: # TOP: Directly after frontmatter. - # AFTER_TITLE: After a header following frontmatter. + # AFTER_TITLE: After the first header following frontmatter. # BOTTOM: The bottom of the note insert_location = "BOTTOM" """ @@ -164,6 +164,7 @@ class VaultConfig: yield "config", self.config yield "path", self.path yield "exclude_paths", self.exclude_paths + yield "insert_location", self.insert_location def _validate_vault_path(self, vault_path: Path | None) -> Path: """Validate the vault path.""" diff --git a/src/obsidian_metadata/_utils/utilities.py b/src/obsidian_metadata/_utils/utilities.py index a6a8183..16a4561 100644 --- a/src/obsidian_metadata/_utils/utilities.py +++ b/src/obsidian_metadata/_utils/utilities.py @@ -35,7 +35,7 @@ def clear_screen() -> None: # pragma: no cover def dict_contains( dictionary: dict[str, list[str]], key: str, value: str = None, is_regex: bool = False ) -> bool: - """Check if a dictionary contains a key. + """Check if a dictionary contains a key or if a specified key contains a value. Args: dictionary (dict): Dictionary to check diff --git a/src/obsidian_metadata/models/enums.py b/src/obsidian_metadata/models/enums.py index b9b2b10..896569d 100644 --- a/src/obsidian_metadata/models/enums.py +++ b/src/obsidian_metadata/models/enums.py @@ -23,5 +23,5 @@ class InsertLocation(Enum): """ TOP = "Top" - AFTER_TITLE = "Header" + AFTER_TITLE = "After title" BOTTOM = "Bottom" diff --git a/src/obsidian_metadata/models/notes.py b/src/obsidian_metadata/models/notes.py index 9ff169e..8721e06 100644 --- a/src/obsidian_metadata/models/notes.py +++ b/src/obsidian_metadata/models/notes.py @@ -1,4 +1,4 @@ -"""Representation of notes and in the vault.""" +"""Representation of a not in the vault.""" import copy @@ -71,29 +71,6 @@ class Note: yield "inline_tags", self.inline_tags yield "inline_metadata", self.inline_metadata - def _delete_inline_metadata(self, key: str, value: str = None) -> None: - """Delete an inline metadata key/value pair from the text of the note. This method does not remove the key/value from the metadata attribute of the note. - - Args: - key (str): Key to delete. - value (str, optional): Value to delete. - """ - all_results = PATTERNS.find_inline_metadata.findall(self.file_content) - stripped_null_values = [tuple(filter(None, x)) for x in all_results] - - for _k, _v in stripped_null_values: - if re.search(key, _k): - if value is None: - _k = re.escape(_k) - _v = re.escape(_v) - self.sub(rf"\[?{_k}:: ?{_v}]?", "", is_regex=True) - return - - if re.search(value, _v): - _k = re.escape(_k) - _v = re.escape(_v) - self.sub(rf"({_k}::) ?{_v}", r"\1", is_regex=True) - def add_metadata( # noqa: C901 self, area: MetadataType, @@ -101,7 +78,7 @@ class Note: value: str | list[str] = None, location: InsertLocation = None, ) -> bool: - """Add metadata to the note if it does not already exist. + """Add metadata to the note if it does not already exist. This method adds specified metadata to the appropriate MetadataType object AND writes the new metadata to the note's file. Args: area (MetadataType): Area to add metadata to. @@ -155,7 +132,7 @@ class Note: return False def commit(self, path: Path = None) -> None: - """Write the note's content to disk. This is a destructive action. + """Write the note's new content to disk. This is a destructive action. Args: path (Path): Path to write the note to. Defaults to the note's path. @@ -238,9 +215,9 @@ class Note: def delete_metadata( self, key: str, value: str = None, area: MetadataType = MetadataType.ALL ) -> bool: - """Delete a key or key-value pair from the note's Frontmatter or InlineMetadata. Regex is supported. + """Delete a key or key-value pair from the note's Metadata object and the content of the note. Regex is supported. - If no value is provided, will delete an entire key. + If no value is provided, will delete an entire specified key. Args: key (str): Key to delete. @@ -252,28 +229,18 @@ class Note: """ changed_value: bool = False - if value is None: - if ( - area == MetadataType.FRONTMATTER or area == MetadataType.ALL - ) and self.frontmatter.delete(key): - self.write_frontmatter() - changed_value = True - if ( - area == MetadataType.INLINE or area == MetadataType.ALL - ) and self.inline_metadata.delete(key): - self._delete_inline_metadata(key, value) - changed_value = True - else: - if ( - area == MetadataType.FRONTMATTER or area == MetadataType.ALL - ) and self.frontmatter.delete(key, value): - self.write_frontmatter() - changed_value = True - if ( - area == MetadataType.INLINE or area == MetadataType.ALL - ) and self.inline_metadata.delete(key, value): - self._delete_inline_metadata(key, value) - changed_value = True + if ( + area == MetadataType.FRONTMATTER or area == MetadataType.ALL + ) and self.frontmatter.delete(key, value): + self.write_frontmatter() + changed_value = True + + if ( + area == MetadataType.INLINE or area == MetadataType.ALL + ) and self.inline_metadata.contains(key, value): + self.write_delete_inline_metadata(key, value) + self.inline_metadata.delete(key, value) + changed_value = True if changed_value: return True @@ -299,12 +266,8 @@ class Note: return False - def print_note(self) -> None: - """Print the note to the console.""" - console.print(self.file_content) - def print_diff(self) -> None: - """Print a diff of the note's original state and it's new state.""" + """Print a diff of the note's content. Compares original state to it's new state.""" a = self.original_file_content.splitlines() b = self.file_content.splitlines() @@ -320,8 +283,12 @@ class Note: console.print(table) + def print_note(self) -> None: + """Print the note to the console.""" + console.print(self.file_content) + def rename_inline_tag(self, tag_1: str, tag_2: str) -> bool: - """Rename an inline tag from the note ONLY if it's not in the metadata as well. + """Rename an inline tag. Updates the Metadata object and the text of the note. Args: tag_1 (str): Tag to rename. @@ -341,9 +308,9 @@ class Note: return False def rename_metadata(self, key: str, value_1: str, value_2: str = None) -> bool: - """Rename a key or key-value pair in the note's InlineMetadata and Frontmatter objects. + """Rename a key or key-value pair in the note's InlineMetadata and Frontmatter objects and the content of the note. - If no value is provided, will rename an entire key. + If no value is provided, will rename the entire specified key. Args: key (str): Key to rename. @@ -359,14 +326,14 @@ class Note: self.write_frontmatter() changed_value = True if self.inline_metadata.rename(key, value_1): - self.write_metadata(key, value_1) + self.write_inline_metadata_change(key, value_1) changed_value = True else: if self.frontmatter.rename(key, value_1, value_2): self.write_frontmatter() changed_value = True if self.inline_metadata.rename(key, value_1, value_2): - self.write_metadata(key, value_1, value_2) + self.write_inline_metadata_change(key, value_1, value_2) changed_value = True if changed_value: @@ -395,12 +362,15 @@ class Note: value: str | list[str] = None, location: InsertLocation = InsertLocation.BOTTOM, ) -> bool: - """Transpose metadata from one type to another. + """Move metadata from one metadata object to another. i.e. Frontmatter to InlineMetadata or vice versa. + + If no key is specified, will transpose all metadata. If a key is specified, but no value, the entire key will be transposed. if a specific value is specified, just that value will be transposed. Args: begin (MetadataType): The type of metadata to transpose from. end (MetadataType): The type of metadata to transpose to. key (str, optional): The key to transpose. Defaults to None. + location (InsertLocation, optional): Where to insert the metadata. Defaults to InsertLocation.BOTTOM. value (str | list[str], optional): The value to transpose. Defaults to None. Returns: @@ -466,8 +436,40 @@ class Note: return False - def write_frontmatter(self, sort_keys: bool = False) -> None: - """Replace the frontmatter in the note with the current frontmatter object.""" + def write_delete_inline_metadata(self, key: str = None, value: str = None) -> bool: + """For a given inline metadata key and/or key-value pair, delete it from the text of the note. If no key is provided, will delete all inline metadata from the text of the note. + + IMPORTANT: This method makes no changes to the InlineMetadata object. + + Args: + key (str, optional): Key to delete. + value (str, optional): Value to delete. + + Returns: + bool: Whether the note was updated. + """ + for _k, _v in self.inline_metadata.dict.items(): + if re.search(key, _k): + for _value in _v: + if value is None: + _k = re.escape(_k) + _value = re.escape(_value) + self.sub(rf"\[?{_k}:: ?{_value}]?", "", is_regex=True) + return True + + if re.search(value, _value): + _k = re.escape(_k) + _value = re.escape(_value) + self.sub(rf"({_k}::) ?{_value}", r"\1", is_regex=True) + return True + return False + + def write_frontmatter(self, sort_keys: bool = False) -> bool: + """Replace the frontmatter in the note with the current Frontmatter object. If the Frontmatter object is empty, will delete the frontmatter from the note. + + Returns: + bool: Whether the note was updated. + """ try: current_frontmatter = PATTERNS.frontmatter_block.search(self.file_content).group( "frontmatter" @@ -476,19 +478,43 @@ class Note: current_frontmatter = None if current_frontmatter is None and self.frontmatter.dict == {}: - return + return False new_frontmatter = self.frontmatter.to_yaml(sort_keys=sort_keys) new_frontmatter = "" if self.frontmatter.dict == {} else f"---\n{new_frontmatter}---\n" if current_frontmatter is None: self.file_content = new_frontmatter + self.file_content - return + return True current_frontmatter = f"{re.escape(current_frontmatter)}\n?" self.sub(current_frontmatter, new_frontmatter, is_regex=True) + return True - def write_metadata(self, key: str, value_1: str, value_2: str = None) -> None: + def write_all_inline_metadata( + self, + location: InsertLocation, + ) -> bool: + """Write all metadata found in the InlineMetadata object to the note at a specified insert location. + + Args: + location (InsertLocation): Where to insert the metadata. + + Returns: + bool: Whether the note was updated. + """ + if self.inline_metadata.dict != {}: + string = "" + for k, v in sorted(self.inline_metadata.dict.items()): + for value in v: + string += f"{k}:: {value}\n" + + if self.write_string(new_string=string, location=location, allow_multiple=True): + return True + + return False + + def write_inline_metadata_change(self, key: str, value_1: str, value_2: str = None) -> None: """Write changes to a specific inline metadata key or value. Args: @@ -503,9 +529,9 @@ class Note: for _k, _v in stripped_null_values: if re.search(key, _k): if value_2 is None: - if re.search(rf"{key}[^\w\d_-]+", _k): - key_text = re.split(r"[^\w\d_-]+$", _k)[0] - key_markdown = re.split(r"^[\w\d_-]+", _k)[1] + if re.search(rf"{key}[^\\w\\d_-]+", _k): + key_text = re.split(r"[^\\w\\d_-]+$", _k)[0] + key_markdown = re.split(r"^[\\w\\d_-]+", _k)[1] self.sub( rf"{key_text}{key_markdown}::", rf"{value_1}{key_markdown}::", @@ -522,20 +548,24 @@ class Note: new_string: str, location: InsertLocation, allow_multiple: bool = False, - ) -> None: + ) -> bool: """Insert a string into the note at a requested location. Args: new_string (str): String to insert at the top of the note. allow_multiple (bool): Whether to allow inserting the string if it already exists in the note. location (InsertLocation): Location to insert the string. + + Returns: + bool: Whether the note was updated. """ if not allow_multiple and len(re.findall(re.escape(new_string), self.file_content)) > 0: - return + return False match location: case InsertLocation.BOTTOM: self.file_content += f"\n{new_string}" + return True case InsertLocation.TOP: try: top = PATTERNS.frontmatter_block.search(self.file_content).group("frontmatter") @@ -544,10 +574,12 @@ class Note: if top == "": self.file_content = f"{new_string}\n{self.file_content}" - else: - new_string = f"{top}\n{new_string}" - top = re.escape(top) - self.sub(top, new_string, is_regex=True) + return True + + new_string = f"{top}\n{new_string}" + top = re.escape(top) + self.sub(top, new_string, is_regex=True) + return True case InsertLocation.AFTER_TITLE: try: top = PATTERNS.top_with_header.search(self.file_content).group("top") @@ -556,10 +588,11 @@ class Note: if top == "": self.file_content = f"{new_string}\n{self.file_content}" - else: - new_string = f"{top}\n{new_string}" - top = re.escape(top) - self.sub(top, new_string, is_regex=True) - case _: + return True + + new_string = f"{top}\n{new_string}" + top = re.escape(top) + self.sub(top, new_string, is_regex=True) + return True + case _: # pragma: no cover raise ValueError(f"Invalid location: {location}") - pass diff --git a/src/obsidian_metadata/models/vault.py b/src/obsidian_metadata/models/vault.py index 72062d6..576032a 100644 --- a/src/obsidian_metadata/models/vault.py +++ b/src/obsidian_metadata/models/vault.py @@ -82,6 +82,7 @@ class Vault: yield "num_notes", len(self.all_notes) yield "num_notes_in_scope", len(self.notes_in_scope) yield "exclude_paths", self.exclude_paths + yield "insert_location", self.insert_location def _filter_notes(self) -> list[Note]: """Filter notes by path and metadata using the filters defined in self.filters. @@ -122,7 +123,7 @@ class Vault: if self.config["insert_location"].upper() == "TOP": return InsertLocation.TOP - if self.config["insert_location"].upper() == "HEADER": + if self.config["insert_location"].upper() == "AFTER_TITLE": return InsertLocation.AFTER_TITLE if self.config["insert_location"].upper() == "BOTTOM": @@ -374,6 +375,7 @@ class Vault: table.add_row("Notes excluded from scope", str(self.num_excluded_notes())) table.add_row("Active filters", str(len(self.filters))) table.add_row("Notes with changes", str(len(self.get_changed_notes()))) + table.add_row("Insert Location", str(self.insert_location.value)) console.print(table) diff --git a/tests/application_test.py b/tests/application_test.py index 7f22240..880d344 100644 --- a/tests/application_test.py +++ b/tests/application_test.py @@ -245,7 +245,7 @@ def test_delete_value(test_application, mocker, capsys) -> None: with pytest.raises(KeyError): app.application_main() captured = remove_ansi(capsys.readouterr().out) - assert r"SUCCESS | Deleted value ^front\w+$ from key area in 8 notes" in captured + assert r"SUCCESS | Deleted value ^front\w+$ from key area in 4 notes" in captured def test_filter_notes(test_application, mocker, capsys) -> None: diff --git a/tests/config_test.py b/tests/config_test.py index 2fc3fac..28bc960 100644 --- a/tests/config_test.py +++ b/tests/config_test.py @@ -108,9 +108,9 @@ def test_no_config_no_vault(tmp_path, mocker) -> None: # Folders within the vault to ignore when indexing metadata exclude_paths = [".git", ".obsidian"] - # Location to add metadata. One of: + # Location to add new metadata. One of: # TOP: Directly after frontmatter. - # AFTER_TITLE: After a header following frontmatter. + # AFTER_TITLE: After the first header following frontmatter. # BOTTOM: The bottom of the note insert_location = "BOTTOM\" """ diff --git a/tests/conftest.py b/tests/conftest.py index ae39b85..6b79001 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -38,8 +38,14 @@ def sample_note(tmp_path) -> Path: @pytest.fixture() -def short_note(tmp_path) -> Path: - """Fixture which creates a temporary short note file.""" +def short_notes(tmp_path) -> Path: + """Fixture which creates two temporary note files. + + Yields: + Tuple[Path, Path]: Tuple of two temporary note files. + 1. Very short note with frontmatter + 2. Very short note without any frontmatter + """ source_file1: Path = Path("tests/fixtures/short_textfile.md") source_file2: Path = Path("tests/fixtures/no_metadata.md") if not source_file1.exists(): diff --git a/tests/metadata_test.py b/tests/metadata_test.py index 9cc5d33..40b5a75 100644 --- a/tests/metadata_test.py +++ b/tests/metadata_test.py @@ -11,7 +11,7 @@ from obsidian_metadata.models.metadata import ( InlineTags, VaultMetadata, ) -from tests.helpers import Regex +from tests.helpers import Regex, remove_ansi FILE_CONTENT: str = Path("tests/fixtures/test_vault/test1.md").read_text() TAG_LIST: list[str] = ["tag 1", "tag 2", "tag 3"] @@ -609,39 +609,39 @@ def test_vault_metadata_print(capsys) -> None: 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") + captured = remove_ansi(capsys.readouterr().out) + assert "All metadata" in captured + assert "All inline tags" in captured + assert "┃ Keys ┃ Values ┃" in captured + assert "│ shared_key1 │ shared_key1_value │" in captured + assert captured == 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 + captured = remove_ansi(capsys.readouterr().out) + assert "All frontmatter" in captured + assert "┃ Keys ┃ Values ┃" in captured + assert "│ shared_key1 │ shared_key1_value │" in captured + assert "value1" not in captured 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 + captured = remove_ansi(capsys.readouterr().out) + assert "All inline" in captured + assert "┃ Keys ┃ Values ┃" in captured + assert "shared_key1" not in captured + assert "│ key1 │ value1 │" in captured 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") + captured = remove_ansi(capsys.readouterr().out) + assert "All inline tags " in captured + assert "┃ Keys ┃ Values ┃" not in captured + assert captured == 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") + captured = remove_ansi(capsys.readouterr().out) + assert "All Keys " in captured + assert "┃ Keys ┃ Values ┃" not in captured + assert captured != Regex("#tag 1 +#tag 2") + assert captured == Regex("frontmatter_Key1 +frontmatter_Key2") def test_vault_metadata_contains() -> None: diff --git a/tests/notes_test.py b/tests/notes_test.py index c1c0679..1a497bc 100644 --- a/tests/notes_test.py +++ b/tests/notes_test.py @@ -13,23 +13,25 @@ from tests.helpers import Regex def test_note_not_exists() -> None: - """Test target not found.""" + """Test target not found. + + GIVEN a path to a non-existent file + WHEN a Note object is created pointing to that file + THEN a typer.Exit exception is raised + """ with pytest.raises(typer.Exit): - note = Note(note_path="nonexistent_file.md") - - assert note.note_path == "tests/test_data/test_note.md" - assert note.file_content == "This is a test note." - assert note.frontmatter == {} - assert note.inline_tags == [] - assert note.inline_metadata == {} - assert note.dry_run is False + Note(note_path="nonexistent_file.md") -def test_note_create(sample_note) -> None: - """Test creating note class.""" +def test_create_note_1(sample_note): + """Test creating a note object. + + GIVEN a path to a markdown file + WHEN a Note object is created pointing to that file + THEN the Note object is created + """ note = Note(note_path=sample_note, dry_run=True) assert note.note_path == Path(sample_note) - assert note.dry_run is True assert "Lorem ipsum dolor" in note.file_content assert note.frontmatter.dict == { @@ -74,111 +76,10 @@ def test_note_create(sample_note) -> None: assert note.original_file_content == content -def test_add_metadata_inline(short_note) -> None: - """Test adding metadata.""" - path1, path2 = short_note - note = Note(note_path=path1) +def test_create_note_2() -> None: + """Test creating a note object. - assert note.inline_metadata.dict == {} - assert ( - note.add_metadata(MetadataType.INLINE, location=InsertLocation.BOTTOM, key="new_key1") - is True - ) - assert note.inline_metadata.dict == {"new_key1": []} - assert "new_key1::" in note.file_content.strip() - - assert ( - note.add_metadata(MetadataType.INLINE, key="new_key1", location=InsertLocation.BOTTOM) - is False - ) - assert ( - note.add_metadata( - MetadataType.INLINE, key="new_key2", value="new_value1", location=InsertLocation.TOP - ) - is True - ) - assert "new_key2:: new_value1" in note.file_content - - assert ( - note.add_metadata( - MetadataType.INLINE, key="new_key2", value="new_value2", location=InsertLocation.BOTTOM - ) - is True - ) - assert "new_key2:: new_value2" in note.file_content - - assert ( - note.add_metadata( - MetadataType.INLINE, key="new_key2", value="new_value2", location=InsertLocation.BOTTOM - ) - is False - ) - - -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 - assert note.frontmatter.dict == { - "date_created": ["2022-12-22"], - "frontmatter_Key1": ["author name"], - "frontmatter_Key2": ["article", "note"], - "new_key1": [], - "shared_key1": ["shared_key1_value", "shared_key1_value3"], - "shared_key2": ["shared_key2_value1"], - "tags": [ - "frontmatter_tag1", - "frontmatter_tag2", - "shared_tag", - "📅/frontmatter_tag3", - ], - } - assert note.add_metadata(MetadataType.FRONTMATTER, "new_key2", "new_key2_value") is True - assert note.frontmatter.dict == { - "date_created": ["2022-12-22"], - "frontmatter_Key1": ["author name"], - "frontmatter_Key2": ["article", "note"], - "new_key1": [], - "new_key2": ["new_key2_value"], - "shared_key1": ["shared_key1_value", "shared_key1_value3"], - "shared_key2": ["shared_key2_value1"], - "tags": [ - "frontmatter_tag1", - "frontmatter_tag2", - "shared_tag", - "📅/frontmatter_tag3", - ], - } - assert ( - note.add_metadata( - MetadataType.FRONTMATTER, "new_key2", ["new_key2_value2", "new_key2_value3"] - ) - is True - ) - assert note.frontmatter.dict == { - "date_created": ["2022-12-22"], - "frontmatter_Key1": ["author name"], - "frontmatter_Key2": ["article", "note"], - "new_key1": [], - "new_key2": ["new_key2_value", "new_key2_value2", "new_key2_value3"], - "shared_key1": ["shared_key1_value", "shared_key1_value3"], - "shared_key2": ["shared_key2_value1"], - "tags": [ - "frontmatter_tag1", - "frontmatter_tag2", - "shared_tag", - "📅/frontmatter_tag3", - ], - } - - -def test_add_metadata_frontmatter_error() -> None: - """Test adding metadata. - - GIVEN a note with broken frontmatter + GIVEN a text file with invalid frontmatter WHEN the note is initialized THEN a typer exit is raised """ @@ -187,32 +88,204 @@ def test_add_metadata_frontmatter_error() -> None: Note(note_path=broken_fm) -def test_add_metadata_tag(sample_note) -> None: - """Test adding inline tags.""" - note = Note(note_path=sample_note) +def test_add_metadata_method_1(short_notes): + """Test adding metadata. + GIVEN calling the add_metadata method + WHEN a key is passed without a value + THEN the key is added to to the InlineMetadata object and the file content + """ + note = Note(note_path=short_notes[0]) + assert note.inline_metadata.dict == {} + + assert ( + note.add_metadata(MetadataType.INLINE, location=InsertLocation.BOTTOM, key="new_key1") + is True + ) + assert note.inline_metadata.dict == {"new_key1": []} + assert "new_key1::" in note.file_content.strip() + + +def test_add_metadata_method_2(short_notes): + """Test adding metadata. + + GIVEN calling the add_metadata method + WHEN a key is passed with a value + THEN the key and value is added to to the InlineMetadata object and the file content + """ + note = Note(note_path=short_notes[0]) + assert note.inline_metadata.dict == {} + + assert ( + note.add_metadata( + MetadataType.INLINE, key="new_key2", value="new_value1", location=InsertLocation.TOP + ) + is True + ) + assert note.inline_metadata.dict == {"new_key2": ["new_value1"]} + assert "new_key2:: new_value1" in note.file_content + + +def test_add_metadata_method_3(short_notes): + """Test adding metadata. + + GIVEN calling the add_metadata method + WHEN a key is passed that already exists + THEN the the method returns False + """ + note = Note(note_path=short_notes[0]) + note.inline_metadata.dict = {"new_key1": []} + assert ( + note.add_metadata(MetadataType.INLINE, location=InsertLocation.BOTTOM, key="new_key1") + is False + ) + + +def test_add_metadata_method_4(short_notes): + """Test adding metadata. + + GIVEN calling the add_metadata method + WHEN a key is passed with a value that already exists + THEN the the method returns False + """ + note = Note(note_path=short_notes[0]) + note.inline_metadata.dict = {"new_key2": ["new_value1"]} + assert ( + note.add_metadata( + MetadataType.INLINE, key="new_key2", value="new_value1", location=InsertLocation.TOP + ) + is False + ) + + +def test_add_metadata_method_5(sample_note): + """Test add_metadata() method. + + GIVEN a note with frontmatter + WHEN add_metadata() is called with a key or value that already exists in the frontmatter + THEN the method returns False + """ + 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 + + +def test_add_metadata_method_6(sample_note): + """Test add_metadata() method. + + GIVEN a note with frontmatter + WHEN add_metadata() is called with a new key + THEN the key is added to the frontmatter + """ + note = Note(note_path=sample_note) + assert "new_key1" not in note.frontmatter.dict + assert note.add_metadata(MetadataType.FRONTMATTER, "new_key1") is True + assert "new_key1" in note.frontmatter.dict + + +def test_add_metadata_method_7(sample_note): + """Test add_metadata() method. + + GIVEN a note with frontmatter + WHEN add_metadata() is called with a new key and value + THEN the key and value is added to the frontmatter + """ + note = Note(note_path=sample_note) + assert "new_key" not in note.frontmatter.dict + assert note.add_metadata(MetadataType.FRONTMATTER, "new_key", "new_value") is True + assert note.frontmatter.dict["new_key"] == ["new_value"] + + +def test_add_metadata_method_8(sample_note): + """Test add_metadata() method. + + GIVEN a note with frontmatter + WHEN add_metadata() is called with an existing key and new value + THEN the new value is appended to the existing key + """ + note = Note(note_path=sample_note) + assert "new_key" not in note.frontmatter.dict + assert note.add_metadata(MetadataType.FRONTMATTER, "new_key", "new_value") is True + assert note.frontmatter.dict["new_key"] == ["new_value"] + assert note.add_metadata(MetadataType.FRONTMATTER, "new_key", "new_value2") is True + assert note.frontmatter.dict["new_key"] == ["new_value", "new_value2"] + + +def test_add_metadata_method_9(sample_note): + """Test add_metadata() method. + + GIVEN a note object + WHEN add_metadata() is with an existing tag + THEN the method returns False + """ + note = Note(note_path=sample_note) assert ( note.add_metadata(MetadataType.TAGS, value="shared_tag", location=InsertLocation.TOP) is False ) + + +def test_add_metadata_method_10(sample_note): + """Test add_metadata() method. + + GIVEN a note object + WHEN add_metadata() is with a new tag + THEN the tag is added to the InlineTags object and the file content + """ + note = Note(note_path=sample_note) + assert "new_tag" not in note.inline_tags.list assert ( - note.add_metadata(MetadataType.TAGS, value="a_new_tag", location=InsertLocation.TOP) is True + note.add_metadata(MetadataType.TAGS, value="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 + assert "new_tag" in note.inline_tags.list + assert "#new_tag" in note.file_content + + +def test_commit_1(sample_note, tmp_path) -> None: + """Test commit() method. + + GIVEN a note object with commit() called + WHEN the note is modified + THEN the updated note is committed to the file system + """ + note = Note(note_path=sample_note) + note.sub(pattern="Heading 1", replacement="Heading 2") + + note.commit() + note = Note(note_path=sample_note) + assert "Heading 2" in note.file_content + assert "Heading 1" not in note.file_content + + new_path = Path(tmp_path / "new_note.md") + note.commit(new_path) + note2 = Note(note_path=new_path) + assert "Heading 2" in note2.file_content + assert "Heading 1" not in note2.file_content + + +def test_commit_2(sample_note, tmp_path) -> None: + """Test commit() method. + + GIVEN a note object with commit() called + WHEN the note is modified and dry_run is True + THEN the note is not committed to the file system + """ + note = Note(note_path=sample_note, dry_run=True) + note.sub(pattern="Heading 1", replacement="Heading 2") + + note.commit() + note = Note(note_path=sample_note) + assert "Heading 1" in note.file_content def test_contains_inline_tag(sample_note) -> None: - """Test contains inline tag.""" + """Test contains_inline_tag method. + + GIVEN a note object + WHEN contains_inline_tag() is called + THEN the method returns True if the tag is found and False if not + + """ note = Note(note_path=sample_note) assert note.contains_inline_tag("intext_tag1") is True assert note.contains_inline_tag("nonexistent_tag") is False @@ -221,7 +294,13 @@ def test_contains_inline_tag(sample_note) -> None: def test_contains_metadata(sample_note) -> None: - """Test contains metadata.""" + """Test contains_metadata method. + + GIVEN a note object + WHEN contains_metadata() is called + THEN the method returns True if the key and/or value are found and False if not + + """ note = Note(note_path=sample_note) assert note.contains_metadata("no key") is False @@ -234,68 +313,112 @@ def test_contains_metadata(sample_note) -> None: assert note.contains_metadata(r"bottom_key\d$", r"bottom_key\d_value", is_regex=True) is True -def test_delete_inline_metadata(sample_note) -> None: - """Test deleting inline metadata.""" - note = Note(note_path=sample_note) - - note._delete_inline_metadata("nonexistent_key") - assert note.file_content == note.original_file_content - note._delete_inline_metadata("frontmatter_Key1") - assert note.file_content == note.original_file_content - - note._delete_inline_metadata("intext_key") - assert note.file_content == Regex(r"dolore eu fugiat", re.DOTALL) - - note._delete_inline_metadata("bottom_key2", "bottom_key2_value") - assert note.file_content != Regex(r"bottom_key2_value") - assert note.file_content == Regex(r"bottom_key2::") - note._delete_inline_metadata("bottom_key1") - assert note.file_content != Regex(r"bottom_key1::") - - def test_delete_inline_tag(sample_note) -> None: - """Test deleting inline tags.""" - note = Note(note_path=sample_note) + """Test delete_inline_tag method. + GIVEN a note object + WHEN delete_inline_tag() is called + THEN the method returns True if the tag is found and deleted and False if not + """ + note = Note(note_path=sample_note) assert note.delete_inline_tag("not_a_tag") is False assert note.delete_inline_tag("intext_tag[1]") is True assert "intext_tag1" not in note.inline_tags.list assert note.file_content == Regex("consequat. Duis") -def test_delete_metadata(sample_note) -> Note: - """Test deleting metadata.""" - note = Note(note_path=sample_note) +def test_delete_metadata_1(sample_note): + """Test delete_metadata() method. + GIVEN a note object + WHEN delete_metadata() is called with a keys or values that do not exist + THEN the method returns False + """ + note = Note(note_path=sample_note) assert note.delete_metadata("nonexistent_key") is False assert note.delete_metadata("frontmatter_Key1", "no value") is False + + +def test_delete_metadata_2(sample_note): + """Test delete_metadata() method. + + GIVEN a note object + WHEN delete_metadata() is called with a frontmatter key and no value + THEN the entire key and all values are deleted + """ + note = Note(note_path=sample_note) + assert "frontmatter_Key1" in note.frontmatter.dict assert note.delete_metadata("frontmatter_Key1") is True assert "frontmatter_Key1" not in note.frontmatter.dict + +def test_delete_metadata_3(sample_note): + """Test delete_metadata() method. + + GIVEN a note object + WHEN delete_metadata() is called with a frontmatter key and value + THEN the value is deleted from the key + """ + note = Note(note_path=sample_note) + assert note.frontmatter.dict["frontmatter_Key2"] == ["article", "note"] assert note.delete_metadata("frontmatter_Key2", "article") is True assert note.frontmatter.dict["frontmatter_Key2"] == ["note"] + +def test_delete_metadata_4(sample_note): + """Test delete_metadata() method. + + GIVEN a note object + WHEN delete_metadata() is called with an inline key and value + THEN the value is deleted from the InlineMetadata object and the file content + """ + note = Note(note_path=sample_note) + assert note.inline_metadata.dict["bottom_key1"] == ["bottom_key1_value"] + assert note.file_content == Regex(r"bottom_key1:: bottom_key1_value\n") assert note.delete_metadata("bottom_key1", "bottom_key1_value") is True assert note.inline_metadata.dict["bottom_key1"] == [] assert note.file_content == Regex(r"bottom_key1::\n") + +def test_delete_metadata_5(sample_note): + """Test delete_metadata() method. + + GIVEN a note object + WHEN delete_metadata() is called with an inline key and no value + THEN the key and all values are deleted from the InlineMetadata object and the file content + """ + note = Note(note_path=sample_note) + assert note.inline_metadata.dict["bottom_key2"] == ["bottom_key2_value"] assert note.delete_metadata("bottom_key2") is True assert "bottom_key2" not in note.inline_metadata.dict assert note.file_content != Regex(r"bottom_key2") - assert note.delete_metadata("shared_key1", area=MetadataType.INLINE) is True - assert note.frontmatter.dict["shared_key1"] == ["shared_key1_value", "shared_key1_value3"] - assert "shared_key1" not in note.inline_metadata.dict - assert note.delete_metadata("shared_key2", area=MetadataType.FRONTMATTER) is True - assert note.inline_metadata.dict["shared_key2"] == ["shared_key2_value2"] - assert "shared_key2" not in note.frontmatter.dict +def test_delete_metadata_6(sample_note): + """Test delete_metadata() method. + + GIVEN a note object + WHEN delete_metadata() is called with an inline key and a single value + THEN the specified value is removed from the InlineMetadata object and the file content and remaining values are untouched + """ + note = Note(note_path=sample_note) + assert note.inline_metadata.dict["shared_key1"] == ["shared_key1_value", "shared_key1_value2"] + assert ( + note.delete_metadata("shared_key1", "shared_key1_value2", area=MetadataType.INLINE) is True + ) + assert note.inline_metadata.dict["shared_key1"] == ["shared_key1_value"] + assert note.file_content == Regex(r"shared_key1_value") + assert note.file_content != Regex(r"shared_key1_value2") def test_has_changes(sample_note) -> None: - """Test has changes.""" - note = Note(note_path=sample_note) + """Test has_changes() method. + GIVEN a note object + WHEN has_changes() is called + THEN the method returns True if the note has changes and False if not + """ + note = Note(note_path=sample_note) assert note.has_changes() is False note.write_string("This is a test string.", location=InsertLocation.BOTTOM) assert note.has_changes() is True @@ -316,9 +439,581 @@ def test_has_changes(sample_note) -> None: assert note.has_changes() is True -def test_write_string_bottom(short_note) -> None: - """Test inserting metadata to bottom of note.""" - path1, path2 = short_note +def test_print_diff(sample_note, capsys) -> None: + """Test print_diff() method. + + GIVEN a note object + WHEN print_diff() is called + THEN the note's diff is printed to stdout + """ + note = Note(note_path=sample_note) + + note.write_string("This is a test string.", location=InsertLocation.BOTTOM) + note.print_diff() + captured = capsys.readouterr() + assert "+ This is a test string." in captured.out + + note.sub("The quick brown fox", "The quick brown hedgehog") + note.print_diff() + captured = capsys.readouterr() + assert "- The quick brown fox" in captured.out + assert "+ The quick brown hedgehog" in captured.out + + +def test_print_note(sample_note, capsys) -> None: + """Test print_note() method. + + GIVEN a note object + WHEN print_note() is called + THEN the note's new content is printed to stdout + """ + note = Note(note_path=sample_note) + note.print_note() + captured = capsys.readouterr() + assert "```python" in captured.out + assert "---" in captured.out + assert "#shared_tag" in captured.out + + +def test_rename_inline_tag_1(sample_note) -> None: + """Test rename_inline_tag() method. + + GIVEN a note object + WHEN rename_inline_tag() is called with a tag that does not exist + THEN the method returns False + """ + note = Note(note_path=sample_note) + assert note.rename_inline_tag("no_note_tag", "intext_tag2") is False + + +def test_rename_inline_tag_2(sample_note) -> None: + """Test rename_inline_tag() method. + + GIVEN a note object + WHEN rename_inline_tag() is called with a tag exists + THEN the tag is renamed in the InlineTags object and the file content + """ + note = Note(note_path=sample_note) + assert "intext_tag1" in note.inline_tags.list + assert note.rename_inline_tag("intext_tag1", "intext_tag26") is True + assert "intext_tag1" not in note.inline_tags.list + assert "intext_tag26" in note.inline_tags.list + assert note.file_content == Regex(r"#intext_tag26") + assert note.file_content != Regex(r"#intext_tag1") + + +def test_rename_metadata_1(sample_note) -> None: + """Test rename_metadata() method. + + GIVEN a note object + WHEN rename_metadata() is called with a key and/or value that does not exist + THEN the method returns False + """ + note = Note(note_path=sample_note) + assert note.rename_metadata("nonexistent_key", "new_key") is False + assert note.rename_metadata("frontmatter_Key1", "nonexistent_value", "article") is False + + +def test_rename_metadata_2(sample_note) -> None: + """Test rename_metadata() method. + + GIVEN a note object + WHEN rename_metadata() is called with key that matches a frontmatter key + THEN the key is renamed in the Frontmatter object and the file content + """ + note = Note(note_path=sample_note) + assert note.frontmatter.dict["frontmatter_Key1"] == ["author name"] + assert note.rename_metadata("frontmatter_Key1", "new_key") is True + assert "frontmatter_Key1" not in note.frontmatter.dict + assert "new_key" in note.frontmatter.dict + assert note.frontmatter.dict["new_key"] == ["author name"] + assert note.file_content == Regex(r"new_key: author name") + + +def test_rename_metadata_3(sample_note) -> None: + """Test rename_metadata() method. + + GIVEN a note object + WHEN rename_metadata() is called with key/value that matches a frontmatter key/value + THEN the key/value is renamed in the Frontmatter object and the file content + """ + note = Note(note_path=sample_note) + assert note.frontmatter.dict["frontmatter_Key2"] == ["article", "note"] + assert note.rename_metadata("frontmatter_Key2", "article", "new_key") is True + assert note.frontmatter.dict["frontmatter_Key2"] == ["new_key", "note"] + assert note.file_content == Regex(r" - new_key") + assert note.file_content != Regex(r" - article") + + +def test_rename_metadata_4(sample_note) -> None: + """Test rename_metadata() method. + + GIVEN a note object + WHEN rename_metadata() is called with key that matches an inline key + THEN the key is renamed in the InlineMetada object and the file content + """ + note = Note(note_path=sample_note) + assert note.rename_metadata("bottom_key1", "new_key") is True + assert "bottom_key1" not in note.inline_metadata.dict + assert "new_key" in note.inline_metadata.dict + assert note.file_content == Regex(r"new_key:: bottom_key1_value") + + +def test_rename_metadata_5(sample_note) -> None: + """Test rename_metadata() method. + + GIVEN a note object + WHEN rename_metadata() is called with key/value that matches an inline key/value + THEN the key/value is renamed in the InlineMetada object and the file content + """ + note = Note(note_path=sample_note) + assert note.rename_metadata("bottom_key1", "bottom_key1_value", "new_value") is True + assert note.inline_metadata.dict["bottom_key1"] == ["new_value"] + assert note.file_content == Regex(r"bottom_key1:: new_value") + + +def test_sub(sample_note) -> None: + """Test the sub() method. + + GIVEN a note object + WHEN sub() is called with a string that exists in the note + THEN the string is replaced in the note's file content + """ + note = Note(note_path=sample_note) + note.sub("#shared_tag", "#unshared_tags", is_regex=True) + assert note.file_content != Regex(r"#shared_tag") + assert note.file_content == Regex(r"#unshared_tags") + + note.sub(" ut ", "") + assert note.file_content != Regex(r" ut ") + assert note.file_content == Regex(r"laboriosam, nisialiquid ex ea") + + +def test_transpose_metadata_1(sample_note): + """Test transpose_metadata() method. + + GIVEN a note object with transpose_metadata() is called + WHEN a metadata object is empty + THEN the method returns False + """ + note = Note(note_path=sample_note) + note.frontmatter.dict = {} + assert note.transpose_metadata(begin=MetadataType.FRONTMATTER, end=MetadataType.INLINE) is False + + note = Note(note_path=sample_note) + note.inline_metadata.dict = {} + assert note.transpose_metadata(begin=MetadataType.INLINE, end=MetadataType.FRONTMATTER) is False + + +def test_transpose_metadata_2(sample_note): + """Test transpose_metadata() method. + + GIVEN a note object with transpose_metadata() is called + WHEN a specified key and/or value does not exist + THEN the method returns False + """ + note = Note(note_path=sample_note) + assert ( + note.transpose_metadata( + begin=MetadataType.FRONTMATTER, + end=MetadataType.INLINE, + key="not_a_key", + ) + is False + ) + assert ( + note.transpose_metadata( + begin=MetadataType.FRONTMATTER, + end=MetadataType.INLINE, + key="frontmatter_Key2", + value="not_a_value", + ) + is False + ) + assert ( + note.transpose_metadata( + begin=MetadataType.FRONTMATTER, + end=MetadataType.INLINE, + key="frontmatter_Key2", + value=["not_a_value", "not_a_value2"], + ) + is False + ) + + +def test_transpose_metadata_3(sample_note): + """Test transpose_metadata() method. + + GIVEN a note object with transpose_metadata() is called + WHEN FRONTMATTER to INLINE and no key or value is specified + THEN all frontmatter is removed and added to the inline metadata object and the file content + """ + note = Note(note_path=sample_note) + assert note.transpose_metadata(begin=MetadataType.FRONTMATTER, end=MetadataType.INLINE) is True + assert note.frontmatter.dict == {} + assert note.inline_metadata.dict == { + "bottom_key1": ["bottom_key1_value"], + "bottom_key2": ["bottom_key2_value"], + "date_created": ["2022-12-22"], + "frontmatter_Key1": ["author name"], + "frontmatter_Key2": ["article", "note"], + "intext_key": ["intext_value"], + "key📅": ["📅_key_value"], + "shared_key1": [ + "shared_key1_value", + "shared_key1_value2", + "shared_key1_value3", + ], + "shared_key2": ["shared_key2_value2", "shared_key2_value1"], + "tags": [ + "frontmatter_tag1", + "frontmatter_tag2", + "shared_tag", + "📅/frontmatter_tag3", + ], + "top_key1": ["top_key1_value"], + "top_key2": ["top_key2_value"], + "top_key3": ["top_key3_value_as_link"], + } + + +def test_transpose_metadata_4(sample_note): + """Test transpose_metadata() method. + + GIVEN a note object with transpose_metadata() is called + WHEN INLINE to FRONTMATTER and no key or value is specified + THEN all inline metadata is removed and added to the frontmatter object and the file content + """ + note = Note(note_path=sample_note) + assert note.transpose_metadata(begin=MetadataType.INLINE, end=MetadataType.FRONTMATTER) is True + assert note.inline_metadata.dict == {} + assert note.frontmatter.dict == { + "bottom_key1": ["bottom_key1_value"], + "bottom_key2": ["bottom_key2_value"], + "date_created": ["2022-12-22"], + "frontmatter_Key1": ["author name"], + "frontmatter_Key2": ["article", "note"], + "intext_key": ["intext_value"], + "key📅": ["📅_key_value"], + "shared_key1": [ + "shared_key1_value", + "shared_key1_value3", + "shared_key1_value", + "shared_key1_value2", + ], + "shared_key2": ["shared_key2_value1", "shared_key2_value2"], + "tags": [ + "frontmatter_tag1", + "frontmatter_tag2", + "shared_tag", + "📅/frontmatter_tag3", + ], + "top_key1": ["top_key1_value"], + "top_key2": ["top_key2_value"], + "top_key3": ["top_key3_value_as_link"], + } + + +def test_transpose_metadata_5(sample_note): + """Test transpose_metadata() method. + + GIVEN a note object with transpose_metadata() is called + WHEN a key exists in both frontmatter and inline metadata + THEN the values for the key are merged in the specified metadata object + """ + note = Note(note_path=sample_note) + assert note.frontmatter.dict["shared_key1"] == ["shared_key1_value", "shared_key1_value3"] + assert note.inline_metadata.dict["shared_key1"] == ["shared_key1_value", "shared_key1_value2"] + assert ( + note.transpose_metadata( + begin=MetadataType.FRONTMATTER, + end=MetadataType.INLINE, + key="shared_key1", + ) + is True + ) + assert "shared_key1" not in note.frontmatter.dict + assert note.inline_metadata.dict["shared_key1"] == [ + "shared_key1_value", + "shared_key1_value2", + "shared_key1_value3", + ] + + +def test_transpose_metadata_6(sample_note): + """Test transpose_metadata() method. + + GIVEN a note object with transpose_metadata() is called + WHEN a specified key with no value is specified + THEN the key is removed from the specified metadata object and added to the target metadata object + """ + note = Note(note_path=sample_note) + assert "top_key1" not in note.frontmatter.dict + assert "top_key1" in note.inline_metadata.dict + assert ( + note.transpose_metadata( + begin=MetadataType.INLINE, + end=MetadataType.FRONTMATTER, + key="top_key1", + ) + is True + ) + assert "top_key1" not in note.inline_metadata.dict + assert note.frontmatter.dict["top_key1"] == ["top_key1_value"] + + +def test_transpose_metadata_7(sample_note): + """Test transpose_metadata() method. + + GIVEN a note object with transpose_metadata() is called + WHEN a specified value is a list + THEN the key/value is removed from the specified metadata object and added to the target metadata object + """ + note = Note(note_path=sample_note) + assert ( + note.transpose_metadata( + begin=MetadataType.FRONTMATTER, + end=MetadataType.INLINE, + key="frontmatter_Key2", + value=["article", "note"], + ) + is True + ) + assert "frontmatter_Key2" not in note.frontmatter.dict + assert note.inline_metadata.dict["frontmatter_Key2"] == ["article", "note"] + + +def test_transpose_metadata_8(sample_note): + """Test transpose_metadata() method. + + GIVEN a note object with transpose_metadata() is called + WHEN a specified value is a string + THEN the key/value is removed from the specified metadata object and added to the target metadata object + """ + note = Note(note_path=sample_note) + assert ( + note.transpose_metadata( + begin=MetadataType.FRONTMATTER, + end=MetadataType.INLINE, + key="frontmatter_Key2", + value="note", + ) + is True + ) + assert note.frontmatter.dict["frontmatter_Key2"] == ["article"] + assert note.inline_metadata.dict["frontmatter_Key2"] == ["note"] + + +def test_write_delete_inline_metadata_1(sample_note) -> None: + """Twrite_delete_inline_metadata() method. + + GIVEN a note object with write_delete_inline_metadata() called + WHEN a key is specified that is not in the inline metadata + THEN the file content is not changed + + """ + note = Note(note_path=sample_note) + note.write_delete_inline_metadata("nonexistent_key") + assert note.file_content == note.original_file_content + note.write_delete_inline_metadata("frontmatter_Key1") + assert note.file_content == note.original_file_content + + +def test_write_delete_inline_metadata_2(sample_note) -> None: + """Twrite_delete_inline_metadata() method. + + GIVEN a note object with write_delete_inline_metadata() called + WHEN a key is specified that is within a body of text + THEN the key/value is removed from the note content + + """ + note = Note(note_path=sample_note) + note.write_delete_inline_metadata("intext_key") + assert note.file_content == Regex(r"dolore eu fugiat", re.DOTALL) + + +def test_write_delete_inline_metadata_3(sample_note) -> None: + """Twrite_delete_inline_metadata() method. + + GIVEN a note object with write_delete_inline_metadata() called + WHEN a key is specified that is not within a body of text + THEN the key/value is removed from the note content + """ + note = Note(note_path=sample_note) + note.write_delete_inline_metadata("bottom_key2", "bottom_key2_value") + assert note.file_content != Regex(r"bottom_key2_value") + assert note.file_content == Regex(r"bottom_key2::") + note.write_delete_inline_metadata("bottom_key1") + assert note.file_content != Regex(r"bottom_key1::") + + +def test_write_frontmatter_1(sample_note) -> None: + """Test writing frontmatter. + + GIVEN a note with frontmatter + WHEN the frontmatter object is different + THEN the old frontmatter is replaced with the new frontmatter + """ + note = Note(note_path=sample_note) + + assert note.rename_metadata("frontmatter_Key1", "author name", "some_new_key_here") is True + assert note.write_frontmatter() is True + new_frontmatter = """--- +date_created: '2022-12-22' +tags: + - frontmatter_tag1 + - frontmatter_tag2 + - shared_tag + - 📅/frontmatter_tag3 +frontmatter_Key1: some_new_key_here +frontmatter_Key2: + - article + - note +shared_key1: + - shared_key1_value + - shared_key1_value3 +shared_key2: shared_key2_value1 +---""" + assert new_frontmatter in note.file_content + assert "# Heading 1" in note.file_content + assert "```python" in note.file_content + + +def test_write_frontmatter_2() -> None: + """Test replacing frontmatter. + + GIVEN a note with no frontmatter + WHEN the frontmatter object has values + THEN the frontmatter is added to the note + """ + note = Note(note_path="tests/fixtures/test_vault/no_metadata.md") + + note.frontmatter.dict = {"key1": "value1", "key2": "value2"} + assert note.write_frontmatter() is True + new_frontmatter = """--- +key1: value1 +key2: value2 +---""" + assert new_frontmatter in note.file_content + assert "Lorem ipsum dolor sit amet" in note.file_content + + +def test_write_frontmatter_3(sample_note) -> None: + """Test replacing frontmatter. + + GIVEN a note with frontmatter + WHEN the frontmatter object is empty + THEN the frontmatter is removed from the note + """ + note = Note(note_path=sample_note) + + note.frontmatter.dict = {} + assert note.write_frontmatter() is True + assert "---" not in note.file_content + assert note.file_content != Regex("date_created:") + assert "Lorem ipsum dolor sit amet" in note.file_content + + +def test_write_frontmatter_4() -> None: + """Test replacing frontmatter. + + GIVEN a note with no frontmatter + WHEN the frontmatter object is empty + THEN the frontmatter is not added to the note + """ + note = Note(note_path="tests/fixtures/test_vault/no_metadata.md") + note.frontmatter.dict = {} + assert note.write_frontmatter() is False + assert "---" not in note.file_content + assert "Lorem ipsum dolor sit amet" in note.file_content + + +def test_write_all_inline_metadata_1(sample_note) -> None: + """Test write_all_inline_metadata() method. + + GIVEN a note object with write_metadata_all() called + WHEN the note has inline metadata + THEN the inline metadata is written to the note + """ + note = Note(note_path=sample_note) + metadata_block = """ +bottom_key1:: bottom_key1_value +bottom_key2:: bottom_key2_value +intext_key:: intext_value +key📅:: 📅_key_value +shared_key1:: shared_key1_value +shared_key1:: shared_key1_value2 +shared_key2:: shared_key2_value2 +top_key1:: top_key1_value +top_key2:: top_key2_value +top_key3:: top_key3_value_as_link""" + assert metadata_block not in note.file_content + assert note.write_all_inline_metadata(location=InsertLocation.BOTTOM) is True + assert metadata_block in note.file_content + + +def test_write_all_inline_metadata_2(sample_note) -> None: + """Test write_all_inline_metadata() method. + + GIVEN a note object with write_metadata_all() called + WHEN the note has no inline metadata + THEN write_all_inline_metadata returns False + """ + note = Note(note_path=sample_note) + note.inline_metadata.dict = {} + assert note.write_all_inline_metadata(location=InsertLocation.BOTTOM) is False + + +def test_write_inline_metadata_change_1(sample_note): + """Test write_inline_metadata_change() method. + + GIVEN a note object with write_inline_metadata_change() called + WHEN the key and/or value is not in the note + THEN the key and/or value is not added to the note + """ + note = Note(note_path=sample_note) + + note.write_inline_metadata_change("nonexistent_key", "new_key") + assert note.file_content == note.original_file_content + note.write_inline_metadata_change("bottom_key1", "no_value", "new_value") + assert note.file_content == note.original_file_content + + +def test_write_inline_metadata_change_2(sample_note): + """Test write_inline_metadata_change() method. + + GIVEN a note object with write_inline_metadata_change() called + WHEN the key is in the note + THEN the key is changed to the new key + """ + note = Note(note_path=sample_note) + + note.write_inline_metadata_change("bottom_key1", "new_key") + assert note.file_content != Regex(r"bottom_key1::") + assert note.file_content == Regex(r"new_key:: bottom_key1_value") + + +def test_write_inline_metadata_change_3(sample_note): + """Test write_inline_metadata_change() method. + + GIVEN a note object with write_inline_metadata_change() called + WHEN the key and value is in the note + THEN the value is changed + """ + note = Note(note_path=sample_note) + note.write_inline_metadata_change("key📅", "📅_key_value", "new_value") + assert note.file_content != Regex(r"key📅:: ?📅_key_value") + assert note.file_content == Regex(r"key📅:: ?new_value") + + +def test_write_string_1(short_notes) -> None: + """Test the write_string() method. + + GIVEN a note object with write_string() called + WHEN the specified location is BOTTOM + THEN the string is written to the bottom of the note + """ + path1, path2 = short_notes note = Note(note_path=str(path1)) note2 = Note(note_path=str(path2)) @@ -366,11 +1061,16 @@ This is a test string. assert note2.file_content == correct_content3.strip() -def test_write_string_after_frontmatter(short_note) -> None: - """Test inserting metadata to bottom of note.""" - path1, path2 = short_note - note = Note(note_path=path1) - note2 = Note(note_path=path2) +def test_write_string_2(short_notes) -> None: + """Test the write_string() method. + + GIVEN a note object with write_string() called + WHEN the specified location is TOP + THEN the string is written to the top of the note + """ + path1, path2 = short_notes + note = Note(note_path=str(path1)) + note2 = Note(note_path=str(path2)) string1 = "This is a test string." string2 = "This is" @@ -411,11 +1111,16 @@ Lorem ipsum dolor sit amet. assert note2.file_content.strip() == correct_content3.strip() -def test_write_string_after_title(short_note) -> None: - """Test inserting metadata to bottom of note.""" - path1, path2 = short_note - note = Note(note_path=path1) - note2 = Note(note_path=path2) +def test_write_string_3(short_notes) -> None: + """Test the write_string() method. + + GIVEN a note object with write_string() called + WHEN the specified location is AFTER_TITLE + THEN the string is written after the title of the note + """ + path1, path2 = short_notes + note = Note(note_path=str(path1)) + note2 = Note(note_path=str(path2)) string1 = "This is a test string." string2 = "This is" @@ -454,406 +1159,3 @@ Lorem ipsum dolor sit amet. note2.write_string(new_string=string1, location=InsertLocation.AFTER_TITLE) assert note2.file_content.strip() == correct_content3.strip() - - -def test_print_note(sample_note, capsys) -> None: - """Test printing note.""" - note = Note(note_path=sample_note) - note.print_note() - captured = capsys.readouterr() - assert "```python" in captured.out - assert "---" in captured.out - assert "#shared_tag" in captured.out - - -def test_print_diff(sample_note, capsys) -> None: - """Test printing diff.""" - note = Note(note_path=sample_note) - - note.write_string("This is a test string.", location=InsertLocation.BOTTOM) - note.print_diff() - captured = capsys.readouterr() - assert "+ This is a test string." in captured.out - - note.sub("The quick brown fox", "The quick brown hedgehog") - note.print_diff() - captured = capsys.readouterr() - assert "- The quick brown fox" in captured.out - assert "+ The quick brown hedgehog" in captured.out - - -def test_sub(sample_note) -> None: - """Test substituting text in a note.""" - note = Note(note_path=sample_note) - note.sub("#shared_tag", "#unshared_tags", is_regex=True) - assert note.file_content != Regex(r"#shared_tag") - assert note.file_content == Regex(r"#unshared_tags") - - note.sub(" ut ", "") - assert note.file_content != Regex(r" ut ") - assert note.file_content == Regex(r"laboriosam, nisialiquid ex ea") - - -def test_rename_inline_tag(sample_note) -> None: - """Test renaming an inline tag.""" - note = Note(note_path=sample_note) - - assert note.rename_inline_tag("no_note_tag", "intext_tag2") is False - assert note.rename_inline_tag("intext_tag1", "intext_tag26") is True - assert note.inline_tags.list == [ - "inline_tag_bottom1", - "inline_tag_bottom2", - "inline_tag_top1", - "inline_tag_top2", - "intext_tag2", - "intext_tag26", - "shared_tag", - ] - assert note.file_content == Regex(r"#intext_tag26") - assert note.file_content != Regex(r"#intext_tag1") - - -def test_write_metadata(sample_note) -> None: - """Test renaming inline metadata.""" - note = Note(note_path=sample_note) - - note.write_metadata("nonexistent_key", "new_key") - assert note.file_content == note.original_file_content - note.write_metadata("bottom_key1", "no_value", "new_value") - assert note.file_content == note.original_file_content - - note.write_metadata("bottom_key1", "new_key") - assert note.file_content != Regex(r"bottom_key1::") - assert note.file_content == Regex(r"new_key::") - - note.write_metadata("key📅", "📅_key_value", "new_value") - assert note.file_content != Regex(r"key📅:: ?📅_key_value") - assert note.file_content == Regex(r"key📅:: ?new_value") - - -def test_rename_metadata(sample_note) -> None: - """Test renaming metadata.""" - note = Note(note_path=sample_note) - - assert note.rename_metadata("nonexistent_key", "new_key") is False - assert note.rename_metadata("frontmatter_Key1", "nonexistent_value", "article") is False - - assert note.rename_metadata("frontmatter_Key1", "new_key") is True - assert "frontmatter_Key1" not in note.frontmatter.dict - assert "new_key" in note.frontmatter.dict - assert note.frontmatter.dict["new_key"] == ["author name"] - assert note.file_content == Regex(r"new_key: author name") - - assert note.rename_metadata("frontmatter_Key2", "article", "new_key") is True - assert note.frontmatter.dict["frontmatter_Key2"] == ["new_key", "note"] - assert note.file_content == Regex(r" - new_key") - assert note.file_content != Regex(r" - article") - - assert note.rename_metadata("bottom_key1", "new_key") is True - assert "bottom_key1" not in note.inline_metadata.dict - assert "new_key" in note.inline_metadata.dict - assert note.file_content == Regex(r"new_key:: bottom_key1_value") - - assert note.rename_metadata("new_key", "bottom_key1_value", "new_value") is True - assert note.inline_metadata.dict["new_key"] == ["new_value"] - assert note.file_content == Regex(r"new_key:: new_value") - - -def test_transpose_frontmatter(sample_note) -> None: - """Test transposing metadata.""" - note = Note(note_path=sample_note) - note.frontmatter.dict = {} - assert note.transpose_metadata(begin=MetadataType.FRONTMATTER, end=MetadataType.INLINE) is False - - note = Note(note_path=sample_note) - assert ( - note.transpose_metadata( - begin=MetadataType.FRONTMATTER, - end=MetadataType.INLINE, - key="not_a_key", - ) - is False - ) - assert ( - note.transpose_metadata( - begin=MetadataType.FRONTMATTER, - end=MetadataType.INLINE, - key="frontmatter_Key2", - value="not_a_value", - ) - is False - ) - assert ( - note.transpose_metadata( - begin=MetadataType.FRONTMATTER, - end=MetadataType.INLINE, - key="frontmatter_Key2", - value=["not_a_value", "not_a_value2"], - ) - is False - ) - - # Transpose all frontmatter metadata to inline metadata - assert note.transpose_metadata(begin=MetadataType.FRONTMATTER, end=MetadataType.INLINE) is True - assert note.frontmatter.dict == {} - assert note.inline_metadata.dict == { - "bottom_key1": ["bottom_key1_value"], - "bottom_key2": ["bottom_key2_value"], - "date_created": ["2022-12-22"], - "frontmatter_Key1": ["author name"], - "frontmatter_Key2": ["article", "note"], - "intext_key": ["intext_value"], - "key📅": ["📅_key_value"], - "shared_key1": [ - "shared_key1_value", - "shared_key1_value2", - "shared_key1_value3", - ], - "shared_key2": ["shared_key2_value2", "shared_key2_value1"], - "tags": [ - "frontmatter_tag1", - "frontmatter_tag2", - "shared_tag", - "📅/frontmatter_tag3", - ], - "top_key1": ["top_key1_value"], - "top_key2": ["top_key2_value"], - "top_key3": ["top_key3_value_as_link"], - } - - # Transpose when key exists in both frontmatter and inline metadata - note = Note(note_path=sample_note) - assert ( - note.transpose_metadata( - begin=MetadataType.FRONTMATTER, - end=MetadataType.INLINE, - key="shared_key1", - ) - is True - ) - assert note.frontmatter.dict == { - "date_created": ["2022-12-22"], - "frontmatter_Key1": ["author name"], - "frontmatter_Key2": ["article", "note"], - "shared_key2": ["shared_key2_value1"], - "tags": [ - "frontmatter_tag1", - "frontmatter_tag2", - "shared_tag", - "📅/frontmatter_tag3", - ], - } - assert note.inline_metadata.dict == { - "bottom_key1": ["bottom_key1_value"], - "bottom_key2": ["bottom_key2_value"], - "intext_key": ["intext_value"], - "key📅": ["📅_key_value"], - "shared_key1": [ - "shared_key1_value", - "shared_key1_value2", - "shared_key1_value3", - ], - "shared_key2": ["shared_key2_value2"], - "top_key1": ["top_key1_value"], - "top_key2": ["top_key2_value"], - "top_key3": ["top_key3_value_as_link"], - } - - # Transpose a single key and it's respective values - note = Note(note_path=sample_note) - assert ( - note.transpose_metadata( - begin=MetadataType.INLINE, - end=MetadataType.FRONTMATTER, - key="top_key1", - ) - is True - ) - assert note.frontmatter.dict == { - "date_created": ["2022-12-22"], - "frontmatter_Key1": ["author name"], - "frontmatter_Key2": ["article", "note"], - "shared_key1": ["shared_key1_value", "shared_key1_value3"], - "shared_key2": ["shared_key2_value1"], - "tags": [ - "frontmatter_tag1", - "frontmatter_tag2", - "shared_tag", - "📅/frontmatter_tag3", - ], - "top_key1": ["top_key1_value"], - } - assert note.inline_metadata.dict == { - "bottom_key1": ["bottom_key1_value"], - "bottom_key2": ["bottom_key2_value"], - "intext_key": ["intext_value"], - "key📅": ["📅_key_value"], - "shared_key1": ["shared_key1_value", "shared_key1_value2"], - "shared_key2": ["shared_key2_value2"], - "top_key2": ["top_key2_value"], - "top_key3": ["top_key3_value_as_link"], - } - - # Transpose a key when it's value is a list - note = Note(note_path=sample_note) - assert ( - note.transpose_metadata( - begin=MetadataType.FRONTMATTER, - end=MetadataType.INLINE, - key="frontmatter_Key2", - value=["article", "note"], - ) - is True - ) - assert note.frontmatter.dict == { - "date_created": ["2022-12-22"], - "frontmatter_Key1": ["author name"], - "shared_key1": ["shared_key1_value", "shared_key1_value3"], - "shared_key2": ["shared_key2_value1"], - "tags": [ - "frontmatter_tag1", - "frontmatter_tag2", - "shared_tag", - "📅/frontmatter_tag3", - ], - } - assert note.inline_metadata.dict == { - "bottom_key1": ["bottom_key1_value"], - "bottom_key2": ["bottom_key2_value"], - "frontmatter_Key2": ["article", "note"], - "intext_key": ["intext_value"], - "key📅": ["📅_key_value"], - "shared_key1": ["shared_key1_value", "shared_key1_value2"], - "shared_key2": ["shared_key2_value2"], - "top_key1": ["top_key1_value"], - "top_key2": ["top_key2_value"], - "top_key3": ["top_key3_value_as_link"], - } - - # Transpose a string value from a key - note = Note(note_path=sample_note) - assert ( - note.transpose_metadata( - begin=MetadataType.FRONTMATTER, - end=MetadataType.INLINE, - key="frontmatter_Key2", - value="note", - ) - is True - ) - assert note.frontmatter.dict == { - "date_created": ["2022-12-22"], - "frontmatter_Key1": ["author name"], - "frontmatter_Key2": ["article"], - "shared_key1": ["shared_key1_value", "shared_key1_value3"], - "shared_key2": ["shared_key2_value1"], - "tags": [ - "frontmatter_tag1", - "frontmatter_tag2", - "shared_tag", - "📅/frontmatter_tag3", - ], - } - assert note.inline_metadata.dict == { - "bottom_key1": ["bottom_key1_value"], - "bottom_key2": ["bottom_key2_value"], - "frontmatter_Key2": ["note"], - "intext_key": ["intext_value"], - "key📅": ["📅_key_value"], - "shared_key1": ["shared_key1_value", "shared_key1_value2"], - "shared_key2": ["shared_key2_value2"], - "top_key1": ["top_key1_value"], - "top_key2": ["top_key2_value"], - "top_key3": ["top_key3_value_as_link"], - } - - # Transpose list values from a key - note = Note(note_path=sample_note) - assert ( - note.transpose_metadata( - begin=MetadataType.FRONTMATTER, - end=MetadataType.INLINE, - key="frontmatter_Key2", - value=["note", "article"], - ) - is True - ) - assert note.frontmatter.dict == { - "date_created": ["2022-12-22"], - "frontmatter_Key1": ["author name"], - "shared_key1": ["shared_key1_value", "shared_key1_value3"], - "shared_key2": ["shared_key2_value1"], - "tags": [ - "frontmatter_tag1", - "frontmatter_tag2", - "shared_tag", - "📅/frontmatter_tag3", - ], - } - assert note.inline_metadata.dict == { - "bottom_key1": ["bottom_key1_value"], - "bottom_key2": ["bottom_key2_value"], - "frontmatter_Key2": ["note", "article"], - "intext_key": ["intext_value"], - "key📅": ["📅_key_value"], - "shared_key1": ["shared_key1_value", "shared_key1_value2"], - "shared_key2": ["shared_key2_value2"], - "top_key1": ["top_key1_value"], - "top_key2": ["top_key2_value"], - "top_key3": ["top_key3_value_as_link"], - } - - -def test_write_frontmatter(sample_note) -> None: - """Test replacing frontmatter.""" - note = Note(note_path=sample_note) - - note.rename_metadata("frontmatter_Key1", "author name", "some_new_key_here") - note.write_frontmatter() - new_frontmatter = """--- -date_created: '2022-12-22' -tags: - - frontmatter_tag1 - - frontmatter_tag2 - - shared_tag - - 📅/frontmatter_tag3 -frontmatter_Key1: some_new_key_here -frontmatter_Key2: - - article - - note -shared_key1: - - shared_key1_value - - shared_key1_value3 -shared_key2: shared_key2_value1 ----""" - assert new_frontmatter in note.file_content - assert "# Heading 1" in note.file_content - assert "```python" in note.file_content - - note2 = Note(note_path="tests/fixtures/test_vault/no_metadata.md") - note2.write_frontmatter() - note2.frontmatter.dict = {"key1": "value1", "key2": "value2"} - note2.write_frontmatter() - new_frontmatter = """--- -key1: value1 -key2: value2 ----""" - assert new_frontmatter in note2.file_content - assert "Lorem ipsum dolor sit amet" in note2.file_content - - -def test_commit(sample_note, tmp_path) -> None: - """Test writing note to file.""" - note = Note(note_path=sample_note) - note.sub(pattern="Heading 1", replacement="Heading 2") - - note.commit() - note = Note(note_path=sample_note) - assert "Heading 2" in note.file_content - assert "Heading 1" not in note.file_content - - new_path = Path(tmp_path / "new_note.md") - note.commit(new_path) - note2 = Note(note_path=new_path) - assert "Heading 2" in note2.file_content - assert "Heading 1" not in note2.file_content