diff --git a/poetry.lock b/poetry.lock index 6852287..23310a1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -37,37 +37,37 @@ tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy [[package]] name = "black" -version = "23.1.0" +version = "23.3.0" description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "black-23.1.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221"}, - {file = "black-23.1.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26"}, - {file = "black-23.1.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b"}, - {file = "black-23.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104"}, - {file = "black-23.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958"}, - {file = "black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a"}, - {file = "black-23.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481"}, - {file = "black-23.1.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad"}, - {file = "black-23.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8"}, - {file = "black-23.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580"}, - {file = "black-23.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468"}, - {file = "black-23.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739"}, - {file = "black-23.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9"}, - {file = "black-23.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555"}, - {file = "black-23.1.0-py3-none-any.whl", hash = "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32"}, - {file = "black-23.1.0.tar.gz", hash = "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, + {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, + {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, + {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, + {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, + {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, + {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, + {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, + {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, + {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, + {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, + {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, + {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, + {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, ] [package.dependencies] @@ -283,14 +283,14 @@ testing = ["pre-commit"] [[package]] name = "filelock" -version = "3.10.6" +version = "3.10.7" description = "A platform independent file lock." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "filelock-3.10.6-py3-none-any.whl", hash = "sha256:52f119747b2b9c4730dac715a7b1ab34b8ee70fd9259cba158ee53da566387ff"}, - {file = "filelock-3.10.6.tar.gz", hash = "sha256:409105becd604d6b176a483f855e7e8903c5cb2873e47f2c64f66a370c046aaf"}, + {file = "filelock-3.10.7-py3-none-any.whl", hash = "sha256:bde48477b15fde2c7e5a0713cbe72721cb5a5ad32ee0b8f419907960b9d75536"}, + {file = "filelock-3.10.7.tar.gz", hash = "sha256:892be14aa8efc01673b5ed6589dbccb95f9a8596f0507e232626155495c18105"}, ] [package.extras] @@ -975,14 +975,14 @@ files = [ [[package]] name = "rich" -version = "13.3.2" +version = "13.3.3" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "main" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.3.2-py3-none-any.whl", hash = "sha256:a104f37270bf677148d8acb07d33be1569eeee87e2d1beb286a4e9113caf6f2f"}, - {file = "rich-13.3.2.tar.gz", hash = "sha256:91954fe80cfb7985727a467ca98a7618e5dd15178cc2da10f553b36a93859001"}, + {file = "rich-13.3.3-py3-none-any.whl", hash = "sha256:540c7d6d26a1178e8e8b37e9ba44573a3cd1464ff6348b99ee7061b95d1c6333"}, + {file = "rich-13.3.3.tar.gz", hash = "sha256:dc84400a9d842b3a9c5ff74addd8eb798d155f36c1c91303888e0a66850d2a15"}, ] [package.dependencies] @@ -1086,14 +1086,14 @@ files = [ [[package]] name = "setuptools" -version = "67.6.0" +version = "67.6.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-67.6.0-py3-none-any.whl", hash = "sha256:b78aaa36f6b90a074c1fa651168723acbf45d14cb1196b6f02c0fd07f17623b2"}, - {file = "setuptools-67.6.0.tar.gz", hash = "sha256:2ee892cd5f29f3373097f5a814697e397cf3ce313616df0af11231e2ad118077"}, + {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"}, + {file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"}, ] [package.extras] @@ -1181,14 +1181,14 @@ files = [ [[package]] name = "tomlkit" -version = "0.11.6" +version = "0.11.7" description = "Style preserving TOML library" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, - {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, + {file = "tomlkit-0.11.7-py3-none-any.whl", hash = "sha256:5325463a7da2ef0c6bbfefb62a3dc883aebe679984709aee32a317907d0a8d3c"}, + {file = "tomlkit-0.11.7.tar.gz", hash = "sha256:f392ef70ad87a672f02519f99967d28a4d3047133e2d1df936511465fbb3791d"}, ] [[package]] @@ -1233,14 +1233,14 @@ test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6. [[package]] name = "types-python-dateutil" -version = "2.8.19.10" +version = "2.8.19.11" description = "Typing stubs for python-dateutil" category = "dev" optional = false python-versions = "*" files = [ - {file = "types-python-dateutil-2.8.19.10.tar.gz", hash = "sha256:c640f2eb71b4b94a9d3bfda4c04250d29a24e51b8bad6e12fddec0cf6e96f7a3"}, - {file = "types_python_dateutil-2.8.19.10-py3-none-any.whl", hash = "sha256:fbecd02c19cac383bf4a16248d45ffcff17c93a04c0794be5f95d42c6aa5de39"}, + {file = "types-python-dateutil-2.8.19.11.tar.gz", hash = "sha256:de66222c54318c2e05ceb4956976d16696240a45fc2c98e54bfe9a56ce5e1eff"}, + {file = "types_python_dateutil-2.8.19.11-py3-none-any.whl", hash = "sha256:357553f8056cfbb8ce8ea0ca4a6a3480268596748360df73a94c2b8c113a5b06"}, ] [[package]] @@ -1321,4 +1321,4 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "2bfb8708d401a708a1e8bc0596e86d3b3cafcf74886160880c0aa2b1a00e1fec" +content-hash = "4eb179bbb559eb12dd73f673a8e7f80706386c88f5d777d5de4ca9b7612ace60" diff --git a/pyproject.toml b/pyproject.toml index fe60b3b..46540b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,10 +21,10 @@ python = "^3.10" questionary = "^1.10.0" regex = "^2023.3.23" - rich = "^13.3.2" + rich = "^13.3.3" ruamel-yaml = "^0.17.21" shellingham = "^1.5.0.post1" - tomlkit = "^0.11.6" + tomlkit = "^0.11.7" typer = "^0.7.0" [tool.poetry.group.test.dependencies] @@ -35,7 +35,7 @@ pytest-xdist = "^3.2.1" [tool.poetry.group.dev.dependencies] - black = "^23.1.0" + black = "^23.3.0" commitizen = "^2.42.1" coverage = "^7.2.2" interrogate = "^1.5.0" @@ -46,7 +46,7 @@ ruff = "^0.0.259" sh = "2.0.3" typeguard = "^3.0.2" - types-python-dateutil = "^2.8.19.10" + types-python-dateutil = "^2.8.19.11" vulture = "^2.7" [tool.black] diff --git a/src/obsidian_metadata/_utils/__init__.py b/src/obsidian_metadata/_utils/__init__.py index 304048d..3740228 100644 --- a/src/obsidian_metadata/_utils/__init__.py +++ b/src/obsidian_metadata/_utils/__init__.py @@ -10,6 +10,7 @@ from obsidian_metadata._utils.utilities import ( dict_keys_to_lower, dict_values_to_lists_strings, docstring_parameter, + inline_metadata_from_string, merge_dictionaries, remove_markdown_sections, rename_in_dict, @@ -27,6 +28,7 @@ __all__ = [ "dict_values_to_lists_strings", "docstring_parameter", "LoggerManager", + "inline_metadata_from_string", "merge_dictionaries", "rename_in_dict", "remove_markdown_sections", diff --git a/src/obsidian_metadata/_utils/alerts.py b/src/obsidian_metadata/_utils/alerts.py index 731ba48..097a7a8 100644 --- a/src/obsidian_metadata/_utils/alerts.py +++ b/src/obsidian_metadata/_utils/alerts.py @@ -178,8 +178,7 @@ class LoggerManager: self.log_level = log_level if self.log_file == Path("/logs") and self.log_to_file: # pragma: no cover - console.print("No log file specified") - raise typer.Exit(1) + raise typer.BadParameter("No log file specified") if self.verbosity >= VerboseLevel.TRACE.value: logger.remove() diff --git a/src/obsidian_metadata/_utils/utilities.py b/src/obsidian_metadata/_utils/utilities.py index 2bb6f2c..74fb9ec 100644 --- a/src/obsidian_metadata/_utils/utilities.py +++ b/src/obsidian_metadata/_utils/utilities.py @@ -183,6 +183,21 @@ def docstring_parameter(*sub: Any) -> Any: return dec +def inline_metadata_from_string(string: str) -> list[tuple[Any, ...]]: + """Search for inline metadata in a string and return a list tuples containing (key, value). + + Args: + string (str): String to get metadata from + + Returns: + tuple[str]: (key, value) + """ + from obsidian_metadata.models import Patterns + + results = Patterns().find_inline_metadata.findall(string) + return [tuple(filter(None, x)) for x in results] + + def merge_dictionaries(dict1: dict, dict2: dict) -> dict: """Merge two dictionaries. When the values are lists, they are merged and sorted. @@ -322,4 +337,4 @@ def version_callback(value: bool) -> None: """Print version and exit.""" if value: console.print(f"{__package__.split('.')[0]}: v{__version__}") - raise typer.Exit() + raise typer.Exit(0) diff --git a/src/obsidian_metadata/cli.py b/src/obsidian_metadata/cli.py index 2159b6a..bc090df 100644 --- a/src/obsidian_metadata/cli.py +++ b/src/obsidian_metadata/cli.py @@ -132,7 +132,7 @@ def main( config: Config = Config(config_path=config_file, vault_path=vault_path) if len(config.vaults) == 0: typer.echo("No vaults configured. Exiting.") - raise typer.Exit(1) + raise typer.BadParameter("No vaults configured. Exiting.") if len(config.vaults) == 1: application = Application(dry_run=dry_run, config=config.vaults[0]) diff --git a/src/obsidian_metadata/models/exceptions.py b/src/obsidian_metadata/models/exceptions.py new file mode 100644 index 0000000..75b28d9 --- /dev/null +++ b/src/obsidian_metadata/models/exceptions.py @@ -0,0 +1,17 @@ +"""Custom exceptions for the obsidian_metadata package.""" + + +class ObsidianMetadataError(Exception): + """Base exception for the obsidian_metadata package.""" + + +class FrontmatterError(ObsidianMetadataError): + """Exception for errors in the frontmatter.""" + + +class InlineMetadataError(ObsidianMetadataError): + """Exception for errors in the inlined metadata.""" + + +class InlineTagError(ObsidianMetadataError): + """Exception for errors in the inline tags.""" diff --git a/src/obsidian_metadata/models/metadata.py b/src/obsidian_metadata/models/metadata.py index c5d66a1..22b0ad9 100644 --- a/src/obsidian_metadata/models/metadata.py +++ b/src/obsidian_metadata/models/metadata.py @@ -13,13 +13,20 @@ from obsidian_metadata._utils import ( delete_from_dict, dict_contains, dict_values_to_lists_strings, + inline_metadata_from_string, merge_dictionaries, remove_markdown_sections, rename_in_dict, ) +from obsidian_metadata._utils.alerts import logger as log from obsidian_metadata._utils.console import console from obsidian_metadata.models import Patterns # isort: ignore from obsidian_metadata.models.enums import MetadataType +from obsidian_metadata.models.exceptions import ( + FrontmatterError, + InlineMetadataError, + InlineTagError, +) PATTERNS = Patterns() INLINE_TAG_KEY: str = "inline_tag" @@ -230,7 +237,7 @@ class Frontmatter: try: frontmatter: dict = yaml.load(frontmatter_block) except Exception as e: # noqa: BLE001 - raise AttributeError(e) from e + raise FrontmatterError(e) from e if frontmatter is None or frontmatter == [None]: return {} @@ -400,15 +407,26 @@ class InlineMetadata: strip_inlinecode=True, strip_frontmatter=True, ) - all_results = PATTERNS.find_inline_metadata.findall(content) - stripped_null_values = [tuple(filter(None, x)) for x in all_results] - + found_inline_metadata = inline_metadata_from_string(content) inline_metadata: dict[str, list[str]] = {} - for k, v in stripped_null_values: - if k in inline_metadata: - inline_metadata[k].append(str(v)) - else: - inline_metadata[k] = [str(v)] + + try: + for k, v in found_inline_metadata: + if not k: + log.trace(f"Skipping empty key associated with value: {v}") + continue + if k in inline_metadata: + inline_metadata[k].append(str(v)) + else: + inline_metadata[k] = [str(v)] + except ValueError as e: + raise InlineMetadataError( + f"Error parsing inline metadata: {found_inline_metadata}" + ) from e + except AttributeError as e: + raise InlineMetadataError( + f"Error parsing inline metadata: {found_inline_metadata}" + ) from e return clean_dictionary(inline_metadata) @@ -537,15 +555,22 @@ class InlineTags: Returns: list[str]: Inline tags from the note. """ - return sorted( - PATTERNS.find_inline_tags.findall( - remove_markdown_sections( - file_content, - strip_codeblocks=True, - strip_inlinecode=True, + try: + return sorted( + PATTERNS.find_inline_tags.findall( + remove_markdown_sections( + file_content, + strip_codeblocks=True, + strip_inlinecode=True, + ) ) ) - ) + except AttributeError as e: + raise InlineTagError("Error parsing inline tags.") from e + except TypeError as e: + raise InlineTagError("Error parsing inline tags.") from e + except ValueError as e: + raise InlineTagError("Error parsing inline tags.") from e def add(self, new_tag: str | list[str]) -> bool: """Add a new inline tag. diff --git a/src/obsidian_metadata/models/notes.py b/src/obsidian_metadata/models/notes.py index 353a177..2204345 100644 --- a/src/obsidian_metadata/models/notes.py +++ b/src/obsidian_metadata/models/notes.py @@ -10,7 +10,7 @@ import rich.repr import typer from rich.table import Table -from obsidian_metadata._utils import alerts +from obsidian_metadata._utils import alerts, inline_metadata_from_string from obsidian_metadata._utils.alerts import logger as log from obsidian_metadata._utils.console import console from obsidian_metadata.models import ( @@ -21,6 +21,11 @@ from obsidian_metadata.models import ( MetadataType, Patterns, ) +from obsidian_metadata.models.exceptions import ( + FrontmatterError, + InlineMetadataError, + InlineTagError, +) PATTERNS = Patterns() @@ -50,19 +55,24 @@ class Note: try: with self.note_path.open(): self.file_content: str = self.note_path.read_text() + self.original_file_content: str = self.file_content except FileNotFoundError as e: alerts.error(f"Note {self.note_path} not found. Exiting") raise typer.Exit(code=1) from e try: self.frontmatter: Frontmatter = Frontmatter(self.file_content) - except AttributeError as e: - alerts.error(f"Note {self.note_path} has invalid frontmatter.\n{e}") + self.inline_metadata: InlineMetadata = InlineMetadata(self.file_content) + self.tags: InlineTags = InlineTags(self.file_content) + except FrontmatterError as e: + alerts.error(f"Invalid frontmatter: {self.note_path}\n{e}") + raise typer.Exit(code=1) from e + except InlineMetadataError as e: + alerts.error(f"Error parsing inline metadata: {self.note_path}.\n{e}") + raise typer.Exit(code=1) from e + except InlineTagError as e: + alerts.error(f"Error parsing inline tags: {self.note_path}\n{e}") raise typer.Exit(code=1) from e - - self.tags: InlineTags = InlineTags(self.file_content) - self.inline_metadata: InlineMetadata = InlineMetadata(self.file_content) - self.original_file_content: str = self.file_content def __rich_repr__(self) -> rich.repr.Result: # pragma: no cover """Define rich representation of Vault.""" @@ -552,10 +562,9 @@ class Note: value_2 (str, optional): New value. """ - all_results = PATTERNS.find_inline_metadata.findall(self.file_content) - stripped_null_values = [tuple(filter(None, x)) for x in all_results] + found_inline_metadata = inline_metadata_from_string(self.file_content) - for _k, _v in stripped_null_values: + for _k, _v in found_inline_metadata: if re.search(key, _k): if value_2 is None: if re.search(rf"{key}[^\\w\\d_-]+", _k): diff --git a/tests/metadata_frontmatter_test.py b/tests/metadata_frontmatter_test.py index 1bd6757..a4bc256 100644 --- a/tests/metadata_frontmatter_test.py +++ b/tests/metadata_frontmatter_test.py @@ -3,6 +3,7 @@ import pytest +from obsidian_metadata.models.exceptions import FrontmatterError from obsidian_metadata.models.metadata import Frontmatter FRONTMATTER_CONTENT: str = """ @@ -84,7 +85,7 @@ tags: tag invalid = = "content" --- """ - with pytest.raises(AttributeError): + with pytest.raises(FrontmatterError): Frontmatter(fn) diff --git a/tests/metadata_inline_test.py b/tests/metadata_inline_test.py index 011e092..b84c78c 100644 --- a/tests/metadata_inline_test.py +++ b/tests/metadata_inline_test.py @@ -1,6 +1,8 @@ # type: ignore """Test inline metadata from metadata.py.""" +import pytest +from obsidian_metadata.models.exceptions import InlineMetadataError from obsidian_metadata.models.metadata import InlineMetadata FRONTMATTER_CONTENT: str = """ @@ -77,6 +79,21 @@ def test__grab_inline_metadata_2(): } +def test__grab_inline_metadata_3(mocker): + """Test grab inline metadata. + + GIVEN content that has inline metadata + WHEN an error occurs parsing the inline metadata + THEN raise an InlineMetadataError and pass the error message + """ + mocker.patch( + "obsidian_metadata.models.metadata.inline_metadata_from_string", + return_value=[("key")], + ) + with pytest.raises(InlineMetadataError, match=r"Error parsing inline metadata: \['key'\]"): + InlineMetadata("") + + def test_add_1(): """Test InlineMetadata add() method. diff --git a/tests/notes_test.py b/tests/notes_test.py index 1948251..a539bea 100644 --- a/tests/notes_test.py +++ b/tests/notes_test.py @@ -8,6 +8,7 @@ import pytest import typer from obsidian_metadata.models.enums import InsertLocation, MetadataType +from obsidian_metadata.models.exceptions import InlineMetadataError, InlineTagError from obsidian_metadata.models.notes import Note from tests.helpers import Regex @@ -88,6 +89,38 @@ def test_create_note_2() -> None: Note(note_path=broken_fm) +def test_create_note_3(sample_note, mocker) -> None: + """Test creating a note object. + + GIVEN a text file with invalid inline metadata + WHEN the note is initialized + THEN a typer exit is raised + """ + mocker.patch( + "obsidian_metadata.models.notes.InlineMetadata", + side_effect=InlineMetadataError("error message"), + ) + + with pytest.raises(typer.Exit): + Note(note_path=sample_note) + + +def test_create_note_4(sample_note, mocker) -> None: + """Test creating a note object. + + GIVEN a text file + WHEN there is an error parsing the inline tags + THEN a typer exit is raised + """ + mocker.patch( + "obsidian_metadata.models.notes.InlineTags", + side_effect=InlineTagError("error message"), + ) + + with pytest.raises(typer.Exit): + Note(note_path=sample_note) + + def test_add_metadata_method_1(short_notes): """Test adding metadata. diff --git a/tests/patterns_test.py b/tests/patterns_test.py index 00363ed..0cf22eb 100644 --- a/tests/patterns_test.py +++ b/tests/patterns_test.py @@ -101,7 +101,7 @@ shared_key1: 'shared_key1_value' """ no_fm_result = '### Header\'s number 3 [📅] "+$2.00" 🤷' - assert pattern.top_with_header.search(no_fm_or_header).group("top") == "" + assert not pattern.top_with_header.search(no_fm_or_header).group("top") assert pattern.top_with_header.search(fm_and_header).group("top") == fm_and_header_result assert pattern.top_with_header.search(no_fm).group("top") == no_fm_result diff --git a/tests/utilities_test.py b/tests/utilities_test.py index 50d2bec..cde6f0b 100644 --- a/tests/utilities_test.py +++ b/tests/utilities_test.py @@ -10,12 +10,12 @@ from obsidian_metadata._utils import ( dict_contains, dict_keys_to_lower, dict_values_to_lists_strings, + inline_metadata_from_string, merge_dictionaries, remove_markdown_sections, rename_in_dict, validate_csv_bulk_imports, ) -from tests.helpers import Regex, remove_ansi def test_clean_dictionary_1(): @@ -427,6 +427,55 @@ def test_dict_values_to_lists_strings_6(): } +def test_inline_metadata_from_string_1(): + """Test inline_metadata_from_string() function. + + GIVEN a string + WHEN the string is empty + THEN the function should return an empty list. + """ + assert inline_metadata_from_string("") == [] + + +def test_inline_metadata_from_string_2(): + """Test inline_metadata_from_string() function. + + GIVEN a string + WHEN the string contains nothing matching the inline metadata regex + THEN the function should return an empty list. + """ + assert inline_metadata_from_string("this is content that has no inline metadata") == [] + + +def test_inline_metadata_from_string_3(): + """Test inline_metadata_from_string() function. + + GIVEN a string + WHEN the string contains inline metadata + THEN the function should return the key value pair as a tuple within a list. + """ + assert inline_metadata_from_string("test::test") == [("test", "test")] + + +def test_inline_metadata_from_string_4(): + """Test inline_metadata_from_string() function. + + GIVEN a string + WHEN the string contains multiple matches of inline metadata + THEN the function should return the key value pairs as a tuple within a list. + """ + content = """ + test::test + paragraph [key::value] paragraph + > test2::test2 + """ + assert inline_metadata_from_string(content) == [ + ("test", "test"), + ("key", "value"), + ("test2", "test2"), + ] + + def test_merge_dictionaries_1(): """Test merge_dictionaries() function.