mirror of
https://github.com/natelandau/obsidian-metadata.git
synced 2025-11-16 08:53:48 -05:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8968127c95 | ||
|
|
4bf1acb775 | ||
|
|
98fa996462 | ||
|
|
fdb1b8b5bc | ||
|
|
08999cb055 | ||
|
|
4e053bda29 | ||
|
|
fa568de369 | ||
|
|
696e19f3e2 | ||
|
|
7b762f1a11 | ||
|
|
c1a40ed8a4 | ||
|
|
6f14076e33 | ||
|
|
ca42823a2f | ||
|
|
36adfece51 | ||
|
|
d636fb2672 | ||
|
|
593dbc3b55 | ||
|
|
009801a691 | ||
|
|
2493db5f23 |
@@ -61,7 +61,7 @@ repos:
|
||||
entry: yamllint --strict --config-file .yamllint.yml
|
||||
|
||||
- repo: "https://github.com/charliermarsh/ruff-pre-commit"
|
||||
rev: "v0.0.254"
|
||||
rev: "v0.0.257"
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: ["--extend-ignore", "I001,D301,D401"]
|
||||
|
||||
31
CHANGELOG.md
31
CHANGELOG.md
@@ -1,3 +1,34 @@
|
||||
## v0.10.0 (2023-03-21)
|
||||
|
||||
### Feat
|
||||
|
||||
- add `--export-template` cli option
|
||||
|
||||
### Fix
|
||||
|
||||
- `--export-template` correctly exports all notes
|
||||
- `--export-csv` exports csv not json
|
||||
- **csv-import**: fail if `type` does not validate
|
||||
|
||||
### Refactor
|
||||
|
||||
- pave the way for non-regex key/value deletions
|
||||
- remove unused code
|
||||
- cleanup rename and delete from dict functions
|
||||
|
||||
## v0.9.0 (2023-03-20)
|
||||
|
||||
### Feat
|
||||
|
||||
- bulk update metadata from a CSV file
|
||||
|
||||
### Fix
|
||||
|
||||
- find more instances of inline metadata
|
||||
- ensure frontmatter values are unique within a key
|
||||
- improve validation of bulk imports
|
||||
- improve logging to screen
|
||||
|
||||
## v0.8.0 (2023-03-12)
|
||||
|
||||
### Feat
|
||||
|
||||
43
README.md
43
README.md
@@ -27,6 +27,7 @@ pip install obsidian-metadata
|
||||
- `--dry-run`: Make no destructive changes
|
||||
- `--export-csv`: Specify a path and create a CSV export of all metadata
|
||||
- `--export-json`: Specify a path and create a JSON export of all metadata
|
||||
- `--export-template`: Specify a path and export all notes with their associated metadata to a CSV file for use as a bulk import template
|
||||
- `--help`: Shows interactive help and exits
|
||||
- `--log-file`: Specify a log file location
|
||||
- `--log-to-file`: Will log to a file
|
||||
@@ -43,13 +44,18 @@ Once installed, run `obsidian-metadata` in your terminal to enter an interactive
|
||||
- Backup: Create a backup of the vault.
|
||||
- Delete Backup: Delete a backup of the vault.
|
||||
|
||||
**Export Metadata**
|
||||
|
||||
- Export all metadata to a CSV organized by metadata type
|
||||
- Export all metadata to a CSV organized by note path
|
||||
- Export all metadata to a JSON file organized by metadata type
|
||||
|
||||
**Inspect Metadata**
|
||||
|
||||
- **View all metadata in the vault**
|
||||
- View all **frontmatter**
|
||||
- View all **inline metadata**
|
||||
- View all **inline tags**
|
||||
- **Export all metadata to CSV or JSON file**
|
||||
|
||||
**Filter Notes in Scope**: Limit the scope of notes to be processed with one or more filters.
|
||||
|
||||
@@ -59,6 +65,8 @@ Once installed, run `obsidian-metadata` in your terminal to enter an interactive
|
||||
- **List and clear filters**: List all current filters and clear one or all
|
||||
- **List notes in scope**: List notes that will be processed.
|
||||
|
||||
**Bulk Edit Metadata** from a CSV file (See the _[Make Bulk Updates](https://github.com/natelandau/obsidian-metadata#make-bulk-updates)_ section below)
|
||||
|
||||
**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.
|
||||
@@ -132,6 +140,39 @@ Below is an example with two vaults.
|
||||
|
||||
To bypass the configuration file and specify a vault to use at runtime use the `--vault-path` option.
|
||||
|
||||
### Make Bulk Updates
|
||||
|
||||
Bulk edits are supported by importing a CSV file containing the following columns. Column headers must be lowercase.
|
||||
|
||||
1. `path` - Path to note relative to the vault root folder
|
||||
2. `type` - Type of metadata. One of `frontmatter`, `inline_metadata`, or `tag`
|
||||
3. `key` - The key to add (leave blank for a tag)
|
||||
4. `value` - the value to add to the key
|
||||
|
||||
An example valid CSV file is
|
||||
|
||||
```csv
|
||||
path,type,key,value
|
||||
folder 1/note1.md,frontmatter,fruits,apple
|
||||
folder 1/note1.md,frontmatter,fruits,banana
|
||||
folder 1/note1.md,inline_metadata,cars,toyota
|
||||
folder 1/note1.md,inline_metadata,cars,honda
|
||||
folder 1/note1.md,tag,,tag1
|
||||
folder 1/note1.md,tag,,tag2
|
||||
```
|
||||
|
||||
How bulk imports work:
|
||||
|
||||
- **Only notes which match the path in the CSV file are updated**
|
||||
- **Effected notes will have ALL of their metadata changed** to reflect the values in the CSV file
|
||||
- **Existing metadata in a matching note will be rewritten**. This may result in it's location and/or formatting within the note being changed
|
||||
- Inline tags ignore any value added to the `key` column
|
||||
|
||||
Create a CSV template for making bulk updates containing all your notes and their associated metadata by
|
||||
|
||||
1. Using the `--export-template` cli command; or
|
||||
2. Selecting the `Metadata by note` option within the `Export Metadata` section of the app
|
||||
|
||||
# Contributing
|
||||
|
||||
## Setup: Once per project
|
||||
|
||||
236
poetry.lock
generated
236
poetry.lock
generated
@@ -165,63 +165,63 @@ typing-extensions = ">=4.0.1,<5.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.2.1"
|
||||
version = "7.2.2"
|
||||
description = "Code coverage measurement for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "coverage-7.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49567ec91fc5e0b15356da07a2feabb421d62f52a9fff4b1ec40e9e19772f5f8"},
|
||||
{file = "coverage-7.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2ef6cae70168815ed91388948b5f4fcc69681480a0061114db737f957719f03"},
|
||||
{file = "coverage-7.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3004765bca3acd9e015794e5c2f0c9a05587f5e698127ff95e9cfba0d3f29339"},
|
||||
{file = "coverage-7.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cca7c0b7f5881dfe0291ef09ba7bb1582cb92ab0aeffd8afb00c700bf692415a"},
|
||||
{file = "coverage-7.2.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2167d116309f564af56f9aa5e75ef710ef871c5f9b313a83050035097b56820"},
|
||||
{file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cb5f152fb14857cbe7f3e8c9a5d98979c4c66319a33cad6e617f0067c9accdc4"},
|
||||
{file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:87dc37f16fb5e3a28429e094145bf7c1753e32bb50f662722e378c5851f7fdc6"},
|
||||
{file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e191a63a05851f8bce77bc875e75457f9b01d42843f8bd7feed2fc26bbe60833"},
|
||||
{file = "coverage-7.2.1-cp310-cp310-win32.whl", hash = "sha256:e3ea04b23b114572b98a88c85379e9e9ae031272ba1fb9b532aa934c621626d4"},
|
||||
{file = "coverage-7.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:0cf557827be7eca1c38a2480484d706693e7bb1929e129785fe59ec155a59de6"},
|
||||
{file = "coverage-7.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:570c21a29493b350f591a4b04c158ce1601e8d18bdcd21db136fbb135d75efa6"},
|
||||
{file = "coverage-7.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e872b082b32065ac2834149dc0adc2a2e6d8203080501e1e3c3c77851b466f9"},
|
||||
{file = "coverage-7.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fac6343bae03b176e9b58104a9810df3cdccd5cfed19f99adfa807ffbf43cf9b"},
|
||||
{file = "coverage-7.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abacd0a738e71b20e224861bc87e819ef46fedba2fb01bc1af83dfd122e9c319"},
|
||||
{file = "coverage-7.2.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9256d4c60c4bbfec92721b51579c50f9e5062c21c12bec56b55292464873508"},
|
||||
{file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:80559eaf6c15ce3da10edb7977a1548b393db36cbc6cf417633eca05d84dd1ed"},
|
||||
{file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0bd7e628f6c3ec4e7d2d24ec0e50aae4e5ae95ea644e849d92ae4805650b4c4e"},
|
||||
{file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09643fb0df8e29f7417adc3f40aaf379d071ee8f0350ab290517c7004f05360b"},
|
||||
{file = "coverage-7.2.1-cp311-cp311-win32.whl", hash = "sha256:1b7fb13850ecb29b62a447ac3516c777b0e7a09ecb0f4bb6718a8654c87dfc80"},
|
||||
{file = "coverage-7.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:617a94ada56bbfe547aa8d1b1a2b8299e2ec1ba14aac1d4b26a9f7d6158e1273"},
|
||||
{file = "coverage-7.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8649371570551d2fd7dee22cfbf0b61f1747cdfb2b7587bb551e4beaaa44cb97"},
|
||||
{file = "coverage-7.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d2b9b5e70a21474c105a133ba227c61bc95f2ac3b66861143ce39a5ea4b3f84"},
|
||||
{file = "coverage-7.2.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae82c988954722fa07ec5045c57b6d55bc1a0890defb57cf4a712ced65b26ddd"},
|
||||
{file = "coverage-7.2.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:861cc85dfbf55a7a768443d90a07e0ac5207704a9f97a8eb753292a7fcbdfcfc"},
|
||||
{file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0339dc3237c0d31c3b574f19c57985fcbe494280153bbcad33f2cdf469f4ac3e"},
|
||||
{file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5928b85416a388dd557ddc006425b0c37e8468bd1c3dc118c1a3de42f59e2a54"},
|
||||
{file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8d3843ca645f62c426c3d272902b9de90558e9886f15ddf5efe757b12dd376f5"},
|
||||
{file = "coverage-7.2.1-cp37-cp37m-win32.whl", hash = "sha256:6a034480e9ebd4e83d1aa0453fd78986414b5d237aea89a8fdc35d330aa13bae"},
|
||||
{file = "coverage-7.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6fce673f79a0e017a4dc35e18dc7bb90bf6d307c67a11ad5e61ca8d42b87cbff"},
|
||||
{file = "coverage-7.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f099da6958ddfa2ed84bddea7515cb248583292e16bb9231d151cd528eab657"},
|
||||
{file = "coverage-7.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:97a3189e019d27e914ecf5c5247ea9f13261d22c3bb0cfcfd2a9b179bb36f8b1"},
|
||||
{file = "coverage-7.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a81dbcf6c6c877986083d00b834ac1e84b375220207a059ad45d12f6e518a4e3"},
|
||||
{file = "coverage-7.2.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2c3dde4c0b9be4b02067185136b7ee4681978228ad5ec1278fa74f5ca3e99"},
|
||||
{file = "coverage-7.2.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a209d512d157379cc9ab697cbdbb4cfd18daa3e7eebaa84c3d20b6af0037384"},
|
||||
{file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f3d07edb912a978915576a776756069dede66d012baa503022d3a0adba1b6afa"},
|
||||
{file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8dca3c1706670297851bca1acff9618455122246bdae623be31eca744ade05ec"},
|
||||
{file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b1991a6d64231a3e5bbe3099fb0dd7c9aeaa4275ad0e0aeff4cb9ef885c62ba2"},
|
||||
{file = "coverage-7.2.1-cp38-cp38-win32.whl", hash = "sha256:22c308bc508372576ffa3d2dbc4824bb70d28eeb4fcd79d4d1aed663a06630d0"},
|
||||
{file = "coverage-7.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:b0c0d46de5dd97f6c2d1b560bf0fcf0215658097b604f1840365296302a9d1fb"},
|
||||
{file = "coverage-7.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4dd34a935de268a133e4741827ae951283a28c0125ddcdbcbba41c4b98f2dfef"},
|
||||
{file = "coverage-7.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0f8318ed0f3c376cfad8d3520f496946977abde080439d6689d7799791457454"},
|
||||
{file = "coverage-7.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:834c2172edff5a08d78e2f53cf5e7164aacabeb66b369f76e7bb367ca4e2d993"},
|
||||
{file = "coverage-7.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4d70c853f0546855f027890b77854508bdb4d6a81242a9d804482e667fff6e6"},
|
||||
{file = "coverage-7.2.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a6450da4c7afc4534305b2b7d8650131e130610cea448ff240b6ab73d7eab63"},
|
||||
{file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:99f4dd81b2bb8fc67c3da68b1f5ee1650aca06faa585cbc6818dbf67893c6d58"},
|
||||
{file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bdd3f2f285ddcf2e75174248b2406189261a79e7fedee2ceeadc76219b6faa0e"},
|
||||
{file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f29351393eb05e6326f044a7b45ed8e38cb4dcc38570d12791f271399dc41431"},
|
||||
{file = "coverage-7.2.1-cp39-cp39-win32.whl", hash = "sha256:e2b50ebc2b6121edf352336d503357321b9d8738bb7a72d06fc56153fd3f4cd8"},
|
||||
{file = "coverage-7.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:bd5a12239c0006252244f94863f1c518ac256160cd316ea5c47fb1a11b25889a"},
|
||||
{file = "coverage-7.2.1-pp37.pp38.pp39-none-any.whl", hash = "sha256:436313d129db7cf5b4ac355dd2bd3f7c7e5294af077b090b85de75f8458b8616"},
|
||||
{file = "coverage-7.2.1.tar.gz", hash = "sha256:c77f2a9093ccf329dd523a9b2b3c854c20d2a3d968b6def3b820272ca6732242"},
|
||||
{file = "coverage-7.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c90e73bdecb7b0d1cea65a08cb41e9d672ac6d7995603d6465ed4914b98b9ad7"},
|
||||
{file = "coverage-7.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e2926b8abedf750c2ecf5035c07515770944acf02e1c46ab08f6348d24c5f94d"},
|
||||
{file = "coverage-7.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57b77b9099f172804e695a40ebaa374f79e4fb8b92f3e167f66facbf92e8e7f5"},
|
||||
{file = "coverage-7.2.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:efe1c0adad110bf0ad7fb59f833880e489a61e39d699d37249bdf42f80590169"},
|
||||
{file = "coverage-7.2.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2199988e0bc8325d941b209f4fd1c6fa007024b1442c5576f1a32ca2e48941e6"},
|
||||
{file = "coverage-7.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:81f63e0fb74effd5be736cfe07d710307cc0a3ccb8f4741f7f053c057615a137"},
|
||||
{file = "coverage-7.2.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:186e0fc9cf497365036d51d4d2ab76113fb74f729bd25da0975daab2e107fd90"},
|
||||
{file = "coverage-7.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:420f94a35e3e00a2b43ad5740f935358e24478354ce41c99407cddd283be00d2"},
|
||||
{file = "coverage-7.2.2-cp310-cp310-win32.whl", hash = "sha256:38004671848b5745bb05d4d621526fca30cee164db42a1f185615f39dc997292"},
|
||||
{file = "coverage-7.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:0ce383d5f56d0729d2dd40e53fe3afeb8f2237244b0975e1427bfb2cf0d32bab"},
|
||||
{file = "coverage-7.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3eb55b7b26389dd4f8ae911ba9bc8c027411163839dea4c8b8be54c4ee9ae10b"},
|
||||
{file = "coverage-7.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d2b96123a453a2d7f3995ddb9f28d01fd112319a7a4d5ca99796a7ff43f02af5"},
|
||||
{file = "coverage-7.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:299bc75cb2a41e6741b5e470b8c9fb78d931edbd0cd009c58e5c84de57c06731"},
|
||||
{file = "coverage-7.2.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e1df45c23d4230e3d56d04414f9057eba501f78db60d4eeecfcb940501b08fd"},
|
||||
{file = "coverage-7.2.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:006ed5582e9cbc8115d2e22d6d2144a0725db542f654d9d4fda86793832f873d"},
|
||||
{file = "coverage-7.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d683d230b5774816e7d784d7ed8444f2a40e7a450e5720d58af593cb0b94a212"},
|
||||
{file = "coverage-7.2.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8efb48fa743d1c1a65ee8787b5b552681610f06c40a40b7ef94a5b517d885c54"},
|
||||
{file = "coverage-7.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4c752d5264053a7cf2fe81c9e14f8a4fb261370a7bb344c2a011836a96fb3f57"},
|
||||
{file = "coverage-7.2.2-cp311-cp311-win32.whl", hash = "sha256:55272f33da9a5d7cccd3774aeca7a01e500a614eaea2a77091e9be000ecd401d"},
|
||||
{file = "coverage-7.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:92ebc1619650409da324d001b3a36f14f63644c7f0a588e331f3b0f67491f512"},
|
||||
{file = "coverage-7.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5afdad4cc4cc199fdf3e18088812edcf8f4c5a3c8e6cb69127513ad4cb7471a9"},
|
||||
{file = "coverage-7.2.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0484d9dd1e6f481b24070c87561c8d7151bdd8b044c93ac99faafd01f695c78e"},
|
||||
{file = "coverage-7.2.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d530191aa9c66ab4f190be8ac8cc7cfd8f4f3217da379606f3dd4e3d83feba69"},
|
||||
{file = "coverage-7.2.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac0f522c3b6109c4b764ffec71bf04ebc0523e926ca7cbe6c5ac88f84faced0"},
|
||||
{file = "coverage-7.2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ba279aae162b20444881fc3ed4e4f934c1cf8620f3dab3b531480cf602c76b7f"},
|
||||
{file = "coverage-7.2.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:53d0fd4c17175aded9c633e319360d41a1f3c6e352ba94edcb0fa5167e2bad67"},
|
||||
{file = "coverage-7.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c99cb7c26a3039a8a4ee3ca1efdde471e61b4837108847fb7d5be7789ed8fd9"},
|
||||
{file = "coverage-7.2.2-cp37-cp37m-win32.whl", hash = "sha256:5cc0783844c84af2522e3a99b9b761a979a3ef10fb87fc4048d1ee174e18a7d8"},
|
||||
{file = "coverage-7.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:817295f06eacdc8623dc4df7d8b49cea65925030d4e1e2a7c7218380c0072c25"},
|
||||
{file = "coverage-7.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6146910231ece63facfc5984234ad1b06a36cecc9fd0c028e59ac7c9b18c38c6"},
|
||||
{file = "coverage-7.2.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:387fb46cb8e53ba7304d80aadca5dca84a2fbf6fe3faf6951d8cf2d46485d1e5"},
|
||||
{file = "coverage-7.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:046936ab032a2810dcaafd39cc4ef6dd295df1a7cbead08fe996d4765fca9fe4"},
|
||||
{file = "coverage-7.2.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e627dee428a176ffb13697a2c4318d3f60b2ccdde3acdc9b3f304206ec130ccd"},
|
||||
{file = "coverage-7.2.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fa54fb483decc45f94011898727802309a109d89446a3c76387d016057d2c84"},
|
||||
{file = "coverage-7.2.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3668291b50b69a0c1ef9f462c7df2c235da3c4073f49543b01e7eb1dee7dd540"},
|
||||
{file = "coverage-7.2.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7c20b731211261dc9739bbe080c579a1835b0c2d9b274e5fcd903c3a7821cf88"},
|
||||
{file = "coverage-7.2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5764e1f7471cb8f64b8cda0554f3d4c4085ae4b417bfeab236799863703e5de2"},
|
||||
{file = "coverage-7.2.2-cp38-cp38-win32.whl", hash = "sha256:4f01911c010122f49a3e9bdc730eccc66f9b72bd410a3a9d3cb8448bb50d65d3"},
|
||||
{file = "coverage-7.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:c448b5c9e3df5448a362208b8d4b9ed85305528313fca1b479f14f9fe0d873b8"},
|
||||
{file = "coverage-7.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfe7085783cda55e53510482fa7b5efc761fad1abe4d653b32710eb548ebdd2d"},
|
||||
{file = "coverage-7.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9d22e94e6dc86de981b1b684b342bec5e331401599ce652900ec59db52940005"},
|
||||
{file = "coverage-7.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:507e4720791977934bba016101579b8c500fb21c5fa3cd4cf256477331ddd988"},
|
||||
{file = "coverage-7.2.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc4803779f0e4b06a2361f666e76f5c2e3715e8e379889d02251ec911befd149"},
|
||||
{file = "coverage-7.2.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db8c2c5ace167fd25ab5dd732714c51d4633f58bac21fb0ff63b0349f62755a8"},
|
||||
{file = "coverage-7.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4f68ee32d7c4164f1e2c8797535a6d0a3733355f5861e0f667e37df2d4b07140"},
|
||||
{file = "coverage-7.2.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d52f0a114b6a58305b11a5cdecd42b2e7f1ec77eb20e2b33969d702feafdd016"},
|
||||
{file = "coverage-7.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:797aad79e7b6182cb49c08cc5d2f7aa7b2128133b0926060d0a8889ac43843be"},
|
||||
{file = "coverage-7.2.2-cp39-cp39-win32.whl", hash = "sha256:db45eec1dfccdadb179b0f9ca616872c6f700d23945ecc8f21bb105d74b1c5fc"},
|
||||
{file = "coverage-7.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:8dbe2647bf58d2c5a6c5bcc685f23b5f371909a5624e9f5cd51436d6a9f6c6ef"},
|
||||
{file = "coverage-7.2.2-pp37.pp38.pp39-none-any.whl", hash = "sha256:872d6ce1f5be73f05bea4df498c140b9e7ee5418bfa2cc8204e7f9b817caa968"},
|
||||
{file = "coverage-7.2.2.tar.gz", hash = "sha256:36dd42da34fe94ed98c39887b86db9d06777b1c8f860520e21126a75507024f2"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -253,14 +253,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"},
|
||||
{file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"},
|
||||
{file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"},
|
||||
{file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -283,30 +283,30 @@ testing = ["pre-commit"]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.9.0"
|
||||
version = "3.10.0"
|
||||
description = "A platform independent file lock."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"},
|
||||
{file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"},
|
||||
{file = "filelock-3.10.0-py3-none-any.whl", hash = "sha256:e90b34656470756edf8b19656785c5fea73afa1953f3e1b0d645cef11cab3182"},
|
||||
{file = "filelock-3.10.0.tar.gz", hash = "sha256:3199fd0d3faea8b911be52b663dfccceb84c95949dd13179aa21436d1a79c4ce"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"]
|
||||
testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"]
|
||||
docs = ["furo (>=2022.12.7)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
|
||||
testing = ["covdefaults (>=2.3)", "coverage (>=7.2.1)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.5.19"
|
||||
version = "2.5.21"
|
||||
description = "File identification library for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "identify-2.5.19-py2.py3-none-any.whl", hash = "sha256:3ee3533e7f6f5023157fbebbd5687bb4b698ce6f305259e0d24b2d7d9efb72bc"},
|
||||
{file = "identify-2.5.19.tar.gz", hash = "sha256:4102ecd051f6884449e7359e55b38ba6cd7aafb6ef27b8e2b38495a5723ea106"},
|
||||
{file = "identify-2.5.21-py2.py3-none-any.whl", hash = "sha256:69edcaffa8e91ae0f77d397af60f148b6b45a8044b2cc6d99cafa5b04793ff00"},
|
||||
{file = "identify-2.5.21.tar.gz", hash = "sha256:7671a05ef9cfaf8ff63b15d45a91a1147a03aaccb2976d4e9bd047cbbc508471"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -584,26 +584,26 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.11.0"
|
||||
version = "0.11.1"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"},
|
||||
{file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"},
|
||||
{file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"},
|
||||
{file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pdoc"
|
||||
version = "13.0.0"
|
||||
version = "13.0.1"
|
||||
description = "API Documentation for Python Projects"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pdoc-13.0.0-py3-none-any.whl", hash = "sha256:f9088b1c10f3296f46a08796e05e307470af5f4253f71d536781f6c305baf912"},
|
||||
{file = "pdoc-13.0.0.tar.gz", hash = "sha256:aadbf6c757c6e65c4754d6c26c4eb6c1bf8c7a9fb893f1fbe5a7b879dde59e46"},
|
||||
{file = "pdoc-13.0.1-py3-none-any.whl", hash = "sha256:16a24914280ed318896ad798674e2b0d11832297fdea95632fa472e3d171e247"},
|
||||
{file = "pdoc-13.0.1.tar.gz", hash = "sha256:4d84056847728203b8789ca8a8d0c8003f25002b3caef3365f6f21a1e4228a1b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -679,14 +679,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "3.1.1"
|
||||
version = "3.2.0"
|
||||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pre_commit-3.1.1-py2.py3-none-any.whl", hash = "sha256:b80254e60668e1dd1f5c03a1c9e0413941d61f568a57d745add265945f65bfe8"},
|
||||
{file = "pre_commit-3.1.1.tar.gz", hash = "sha256:d63e6537f9252d99f65755ae5b79c989b462d511ebbc481b561db6a297e1e865"},
|
||||
{file = "pre_commit-3.2.0-py2.py3-none-any.whl", hash = "sha256:f712d3688102e13c8e66b7d7dbd8934a6dda157e58635d89f7d6fecdca39ce8a"},
|
||||
{file = "pre_commit-3.2.0.tar.gz", hash = "sha256:818f0d998059934d0f81bb3667e3ccdc32da6ed7ccaac33e43dc231561ddaaa9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -738,21 +738,6 @@ files = [
|
||||
[package.extras]
|
||||
plugins = ["importlib-metadata"]
|
||||
|
||||
[[package]]
|
||||
name = "pysnooper"
|
||||
version = "1.1.1"
|
||||
description = "A poor man's debugger for Python."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "PySnooper-1.1.1-py2.py3-none-any.whl", hash = "sha256:378f13d731a3e04d3d0350e5f295bdd0f1b49fc8a8b8bf2067fe1e5290bd20be"},
|
||||
{file = "PySnooper-1.1.1.tar.gz", hash = "sha256:d17dc91cca1593c10230dce45e46b1d3ff0f8910f0c38e941edf6ba1260b3820"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
tests = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.2.2"
|
||||
@@ -831,14 +816,14 @@ test = ["pytest-adaptavist (>=5.1.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-xdist"
|
||||
version = "3.2.0"
|
||||
version = "3.2.1"
|
||||
description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pytest-xdist-3.2.0.tar.gz", hash = "sha256:fa10f95a2564cd91652f2d132725183c3b590d9fdcdec09d3677386ecf4c1ce9"},
|
||||
{file = "pytest_xdist-3.2.0-py3-none-any.whl", hash = "sha256:336098e3bbd8193276867cc87db8b22903c3927665dff9d1ac8684c02f597b68"},
|
||||
{file = "pytest-xdist-3.2.1.tar.gz", hash = "sha256:1849bd98d8b242b948e472db7478e090bf3361912a8fed87992ed94085f54727"},
|
||||
{file = "pytest_xdist-3.2.1-py3-none-any.whl", hash = "sha256:37290d161638a20b672401deef1cba812d110ac27e35d213f091d15b8beb40c9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1102,29 +1087,29 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.254"
|
||||
version = "0.0.257"
|
||||
description = "An extremely fast Python linter, written in Rust."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{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"},
|
||||
{file = "ruff-0.0.257-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:7280640690c1d0046b20e0eb924319a89d8e22925d7d232180ce31196e7478f8"},
|
||||
{file = "ruff-0.0.257-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:4582b73da61ab410ffda35b2987a6eacb33f18263e1c91810f0b9779ec4f41a9"},
|
||||
{file = "ruff-0.0.257-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5acae9878f1136893e266348acdb9d30dfae23c296d3012043816432a5abdd51"},
|
||||
{file = "ruff-0.0.257-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d9f0912d045eee15e8e02e335c16d7a7f9fb6821aa5eb1628eeb5bbfa3d88908"},
|
||||
{file = "ruff-0.0.257-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a9542c34ee5298b31be6c6ba304f14b672dcf104846ee65adb2466d3e325870"},
|
||||
{file = "ruff-0.0.257-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3464f1ad4cea6c4b9325da13ae306bd22bf15d226e18d19c52db191b1f4355ac"},
|
||||
{file = "ruff-0.0.257-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a54bfd559e558ee0df2a2f3756423fe6a9de7307bc290d807c3cdf351cb4c24"},
|
||||
{file = "ruff-0.0.257-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3438fd38446e1a0915316f4085405c9feca20fe00a4b614995ab7034dbfaa7ff"},
|
||||
{file = "ruff-0.0.257-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:358cc2b547bd6451dcf2427b22a9c29a2d9c34e66576c693a6381c5f2ed3011d"},
|
||||
{file = "ruff-0.0.257-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:783390f1e94a168c79d7004426dae3e4ae2999cc85f7d00fdd86c62262b71854"},
|
||||
{file = "ruff-0.0.257-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aaa3b5b6929c63a854b6bcea7a229453b455ab26337100b2905fae4523ca5667"},
|
||||
{file = "ruff-0.0.257-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4ecd7a84db4816df2dcd0f11c5365a9a2cf4fa70a19b3ac161b7b0bfa592959d"},
|
||||
{file = "ruff-0.0.257-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3db8d77d5651a2c0d307102d717627a025d4488d406f54c2764b21cfbe11d822"},
|
||||
{file = "ruff-0.0.257-py3-none-win32.whl", hash = "sha256:d2c8755fa4f6c5e5ec032ad341ca3beeecd16786e12c3f26e6b0cc40418ae998"},
|
||||
{file = "ruff-0.0.257-py3-none-win_amd64.whl", hash = "sha256:3cec07d6fecb1ebbc45ea8eeb1047b929caa2f7dfb8dd4b0e1869ff789326da5"},
|
||||
{file = "ruff-0.0.257-py3-none-win_arm64.whl", hash = "sha256:352f1bdb9b433b3b389aee512ffb0b82226ae1e25b3d92e4eaf0e7be6b1b6f6a"},
|
||||
{file = "ruff-0.0.257.tar.gz", hash = "sha256:fedfd06a37ddc17449203c3e38fc83fb68de7f20b5daa0ee4e60d3599b38bab0"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1144,6 +1129,18 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-g
|
||||
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "sh"
|
||||
version = "2.0.3"
|
||||
description = "Python subprocess replacement"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.8.1,<4.0"
|
||||
files = [
|
||||
{file = "sh-2.0.3-py3-none-any.whl", hash = "sha256:351f8968a2ed99755665fef62f038d60b5245999d73c2f1b1705f48b22e2a853"},
|
||||
{file = "sh-2.0.3.tar.gz", hash = "sha256:800efeda403b63879b0a5625f65a0021fd1ea61ed181954da0346372a7b2a341"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.0.post1"
|
||||
@@ -1224,19 +1221,22 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "typeguard"
|
||||
version = "2.13.3"
|
||||
version = "3.0.1"
|
||||
description = "Run-time type checker for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5.3"
|
||||
python-versions = ">=3.7.4"
|
||||
files = [
|
||||
{file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"},
|
||||
{file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"},
|
||||
{file = "typeguard-3.0.1-py3-none-any.whl", hash = "sha256:15628045c830abf68533247afd2cb04683b5ce6f4e30d5401a5ef6f5182280de"},
|
||||
{file = "typeguard-3.0.1.tar.gz", hash = "sha256:beb0e67c5dc76eea4a6d00a6606d444d899589908362960769d0c4a1d32bca70"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
|
||||
test = ["mypy", "pytest", "typing-extensions"]
|
||||
doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
|
||||
test = ["mypy (>=0.991)", "pytest (>=7)"]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
@@ -1285,14 +1285,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.20.0"
|
||||
version = "20.21.0"
|
||||
description = "Virtual Python Environment builder"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "virtualenv-20.20.0-py3-none-any.whl", hash = "sha256:3c22fa5a7c7aa106ced59934d2c20a2ecb7f49b4130b8bf444178a16b880fa45"},
|
||||
{file = "virtualenv-20.20.0.tar.gz", hash = "sha256:a8a4b8ca1e28f864b7514a253f98c1d62b64e31e77325ba279248c65fb4fcef4"},
|
||||
{file = "virtualenv-20.21.0-py3-none-any.whl", hash = "sha256:31712f8f2a17bd06234fa97fdf19609e789dd4e3e4bf108c3da71d710651adbc"},
|
||||
{file = "virtualenv-20.21.0.tar.gz", hash = "sha256:f50e3e60f990a0757c9b68333c9fdaa72d7188caa417f96af9e52407831a3b68"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1349,4 +1349,4 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "b77f1653a71eca187c8d47f8b27d2677191fe37d932fc9ae8e696c462d2ea999"
|
||||
content-hash = "8fa62f96cc77eac773497573dcbdd5666173cbec56374fea73a814f3fb7f5338"
|
||||
|
||||
192
pyproject.toml
192
pyproject.toml
@@ -11,7 +11,7 @@
|
||||
name = "obsidian-metadata"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/natelandau/obsidian-metadata"
|
||||
version = "0.8.0"
|
||||
version = "0.10.0"
|
||||
|
||||
[tool.poetry.scripts] # https://python-poetry.org/docs/pyproject/#scripts
|
||||
obsidian-metadata = "obsidian_metadata.cli:app"
|
||||
@@ -32,76 +32,33 @@
|
||||
pytest-clarity = "^1.0.1"
|
||||
pytest-mock = "^3.10.0"
|
||||
pytest-pretty-terminal = "^1.1.0"
|
||||
pytest-xdist = "^3.2.0"
|
||||
pytest-xdist = "^3.2.1"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^23.1.0"
|
||||
commitizen = "^2.42.1"
|
||||
coverage = "^7.2.1"
|
||||
coverage = "^7.2.2"
|
||||
interrogate = "^1.5.0"
|
||||
mypy = "^1.1.1"
|
||||
pdoc = "^13.0.0"
|
||||
pdoc = "^13.0.1"
|
||||
poethepoet = "^0.18.1"
|
||||
pre-commit = "^3.1.1"
|
||||
pysnooper = "^1.1.1"
|
||||
ruff = "^0.0.254"
|
||||
typeguard = "^2.13.3"
|
||||
pre-commit = "^3.2.0"
|
||||
ruff = "^0.0.257"
|
||||
sh = "2.0.3"
|
||||
typeguard = "^3.0.1"
|
||||
types-python-dateutil = "^2.8.19.10"
|
||||
vulture = "^2.7"
|
||||
|
||||
[tool.ruff] # https://github.com/charliermarsh/ruff
|
||||
fix = true
|
||||
ignore = [
|
||||
"B006",
|
||||
"B008",
|
||||
"D107",
|
||||
"D203",
|
||||
"D204",
|
||||
"D213",
|
||||
"D215",
|
||||
"D404",
|
||||
"D406",
|
||||
"D407",
|
||||
"D408",
|
||||
"D409",
|
||||
"D413",
|
||||
"E501",
|
||||
"N805",
|
||||
"PGH001",
|
||||
"PGH003",
|
||||
"UP007",
|
||||
]
|
||||
ignore-init-module-imports = true
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
per-file-ignores = { "cli.py" = ["PLR0912", "PLR0913"], "tests/*.py" = ["PLR0913", "PLR2004"] }
|
||||
select = [
|
||||
"A",
|
||||
"B",
|
||||
"BLE",
|
||||
"C4",
|
||||
"C90",
|
||||
"D",
|
||||
"E",
|
||||
"ERA",
|
||||
"F",
|
||||
"I",
|
||||
"N",
|
||||
"PGH",
|
||||
"PLC",
|
||||
"PLE",
|
||||
"PLR",
|
||||
"PLW",
|
||||
"RET",
|
||||
"RUF",
|
||||
"SIM",
|
||||
"TID",
|
||||
"UP",
|
||||
"W",
|
||||
"YTT",
|
||||
]
|
||||
src = ["src", "tests"]
|
||||
target-version = "py310"
|
||||
unfixable = ["ERA001", "F401", "F841", "UP007"]
|
||||
|
||||
[tool.commitizen]
|
||||
bump_message = "bump(release): v$current_version → v$new_version"
|
||||
changelog_incremental = true
|
||||
tag_format = "v$version"
|
||||
update_changelog_on_bump = true
|
||||
version = "0.10.0"
|
||||
version_files = ["pyproject.toml:version", "src/obsidian_metadata/__version__.py:__version__"]
|
||||
|
||||
[tool.coverage.report] # https://coverage.readthedocs.io/en/latest/config.html#report
|
||||
exclude_lines = [
|
||||
@@ -134,17 +91,6 @@
|
||||
[tool.coverage.xml]
|
||||
output = "reports/coverage.xml"
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
|
||||
[tool.commitizen]
|
||||
bump_message = "bump(release): v$current_version → v$new_version"
|
||||
changelog_incremental = true
|
||||
tag_format = "v$version"
|
||||
update_changelog_on_bump = true
|
||||
version = "0.8.0"
|
||||
version_files = ["pyproject.toml:version", "src/obsidian_metadata/__version__.py:__version__"]
|
||||
|
||||
[tool.interrogate]
|
||||
exclude = ["build", "docs", "tests"]
|
||||
fail-under = 90
|
||||
@@ -178,6 +124,108 @@
|
||||
testpaths = ["src", "tests"]
|
||||
xfail_strict = true
|
||||
|
||||
[tool.ruff] # https://github.com/charliermarsh/ruff
|
||||
exclude = [
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".hg",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"venv",
|
||||
]
|
||||
# Avoiding flagging (and removing) `V101` from any `# noqa`
|
||||
# directives, despite Ruff's lack of support for `vulture`.
|
||||
external = ["V101"]
|
||||
fix = true
|
||||
ignore = [
|
||||
"B006",
|
||||
"B008",
|
||||
"D107",
|
||||
"D203",
|
||||
"D204",
|
||||
"D213",
|
||||
"D215",
|
||||
"D404",
|
||||
"D406",
|
||||
"D407",
|
||||
"D408",
|
||||
"D409",
|
||||
"D413",
|
||||
"E501",
|
||||
"N805",
|
||||
"PGH001",
|
||||
"PGH003",
|
||||
"UP007",
|
||||
]
|
||||
ignore-init-module-imports = true
|
||||
line-length = 100
|
||||
per-file-ignores = { "cli.py" = [
|
||||
"PLR0912",
|
||||
"PLR0913",
|
||||
], "tests/*.py" = [
|
||||
"PLR0913",
|
||||
"PLR2004",
|
||||
"S101",
|
||||
] }
|
||||
select = [
|
||||
"A", # flake8-builtins
|
||||
"ARG", # flake8-unused-arguments
|
||||
"B", # flake8-bugbear
|
||||
"BLE", # flake8-blind-exception
|
||||
"C40", # flake8-comprehensions
|
||||
"C90", # McCabe
|
||||
"D", # pydocstyle
|
||||
"E", # pycodestyle Errors
|
||||
"ERA", # flake8-eradicate
|
||||
"EXE", # flake8-executable
|
||||
"F", # pyflakes
|
||||
"I", # iSort
|
||||
"N", # Pep8-naming
|
||||
"PGH", # pygrep-hooks
|
||||
"PLC", # pylint Convention
|
||||
"PLE", # pylint Error
|
||||
"PLR", # pylint Refactor
|
||||
"PLW", # pylint Warning
|
||||
"PT", # flake8-pytest-style
|
||||
"PTH", # flake8-use-pathlib
|
||||
"Q", # flake8-quotes
|
||||
"RET", # flake8-return
|
||||
"RUF", # Ruff-specific rules
|
||||
"S", # flake8-bandit
|
||||
"SIM", # flake8-simplify
|
||||
"TID", # flake8-tidy-imports
|
||||
"UP", # pyupgrade
|
||||
"W", # pycodestyle Warnings
|
||||
"YTT", # flake8-2020
|
||||
]
|
||||
src = ["src", "tests"]
|
||||
target-version = "py310"
|
||||
unfixable = ["ERA001", "F401", "F841", "UP007"]
|
||||
|
||||
[tool.ruff.mccabe]
|
||||
# Unlike Flake8, default to a complexity level of 10.
|
||||
max-complexity = 10
|
||||
|
||||
[tool.ruff.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
[tool.ruff.pylint]
|
||||
max-args = 6
|
||||
|
||||
[tool.vulture] # https://pypi.org/project/vulture/
|
||||
# exclude = ["file*.py", "dir/"]
|
||||
# ignore_decorators = ["@app.route", "@require_*"]
|
||||
@@ -203,7 +251,7 @@
|
||||
help = "Lint this package"
|
||||
|
||||
[[tool.poe.tasks.lint.sequence]]
|
||||
shell = "ruff --extend-ignore=I001,D301,D401 src/"
|
||||
shell = "ruff src/ --no-fix"
|
||||
|
||||
[[tool.poe.tasks.lint.sequence]]
|
||||
shell = "black --check src/ tests/"
|
||||
|
||||
150
scripts/update_dependencies.py
Executable file
150
scripts/update_dependencies.py
Executable file
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python
|
||||
"""Script to update the pyproject.toml file with the latest versions of the dependencies."""
|
||||
from pathlib import Path
|
||||
from textwrap import wrap
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError: # pragma: no cover
|
||||
import tomli as tomllib # type: ignore [no-redef]
|
||||
|
||||
import sh
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def dryrun(msg: str) -> None:
|
||||
"""Print a message if the dry run flag is set.
|
||||
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
console.print(f"[cyan]DRYRUN | {msg}[/cyan]")
|
||||
|
||||
|
||||
def success(msg: str) -> None:
|
||||
"""Print a success message without using logging.
|
||||
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
console.print(f"[green]SUCCESS | {msg}[/green]")
|
||||
|
||||
|
||||
def warning(msg: str) -> None:
|
||||
"""Print a warning message without using logging.
|
||||
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
console.print(f"[yellow]WARNING | {msg}[/yellow]")
|
||||
|
||||
|
||||
def error(msg: str) -> None:
|
||||
"""Print an error message without using logging.
|
||||
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
console.print(f"[red]ERROR | {msg}[/red]")
|
||||
|
||||
|
||||
def notice(msg: str) -> None:
|
||||
"""Print a notice message without using logging.
|
||||
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
console.print(f"[bold]NOTICE | {msg}[/bold]")
|
||||
|
||||
|
||||
def info(msg: str) -> None:
|
||||
"""Print a notice message without using logging.
|
||||
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
console.print(f"INFO | {msg}")
|
||||
|
||||
|
||||
def usage(msg: str, width: int = 80) -> None:
|
||||
"""Print a usage message without using logging.
|
||||
|
||||
Args:
|
||||
msg: Message to print
|
||||
width (optional): Width of the message
|
||||
"""
|
||||
for _n, line in enumerate(wrap(msg, width=width)):
|
||||
if _n == 0:
|
||||
console.print(f"[dim]USAGE | {line}")
|
||||
else:
|
||||
console.print(f"[dim] | {line}")
|
||||
|
||||
|
||||
def debug(msg: str) -> None:
|
||||
"""Print a debug message without using logging.
|
||||
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
console.print(f"[blue]DEBUG | {msg}[/blue]")
|
||||
|
||||
|
||||
def dim(msg: str) -> None:
|
||||
"""Print a message in dimmed color.
|
||||
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
console.print(f"[dim]{msg}[/dim]")
|
||||
|
||||
|
||||
# Load the pyproject.toml file
|
||||
pyproject = Path(__file__).parents[1] / "pyproject.toml"
|
||||
|
||||
if not pyproject.exists():
|
||||
console.print("pyproject.toml file not found")
|
||||
raise SystemExit(1)
|
||||
|
||||
with pyproject.open("rb") as f:
|
||||
try:
|
||||
data = tomllib.load(f)
|
||||
except tomllib.TOMLDecodeError as e:
|
||||
raise SystemExit(1) from e
|
||||
|
||||
|
||||
# Get the latest versions of all dependencies
|
||||
info("Getting latest versions of dependencies...")
|
||||
packages: dict = {}
|
||||
for line in sh.poetry("--no-ansi", "show", "--outdated").splitlines():
|
||||
package, current, latest = line.split()[:3]
|
||||
packages[package] = {"current_version": current, "new_version": latest}
|
||||
|
||||
if not packages:
|
||||
success("All dependencies are up to date")
|
||||
raise SystemExit(0)
|
||||
|
||||
|
||||
dependencies = data["tool"]["poetry"]["dependencies"]
|
||||
groups = data["tool"]["poetry"]["group"]
|
||||
|
||||
for p in dependencies:
|
||||
if p in packages:
|
||||
notice(
|
||||
f"Updating {p} from {packages[p]['current_version']} to {packages[p]['new_version']}"
|
||||
)
|
||||
sh.poetry("add", f"{p}@latest", _fg=True)
|
||||
|
||||
|
||||
for group in groups:
|
||||
for p in groups[group]["dependencies"]:
|
||||
if p in packages:
|
||||
notice(
|
||||
f"Updating {p} from {packages[p]['current_version']} to {packages[p]['new_version']}"
|
||||
)
|
||||
sh.poetry("add", f"{p}@latest", "--group", group, _fg=True)
|
||||
|
||||
sh.poetry("update", _fg=True)
|
||||
success("All dependencies are up to date")
|
||||
raise SystemExit(0)
|
||||
@@ -1,2 +1,2 @@
|
||||
"""obsidian-metadata version."""
|
||||
__version__ = "0.8.0"
|
||||
__version__ = "0.10.0"
|
||||
|
||||
@@ -91,7 +91,7 @@ class Config:
|
||||
def _load_config(self) -> dict[str, Any]:
|
||||
"""Load the configuration file."""
|
||||
try:
|
||||
with open(self.config_path, encoding="utf-8") as fp:
|
||||
with self.config_path.open(encoding="utf-8") as fp:
|
||||
return tomlkit.load(fp)
|
||||
except tomlkit.exceptions.TOMLKitError as e:
|
||||
alerts.error(f"Could not parse '{self.config_path}'")
|
||||
|
||||
@@ -5,11 +5,15 @@ from obsidian_metadata._utils.alerts import LoggerManager
|
||||
from obsidian_metadata._utils.utilities import (
|
||||
clean_dictionary,
|
||||
clear_screen,
|
||||
delete_from_dict,
|
||||
dict_contains,
|
||||
dict_keys_to_lower,
|
||||
dict_values_to_lists_strings,
|
||||
docstring_parameter,
|
||||
merge_dictionaries,
|
||||
remove_markdown_sections,
|
||||
rename_in_dict,
|
||||
validate_csv_bulk_imports,
|
||||
version_callback,
|
||||
)
|
||||
|
||||
@@ -17,12 +21,15 @@ __all__ = [
|
||||
"alerts",
|
||||
"clean_dictionary",
|
||||
"clear_screen",
|
||||
"delete_from_dict",
|
||||
"dict_contains",
|
||||
"dict_keys_to_lower",
|
||||
"dict_values_to_lists_strings",
|
||||
"docstring_parameter",
|
||||
"LoggerManager",
|
||||
"merge_dictionaries",
|
||||
"rename_in_dict",
|
||||
"remove_markdown_sections",
|
||||
"vault_validation",
|
||||
"validate_csv_bulk_imports",
|
||||
"version_callback",
|
||||
]
|
||||
|
||||
@@ -87,13 +87,16 @@ def info(msg: str) -> None:
|
||||
console.print(f"INFO | {msg}")
|
||||
|
||||
|
||||
def usage(msg: str, width: int = 80) -> None:
|
||||
def usage(msg: str, width: int = None) -> None:
|
||||
"""Print a usage message without using logging.
|
||||
|
||||
Args:
|
||||
msg: Message to print
|
||||
width (optional): Width of the message
|
||||
"""
|
||||
if width is None:
|
||||
width = console.width - 15
|
||||
|
||||
for _n, line in enumerate(wrap(msg, width=width)):
|
||||
if _n == 0:
|
||||
console.print(f"[dim]USAGE | {line}")
|
||||
@@ -126,9 +129,12 @@ def _log_formatter(record: dict) -> str:
|
||||
or record["level"].name == "SUCCESS"
|
||||
or record["level"].name == "WARNING"
|
||||
):
|
||||
return "<level>{level: <8}</level> | <level>{message}</level>\n{exception}"
|
||||
return "<level><normal>{level: <8} | {message}</normal></level>\n{exception}"
|
||||
|
||||
return "<level>{level: <8}</level> | <level>{message}</level> <fg #c5c5c5>({name}:{function}:{line})</fg #c5c5c5>\n{exception}"
|
||||
if record["level"].name == "TRACE" or record["level"].name == "DEBUG":
|
||||
return "<level><normal>{level: <8} | {message}</normal></level> <fg #c5c5c5>({name}:{function}:{line})</fg #c5c5c5>\n{exception}"
|
||||
|
||||
return "<level>{level: <8} | {message}</level> <fg #c5c5c5>({name}:{function}:{line})</fg #c5c5c5>\n{exception}"
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"""Utility functions."""
|
||||
import copy
|
||||
import csv
|
||||
import re
|
||||
from os import name, system
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import typer
|
||||
@@ -63,6 +66,18 @@ def dict_contains(
|
||||
return key in dictionary and value in dictionary[key]
|
||||
|
||||
|
||||
def dict_keys_to_lower(dictionary: dict) -> dict:
|
||||
"""Convert all keys in a dictionary to lowercase.
|
||||
|
||||
Args:
|
||||
dictionary (dict): Dictionary to convert
|
||||
|
||||
Returns:
|
||||
dict: Dictionary with all keys converted to lowercase
|
||||
"""
|
||||
return {key.lower(): value for key, value in dictionary.items()}
|
||||
|
||||
|
||||
def dict_values_to_lists_strings(
|
||||
dictionary: dict,
|
||||
strip_null_values: bool = False,
|
||||
@@ -86,7 +101,7 @@ def dict_values_to_lists_strings(
|
||||
new_dict[key] = sorted([str(item) for item in value if item is not None])
|
||||
elif isinstance(value, dict):
|
||||
new_dict[key] = dict_values_to_lists_strings(value) # type: ignore[assignment]
|
||||
elif value is None or value == "None" or value == "":
|
||||
elif value is None or value == "None" or not value:
|
||||
new_dict[key] = []
|
||||
else:
|
||||
new_dict[key] = [str(value)]
|
||||
@@ -104,6 +119,48 @@ def dict_values_to_lists_strings(
|
||||
return new_dict
|
||||
|
||||
|
||||
def delete_from_dict( # noqa: C901
|
||||
dictionary: dict, key: str, value: str = None, is_regex: bool = False
|
||||
) -> dict:
|
||||
"""Delete a key or a value from a dictionary.
|
||||
|
||||
Args:
|
||||
dictionary (dict): Dictionary to delete from
|
||||
is_regex (bool, optional): Whether the key is a regex. Defaults to False.
|
||||
key (str): Key to delete
|
||||
value (str, optional): Value to delete. Defaults to None.
|
||||
|
||||
Returns:
|
||||
dict: Dictionary without the key
|
||||
"""
|
||||
dictionary = copy.deepcopy(dictionary)
|
||||
|
||||
if value is None:
|
||||
if is_regex:
|
||||
return {k: v for k, v in dictionary.items() if not re.search(key, str(k))}
|
||||
|
||||
return {k: v for k, v in dictionary.items() if k != key}
|
||||
|
||||
if is_regex:
|
||||
keys_to_delete = []
|
||||
for _key in dictionary:
|
||||
if re.search(key, str(_key)):
|
||||
if isinstance(dictionary[_key], list):
|
||||
dictionary[_key] = [v for v in dictionary[_key] if not re.search(value, v)]
|
||||
elif isinstance(dictionary[_key], str) and re.search(value, dictionary[_key]):
|
||||
keys_to_delete.append(_key)
|
||||
|
||||
for key in keys_to_delete:
|
||||
dictionary.pop(key)
|
||||
|
||||
elif key in dictionary and isinstance(dictionary[key], list):
|
||||
dictionary[key] = [v for v in dictionary[key] if v != value]
|
||||
elif key in dictionary and dictionary[key] == value:
|
||||
dictionary.pop(key)
|
||||
|
||||
return dictionary
|
||||
|
||||
|
||||
def docstring_parameter(*sub: Any) -> Any:
|
||||
"""Replace variables within docstrings.
|
||||
|
||||
@@ -153,6 +210,31 @@ def merge_dictionaries(dict1: dict, dict2: dict) -> dict:
|
||||
return dict(sorted(dict1.items()))
|
||||
|
||||
|
||||
def rename_in_dict(
|
||||
dictionary: dict[str, list[str]], key: str, value_1: str, value_2: str = None
|
||||
) -> dict:
|
||||
"""Rename a key or a value in a dictionary who's values are lists of strings.
|
||||
|
||||
Args:
|
||||
dictionary (dict): Dictionary to rename in.
|
||||
key (str): Key to check.
|
||||
value_1 (str): `With value_2` this is the value to rename. If `value_2` is None this is the renamed key
|
||||
value_2 (str, Optional): New value.
|
||||
|
||||
Returns:
|
||||
dict: Dictionary with renamed key or value
|
||||
"""
|
||||
dictionary = copy.deepcopy(dictionary)
|
||||
|
||||
if value_2 is None:
|
||||
if key in dictionary and value_1 not in dictionary:
|
||||
dictionary[value_1] = dictionary.pop(key)
|
||||
elif key in dictionary and value_1 in dictionary[key]:
|
||||
dictionary[key] = sorted({value_2 if x == value_1 else x for x in dictionary[key]})
|
||||
|
||||
return dictionary
|
||||
|
||||
|
||||
def remove_markdown_sections(
|
||||
text: str,
|
||||
strip_codeblocks: bool = False,
|
||||
@@ -179,7 +261,58 @@ def remove_markdown_sections(
|
||||
if strip_frontmatter:
|
||||
text = re.sub(r"^\s*---.*?---", "", text, flags=re.DOTALL)
|
||||
|
||||
return text # noqa: RET504
|
||||
return text
|
||||
|
||||
|
||||
def validate_csv_bulk_imports(csv_path: Path, note_paths: list) -> dict[str, list[dict[str, str]]]:
|
||||
"""Validate the bulk import CSV file.
|
||||
|
||||
Args:
|
||||
csv_path (dict): Dictionary to validate
|
||||
note_paths (list): List of paths to all notes in vault
|
||||
|
||||
Returns:
|
||||
dict: Validated dictionary
|
||||
"""
|
||||
csv_dict: dict[str, Any] = {}
|
||||
with csv_path.expanduser().open("r") as csv_file:
|
||||
csv_reader = csv.DictReader(csv_file, delimiter=",")
|
||||
row_num = 0
|
||||
for row in csv_reader:
|
||||
if row_num == 0:
|
||||
if "path" not in row:
|
||||
raise typer.BadParameter("Missing 'path' column in CSV file")
|
||||
if "type" not in row:
|
||||
raise typer.BadParameter("Missing 'type' column in CSV file")
|
||||
if "key" not in row:
|
||||
raise typer.BadParameter("Missing 'key' column in CSV file")
|
||||
if "value" not in row:
|
||||
raise typer.BadParameter("Missing 'value' column in CSV file")
|
||||
row_num += 1
|
||||
|
||||
if row_num > 0 and row["type"] not in ["tag", "frontmatter", "inline_metadata"]:
|
||||
raise typer.BadParameter(
|
||||
f"Invalid type '{row['type']}' in CSV file. Must be one of 'tag', 'frontmatter', 'inline_metadata'"
|
||||
)
|
||||
|
||||
if row["path"] not in csv_dict:
|
||||
csv_dict[row["path"]] = []
|
||||
|
||||
csv_dict[row["path"]].append(
|
||||
{"type": row["type"], "key": row["key"], "value": row["value"]}
|
||||
)
|
||||
|
||||
if row_num == 0 or row_num == 1:
|
||||
raise typer.BadParameter("Empty CSV file")
|
||||
|
||||
paths_to_remove = [x for x in csv_dict if x not in note_paths]
|
||||
|
||||
for _path in paths_to_remove:
|
||||
raise typer.BadParameter(
|
||||
f"'{_path}' in CSV does not exist in vault. Ensure all paths are relative to the vault root."
|
||||
)
|
||||
|
||||
return csv_dict
|
||||
|
||||
|
||||
def version_callback(value: bool) -> None:
|
||||
|
||||
@@ -34,14 +34,21 @@ def main(
|
||||
),
|
||||
export_csv: Path = typer.Option(
|
||||
None,
|
||||
help="Exports all metadata to a specified CSV file and exits. (Will overwrite any existing file)",
|
||||
help="Exports all metadata to a specified CSV file and exits.",
|
||||
show_default=False,
|
||||
dir_okay=False,
|
||||
file_okay=True,
|
||||
),
|
||||
export_json: Path = typer.Option(
|
||||
None,
|
||||
help="Exports all metadata to a specified JSON file and exits. (Will overwrite any existing file)",
|
||||
help="Exports all metadata to a specified JSON file and exits.",
|
||||
show_default=False,
|
||||
dir_okay=False,
|
||||
file_okay=True,
|
||||
),
|
||||
export_template: Path = typer.Option(
|
||||
None,
|
||||
help="Exports all notes and their metadata to a specified CSV file and exits. Use to create a template for batch updates.",
|
||||
show_default=False,
|
||||
dir_okay=False,
|
||||
file_okay=True,
|
||||
@@ -79,7 +86,7 @@ def main(
|
||||
help="""Set verbosity level (0=WARN, 1=INFO, 2=DEBUG, 3=TRACE)""",
|
||||
count=True,
|
||||
),
|
||||
version: Optional[bool] = typer.Option(
|
||||
version: Optional[bool] = typer.Option( # noqa: ARG001
|
||||
None, "--version", help="Print version and exit", callback=version_callback, is_eager=True
|
||||
),
|
||||
) -> None:
|
||||
@@ -91,74 +98,7 @@ def main(
|
||||
[bold underline]Configuration:[/]
|
||||
Configuration is specified in a configuration file. On First run, this file will be created at [tan]~/.{0}.env[/]. Any options specified on the command line will override the configuration file.
|
||||
|
||||
[bold underline]Usage:[/]
|
||||
[tan]Obsidian-metadata[/] provides a menu of sub-commands.
|
||||
|
||||
[bold underline]Vault Actions[/]
|
||||
Create or delete a backup of your vault.
|
||||
• Backup: Create a backup of the vault.
|
||||
• Delete Backup: Delete a backup of the vault.
|
||||
|
||||
[bold underline]Inspect Metadata[/]
|
||||
Inspect the metadata in your vault.
|
||||
• View all metadata in the vault
|
||||
• View all frontmatter
|
||||
• View all inline metadata
|
||||
• View all inline tags
|
||||
• Export all metadata to CSV or JSON file
|
||||
|
||||
[bold underline]Filter Notes in Scope[/]
|
||||
Limit the scope of notes to be processed with one or more filters.
|
||||
• Path filter (regex): Limit scope based on the path or filename
|
||||
• Metadata filter: Limit scope based on a key or key/value pair
|
||||
• Tag filter: Limit scope based on an in-text tag
|
||||
• List and clear filters: List all current filters and clear one or all
|
||||
• List notes in scope: List notes that will be processed.
|
||||
|
||||
[bold underline]Add Metadata[/]
|
||||
Add new metadata to your vault.
|
||||
• Add new metadata to the frontmatter
|
||||
• Add new inline metadata - Set `insert_location` in the config to
|
||||
control where the new metadata is inserted. (Default: Bottom)
|
||||
• Add new inline tag - Set `insert_location` in the config to
|
||||
control where the new tag is inserted. (Default: Bottom)
|
||||
|
||||
[bold underline]Rename Metadata[/]
|
||||
Rename either a key and all associated values, a specific value within a key. or an in-text tag.
|
||||
• Rename a key
|
||||
• Rename a value
|
||||
• rename an inline tag
|
||||
|
||||
[bold underline]Delete Metadata[/]
|
||||
Delete either a key and all associated values, or a specific value.
|
||||
• Delete a key and associated values
|
||||
• Delete a value from a key
|
||||
• Delete an inline tag
|
||||
|
||||
[bold underline]Move Inline Metadata[/]
|
||||
Move inline metadata to a specified location with a note
|
||||
|
||||
• Move to Top - Move all inline metadata beneath the frontmatter
|
||||
• Move to After Title* - Move all inline metadata beneath the first markdown header
|
||||
• Move to Bottom - Move all inline metadata to the bottom of the note
|
||||
|
||||
[bold underline]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
|
||||
• Transpose value - Transpose a specific key:value pair
|
||||
|
||||
[bold underline]Review Changes[/]
|
||||
Prior to committing changes, review all changes that will be made.
|
||||
• View a diff of the changes that will be made
|
||||
|
||||
[bold underline]Commit Changes[/]
|
||||
Write the changes to disk. This step is not undoable.
|
||||
• Commit changes to the vault
|
||||
Full usage information is available at https://github.com/natelandau/obsidian-metadata
|
||||
|
||||
"""
|
||||
# Instantiate logger
|
||||
@@ -209,6 +149,10 @@ def main(
|
||||
path = Path(export_json).expanduser().resolve()
|
||||
application.noninteractive_export_csv(path)
|
||||
raise typer.Exit(code=0)
|
||||
if export_template is not None:
|
||||
path = Path(export_template).expanduser().resolve()
|
||||
application.noninteractive_export_template(path)
|
||||
raise typer.Exit(code=0)
|
||||
|
||||
application.application_main()
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from rich import box
|
||||
from rich.table import Table
|
||||
|
||||
from obsidian_metadata._config import VaultConfig
|
||||
from obsidian_metadata._utils import alerts
|
||||
from obsidian_metadata._utils import alerts, validate_csv_bulk_imports
|
||||
from obsidian_metadata._utils.console import console
|
||||
from obsidian_metadata.models import InsertLocation, Vault, VaultFilter
|
||||
from obsidian_metadata.models.enums import MetadataType
|
||||
@@ -53,8 +53,12 @@ class Application:
|
||||
match self.questions.ask_application_main():
|
||||
case "vault_actions":
|
||||
self.application_vault()
|
||||
case "export_metadata":
|
||||
self.application_export_metadata()
|
||||
case "inspect_metadata":
|
||||
self.application_inspect_metadata()
|
||||
case "import_from_csv":
|
||||
self.application_import_csv()
|
||||
case "filter_notes":
|
||||
self.application_filter()
|
||||
case "add_metadata":
|
||||
@@ -73,7 +77,6 @@ class Application:
|
||||
break
|
||||
|
||||
console.print("Done!")
|
||||
return
|
||||
|
||||
def application_add_metadata(self) -> None:
|
||||
"""Add metadata."""
|
||||
@@ -125,7 +128,8 @@ class Application:
|
||||
alerts.usage("Delete either a key and all associated values, or a specific value.")
|
||||
|
||||
choices = [
|
||||
{"name": "Delete inline tag", "value": "delete_inline_tag"},
|
||||
questionary.Separator(),
|
||||
{"name": "Delete inline tag", "value": "delete_tag"},
|
||||
{"name": "Delete key", "value": "delete_key"},
|
||||
{"name": "Delete value", "value": "delete_value"},
|
||||
questionary.Separator(),
|
||||
@@ -138,8 +142,8 @@ class Application:
|
||||
self.delete_key()
|
||||
case "delete_value":
|
||||
self.delete_value()
|
||||
case "delete_inline_tag":
|
||||
self.delete_inline_tag()
|
||||
case "delete_tag":
|
||||
self.delete_tag()
|
||||
case _: # pragma: no cover
|
||||
return
|
||||
|
||||
@@ -148,7 +152,8 @@ class Application:
|
||||
alerts.usage("Select the type of metadata to rename.")
|
||||
|
||||
choices = [
|
||||
{"name": "Rename inline tag", "value": "rename_inline_tag"},
|
||||
questionary.Separator(),
|
||||
{"name": "Rename inline tag", "value": "rename_tag"},
|
||||
{"name": "Rename key", "value": "rename_key"},
|
||||
{"name": "Rename value", "value": "rename_value"},
|
||||
questionary.Separator(),
|
||||
@@ -161,8 +166,8 @@ class Application:
|
||||
self.rename_key()
|
||||
case "rename_value":
|
||||
self.rename_value()
|
||||
case "rename_inline_tag":
|
||||
self.rename_inline_tag()
|
||||
case "rename_tag":
|
||||
self.rename_tag()
|
||||
case _: # pragma: no cover
|
||||
return
|
||||
|
||||
@@ -171,6 +176,7 @@ class Application:
|
||||
alerts.usage("Limit the scope of notes to be processed with one or more filters.")
|
||||
|
||||
choices = [
|
||||
questionary.Separator(),
|
||||
{"name": "Apply new regex path filter", "value": "apply_path_filter"},
|
||||
{"name": "Apply new metadata filter", "value": "apply_metadata_filter"},
|
||||
{"name": "Apply new in-text tag filter", "value": "apply_tag_filter"},
|
||||
@@ -183,7 +189,7 @@ class Application:
|
||||
match self.questions.ask_selection(choices=choices, question="Select an action"):
|
||||
case "apply_path_filter":
|
||||
path = self.questions.ask_filter_path()
|
||||
if path is None or path == "": # pragma: no cover
|
||||
if path is None or not path: # pragma: no cover
|
||||
return
|
||||
|
||||
self.filters.append(VaultFilter(path_filter=path))
|
||||
@@ -200,15 +206,15 @@ class Application:
|
||||
)
|
||||
if value is None: # pragma: no cover
|
||||
return
|
||||
if value == "":
|
||||
if not value:
|
||||
self.filters.append(VaultFilter(key_filter=key))
|
||||
else:
|
||||
self.filters.append(VaultFilter(key_filter=key, value_filter=value))
|
||||
self._load_vault()
|
||||
|
||||
case "apply_tag_filter":
|
||||
tag = self.questions.ask_existing_inline_tag()
|
||||
if tag is None or tag == "":
|
||||
tag = self.questions.ask_existing_tag()
|
||||
if tag is None or not tag:
|
||||
return
|
||||
|
||||
self.filters.append(VaultFilter(tag_filter=tag))
|
||||
@@ -277,6 +283,76 @@ class Application:
|
||||
case _:
|
||||
return
|
||||
|
||||
def application_import_csv(self) -> None:
|
||||
"""Import CSV for bulk changes to metadata."""
|
||||
alerts.usage(
|
||||
"Import CSV to make build changes to metadata. The CSV must have the following columns: path, type, key, value. Where type is one of 'frontmatter', 'inline_metadata', or 'tag'. Note: this will not create new notes."
|
||||
)
|
||||
|
||||
path = self.questions.ask_path(question="Enter path to a CSV file", valid_file=True)
|
||||
|
||||
if path is None:
|
||||
return
|
||||
|
||||
csv_path = Path(path).expanduser()
|
||||
|
||||
if "csv" not in csv_path.suffix.lower():
|
||||
alerts.error("File must be a CSV file")
|
||||
return
|
||||
|
||||
note_paths = [
|
||||
str(n.note_path.relative_to(self.vault.vault_path)) for n in self.vault.all_notes
|
||||
]
|
||||
|
||||
dict_from_csv = validate_csv_bulk_imports(csv_path, note_paths)
|
||||
num_changed = self.vault.update_from_dict(dict_from_csv)
|
||||
|
||||
if num_changed == 0:
|
||||
alerts.warning("No notes were changed")
|
||||
return
|
||||
|
||||
alerts.success(f"Rewrote metadata for {num_changed} notes.")
|
||||
|
||||
def application_export_metadata(self) -> None:
|
||||
"""Export metadata to various formats."""
|
||||
alerts.usage(
|
||||
"Export the metadata in your vault. Note, uncommitted changes will be reflected in these files. The notes csv export can be used as template for importing bulk changes"
|
||||
)
|
||||
choices = [
|
||||
questionary.Separator(),
|
||||
{"name": "Metadata by type to CSV", "value": "export_csv"},
|
||||
{"name": "Metadata by type to JSON", "value": "export_json"},
|
||||
{
|
||||
"name": "Metadata by note to CSV [Bulk import template]",
|
||||
"value": "export_notes_csv",
|
||||
},
|
||||
questionary.Separator(),
|
||||
{"name": "Back", "value": "back"},
|
||||
]
|
||||
while True:
|
||||
match self.questions.ask_selection(choices=choices, question="Export format"):
|
||||
case "export_csv":
|
||||
path = self.questions.ask_path(question="Enter a path for the CSV file")
|
||||
if path is None:
|
||||
return
|
||||
self.vault.export_metadata(path=path, export_format="csv")
|
||||
alerts.success(f"CSV written to {path}")
|
||||
case "export_json":
|
||||
path = self.questions.ask_path(question="Enter a path for the JSON file")
|
||||
if path is None:
|
||||
return
|
||||
self.vault.export_metadata(path=path, export_format="json")
|
||||
alerts.success(f"JSON written to {path}")
|
||||
case "export_notes_csv":
|
||||
path = self.questions.ask_path(question="Enter a path for the CSV file")
|
||||
if path is None:
|
||||
return
|
||||
self.vault.export_notes_to_csv(path=path)
|
||||
alerts.success(f"CSV written to {path}")
|
||||
return
|
||||
case _:
|
||||
return
|
||||
|
||||
def application_inspect_metadata(self) -> None:
|
||||
"""View metadata."""
|
||||
alerts.usage(
|
||||
@@ -284,19 +360,17 @@ class Application:
|
||||
)
|
||||
|
||||
choices = [
|
||||
questionary.Separator(),
|
||||
{"name": "View all frontmatter", "value": "all_frontmatter"},
|
||||
{"name": "View all inline metadata", "value": "all_inline"},
|
||||
{"name": "View all inline tags", "value": "all_tags"},
|
||||
{"name": "View all keys", "value": "all_keys"},
|
||||
{"name": "View all metadata", "value": "all_metadata"},
|
||||
questionary.Separator(),
|
||||
{"name": "Write all metadata to CSV", "value": "export_csv"},
|
||||
{"name": "Write all metadata to JSON file", "value": "export_json"},
|
||||
questionary.Separator(),
|
||||
{"name": "Back", "value": "back"},
|
||||
]
|
||||
while True:
|
||||
match self.questions.ask_selection(choices=choices, question="Select a vault action"):
|
||||
match self.questions.ask_selection(choices=choices, question="Select an action"):
|
||||
case "all_metadata":
|
||||
console.print("")
|
||||
self.vault.metadata.print_metadata(area=MetadataType.ALL)
|
||||
@@ -317,18 +391,6 @@ class Application:
|
||||
console.print("")
|
||||
self.vault.metadata.print_metadata(area=MetadataType.TAGS)
|
||||
console.print("")
|
||||
case "export_csv":
|
||||
path = self.questions.ask_path(question="Enter a path for the CSV file")
|
||||
if path is None:
|
||||
return
|
||||
self.vault.export_metadata(path=path, export_format="csv")
|
||||
alerts.success(f"Metadata written to {path}")
|
||||
case "export_json":
|
||||
path = self.questions.ask_path(question="Enter a path for the JSON file")
|
||||
if path is None:
|
||||
return
|
||||
self.vault.export_metadata(path=path, export_format="json")
|
||||
alerts.success(f"Metadata written to {path}")
|
||||
case _:
|
||||
return
|
||||
|
||||
@@ -343,6 +405,7 @@ class Application:
|
||||
alerts.usage(" 2. Move the location of inline metadata within a note.")
|
||||
|
||||
choices = [
|
||||
questionary.Separator(),
|
||||
{"name": "Move inline metadata to top of note", "value": "move_to_top"},
|
||||
{
|
||||
"name": "Move inline metadata beneath the first header",
|
||||
@@ -375,6 +438,7 @@ class Application:
|
||||
alerts.usage("Create or delete a backup of your vault.")
|
||||
|
||||
choices = [
|
||||
questionary.Separator(),
|
||||
{"name": "Backup vault", "value": "backup_vault"},
|
||||
{"name": "Delete vault backup", "value": "delete_backup"},
|
||||
questionary.Separator(),
|
||||
@@ -418,11 +482,11 @@ class Application:
|
||||
|
||||
return True
|
||||
|
||||
def delete_inline_tag(self) -> None:
|
||||
def delete_tag(self) -> None:
|
||||
"""Delete an inline tag."""
|
||||
tag = self.questions.ask_existing_inline_tag(question="Which tag would you like to delete?")
|
||||
tag = self.questions.ask_existing_tag(question="Which tag would you like to delete?")
|
||||
|
||||
num_changed = self.vault.delete_inline_tag(tag)
|
||||
num_changed = self.vault.delete_tag(tag)
|
||||
if num_changed == 0:
|
||||
alerts.warning("No notes were changed")
|
||||
return
|
||||
@@ -438,7 +502,9 @@ class Application:
|
||||
if key_to_delete is None: # pragma: no cover
|
||||
return
|
||||
|
||||
num_changed = self.vault.delete_metadata(key_to_delete)
|
||||
num_changed = self.vault.delete_metadata(
|
||||
key=key_to_delete, area=MetadataType.ALL, is_regex=True
|
||||
)
|
||||
if num_changed == 0:
|
||||
alerts.warning(f"No notes found with a key matching: [reverse]{key_to_delete}[/]")
|
||||
return
|
||||
@@ -460,7 +526,9 @@ class Application:
|
||||
if value is None: # pragma: no cover
|
||||
return
|
||||
|
||||
num_changed = self.vault.delete_metadata(key, value)
|
||||
num_changed = self.vault.delete_metadata(
|
||||
key=key, value=value, area=MetadataType.ALL, is_regex=True
|
||||
)
|
||||
if num_changed == 0:
|
||||
alerts.warning(f"No notes found matching: {key}: {value}")
|
||||
return
|
||||
@@ -483,7 +551,7 @@ class Application:
|
||||
def noninteractive_export_csv(self, path: Path) -> None:
|
||||
"""Export the vault metadata to CSV."""
|
||||
self._load_vault()
|
||||
self.vault.export_metadata(export_format="json", path=str(path))
|
||||
self.vault.export_metadata(export_format="csv", path=str(path))
|
||||
alerts.success(f"Exported metadata to {path}")
|
||||
|
||||
def noninteractive_export_json(self, path: Path) -> None:
|
||||
@@ -492,6 +560,16 @@ class Application:
|
||||
self.vault.export_metadata(export_format="json", path=str(path))
|
||||
alerts.success(f"Exported metadata to {path}")
|
||||
|
||||
def noninteractive_export_template(self, path: Path) -> None:
|
||||
"""Export the vault metadata to CSV."""
|
||||
self._load_vault()
|
||||
with console.status(
|
||||
"Preparing export... [dim](Can take a while for large vaults)[/]",
|
||||
spinner="bouncingBall",
|
||||
):
|
||||
self.vault.export_notes_to_csv(path=str(path))
|
||||
alerts.success(f"Exported metadata to {path}")
|
||||
|
||||
def rename_key(self) -> None:
|
||||
"""Rename a key in the vault."""
|
||||
original_key = self.questions.ask_existing_key(
|
||||
@@ -513,9 +591,9 @@ class Application:
|
||||
f"Renamed [reverse]{original_key}[/] to [reverse]{new_key}[/] in {num_changed} notes"
|
||||
)
|
||||
|
||||
def rename_inline_tag(self) -> None:
|
||||
def rename_tag(self) -> None:
|
||||
"""Rename an inline tag."""
|
||||
original_tag = self.questions.ask_existing_inline_tag(question="Which tag to rename?")
|
||||
original_tag = self.questions.ask_existing_tag(question="Which tag to rename?")
|
||||
if original_tag is None: # pragma: no cover
|
||||
return
|
||||
|
||||
@@ -523,7 +601,7 @@ class Application:
|
||||
if new_tag is None: # pragma: no cover
|
||||
return
|
||||
|
||||
num_changed = self.vault.rename_inline_tag(original_tag, new_tag)
|
||||
num_changed = self.vault.rename_tag(original_tag, new_tag)
|
||||
if num_changed == 0:
|
||||
alerts.warning("No notes were changed")
|
||||
return
|
||||
@@ -565,6 +643,7 @@ class Application:
|
||||
|
||||
alerts.info(f"Found {len(changed_notes)} changed notes in the vault")
|
||||
choices: list[dict[str, Any] | questionary.Separator] = []
|
||||
choices.append(questionary.Separator())
|
||||
for n, note in enumerate(changed_notes, start=1):
|
||||
_selection = {
|
||||
"name": f"{n}: {note.note_path.relative_to(self.vault.vault_path)}",
|
||||
|
||||
@@ -10,10 +10,12 @@ from ruamel.yaml import YAML
|
||||
|
||||
from obsidian_metadata._utils import (
|
||||
clean_dictionary,
|
||||
delete_from_dict,
|
||||
dict_contains,
|
||||
dict_values_to_lists_strings,
|
||||
merge_dictionaries,
|
||||
remove_markdown_sections,
|
||||
rename_in_dict,
|
||||
)
|
||||
from obsidian_metadata._utils.console import console
|
||||
from obsidian_metadata.models import Patterns # isort: ignore
|
||||
@@ -24,7 +26,14 @@ INLINE_TAG_KEY: str = "inline_tag"
|
||||
|
||||
|
||||
class VaultMetadata:
|
||||
"""Representation of all Metadata in the Vault."""
|
||||
"""Representation of all Metadata in the Vault.
|
||||
|
||||
Attributes:
|
||||
dict (dict): Dictionary of all frontmatter and inline metadata. Does not include tags.
|
||||
frontmatter (dict): Dictionary of all frontmatter metadata.
|
||||
inline_metadata (dict): Dictionary of all inline metadata.
|
||||
tags (list): List of all tags.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.dict: dict[str, list[str]] = {}
|
||||
@@ -59,7 +68,7 @@ class VaultMetadata:
|
||||
self.tags.extend(metadata)
|
||||
self.tags = sorted({s.strip("#") for s in self.tags})
|
||||
|
||||
def contains( # noqa: PLR0911
|
||||
def contains(
|
||||
self, area: MetadataType, key: str = None, value: str = None, is_regex: bool = False
|
||||
) -> bool:
|
||||
"""Check if a key and/or a value exists in the metadata.
|
||||
@@ -82,13 +91,7 @@ class VaultMetadata:
|
||||
|
||||
match area:
|
||||
case MetadataType.ALL:
|
||||
if dict_contains(self.dict, key, value, is_regex):
|
||||
return True
|
||||
if key is None and value is not None:
|
||||
if is_regex:
|
||||
return any(re.search(value, tag) for tag in self.tags)
|
||||
return value in self.tags
|
||||
|
||||
return dict_contains(self.dict, key, value, is_regex)
|
||||
case MetadataType.FRONTMATTER:
|
||||
return dict_contains(self.frontmatter, key, value, is_regex)
|
||||
case MetadataType.INLINE:
|
||||
@@ -102,10 +105,8 @@ class VaultMetadata:
|
||||
return any(re.search(value, tag) for tag in self.tags)
|
||||
return value in self.tags
|
||||
|
||||
return False
|
||||
|
||||
def delete(self, key: str, value_to_delete: str = None) -> bool:
|
||||
"""Delete a key or a key's value from the metadata. Regex is supported to allow deleting more than one key or value.
|
||||
"""Delete a key or a value from the VaultMetadata dict object. Regex is supported to allow deleting more than one key or value.
|
||||
|
||||
Args:
|
||||
key (str): Key to check.
|
||||
@@ -114,17 +115,12 @@ class VaultMetadata:
|
||||
Returns:
|
||||
bool: True if a value was deleted
|
||||
"""
|
||||
new_dict = copy.deepcopy(self.dict)
|
||||
|
||||
if value_to_delete is None:
|
||||
for _k in list(new_dict):
|
||||
if re.search(key, _k):
|
||||
del new_dict[_k]
|
||||
else:
|
||||
for _k, _v in new_dict.items():
|
||||
if re.search(key, _k):
|
||||
new_values = [x for x in _v if not re.search(value_to_delete, x)]
|
||||
new_dict[_k] = sorted(new_values)
|
||||
new_dict = delete_from_dict(
|
||||
dictionary=self.dict,
|
||||
key=key,
|
||||
value=value_to_delete,
|
||||
is_regex=True,
|
||||
)
|
||||
|
||||
if new_dict != self.dict:
|
||||
self.dict = dict(new_dict)
|
||||
@@ -138,28 +134,24 @@ class VaultMetadata:
|
||||
Args:
|
||||
area (MetadataType): Type of metadata to print
|
||||
"""
|
||||
dict_to_print: dict[str, list[str]] = None
|
||||
list_to_print: list[str] = None
|
||||
dict_to_print = None
|
||||
list_to_print = None
|
||||
match area:
|
||||
case MetadataType.INLINE:
|
||||
dict_to_print = self.inline_metadata.copy()
|
||||
dict_to_print = self.inline_metadata
|
||||
header = "All inline metadata"
|
||||
case MetadataType.FRONTMATTER:
|
||||
dict_to_print = self.frontmatter.copy()
|
||||
dict_to_print = self.frontmatter
|
||||
header = "All frontmatter"
|
||||
case MetadataType.TAGS:
|
||||
list_to_print = []
|
||||
for tag in self.tags:
|
||||
list_to_print.append(f"#{tag}")
|
||||
list_to_print = [f"#{x}" for x in self.tags]
|
||||
header = "All inline tags"
|
||||
case MetadataType.KEYS:
|
||||
list_to_print = sorted(self.dict.keys())
|
||||
header = "All Keys"
|
||||
case MetadataType.ALL:
|
||||
dict_to_print = self.dict.copy()
|
||||
list_to_print = []
|
||||
for tag in self.tags:
|
||||
list_to_print.append(f"#{tag}")
|
||||
dict_to_print = self.dict
|
||||
list_to_print = [f"#{x}" for x in self.tags]
|
||||
header = "All metadata"
|
||||
|
||||
if dict_to_print is not None:
|
||||
@@ -189,19 +181,14 @@ class VaultMetadata:
|
||||
key (str): Key to check.
|
||||
value_1 (str): `With value_2` this is the value to rename. If `value_2` is None this is the renamed key
|
||||
value_2 (str, Optional): New value.
|
||||
bypass_check (bool, optional): Bypass the check if the key exists. Defaults to False.
|
||||
|
||||
Returns:
|
||||
bool: True if a value was renamed
|
||||
"""
|
||||
if value_2 is None:
|
||||
if key in self.dict and value_1 not in self.dict:
|
||||
self.dict[value_1] = self.dict.pop(key)
|
||||
return True
|
||||
return False
|
||||
new_dict = rename_in_dict(dictionary=self.dict, key=key, value_1=value_1, value_2=value_2)
|
||||
|
||||
if key in self.dict and value_1 in self.dict[key]:
|
||||
self.dict[key] = sorted({value_2 if x == value_1 else x for x in self.dict[key]})
|
||||
if new_dict != self.dict:
|
||||
self.dict = dict(new_dict)
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -210,7 +197,7 @@ class VaultMetadata:
|
||||
class Frontmatter:
|
||||
"""Representation of frontmatter metadata."""
|
||||
|
||||
def __init__(self, file_content: str):
|
||||
def __init__(self, file_content: str) -> None:
|
||||
self.dict: dict[str, list[str]] = self._grab_note_frontmatter(file_content)
|
||||
self.dict_original: dict[str, list[str]] = copy.deepcopy(self.dict)
|
||||
|
||||
@@ -245,6 +232,9 @@ class Frontmatter:
|
||||
except Exception as e: # noqa: BLE001
|
||||
raise AttributeError(e) from e
|
||||
|
||||
if frontmatter is None or frontmatter == [None]:
|
||||
return {}
|
||||
|
||||
for k in frontmatter:
|
||||
if frontmatter[k] is None:
|
||||
frontmatter[k] = []
|
||||
@@ -278,6 +268,7 @@ class Frontmatter:
|
||||
if key in self.dict and value not in self.dict[key]:
|
||||
if isinstance(value, list):
|
||||
self.dict[key].extend(value)
|
||||
self.dict[key] = list(sorted(set(self.dict[key])))
|
||||
return True
|
||||
|
||||
self.dict[key].append(value)
|
||||
@@ -298,27 +289,23 @@ class Frontmatter:
|
||||
"""
|
||||
return dict_contains(self.dict, key, value, is_regex)
|
||||
|
||||
def delete(self, key: str, value_to_delete: str = None) -> bool:
|
||||
def delete(self, key: str, value_to_delete: str = None, is_regex: bool = False) -> bool:
|
||||
"""Delete a value or key in the frontmatter. Regex is supported to allow deleting more than one key or value.
|
||||
|
||||
Args:
|
||||
is_regex (bool, optional): Use regex to check. Defaults to False.
|
||||
key (str): If no value, key to delete. If value, key containing the value.
|
||||
value_to_delete (str, optional): Value to delete.
|
||||
|
||||
Returns:
|
||||
bool: True if a value was deleted
|
||||
"""
|
||||
new_dict = dict(self.dict)
|
||||
|
||||
if value_to_delete is None:
|
||||
for _k in list(new_dict):
|
||||
if re.search(key, _k):
|
||||
del new_dict[_k]
|
||||
else:
|
||||
for _k, _v in new_dict.items():
|
||||
if re.search(key, _k):
|
||||
new_values = [x for x in _v if not re.search(value_to_delete, x)]
|
||||
new_dict[_k] = sorted(new_values)
|
||||
new_dict = delete_from_dict(
|
||||
dictionary=self.dict,
|
||||
key=key,
|
||||
value=value_to_delete,
|
||||
is_regex=is_regex,
|
||||
)
|
||||
|
||||
if new_dict != self.dict:
|
||||
self.dict = dict(new_dict)
|
||||
@@ -326,6 +313,10 @@ class Frontmatter:
|
||||
|
||||
return False
|
||||
|
||||
def delete_all(self) -> None:
|
||||
"""Delete all Frontmatter from the note."""
|
||||
self.dict = {}
|
||||
|
||||
def has_changes(self) -> bool:
|
||||
"""Check if the frontmatter has changes.
|
||||
|
||||
@@ -345,14 +336,10 @@ class Frontmatter:
|
||||
Returns:
|
||||
bool: True if a value was renamed
|
||||
"""
|
||||
if value_2 is None:
|
||||
if key in self.dict and value_1 not in self.dict:
|
||||
self.dict[value_1] = self.dict.pop(key)
|
||||
return True
|
||||
return False
|
||||
new_dict = rename_in_dict(dictionary=self.dict, key=key, value_1=value_1, value_2=value_2)
|
||||
|
||||
if key in self.dict and value_1 in self.dict[key]:
|
||||
self.dict[key] = sorted({value_2 if x == value_1 else x for x in self.dict[key]})
|
||||
if new_dict != self.dict:
|
||||
self.dict = dict(new_dict)
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -389,8 +376,8 @@ class Frontmatter:
|
||||
class InlineMetadata:
|
||||
"""Representation of inline metadata in the form of `key:: value`."""
|
||||
|
||||
def __init__(self, file_content: str):
|
||||
self.dict: dict[str, list[str]] = self.grab_inline_metadata(file_content)
|
||||
def __init__(self, file_content: str) -> None:
|
||||
self.dict: dict[str, list[str]] = self._grab_inline_metadata(file_content)
|
||||
self.dict_original: dict[str, list[str]] = copy.deepcopy(self.dict)
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover
|
||||
@@ -401,6 +388,30 @@ class InlineMetadata:
|
||||
"""
|
||||
return f"InlineMetadata(inline_metadata={self.dict})"
|
||||
|
||||
def _grab_inline_metadata(self, file_content: str) -> dict[str, list[str]]:
|
||||
"""Grab inline metadata from a note.
|
||||
|
||||
Returns:
|
||||
dict[str, str]: Inline metadata from the note.
|
||||
"""
|
||||
content = remove_markdown_sections(
|
||||
file_content,
|
||||
strip_codeblocks=True,
|
||||
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]
|
||||
|
||||
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)]
|
||||
|
||||
return clean_dictionary(inline_metadata)
|
||||
|
||||
def add(self, key: str, value: str | list[str] = None) -> bool: # noqa: PLR0911
|
||||
"""Add a key and value to the inline metadata.
|
||||
|
||||
@@ -428,6 +439,7 @@ class InlineMetadata:
|
||||
if key in self.dict and value not in self.dict[key]:
|
||||
if isinstance(value, list):
|
||||
self.dict[key].extend(value)
|
||||
self.dict[key] = list(sorted(set(self.dict[key])))
|
||||
return True
|
||||
|
||||
self.dict[key].append(value)
|
||||
@@ -448,27 +460,23 @@ class InlineMetadata:
|
||||
"""
|
||||
return dict_contains(self.dict, key, value, is_regex)
|
||||
|
||||
def delete(self, key: str, value_to_delete: str = None) -> bool:
|
||||
def delete(self, key: str, value_to_delete: str = None, is_regex: bool = False) -> bool:
|
||||
"""Delete a value or key in the inline metadata. Regex is supported to allow deleting more than one key or value.
|
||||
|
||||
Args:
|
||||
is_regex (bool, optional): If True, key and value are treated as regex. Defaults to False.
|
||||
key (str): If no value, key to delete. If value, key containing the value.
|
||||
value_to_delete (str, optional): Value to delete.
|
||||
|
||||
Returns:
|
||||
bool: True if a value was deleted
|
||||
"""
|
||||
new_dict = dict(self.dict)
|
||||
|
||||
if value_to_delete is None:
|
||||
for _k in list(new_dict):
|
||||
if re.search(key, _k):
|
||||
del new_dict[_k]
|
||||
else:
|
||||
for _k, _v in new_dict.items():
|
||||
if re.search(key, _k):
|
||||
new_values = [x for x in _v if not re.search(value_to_delete, x)]
|
||||
new_dict[_k] = sorted(new_values)
|
||||
new_dict = delete_from_dict(
|
||||
dictionary=self.dict,
|
||||
key=key,
|
||||
value=value_to_delete,
|
||||
is_regex=is_regex,
|
||||
)
|
||||
|
||||
if new_dict != self.dict:
|
||||
self.dict = dict(new_dict)
|
||||
@@ -476,30 +484,6 @@ class InlineMetadata:
|
||||
|
||||
return False
|
||||
|
||||
def grab_inline_metadata(self, file_content: str) -> dict[str, list[str]]:
|
||||
"""Grab inline metadata from a note.
|
||||
|
||||
Returns:
|
||||
dict[str, str]: Inline metadata from the note.
|
||||
"""
|
||||
content = remove_markdown_sections(
|
||||
file_content,
|
||||
strip_codeblocks=True,
|
||||
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]
|
||||
|
||||
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)]
|
||||
|
||||
return clean_dictionary(inline_metadata)
|
||||
|
||||
def has_changes(self) -> bool:
|
||||
"""Check if the metadata has changes.
|
||||
|
||||
@@ -519,14 +503,10 @@ class InlineMetadata:
|
||||
Returns:
|
||||
bool: True if a value was renamed
|
||||
"""
|
||||
if value_2 is None:
|
||||
if key in self.dict and value_1 not in self.dict:
|
||||
self.dict[value_1] = self.dict.pop(key)
|
||||
return True
|
||||
return False
|
||||
new_dict = rename_in_dict(dictionary=self.dict, key=key, value_1=value_1, value_2=value_2)
|
||||
|
||||
if key in self.dict and value_1 in self.dict[key]:
|
||||
self.dict[key] = sorted({value_2 if x == value_1 else x for x in self.dict[key]})
|
||||
if new_dict != self.dict:
|
||||
self.dict = dict(new_dict)
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -535,7 +515,7 @@ class InlineMetadata:
|
||||
class InlineTags:
|
||||
"""Representation of inline tags."""
|
||||
|
||||
def __init__(self, file_content: str):
|
||||
def __init__(self, file_content: str) -> None:
|
||||
self.metadata_key = INLINE_TAG_KEY
|
||||
self.list: list[str] = self._grab_inline_tags(file_content)
|
||||
self.list_original: list[str] = self.list.copy()
|
||||
@@ -571,22 +551,26 @@ class InlineTags:
|
||||
"""Add a new inline tag.
|
||||
|
||||
Args:
|
||||
new_tag (str): Tag to add.
|
||||
new_tag (str, list[str]): Tag to add.
|
||||
|
||||
Returns:
|
||||
bool: True if a tag was added.
|
||||
"""
|
||||
added_tag = False
|
||||
if isinstance(new_tag, list):
|
||||
for _tag in new_tag:
|
||||
if _tag.startswith("#"):
|
||||
_tag = _tag[1:]
|
||||
if _tag in self.list:
|
||||
return False
|
||||
new_list = self.list.copy()
|
||||
new_list.append(_tag)
|
||||
self.list = sorted(new_list)
|
||||
continue
|
||||
self.list.append(_tag)
|
||||
added_tag = True
|
||||
|
||||
if added_tag:
|
||||
self.list = sorted(self.list)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
if new_tag.startswith("#"):
|
||||
new_tag = new_tag[1:]
|
||||
if new_tag in self.list:
|
||||
@@ -596,8 +580,6 @@ class InlineTags:
|
||||
self.list = sorted(new_list)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def contains(self, tag: str, is_regex: bool = False) -> bool:
|
||||
"""Check if a tag exists in the metadata.
|
||||
|
||||
@@ -644,13 +626,13 @@ class InlineTags:
|
||||
"""Replace an inline tag with another string.
|
||||
|
||||
Args:
|
||||
old_tag (str): `With value_2` this is the value to rename. If `value_2` is None this is the renamed key
|
||||
new_tag (str, Optional): New value.
|
||||
old_tag (str): `With value_2` this is the value to rename.
|
||||
new_tag (str): New value
|
||||
|
||||
Returns:
|
||||
bool: True if a value was renamed
|
||||
"""
|
||||
if old_tag in self.list:
|
||||
self.list = sorted([new_tag if i == old_tag else i for i in self.list])
|
||||
if old_tag in self.list and new_tag is not None and new_tag:
|
||||
self.list = sorted({new_tag if i == old_tag else i for i in self.list})
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -37,11 +37,12 @@ class Note:
|
||||
dry_run (bool): Whether to run in dry-run mode.
|
||||
file_content (str): Total contents of the note file (frontmatter and content).
|
||||
frontmatter (dict): Frontmatter of the note.
|
||||
inline_tags (list): List of inline tags in the note.
|
||||
tags (list): List of inline tags in the note.
|
||||
inline_metadata (dict): Dictionary of inline metadata in the note.
|
||||
original_file_content (str): Original contents of the note file (frontmatter and content)
|
||||
"""
|
||||
|
||||
def __init__(self, note_path: Path, dry_run: bool = False):
|
||||
def __init__(self, note_path: Path, dry_run: bool = False) -> None:
|
||||
log.trace(f"Creating Note object for {note_path}")
|
||||
self.note_path: Path = Path(note_path)
|
||||
self.dry_run: bool = dry_run
|
||||
@@ -59,7 +60,7 @@ class Note:
|
||||
alerts.error(f"Note {self.note_path} has invalid frontmatter.\n{e}")
|
||||
raise typer.Exit(code=1) from e
|
||||
|
||||
self.inline_tags: InlineTags = InlineTags(self.file_content)
|
||||
self.tags: InlineTags = InlineTags(self.file_content)
|
||||
self.inline_metadata: InlineMetadata = InlineMetadata(self.file_content)
|
||||
self.original_file_content: str = self.file_content
|
||||
|
||||
@@ -68,7 +69,7 @@ class Note:
|
||||
yield "note_path", self.note_path
|
||||
yield "dry_run", self.dry_run
|
||||
yield "frontmatter", self.frontmatter
|
||||
yield "inline_tags", self.inline_tags
|
||||
yield "tags", self.tags
|
||||
yield "inline_metadata", self.inline_metadata
|
||||
|
||||
def add_metadata( # noqa: C901
|
||||
@@ -114,8 +115,8 @@ class Note:
|
||||
case MetadataType.TAGS:
|
||||
new_values = []
|
||||
if isinstance(value, list):
|
||||
new_values = [_v for _v in value if self.inline_tags.add(_v)]
|
||||
elif self.inline_tags.add(value):
|
||||
new_values = [_v for _v in value if self.tags.add(_v)]
|
||||
elif self.tags.add(value):
|
||||
new_values = [value]
|
||||
|
||||
if new_values:
|
||||
@@ -146,14 +147,14 @@ class Note:
|
||||
return
|
||||
|
||||
try:
|
||||
with open(p, "w") as f:
|
||||
with p.open(mode="w") as f:
|
||||
log.trace(f"Writing note {p} to disk")
|
||||
f.write(self.file_content)
|
||||
except FileNotFoundError as e:
|
||||
alerts.error(f"Note {p} not found. Exiting")
|
||||
raise typer.Exit(code=1) from e
|
||||
|
||||
def contains_inline_tag(self, tag: str, is_regex: bool = False) -> bool:
|
||||
def contains_tag(self, tag: str, is_regex: bool = False) -> bool:
|
||||
"""Check if a note contains the specified inline tag.
|
||||
|
||||
Args:
|
||||
@@ -163,7 +164,7 @@ class Note:
|
||||
Returns:
|
||||
bool: Whether the note has inline tags.
|
||||
"""
|
||||
return self.inline_tags.contains(tag, is_regex=is_regex)
|
||||
return self.tags.contains(tag, is_regex=is_regex)
|
||||
|
||||
def contains_metadata(self, key: str, value: str = None, is_regex: bool = False) -> bool:
|
||||
"""Check if a note has a key or a key-value pair in its Frontmatter or InlineMetadata.
|
||||
@@ -190,8 +191,19 @@ class Note:
|
||||
|
||||
return False
|
||||
|
||||
def delete_inline_tag(self, tag: str) -> bool:
|
||||
"""Delete an inline tag from the `inline_tags` attribute AND removes the tag from the text of the note if it exists.
|
||||
def delete_all_metadata(self) -> None:
|
||||
"""Delete all metadata from the note. Removes all frontmatter and inline metadata and tags from the body of the note and from the associated metadata objects."""
|
||||
for key in self.inline_metadata.dict:
|
||||
self.delete_metadata(key=key, area=MetadataType.INLINE)
|
||||
|
||||
for tag in self.tags.list:
|
||||
self.delete_tag(tag=tag)
|
||||
|
||||
self.frontmatter.delete_all()
|
||||
self.write_frontmatter()
|
||||
|
||||
def delete_tag(self, tag: str) -> bool:
|
||||
"""Delete an inline tag from the `tags` attribute AND removes the tag from the text of the note if it exists.
|
||||
|
||||
Args:
|
||||
tag (str): Tag to delete.
|
||||
@@ -199,30 +211,35 @@ class Note:
|
||||
Returns:
|
||||
bool: Whether the tag was deleted.
|
||||
"""
|
||||
new_list = self.inline_tags.list.copy()
|
||||
new_list = self.tags.list.copy()
|
||||
|
||||
for _t in new_list:
|
||||
if re.search(tag, _t):
|
||||
_t = re.escape(_t)
|
||||
self.sub(rf"#{_t}([ \|,;:\*\(\)\[\]\\\.\n#&])", r"\1", is_regex=True)
|
||||
self.inline_tags.delete(tag)
|
||||
self.tags.delete(tag)
|
||||
|
||||
if new_list != self.inline_tags.list:
|
||||
if new_list != self.tags.list:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def delete_metadata(
|
||||
self, key: str, value: str = None, area: MetadataType = MetadataType.ALL
|
||||
self,
|
||||
key: str,
|
||||
value: str = None,
|
||||
area: MetadataType = MetadataType.ALL,
|
||||
is_regex: bool = False,
|
||||
) -> bool:
|
||||
"""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 specified key.
|
||||
|
||||
Args:
|
||||
area (MetadataType, optional): Area to delete metadata from. Defaults to MetadataType.ALL.
|
||||
is_regex (bool, optional): Whether to use regex to match the key/value.
|
||||
key (str): Key to delete.
|
||||
value (str, optional): Value to delete.
|
||||
area (MetadataType, optional): Area to delete metadata from. Defaults to MetadataType.ALL.
|
||||
|
||||
Returns:
|
||||
bool: Whether the key or key-value pair was deleted.
|
||||
@@ -231,15 +248,15 @@ class Note:
|
||||
|
||||
if (
|
||||
area == MetadataType.FRONTMATTER or area == MetadataType.ALL
|
||||
) and self.frontmatter.delete(key, value):
|
||||
) and self.frontmatter.delete(key=key, value_to_delete=value, is_regex=is_regex):
|
||||
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)
|
||||
self.write_delete_inline_metadata(key=key, value=value, is_regex=is_regex)
|
||||
self.inline_metadata.delete(key=key, value_to_delete=value, is_regex=is_regex)
|
||||
changed_value = True
|
||||
|
||||
if changed_value:
|
||||
@@ -255,7 +272,7 @@ class Note:
|
||||
if self.frontmatter.has_changes():
|
||||
return True
|
||||
|
||||
if self.inline_tags.has_changes():
|
||||
if self.tags.has_changes():
|
||||
return True
|
||||
|
||||
if self.inline_metadata.has_changes():
|
||||
@@ -287,7 +304,7 @@ class Note:
|
||||
"""Print the note to the console."""
|
||||
console.print(self.file_content)
|
||||
|
||||
def rename_inline_tag(self, tag_1: str, tag_2: str) -> bool:
|
||||
def rename_tag(self, tag_1: str, tag_2: str) -> bool:
|
||||
"""Rename an inline tag. Updates the Metadata object and the text of the note.
|
||||
|
||||
Args:
|
||||
@@ -297,13 +314,13 @@ class Note:
|
||||
Returns:
|
||||
bool: Whether the tag was renamed.
|
||||
"""
|
||||
if tag_1 in self.inline_tags.list:
|
||||
if tag_1 in self.tags.list:
|
||||
self.sub(
|
||||
rf"#{tag_1}([ \|,;:\*\(\)\[\]\\\.\n#&])",
|
||||
rf"#{tag_2}\1",
|
||||
is_regex=True,
|
||||
)
|
||||
self.inline_tags.rename(tag_1, tag_2)
|
||||
self.tags.rename(tag_1, tag_2)
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -354,7 +371,7 @@ class Note:
|
||||
|
||||
self.file_content = re.sub(pattern, replacement, self.file_content, re.MULTILINE)
|
||||
|
||||
def transpose_metadata( # noqa: C901, PLR0912, PLR0911, PLR0913
|
||||
def transpose_metadata( # noqa: C901, PLR0912, PLR0911
|
||||
self,
|
||||
begin: MetadataType,
|
||||
end: MetadataType,
|
||||
@@ -436,12 +453,15 @@ class Note:
|
||||
|
||||
return False
|
||||
|
||||
def write_delete_inline_metadata(self, key: str = None, value: str = None) -> bool:
|
||||
def write_delete_inline_metadata(
|
||||
self, key: str = None, value: str = None, is_regex: bool = False
|
||||
) -> 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:
|
||||
is_regex (bool, optional): Whether the key is a regex pattern or plain text. Defaults to False.
|
||||
key (str, optional): Key to delete.
|
||||
value (str, optional): Value to delete.
|
||||
|
||||
@@ -458,13 +478,15 @@ class Note:
|
||||
return True
|
||||
|
||||
for _k, _v in self.inline_metadata.dict.items():
|
||||
if re.search(key, _k):
|
||||
if (is_regex and re.search(key, _k)) or (not is_regex and 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)
|
||||
elif re.search(value, _value):
|
||||
elif (is_regex and re.search(value, _value)) or (
|
||||
not is_regex and value == _value
|
||||
):
|
||||
_k = re.escape(_k)
|
||||
_value = re.escape(_value)
|
||||
self.sub(rf"\[?({_k}::) ?\[?\[?{_value}\]?\]?", r"\1", is_regex=True)
|
||||
@@ -579,7 +601,7 @@ class Note:
|
||||
except AttributeError:
|
||||
top = ""
|
||||
|
||||
if top == "":
|
||||
if not top:
|
||||
self.file_content = f"{new_string}\n{self.file_content}"
|
||||
return True
|
||||
|
||||
@@ -593,7 +615,7 @@ class Note:
|
||||
except AttributeError:
|
||||
top = ""
|
||||
|
||||
if top == "":
|
||||
if not top:
|
||||
self.file_content = f"{new_string}\n{self.file_content}"
|
||||
return True
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class Patterns:
|
||||
([-_\w\d\/\*\u263a-\U0001f999]+?)::[ ]? # Find key
|
||||
(.*?)\] # Find value until closing bracket
|
||||
| # Else look for key values at start of line
|
||||
(?:^|[^ \w\d]+| \[) # Any non-word or non-digit character
|
||||
(?:^|[^ \w\d]+|^ *>?[-\d\|]?\.? ) # Any non-word or non-digit character
|
||||
([-_\w\d\/\*\u263a-\U0001f9995]+?)::(?!\n)(?:[ ](?!\n))? # Capture the key if not a new line
|
||||
(.*?)$ # Capture the value
|
||||
""",
|
||||
|
||||
@@ -86,7 +86,7 @@ class Questions:
|
||||
self.vault = vault
|
||||
self.key = key
|
||||
|
||||
def _validate_existing_inline_tag(self, text: str) -> bool | str:
|
||||
def _validate_existing_tag(self, text: str) -> bool | str:
|
||||
"""Validate an existing inline tag.
|
||||
|
||||
Returns:
|
||||
@@ -200,6 +200,23 @@ class Questions:
|
||||
|
||||
return True
|
||||
|
||||
def _validate_path_is_file(self, text: str) -> bool | str:
|
||||
"""Validate a path is a file.
|
||||
|
||||
Args:
|
||||
text (str): The path to validate.
|
||||
|
||||
Returns:
|
||||
bool | str: True if the path is valid, otherwise a string with the error message.
|
||||
"""
|
||||
path_to_validate: Path = Path(text).expanduser().resolve()
|
||||
if not path_to_validate.exists():
|
||||
return f"Path does not exist: {path_to_validate}"
|
||||
if not path_to_validate.is_file():
|
||||
return f"Path is not a file: {path_to_validate}"
|
||||
|
||||
return True
|
||||
|
||||
def _validate_valid_vault_regex(self, text: str) -> bool | str:
|
||||
"""Validate a valid regex.
|
||||
|
||||
@@ -276,9 +293,11 @@ class Questions:
|
||||
choices=[
|
||||
questionary.Separator("-------------------------------"),
|
||||
{"name": "Vault Actions", "value": "vault_actions"},
|
||||
{"name": "Export Metadata", "value": "export_metadata"},
|
||||
{"name": "Inspect Metadata", "value": "inspect_metadata"},
|
||||
{"name": "Filter Notes in Scope", "value": "filter_notes"},
|
||||
questionary.Separator("-------------------------------"),
|
||||
{"name": "Bulk changes from imported CSV", "value": "import_from_csv"},
|
||||
{"name": "Add Metadata", "value": "add_metadata"},
|
||||
{"name": "Delete Metadata", "value": "delete_metadata"},
|
||||
{"name": "Rename Metadata", "value": "rename_metadata"},
|
||||
@@ -325,11 +344,11 @@ class Questions:
|
||||
question, default=default, style=self.style, qmark="INPUT |"
|
||||
).ask()
|
||||
|
||||
def ask_existing_inline_tag(self, question: str = "Enter a tag") -> str: # pragma: no cover
|
||||
def ask_existing_tag(self, question: str = "Enter a tag") -> str: # pragma: no cover
|
||||
"""Ask the user for an existing inline tag."""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_existing_inline_tag,
|
||||
validate=self._validate_existing_tag,
|
||||
style=self.style,
|
||||
qmark="INPUT |",
|
||||
).ask()
|
||||
@@ -423,7 +442,7 @@ class Questions:
|
||||
|
||||
return self.ask_selection(
|
||||
choices=choices,
|
||||
question="Select the location for the metadata",
|
||||
question=question,
|
||||
)
|
||||
|
||||
def ask_new_key(self, question: str = "New key name") -> str: # pragma: no cover
|
||||
@@ -475,15 +494,27 @@ class Questions:
|
||||
question, validate=self._validate_number, style=self.style, qmark="INPUT |"
|
||||
).ask()
|
||||
|
||||
def ask_path(self, question: str = "Enter a path") -> str: # pragma: no cover
|
||||
def ask_path(
|
||||
self, question: str = "Enter a path", valid_file: bool = False
|
||||
) -> str: # pragma: no cover
|
||||
"""Ask the user for a path.
|
||||
|
||||
Args:
|
||||
question (str, optional): The question to ask. Defaults to "Enter a path".
|
||||
valid_file (bool, optional): Whether the path should be a valid file. Defaults to False.
|
||||
|
||||
Returns:
|
||||
str: A path.
|
||||
"""
|
||||
if valid_file:
|
||||
return questionary.path(
|
||||
question,
|
||||
only_directories=False,
|
||||
style=self.style,
|
||||
validate=self._validate_path_is_file,
|
||||
qmark="INPUT |",
|
||||
).ask()
|
||||
|
||||
return questionary.path(question, style=self.style, qmark="INPUT |").ask()
|
||||
|
||||
def ask_selection(
|
||||
@@ -498,7 +529,6 @@ class Questions:
|
||||
Returns:
|
||||
any: The selected item value.
|
||||
"""
|
||||
choices.insert(0, questionary.Separator())
|
||||
return questionary.select(
|
||||
question,
|
||||
choices=choices,
|
||||
|
||||
@@ -6,11 +6,11 @@ import re
|
||||
import shutil
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import rich.repr
|
||||
import typer
|
||||
from rich import box
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn
|
||||
from rich.prompt import Confirm
|
||||
from rich.table import Table
|
||||
|
||||
@@ -47,7 +47,7 @@ class Vault:
|
||||
config: VaultConfig,
|
||||
dry_run: bool = False,
|
||||
filters: list[VaultFilter] = [],
|
||||
):
|
||||
) -> None:
|
||||
self.config = config.config
|
||||
self.vault_path: Path = config.path
|
||||
self.name = self.vault_path.name
|
||||
@@ -63,12 +63,10 @@ class Vault:
|
||||
self.filters = filters
|
||||
self.all_note_paths = self._find_markdown_notes()
|
||||
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
transient=True,
|
||||
) as progress:
|
||||
progress.add_task(description="Processing notes...", total=None)
|
||||
with console.status(
|
||||
"Processing notes... [dim](Can take a while for a large vault)[/]",
|
||||
spinner="bouncingBall",
|
||||
):
|
||||
self.all_notes: list[Note] = [
|
||||
Note(note_path=p, dry_run=self.dry_run) for p in self.all_note_paths
|
||||
]
|
||||
@@ -106,7 +104,7 @@ class Vault:
|
||||
]
|
||||
|
||||
if _filter.tag_filter is not None:
|
||||
notes_list = [n for n in notes_list if n.contains_inline_tag(_filter.tag_filter)]
|
||||
notes_list = [n for n in notes_list if n.contains_tag(_filter.tag_filter)]
|
||||
|
||||
if _filter.key_filter is not None and _filter.value_filter is not None:
|
||||
notes_list = [
|
||||
@@ -171,12 +169,10 @@ class Vault:
|
||||
def _rebuild_vault_metadata(self) -> None:
|
||||
"""Rebuild vault metadata."""
|
||||
self.metadata = VaultMetadata()
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
transient=True,
|
||||
) as progress:
|
||||
progress.add_task(description="Processing notes...", total=None)
|
||||
with console.status(
|
||||
"Processing notes... [dim](Can take a while for a large vault)[/]",
|
||||
spinner="bouncingBall",
|
||||
):
|
||||
for _note in self.notes_in_scope:
|
||||
self.metadata.index_metadata(
|
||||
area=MetadataType.FRONTMATTER, metadata=_note.frontmatter.dict
|
||||
@@ -186,7 +182,7 @@ class Vault:
|
||||
)
|
||||
self.metadata.index_metadata(
|
||||
area=MetadataType.TAGS,
|
||||
metadata=_note.inline_tags.list,
|
||||
metadata=_note.tags.list,
|
||||
)
|
||||
|
||||
def add_metadata(
|
||||
@@ -272,7 +268,7 @@ class Vault:
|
||||
else:
|
||||
alerts.info("No backup found")
|
||||
|
||||
def delete_inline_tag(self, tag: str) -> int:
|
||||
def delete_tag(self, tag: str) -> int:
|
||||
"""Delete an inline tag in the vault.
|
||||
|
||||
Args:
|
||||
@@ -284,7 +280,7 @@ class Vault:
|
||||
num_changed = 0
|
||||
|
||||
for _note in self.notes_in_scope:
|
||||
if _note.delete_inline_tag(tag):
|
||||
if _note.delete_tag(tag):
|
||||
log.trace(f"Deleted tag from {_note.note_path}")
|
||||
num_changed += 1
|
||||
|
||||
@@ -293,10 +289,18 @@ class Vault:
|
||||
|
||||
return num_changed
|
||||
|
||||
def delete_metadata(self, key: str, value: str = None) -> int:
|
||||
def delete_metadata(
|
||||
self,
|
||||
key: str,
|
||||
value: str = None,
|
||||
area: MetadataType = MetadataType.ALL,
|
||||
is_regex: bool = False,
|
||||
) -> int:
|
||||
"""Delete metadata in the vault.
|
||||
|
||||
Args:
|
||||
area (MetadataType): Area of metadata to delete from.
|
||||
is_regex (bool): Whether to use regex for key and value. Defaults to False.
|
||||
key (str): Key to delete. Regex is supported
|
||||
value (str, optional): Value to delete. Regex is supported
|
||||
|
||||
@@ -306,7 +310,7 @@ class Vault:
|
||||
num_changed = 0
|
||||
|
||||
for _note in self.notes_in_scope:
|
||||
if _note.delete_metadata(key, value):
|
||||
if _note.delete_metadata(key=key, value=value, area=area, is_regex=is_regex):
|
||||
log.trace(f"Deleted metadata from {_note.note_path}")
|
||||
num_changed += 1
|
||||
|
||||
@@ -329,7 +333,7 @@ class Vault:
|
||||
|
||||
match export_format:
|
||||
case "csv":
|
||||
with open(export_file, "w", encoding="UTF8") as f:
|
||||
with export_file.open(mode="w", encoding="UTF8") as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(["Metadata Type", "Key", "Value"])
|
||||
|
||||
@@ -357,9 +361,47 @@ class Vault:
|
||||
"tags": self.metadata.tags,
|
||||
}
|
||||
|
||||
with open(export_file, "w", encoding="UTF8") as f:
|
||||
with export_file.open(mode="w", encoding="UTF8") as f:
|
||||
json.dump(dict_to_dump, f, indent=4, ensure_ascii=False, sort_keys=True)
|
||||
|
||||
def export_notes_to_csv(self, path: str) -> None:
|
||||
"""Export notes and their associated metadata to a csv file. This is useful as a template for importing metadata changes to a vault.
|
||||
|
||||
Args:
|
||||
path (str): Path to write csv file to.
|
||||
"""
|
||||
export_file = Path(path).expanduser().resolve()
|
||||
if not export_file.parent.exists():
|
||||
alerts.error(f"Path does not exist: {export_file.parent}")
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
with export_file.open(mode="w", encoding="UTF8") as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(["path", "type", "key", "value"])
|
||||
|
||||
for _note in self.all_notes:
|
||||
for key, value in _note.frontmatter.dict.items():
|
||||
for v in value:
|
||||
writer.writerow(
|
||||
[_note.note_path.relative_to(self.vault_path), "frontmatter", key, v]
|
||||
)
|
||||
|
||||
for key, value in _note.inline_metadata.dict.items():
|
||||
for v in value:
|
||||
writer.writerow(
|
||||
[
|
||||
_note.note_path.relative_to(self.vault_path),
|
||||
"inline_metadata",
|
||||
key,
|
||||
v,
|
||||
]
|
||||
)
|
||||
|
||||
for tag in _note.tags.list:
|
||||
writer.writerow(
|
||||
[_note.note_path.relative_to(self.vault_path), "tag", "", f"{tag}"]
|
||||
)
|
||||
|
||||
def get_changed_notes(self) -> list[Note]:
|
||||
"""Return a list of notes that have changes.
|
||||
|
||||
@@ -424,7 +466,7 @@ class Vault:
|
||||
"""Count number of excluded notes."""
|
||||
return len(self.all_notes) - len(self.notes_in_scope)
|
||||
|
||||
def rename_inline_tag(self, old_tag: str, new_tag: str) -> int:
|
||||
def rename_tag(self, old_tag: str, new_tag: str) -> int:
|
||||
"""Rename an inline tag in the vault.
|
||||
|
||||
Args:
|
||||
@@ -437,7 +479,7 @@ class Vault:
|
||||
num_changed = 0
|
||||
|
||||
for _note in self.notes_in_scope:
|
||||
if _note.rename_inline_tag(old_tag, new_tag):
|
||||
if _note.rename_tag(old_tag, new_tag):
|
||||
log.trace(f"Renamed inline tag in {_note.note_path}")
|
||||
num_changed += 1
|
||||
|
||||
@@ -471,7 +513,7 @@ class Vault:
|
||||
|
||||
return num_changed
|
||||
|
||||
def transpose_metadata( # noqa: PLR0913
|
||||
def transpose_metadata(
|
||||
self,
|
||||
begin: MetadataType,
|
||||
end: MetadataType,
|
||||
@@ -510,3 +552,54 @@ class Vault:
|
||||
self._rebuild_vault_metadata()
|
||||
|
||||
return num_changed
|
||||
|
||||
def update_from_dict(self, dictionary: dict[str, Any]) -> int:
|
||||
"""Update note metadata from a dictionary. This is a destructive operation. All metadata in the specified notes not in the dictionary will be removed.
|
||||
|
||||
Requires a dictionary with the note path as the key and a dictionary of metadata as the value. Each key must have a list of associated dictionaries in the following format:
|
||||
|
||||
{
|
||||
'type': 'frontmatter|inline_metadata|tag',
|
||||
'key': 'string',
|
||||
'value': 'string'
|
||||
}
|
||||
|
||||
Args:
|
||||
dictionary (dict[str, Any]): Dictionary to update metadata from.
|
||||
|
||||
Returns:
|
||||
int: Number of notes that had metadata updated.
|
||||
"""
|
||||
num_changed = 0
|
||||
|
||||
for _note in self.all_notes:
|
||||
path = _note.note_path.relative_to(self.vault_path)
|
||||
if str(path) in dictionary:
|
||||
log.debug(f"Bulk update metadata for '{path}'")
|
||||
num_changed += 1
|
||||
_note.delete_all_metadata()
|
||||
for row in dictionary[str(path)]:
|
||||
if row["type"].lower() == "frontmatter":
|
||||
_note.add_metadata(
|
||||
area=MetadataType.FRONTMATTER, key=row["key"], value=row["value"]
|
||||
)
|
||||
|
||||
if row["type"].lower() == "inline_metadata":
|
||||
_note.add_metadata(
|
||||
area=MetadataType.INLINE,
|
||||
key=row["key"],
|
||||
value=row["value"],
|
||||
location=self.insert_location,
|
||||
)
|
||||
|
||||
if row["type"].lower() == "tag":
|
||||
_note.add_metadata(
|
||||
area=MetadataType.TAGS,
|
||||
value=row["value"],
|
||||
location=self.insert_location,
|
||||
)
|
||||
|
||||
if num_changed > 0:
|
||||
self._rebuild_vault_metadata()
|
||||
|
||||
return num_changed
|
||||
|
||||
@@ -58,7 +58,8 @@ def test_usage(capsys):
|
||||
assert captured.out == "USAGE | This prints in usage\n"
|
||||
|
||||
alerts.usage(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
|
||||
width=80,
|
||||
)
|
||||
captured = capsys.readouterr()
|
||||
assert "USAGE | Lorem ipsum dolor sit amet" in captured.out
|
||||
@@ -106,7 +107,7 @@ def test_logging(capsys, tmp_path, verbosity, log_to_file) -> None:
|
||||
if verbosity >= 3:
|
||||
assert logging.is_trace() is True
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == ""
|
||||
assert not captured.out
|
||||
|
||||
assert logging.is_trace("trace text") is True
|
||||
captured = capsys.readouterr()
|
||||
@@ -127,7 +128,7 @@ def test_logging(capsys, tmp_path, verbosity, log_to_file) -> None:
|
||||
if verbosity >= 2:
|
||||
assert logging.is_debug() is True
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == ""
|
||||
assert not captured.out
|
||||
|
||||
assert logging.is_debug("debug text") is True
|
||||
captured = capsys.readouterr()
|
||||
@@ -148,7 +149,7 @@ def test_logging(capsys, tmp_path, verbosity, log_to_file) -> None:
|
||||
if verbosity >= 1:
|
||||
assert logging.is_info() is True
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == ""
|
||||
assert not captured.out
|
||||
|
||||
assert logging.is_info("info text") is True
|
||||
captured = capsys.readouterr()
|
||||
@@ -164,11 +165,11 @@ def test_logging(capsys, tmp_path, verbosity, log_to_file) -> None:
|
||||
|
||||
log.info("This is Info logging")
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == ""
|
||||
assert not captured.out
|
||||
|
||||
assert logging.is_default() is True
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == ""
|
||||
assert not captured.out
|
||||
|
||||
assert logging.is_default("default text") is True
|
||||
captured = capsys.readouterr()
|
||||
|
||||
@@ -144,7 +144,7 @@ def test_add_metadata_tag(test_application, mocker, capsys) -> None:
|
||||
assert captured == Regex(r"SUCCESS +\| Added metadata to \d+ notes", re.DOTALL)
|
||||
|
||||
|
||||
def test_delete_inline_tag_1(test_application, mocker, capsys) -> None:
|
||||
def test_delete_tag_1(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming an inline tag.
|
||||
|
||||
GIVEN an application
|
||||
@@ -159,10 +159,10 @@ def test_delete_inline_tag_1(test_application, mocker, capsys) -> None:
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["delete_inline_tag", "back"],
|
||||
side_effect=["delete_tag", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_inline_tag",
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_tag",
|
||||
return_value="breakfast",
|
||||
)
|
||||
|
||||
@@ -172,7 +172,7 @@ def test_delete_inline_tag_1(test_application, mocker, capsys) -> None:
|
||||
assert captured == Regex(r"SUCCESS +\| Deleted inline tag: breakfast in \d+ notes", re.DOTALL)
|
||||
|
||||
|
||||
def test_delete_inline_tag_2(test_application, mocker, capsys) -> None:
|
||||
def test_delete_tag_2(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming an inline tag.
|
||||
|
||||
GIVEN an application
|
||||
@@ -187,10 +187,10 @@ def test_delete_inline_tag_2(test_application, mocker, capsys) -> None:
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["delete_inline_tag", "back"],
|
||||
side_effect=["delete_tag", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_inline_tag",
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_tag",
|
||||
return_value="not_a_tag_in_vault",
|
||||
)
|
||||
|
||||
@@ -388,7 +388,7 @@ def test_inspect_metadata_all(test_application, mocker, capsys) -> None:
|
||||
assert captured == Regex(r"type +│ article", re.DOTALL)
|
||||
|
||||
|
||||
def test_rename_inline_tag(test_application, mocker, capsys) -> None:
|
||||
def test_rename_tag(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming an inline tag."""
|
||||
app = test_application
|
||||
app._load_vault()
|
||||
@@ -398,10 +398,10 @@ def test_rename_inline_tag(test_application, mocker, capsys) -> None:
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["rename_inline_tag", "back"],
|
||||
side_effect=["rename_tag", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_inline_tag",
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_tag",
|
||||
return_value="not_a_tag",
|
||||
)
|
||||
mocker.patch(
|
||||
@@ -420,10 +420,10 @@ def test_rename_inline_tag(test_application, mocker, capsys) -> None:
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["rename_inline_tag", "back"],
|
||||
side_effect=["rename_tag", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_inline_tag",
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_tag",
|
||||
return_value="breakfast",
|
||||
)
|
||||
mocker.patch(
|
||||
@@ -613,7 +613,7 @@ def test_transpose_metadata_1(test_application, mocker, capsys) -> None:
|
||||
assert "SUCCESS | Transposed Inline Metadata to Frontmatter in 5 notes" in captured
|
||||
|
||||
|
||||
def test_transpose_metadata_2(test_application, mocker, capsys) -> None:
|
||||
def test_transpose_metadata_2(test_application, mocker) -> None:
|
||||
"""Transpose metadata.
|
||||
|
||||
GIVEN a test application
|
||||
|
||||
@@ -17,7 +17,7 @@ def test_version() -> None:
|
||||
"""Test printing version and then exiting."""
|
||||
result = runner.invoke(app, ["--version"])
|
||||
assert result.exit_code == 0
|
||||
assert result.output == Regex(r"obsidian_metadata: v\d+\.\d+\.\d+$")
|
||||
assert "obsidian_metadata: v" in result.output
|
||||
|
||||
|
||||
def test_application(tmp_path) -> None:
|
||||
@@ -51,3 +51,25 @@ def test_application(tmp_path) -> None:
|
||||
|
||||
assert banner in result.output
|
||||
assert result.exit_code == 1
|
||||
|
||||
|
||||
def test_export_template(tmp_path) -> None:
|
||||
"""Test the export template command."""
|
||||
source_dir = Path(__file__).parent / "fixtures" / "test_vault"
|
||||
dest_dir = Path(tmp_path / "vault")
|
||||
|
||||
if not source_dir.exists():
|
||||
raise FileNotFoundError(f"Sample vault not found: {source_dir}")
|
||||
|
||||
shutil.copytree(source_dir, dest_dir)
|
||||
|
||||
config_path = tmp_path / "config.toml"
|
||||
export_path = tmp_path / "export_template.csv"
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["--vault-path", dest_dir, "--config-file", config_path, "--export-template", export_path],
|
||||
)
|
||||
|
||||
assert "SUCCESS | Exported metadata to" in result.output
|
||||
assert result.exit_code == 0
|
||||
assert export_path.exists()
|
||||
|
||||
530
tests/metadata_frontmatter_test.py
Normal file
530
tests/metadata_frontmatter_test.py
Normal file
@@ -0,0 +1,530 @@
|
||||
# type: ignore
|
||||
"""Test the Frontmatter object from metadata.py."""
|
||||
|
||||
import pytest
|
||||
|
||||
from obsidian_metadata.models.metadata import Frontmatter
|
||||
|
||||
FRONTMATTER_CONTENT: str = """
|
||||
---
|
||||
tags:
|
||||
- tag_1
|
||||
- tag_2
|
||||
-
|
||||
- 📅/tag_3
|
||||
frontmatter_Key1: "frontmatter_Key1_value"
|
||||
frontmatter_Key2: ["note", "article"]
|
||||
shared_key1: "shared_key1_value"
|
||||
---
|
||||
more content
|
||||
|
||||
---
|
||||
horizontal: rule
|
||||
---
|
||||
"""
|
||||
|
||||
INLINE_CONTENT = """\
|
||||
repeated_key:: repeated_key_value1
|
||||
#inline_tag_top1,#inline_tag_top2
|
||||
**bold_key1**:: bold_key1_value
|
||||
**bold_key2:: bold_key2_value**
|
||||
link_key:: [[link_key_value]]
|
||||
tag_key:: #tag_key_value
|
||||
emoji_📅_key:: emoji_📅_key_value
|
||||
**#bold_tag**
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. [in_text_key1:: in_text_key1_value] Ut enim ad minim veniam, quis nostrud exercitation [in_text_key2:: in_text_key2_value] ullamco laboris nisi ut aliquip ex ea commodo consequat. #in_text_tag Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
|
||||
```python
|
||||
#ffffff
|
||||
# This is sample text [no_key:: value]with tags and metadata
|
||||
#in_codeblock_tag1
|
||||
#ffffff;
|
||||
in_codeblock_key:: in_codeblock_value
|
||||
The quick brown fox jumped over the #in_codeblock_tag2
|
||||
```
|
||||
repeated_key:: repeated_key_value2
|
||||
"""
|
||||
|
||||
|
||||
def test_create_1() -> None:
|
||||
"""Test frontmatter creation.
|
||||
|
||||
GIVEN valid frontmatter content
|
||||
WHEN a Frontmatter object is created
|
||||
THEN parse the YAML frontmatter and add it to the object
|
||||
"""
|
||||
frontmatter = Frontmatter(INLINE_CONTENT)
|
||||
assert frontmatter.dict == {}
|
||||
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.dict == {
|
||||
"frontmatter_Key1": ["frontmatter_Key1_value"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"tags": ["tag_1", "tag_2", "📅/tag_3"],
|
||||
}
|
||||
assert frontmatter.dict_original == {
|
||||
"frontmatter_Key1": ["frontmatter_Key1_value"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"tags": ["tag_1", "tag_2", "📅/tag_3"],
|
||||
}
|
||||
|
||||
|
||||
def test_create_2() -> None:
|
||||
"""Test frontmatter creation error.
|
||||
|
||||
GIVEN invalid frontmatter content
|
||||
WHEN a Frontmatter object is created
|
||||
THEN raise ValueError
|
||||
"""
|
||||
fn = """---
|
||||
tags: tag
|
||||
invalid = = "content"
|
||||
---
|
||||
"""
|
||||
with pytest.raises(AttributeError):
|
||||
Frontmatter(fn)
|
||||
|
||||
|
||||
def test_create_3():
|
||||
"""Test frontmatter creation error.
|
||||
|
||||
GIVEN empty frontmatter content
|
||||
WHEN a Frontmatter object is created
|
||||
THEN set the dict to an empty dict
|
||||
"""
|
||||
content = "---\n\n---"
|
||||
frontmatter = Frontmatter(content)
|
||||
assert frontmatter.dict == {}
|
||||
|
||||
|
||||
def test_create_4():
|
||||
"""Test frontmatter creation error.
|
||||
|
||||
GIVEN empty frontmatter content with a yaml marker
|
||||
WHEN a Frontmatter object is created
|
||||
THEN set the dict to an empty dict
|
||||
"""
|
||||
content = "---\n-\n---"
|
||||
frontmatter = Frontmatter(content)
|
||||
assert frontmatter.dict == {}
|
||||
|
||||
|
||||
def test_add_1():
|
||||
"""Test frontmatter add() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the add() method is called with an existing key
|
||||
THEN return False
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
|
||||
assert frontmatter.add("frontmatter_Key1") is False
|
||||
|
||||
|
||||
def test_add_2():
|
||||
"""Test frontmatter add() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the add() method is called with an existing key and existing value
|
||||
THEN return False
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.add("frontmatter_Key1", "frontmatter_Key1_value") is False
|
||||
|
||||
|
||||
def test_add_3():
|
||||
"""Test frontmatter add() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the add() method is called with a new key
|
||||
THEN return True and add the key to the dict
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.add("added_key") is True
|
||||
assert "added_key" in frontmatter.dict
|
||||
|
||||
|
||||
def test_add_4():
|
||||
"""Test frontmatter add() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the add() method is called with a new key and a new value
|
||||
THEN return True and add the key and the value to the dict
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.add("added_key", "added_value") is True
|
||||
assert frontmatter.dict["added_key"] == ["added_value"]
|
||||
|
||||
|
||||
def test_add_5():
|
||||
"""Test frontmatter add() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the add() method is called with an existing key and a new value
|
||||
THEN return True and add the value to the dict
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.add("frontmatter_Key1", "new_value") is True
|
||||
assert frontmatter.dict["frontmatter_Key1"] == ["frontmatter_Key1_value", "new_value"]
|
||||
|
||||
|
||||
def test_add_6():
|
||||
"""Test frontmatter add() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the add() method is called with an existing key and a list of new values
|
||||
THEN return True and add the values to the dict
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.add("frontmatter_Key1", ["new_value", "new_value2"]) is True
|
||||
assert frontmatter.dict["frontmatter_Key1"] == [
|
||||
"frontmatter_Key1_value",
|
||||
"new_value",
|
||||
"new_value2",
|
||||
]
|
||||
|
||||
|
||||
def test_add_7():
|
||||
"""Test frontmatter add() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the add() method is called with an existing key and a list of values including an existing value
|
||||
THEN return True and add the new values to the dict
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert (
|
||||
frontmatter.add("frontmatter_Key1", ["frontmatter_Key1_value", "new_value", "new_value2"])
|
||||
is True
|
||||
)
|
||||
assert frontmatter.dict["frontmatter_Key1"] == [
|
||||
"frontmatter_Key1_value",
|
||||
"new_value",
|
||||
"new_value2",
|
||||
]
|
||||
|
||||
|
||||
def test_contains_1():
|
||||
"""Test frontmatter contains() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the contains() method is called with a key
|
||||
THEN return True if the key is found
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.contains("frontmatter_Key1") is True
|
||||
|
||||
|
||||
def test_contains_2():
|
||||
"""Test frontmatter contains() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the contains() method is called with a key
|
||||
THEN return False if the key is not found
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.contains("no_key") is False
|
||||
|
||||
|
||||
def test_contains_3():
|
||||
"""Test frontmatter contains() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the contains() method is called with a key and a value
|
||||
THEN return True if the key and value is found
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.contains("frontmatter_Key2", "article") is True
|
||||
|
||||
|
||||
def test_contains_4():
|
||||
"""Test frontmatter contains() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the contains() method is called with a key and a value
|
||||
THEN return False if the key and value is not found
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.contains("frontmatter_Key2", "no value") is False
|
||||
|
||||
|
||||
def test_contains_5():
|
||||
"""Test frontmatter contains() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the contains() method is called with a key regex
|
||||
THEN return True if a key matches the regex
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.contains(r"\d$", is_regex=True) is True
|
||||
|
||||
|
||||
def test_contains_6():
|
||||
"""Test frontmatter contains() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the contains() method is called with a key regex
|
||||
THEN return False if no key matches the regex
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.contains(r"^\d", is_regex=True) is False
|
||||
|
||||
|
||||
def test_contains_7():
|
||||
"""Test frontmatter contains() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the contains() method is called with a key and value regex
|
||||
THEN return True if a value matches the regex
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.contains("key", r"\w\d_", is_regex=True) is True
|
||||
|
||||
|
||||
def test_contains_8():
|
||||
"""Test frontmatter contains() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the contains() method is called with a key and value regex
|
||||
THEN return False if a value does not match the regex
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.contains("key", r"_\d", is_regex=True) is False
|
||||
|
||||
|
||||
def test_delete_1():
|
||||
"""Test frontmatter delete() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the delete() method is called with a key that does not exist
|
||||
THEN return False
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.delete("no key") is False
|
||||
|
||||
|
||||
def test_delete_2():
|
||||
"""Test frontmatter delete() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the delete() method is called with an existing key and a value that does not exist
|
||||
THEN return False
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.delete("tags", "no value") is False
|
||||
|
||||
|
||||
def test_delete_3():
|
||||
"""Test frontmatter delete() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the delete() method is called with a regex that does not match any keys
|
||||
THEN return False
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.delete(r"\d{3}", is_regex=True) is False
|
||||
|
||||
|
||||
def test_delete_4():
|
||||
"""Test frontmatter delete() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the delete() method is called with an existing key and a regex that does not match any values
|
||||
THEN return False
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.delete("tags", r"\d{5}", is_regex=True) is False
|
||||
|
||||
|
||||
def test_delete_5():
|
||||
"""Test frontmatter delete() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the delete() method is called with an existing key and an existing value
|
||||
THEN return True and delete the value from the dict
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.delete("tags", "tag_2") is True
|
||||
assert "tag_2" not in frontmatter.dict["tags"]
|
||||
assert "tags" in frontmatter.dict
|
||||
|
||||
|
||||
def test_delete_6():
|
||||
"""Test frontmatter delete() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the delete() method is called with an existing key
|
||||
THEN return True and delete the key from the dict
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.delete("tags") is True
|
||||
assert "tags" not in frontmatter.dict
|
||||
|
||||
|
||||
def test_delete_7():
|
||||
"""Test frontmatter delete() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the delete() method is called with a regex that matches a key
|
||||
THEN return True and delete the matching keys from the dict
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.delete(r"front\w+", is_regex=True) is True
|
||||
assert "frontmatter_Key1" not in frontmatter.dict
|
||||
assert "frontmatter_Key2" not in frontmatter.dict
|
||||
|
||||
|
||||
def test_delete_8():
|
||||
"""Test frontmatter delete() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the delete() method is called with an existing key and a regex that matches values
|
||||
THEN return True and delete the matching values
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.delete("tags", r"\w+_[23]", is_regex=True) is True
|
||||
assert "tag_2" not in frontmatter.dict["tags"]
|
||||
assert "📅/tag_3" not in frontmatter.dict["tags"]
|
||||
assert "tag_1" in frontmatter.dict["tags"]
|
||||
|
||||
|
||||
def test_delete_all():
|
||||
"""Test Frontmatter delete_all method.
|
||||
|
||||
GIVEN Frontmatter with multiple keys
|
||||
WHEN delete_all is called
|
||||
THEN all keys and values are deleted
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
frontmatter.delete_all()
|
||||
assert frontmatter.dict == {}
|
||||
|
||||
|
||||
def test_has_changes_1():
|
||||
"""Test frontmatter has_changes() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN no changes have been made to the object
|
||||
THEN return False
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.has_changes() is False
|
||||
|
||||
|
||||
def test_has_changes_2():
|
||||
"""Test frontmatter has_changes() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN changes have been made to the object
|
||||
THEN return True
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
frontmatter.dict["new key"] = ["new value"]
|
||||
assert frontmatter.has_changes() is True
|
||||
|
||||
|
||||
def test_rename_1():
|
||||
"""Test frontmatter rename() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the rename() method is called with a key
|
||||
THEN return False if the key is not found
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.rename("no key", "new key") is False
|
||||
|
||||
|
||||
def test_rename_2():
|
||||
"""Test frontmatter rename() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the rename() method is called with an existing key and non-existing value
|
||||
THEN return False
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.rename("tags", "no tag", "new key") is False
|
||||
|
||||
|
||||
def test_rename_3():
|
||||
"""Test frontmatter rename() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the rename() method is called with an existing key
|
||||
THEN return True and rename the key
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.rename("frontmatter_Key1", "new key") is True
|
||||
assert "frontmatter_Key1" not in frontmatter.dict
|
||||
assert frontmatter.dict["new key"] == ["frontmatter_Key1_value"]
|
||||
|
||||
|
||||
def test_rename_4():
|
||||
"""Test frontmatter rename() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the rename() method is called with an existing key and value
|
||||
THEN return True and rename the value
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.rename("tags", "tag_2", "new tag") is True
|
||||
assert "tag_2" not in frontmatter.dict["tags"]
|
||||
assert "new tag" in frontmatter.dict["tags"]
|
||||
|
||||
|
||||
def test_rename_5():
|
||||
"""Test frontmatter rename() method.
|
||||
|
||||
GIVEN a Frontmatter object
|
||||
WHEN the rename() method is called with an existing key and value and the new value already exists
|
||||
THEN return True and remove the old value leaving one instance of the new value
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.rename("tags", "tag_1", "tag_2") is True
|
||||
assert "tag_1" not in frontmatter.dict["tags"]
|
||||
assert frontmatter.dict["tags"] == ["tag_2", "📅/tag_3"]
|
||||
|
||||
|
||||
def test_to_yaml_1():
|
||||
"""Test Frontmatter to_yaml method.
|
||||
|
||||
GIVEN a dictionary
|
||||
WHEN the to_yaml method is called
|
||||
THEN return a string with the yaml representation of the dictionary
|
||||
"""
|
||||
new_frontmatter: str = """\
|
||||
tags:
|
||||
- tag_1
|
||||
- tag_2
|
||||
- 📅/tag_3
|
||||
frontmatter_Key1: frontmatter_Key1_value
|
||||
frontmatter_Key2:
|
||||
- article
|
||||
- note
|
||||
shared_key1: shared_key1_value
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.to_yaml() == new_frontmatter
|
||||
|
||||
|
||||
def test_to_yaml_2():
|
||||
"""Test Frontmatter to_yaml method.
|
||||
|
||||
GIVEN a dictionary
|
||||
WHEN the to_yaml method is called with sort_keys=True
|
||||
THEN return a string with the sorted yaml representation of the dictionary
|
||||
"""
|
||||
new_frontmatter_sorted: str = """\
|
||||
frontmatter_Key1: frontmatter_Key1_value
|
||||
frontmatter_Key2:
|
||||
- article
|
||||
- note
|
||||
shared_key1: shared_key1_value
|
||||
tags:
|
||||
- tag_1
|
||||
- tag_2
|
||||
- 📅/tag_3
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.to_yaml(sort_keys=True) == new_frontmatter_sorted
|
||||
438
tests/metadata_inline_test.py
Normal file
438
tests/metadata_inline_test.py
Normal file
@@ -0,0 +1,438 @@
|
||||
# type: ignore
|
||||
"""Test inline metadata from metadata.py."""
|
||||
|
||||
from obsidian_metadata.models.metadata import InlineMetadata
|
||||
|
||||
FRONTMATTER_CONTENT: str = """
|
||||
---
|
||||
tags:
|
||||
- tag_1
|
||||
- tag_2
|
||||
-
|
||||
- 📅/tag_3
|
||||
frontmatter_Key1: "frontmatter_Key1_value"
|
||||
frontmatter_Key2: ["note", "article"]
|
||||
shared_key1: "shared_key1_value"
|
||||
---
|
||||
more content
|
||||
|
||||
---
|
||||
horizontal: rule
|
||||
---
|
||||
"""
|
||||
|
||||
INLINE_CONTENT = """\
|
||||
key1:: value1
|
||||
key1:: value2
|
||||
key1:: value3
|
||||
key2:: value1
|
||||
Paragraph of text with an [inline_key:: value1] and [inline_key:: value2] and [inline_key:: value3] which should do it.
|
||||
> blockquote_key:: value1
|
||||
> blockquote_key:: value2
|
||||
|
||||
- list_key:: value1
|
||||
- list_key:: value2
|
||||
|
||||
1. list_key:: value1
|
||||
2. list_key:: value2
|
||||
"""
|
||||
|
||||
|
||||
def test__grab_inline_metadata_1():
|
||||
"""Test grab inline metadata.
|
||||
|
||||
GIVEN content that has no inline metadata
|
||||
WHEN grab_inline_metadata is called
|
||||
THEN an empty dict is returned
|
||||
|
||||
"""
|
||||
content = """
|
||||
---
|
||||
frontmatter_key1: frontmatter_key1_value
|
||||
---
|
||||
not_a_key: not_a_value
|
||||
```
|
||||
key:: in_codeblock
|
||||
```
|
||||
"""
|
||||
inline = InlineMetadata(content)
|
||||
assert inline.dict == {}
|
||||
|
||||
|
||||
def test__grab_inline_metadata_2():
|
||||
"""Test grab inline metadata.
|
||||
|
||||
GIVEN content that has inline metadata
|
||||
WHEN grab_inline_metadata is called
|
||||
THEN the inline metadata is parsed and returned as a dict
|
||||
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.dict == {
|
||||
"blockquote_key": ["value1", "value2"],
|
||||
"inline_key": ["value1", "value2", "value3"],
|
||||
"key1": ["value1", "value2", "value3"],
|
||||
"key2": ["value1"],
|
||||
"list_key": ["value1", "value2", "value1", "value2"],
|
||||
}
|
||||
|
||||
|
||||
def test_add_1():
|
||||
"""Test InlineMetadata add() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the add() method is called with an existing key
|
||||
THEN return False
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.add("key1") is False
|
||||
|
||||
|
||||
def test_add_2():
|
||||
"""Test InlineMetadata add() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the add() method is called with an existing key and existing value
|
||||
THEN return False
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.add("key1", "value1") is False
|
||||
|
||||
|
||||
def test_add_3():
|
||||
"""Test InlineMetadata add() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the add() method is called with a new key
|
||||
THEN return True and add the key to the dict
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.add("added_key") is True
|
||||
assert "added_key" in inline.dict
|
||||
|
||||
|
||||
def test_add_4():
|
||||
"""Test InlineMetadata add() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the add() method is called with a new key and a new value
|
||||
THEN return True and add the key and the value to the dict
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.add("added_key", "added_value") is True
|
||||
assert inline.dict["added_key"] == ["added_value"]
|
||||
|
||||
|
||||
def test_add_5():
|
||||
"""Test InlineMetadata add() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the add() method is called with an existing key and a new value
|
||||
THEN return True and add the value to the dict
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.add("key1", "new_value") is True
|
||||
assert inline.dict["key1"] == ["value1", "value2", "value3", "new_value"]
|
||||
|
||||
|
||||
def test_add_6():
|
||||
"""Test InlineMetadata add() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the add() method is called with an existing key and a list of new values
|
||||
THEN return True and add the values to the dict
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.add("key2", ["new_value", "new_value2"]) is True
|
||||
assert inline.dict["key2"] == ["new_value", "new_value2", "value1"]
|
||||
|
||||
|
||||
def test_add_7():
|
||||
"""Test InlineMetadata add() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the add() method is called with an existing key and a list of values including an existing value
|
||||
THEN return True and add the new values to the dict
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.add("key1", ["value1", "new_value", "new_value2"]) is True
|
||||
assert inline.dict["key1"] == ["new_value", "new_value2", "value1", "value2", "value3"]
|
||||
|
||||
|
||||
def test_add_8():
|
||||
"""Test InlineMetadata add() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the add() method is called with a new key and a list of values
|
||||
THEN return True and add the new values to the dict
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.add("new_key", ["value1", "new_value", "new_value2"]) is True
|
||||
assert inline.dict["new_key"] == ["value1", "new_value", "new_value2"]
|
||||
|
||||
|
||||
def test_contains_1():
|
||||
"""Test InlineMetadata contains() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the contains() method is called with a key
|
||||
THEN return True if the key is found
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.contains("key1") is True
|
||||
|
||||
|
||||
def test_contains_2():
|
||||
"""Test InlineMetadata contains() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the contains() method is called with a key
|
||||
THEN return False if the key is not found
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.contains("no_key") is False
|
||||
|
||||
|
||||
def test_contains_3():
|
||||
"""Test InlineMetadata contains() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the contains() method is called with a key and a value
|
||||
THEN return True if the key and value is found
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.contains("key1", "value1") is True
|
||||
|
||||
|
||||
def test_contains_4():
|
||||
"""Test InlineMetadata contains() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the contains() method is called with a key and a value
|
||||
THEN return False if the key and value is not found
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.contains("key1", "no value") is False
|
||||
|
||||
|
||||
def test_contains_5():
|
||||
"""Test InlineMetadata contains() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the contains() method is called with a key regex
|
||||
THEN return True if a key matches the regex
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.contains(r"\d$", is_regex=True) is True
|
||||
|
||||
|
||||
def test_contains_6():
|
||||
"""Test InlineMetadata contains() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the contains() method is called with a key regex
|
||||
THEN return False if no key matches the regex
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.contains(r"^\d", is_regex=True) is False
|
||||
|
||||
|
||||
def test_contains_7():
|
||||
"""Test InlineMetadata contains() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the contains() method is called with a key and value regex
|
||||
THEN return True if a value matches the regex
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.contains(r"key\d", r"\w\d", is_regex=True) is True
|
||||
|
||||
|
||||
def test_contains_8():
|
||||
"""Test InlineMetadata contains() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the contains() method is called with a key and value regex
|
||||
THEN return False if a value does not match the regex
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.contains("key1", r"_\d", is_regex=True) is False
|
||||
|
||||
|
||||
def test_delete_1():
|
||||
"""Test InlineMetadata delete() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the delete() method is called with a key that does not exist
|
||||
THEN return False
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.delete("no key") is False
|
||||
|
||||
|
||||
def test_delete_2():
|
||||
"""Test InlineMetadata delete() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the delete() method is called with an existing key and a value that does not exist
|
||||
THEN return False
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.delete("key1", "no value") is False
|
||||
|
||||
|
||||
def test_delete_3():
|
||||
"""Test InlineMetadata delete() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the delete() method is called with a regex that does not match any keys
|
||||
THEN return False
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.delete(r"\d{3}", is_regex=True) is False
|
||||
|
||||
|
||||
def test_delete_4():
|
||||
"""Test InlineMetadata delete() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the delete() method is called with an existing key and a regex that does not match any values
|
||||
THEN return False
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.delete("key1", r"\d{5}", is_regex=True) is False
|
||||
|
||||
|
||||
def test_delete_5():
|
||||
"""Test InlineMetadata delete() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the delete() method is called with an existing key and an existing value
|
||||
THEN return True and delete the value from the dict
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.delete("key1", "value1") is True
|
||||
assert "value1" not in inline.dict["key1"]
|
||||
assert "key1" in inline.dict
|
||||
|
||||
|
||||
def test_delete_6():
|
||||
"""Test InlineMetadata delete() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the delete() method is called with an existing key
|
||||
THEN return True and delete the key from the dict
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.delete("key1") is True
|
||||
assert "key1" not in inline.dict
|
||||
|
||||
|
||||
def test_delete_7():
|
||||
"""Test InlineMetadata delete() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the delete() method is called with a regex that matches a key
|
||||
THEN return True and delete the matching keys from the dict
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.delete(r"key\w+", is_regex=True) is True
|
||||
assert "key1" not in inline.dict
|
||||
assert "key2" not in inline.dict
|
||||
|
||||
|
||||
def test_delete_8():
|
||||
"""Test InlineMetadata delete() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the delete() method is called with an existing key and a regex that matches values
|
||||
THEN return True and delete the matching values
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.delete("key1", r"\w+\d", is_regex=True) is True
|
||||
assert "value1" not in inline.dict["key1"]
|
||||
assert "value2" not in inline.dict["key1"]
|
||||
assert "value3" not in inline.dict["key1"]
|
||||
|
||||
|
||||
def test_has_changes_1():
|
||||
"""Test InlineMetadata has_changes() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN no changes have been made to the object
|
||||
THEN return False
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.has_changes() is False
|
||||
|
||||
|
||||
def test_has_changes_2():
|
||||
"""Test InlineMetadata has_changes() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN changes have been made to the object
|
||||
THEN return True
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
inline.dict["new key"] = ["new value"]
|
||||
assert inline.has_changes() is True
|
||||
|
||||
|
||||
def test_rename_1():
|
||||
"""Test InlineMetadata rename() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the rename() method is called with a key
|
||||
THEN return False if the key is not found
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.rename("no key", "new key") is False
|
||||
|
||||
|
||||
def test_rename_2():
|
||||
"""Test InlineMetadata rename() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the rename() method is called with an existing key and non-existing value
|
||||
THEN return False
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.rename("key1", "no value", "new value") is False
|
||||
|
||||
|
||||
def test_rename_3():
|
||||
"""Test InlineMetadata rename() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the rename() method is called with an existing key
|
||||
THEN return True and rename the key
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.rename("key1", "new key") is True
|
||||
assert "key1" not in inline.dict
|
||||
assert inline.dict["new key"] == ["value1", "value2", "value3"]
|
||||
|
||||
|
||||
def test_rename_4():
|
||||
"""Test InlineMetadata rename() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the rename() method is called with an existing key and value
|
||||
THEN return True and rename the value
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.rename("key1", "value1", "new value") is True
|
||||
assert "value1" not in inline.dict["key1"]
|
||||
assert "new value" in inline.dict["key1"]
|
||||
|
||||
|
||||
def test_rename_5():
|
||||
"""Test InlineMetadata rename() method.
|
||||
|
||||
GIVEN a InlineMetadata object
|
||||
WHEN the rename() method is called with an existing key and value and the new value already exists
|
||||
THEN return True and remove the old value leaving one instance of the new value
|
||||
"""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.rename("key1", "value1", "value2") is True
|
||||
assert inline.dict["key1"] == ["value2", "value3"]
|
||||
367
tests/metadata_tags_test.py
Normal file
367
tests/metadata_tags_test.py
Normal file
@@ -0,0 +1,367 @@
|
||||
# type: ignore
|
||||
"""Test inline tags from metadata.py."""
|
||||
|
||||
from obsidian_metadata.models.metadata import InlineTags
|
||||
|
||||
CONTENT = """\
|
||||
#tag1 #tag2
|
||||
> #tag3
|
||||
**#tag4**
|
||||
I am a sentence with #tag5 and #tag6 in the middle
|
||||
#tag🙈7
|
||||
#tag/8
|
||||
#tag/👋/9
|
||||
"""
|
||||
|
||||
|
||||
def test__grab_inline_tags_1() -> None:
|
||||
"""Test _grab_inline_tags() method.
|
||||
|
||||
GIVEN a string with a codeblock
|
||||
WHEN the method is called
|
||||
THEN the codeblock is ignored
|
||||
"""
|
||||
content = """
|
||||
some text
|
||||
|
||||
```python
|
||||
#tag1
|
||||
#tag2
|
||||
```
|
||||
|
||||
```
|
||||
#tag3
|
||||
#tag4
|
||||
```
|
||||
"""
|
||||
tags = InlineTags(content)
|
||||
assert tags.list == []
|
||||
assert tags.list_original == []
|
||||
|
||||
|
||||
def test__grab_inline_tags_2() -> None:
|
||||
"""Test _grab_inline_tags() method.
|
||||
|
||||
GIVEN a string with tags
|
||||
WHEN the method is called
|
||||
THEN the tags are extracted
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
assert tags.list == [
|
||||
"tag/8",
|
||||
"tag/👋/9",
|
||||
"tag1",
|
||||
"tag2",
|
||||
"tag3",
|
||||
"tag4",
|
||||
"tag5",
|
||||
"tag6",
|
||||
"tag🙈7",
|
||||
]
|
||||
assert tags.list_original == [
|
||||
"tag/8",
|
||||
"tag/👋/9",
|
||||
"tag1",
|
||||
"tag2",
|
||||
"tag3",
|
||||
"tag4",
|
||||
"tag5",
|
||||
"tag6",
|
||||
"tag🙈7",
|
||||
]
|
||||
|
||||
|
||||
def test_add_1():
|
||||
"""Test add() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the add() method is called with a tag that exists in the list
|
||||
THEN return False
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
assert tags.add("tag1") is False
|
||||
|
||||
|
||||
def test_add_2():
|
||||
"""Test add() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the add() method is called with a new tag
|
||||
THEN return True and add the tag to the list
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
assert tags.add("new_tag") is True
|
||||
assert "new_tag" in tags.list
|
||||
|
||||
|
||||
def test_add_3():
|
||||
"""Test add() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the add() method is called with a list of new tags
|
||||
THEN return True and add the tags to the list
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
new_tags = ["new_tag1", "new_tag2"]
|
||||
assert tags.add(new_tags) is True
|
||||
assert "new_tag1" in tags.list
|
||||
assert "new_tag2" in tags.list
|
||||
|
||||
|
||||
def test_add_4():
|
||||
"""Test add() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the add() method is called with a list of tags, some of which already exist
|
||||
THEN return True and add only the new tags to the list
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
new_tags = ["new_tag1", "new_tag2", "tag1", "tag2"]
|
||||
assert tags.add(new_tags) is True
|
||||
assert tags.list == [
|
||||
"new_tag1",
|
||||
"new_tag2",
|
||||
"tag/8",
|
||||
"tag/👋/9",
|
||||
"tag1",
|
||||
"tag2",
|
||||
"tag3",
|
||||
"tag4",
|
||||
"tag5",
|
||||
"tag6",
|
||||
"tag🙈7",
|
||||
]
|
||||
|
||||
|
||||
def test_add_5():
|
||||
"""Test add() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the add() method is called with a list of tags which are already in the list
|
||||
THEN return False
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
new_tags = ["tag1", "tag2"]
|
||||
assert tags.add(new_tags) is False
|
||||
assert "tag1" in tags.list
|
||||
assert "tag2" in tags.list
|
||||
|
||||
|
||||
def test_add_6():
|
||||
"""Test add() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the add() method is called with a list of tags which have a # in the name
|
||||
THEN strip the # from the tag name
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
new_tags = ["#tag1", "#tag2", "#new_tag"]
|
||||
assert tags.add(new_tags) is True
|
||||
assert tags.list == [
|
||||
"new_tag",
|
||||
"tag/8",
|
||||
"tag/👋/9",
|
||||
"tag1",
|
||||
"tag2",
|
||||
"tag3",
|
||||
"tag4",
|
||||
"tag5",
|
||||
"tag6",
|
||||
"tag🙈7",
|
||||
]
|
||||
|
||||
|
||||
def test_add_7():
|
||||
"""Test add() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the add() method is called with a tag which has a # in the name
|
||||
THEN strip the # from the tag name
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
assert tags.add("#tag1") is False
|
||||
assert tags.add("#new_tag") is True
|
||||
assert "new_tag" in tags.list
|
||||
|
||||
|
||||
def test_contains_1():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the contains() method is called with a tag that exists in the list
|
||||
THEN return True
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
assert tags.contains("tag1") is True
|
||||
|
||||
|
||||
def test_contains_2():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the contains() method is called with a tag that does not exist in the list
|
||||
THEN return False
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
assert tags.contains("no_tag") is False
|
||||
|
||||
|
||||
def test_contains_3():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the contains() method is called with a regex that matches a tag in the list
|
||||
THEN return True
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
assert tags.contains(r"tag\d", is_regex=True) is True
|
||||
|
||||
|
||||
def test_contains_4():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the contains() method is called with a regex that does not match any tags in the list
|
||||
THEN return False
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
assert tags.contains(r"tag\d\d", is_regex=True) is False
|
||||
|
||||
|
||||
def test_delete_1():
|
||||
"""Test delete() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the delete() method is called with a tag that exists in the list
|
||||
THEN return True and remove the tag from the list
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
assert tags.delete("tag1") is True
|
||||
assert "tag1" not in tags.list
|
||||
|
||||
|
||||
def test_delete_2():
|
||||
"""Test delete() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the delete() method is called with a tag that does not exist in the list
|
||||
THEN return False
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
assert tags.delete("no_tag") is False
|
||||
|
||||
|
||||
def test_delete_3():
|
||||
"""Test delete() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the delete() method is called with a regex that matches a tag in the list
|
||||
THEN return True and remove the tag from the list
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
assert tags.delete(r"tag\d") is True
|
||||
assert tags.list == ["tag/8", "tag/👋/9", "tag🙈7"]
|
||||
|
||||
|
||||
def test_delete_4():
|
||||
"""Test delete() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the delete() method is called with a regex that does not match any tags in the list
|
||||
THEN return False
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
assert tags.delete(r"tag\d\d") is False
|
||||
|
||||
|
||||
def test_has_changes_1():
|
||||
"""Test has_changes() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the has_changes() method is called
|
||||
THEN return False
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
assert tags.has_changes() is False
|
||||
|
||||
|
||||
def test_has_changes_2():
|
||||
"""Test has_changes() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the has_changes() method after the list has been updated
|
||||
THEN return True
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
tags.list = ["new_tag"]
|
||||
assert tags.has_changes() is True
|
||||
|
||||
|
||||
def test_rename_1():
|
||||
"""Test rename() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the rename() method is called with a tag that exists in the list
|
||||
THEN return True and rename the tag in the list
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
assert tags.rename("tag1", "new_tag") is True
|
||||
assert "tag1" not in tags.list
|
||||
assert "new_tag" in tags.list
|
||||
|
||||
|
||||
def test_rename_2():
|
||||
"""Test rename() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the rename() method is called with a tag that does not exist in the list
|
||||
THEN return False
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
assert tags.rename("no_tag", "new_tag") is False
|
||||
assert "new_tag" not in tags.list
|
||||
|
||||
|
||||
def test_rename_3():
|
||||
"""Test rename() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the rename() method is called with a tag that exists and the new tag name already exists in the list
|
||||
THEN return True and ensure the new tag name is only in the list once
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
assert tags.rename(r"tag1", "tag2") is True
|
||||
assert tags.list == [
|
||||
"tag/8",
|
||||
"tag/👋/9",
|
||||
"tag2",
|
||||
"tag3",
|
||||
"tag4",
|
||||
"tag5",
|
||||
"tag6",
|
||||
"tag🙈7",
|
||||
]
|
||||
|
||||
|
||||
def test_rename_4():
|
||||
"""Test rename() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the rename() method is called with a new tag value that is None
|
||||
THEN return False
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
assert tags.rename("tag1", None) is False
|
||||
assert "tag1" in tags.list
|
||||
|
||||
|
||||
def test_rename_5():
|
||||
"""Test rename() method.
|
||||
|
||||
GIVEN a InlineTag object
|
||||
WHEN the rename() method is called with a new tag value that is empty
|
||||
THEN return False
|
||||
"""
|
||||
tags = InlineTags(CONTENT)
|
||||
assert tags.rename("tag1", "") is False
|
||||
assert "tag1" in tags.list
|
||||
@@ -1,760 +0,0 @@
|
||||
# type: ignore
|
||||
"""Test metadata.py."""
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from obsidian_metadata.models.enums import MetadataType
|
||||
from obsidian_metadata.models.metadata import (
|
||||
Frontmatter,
|
||||
InlineMetadata,
|
||||
InlineTags,
|
||||
VaultMetadata,
|
||||
)
|
||||
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"]
|
||||
METADATA: dict[str, list[str]] = {
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["note", "article"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"shared_key2": ["shared_key2_value"],
|
||||
"tags": ["tag 2", "tag 1", "tag 3"],
|
||||
"top_key1": ["top_key1_value"],
|
||||
"top_key2": ["top_key2_value"],
|
||||
"top_key3": ["top_key3_value"],
|
||||
"intext_key": ["intext_key_value"],
|
||||
}
|
||||
METADATA_2: dict[str, list[str]] = {"key1": ["value1"], "key2": ["value2", "value3"]}
|
||||
FRONTMATTER_CONTENT: str = """
|
||||
---
|
||||
tags:
|
||||
- tag_1
|
||||
- tag_2
|
||||
-
|
||||
- 📅/tag_3
|
||||
frontmatter_Key1: "frontmatter_Key1_value"
|
||||
frontmatter_Key2: ["note", "article"]
|
||||
shared_key1: "shared_key1_value"
|
||||
---
|
||||
more content
|
||||
|
||||
---
|
||||
horizontal: rule
|
||||
---
|
||||
"""
|
||||
INLINE_CONTENT = """\
|
||||
repeated_key:: repeated_key_value1
|
||||
#inline_tag_top1,#inline_tag_top2
|
||||
**bold_key1**:: bold_key1_value
|
||||
**bold_key2:: bold_key2_value**
|
||||
link_key:: [[link_key_value]]
|
||||
tag_key:: #tag_key_value
|
||||
emoji_📅_key:: emoji_📅_key_value
|
||||
**#bold_tag**
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. [in_text_key1:: in_text_key1_value] Ut enim ad minim veniam, quis nostrud exercitation [in_text_key2:: in_text_key2_value] ullamco laboris nisi ut aliquip ex ea commodo consequat. #in_text_tag Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
|
||||
```python
|
||||
#ffffff
|
||||
# This is sample text [no_key:: value]with tags and metadata
|
||||
#in_codeblock_tag1
|
||||
#ffffff;
|
||||
in_codeblock_key:: in_codeblock_value
|
||||
The quick brown fox jumped over the #in_codeblock_tag2
|
||||
```
|
||||
repeated_key:: repeated_key_value2
|
||||
"""
|
||||
|
||||
|
||||
def test_frontmatter_create() -> None:
|
||||
"""Test frontmatter creation."""
|
||||
frontmatter = Frontmatter(INLINE_CONTENT)
|
||||
assert frontmatter.dict == {}
|
||||
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.dict == {
|
||||
"frontmatter_Key1": ["frontmatter_Key1_value"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"tags": ["tag_1", "tag_2", "📅/tag_3"],
|
||||
}
|
||||
assert frontmatter.dict_original == {
|
||||
"frontmatter_Key1": ["frontmatter_Key1_value"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"tags": ["tag_1", "tag_2", "📅/tag_3"],
|
||||
}
|
||||
|
||||
|
||||
def test_frontmatter_create_error() -> None:
|
||||
"""Test frontmatter creation error.
|
||||
|
||||
GIVEN frontmatter content
|
||||
WHEN frontmatter is invalid
|
||||
THEN raise ValueError
|
||||
"""
|
||||
fn = """---
|
||||
tags: tag
|
||||
invalid = = "content"
|
||||
---
|
||||
"""
|
||||
with pytest.raises(AttributeError):
|
||||
Frontmatter(fn)
|
||||
|
||||
|
||||
def test_frontmatter_contains() -> None:
|
||||
"""Test frontmatter contains."""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
|
||||
assert frontmatter.contains("frontmatter_Key1") is True
|
||||
assert frontmatter.contains("frontmatter_Key2", "article") is True
|
||||
assert frontmatter.contains("frontmatter_Key3") is False
|
||||
assert frontmatter.contains("frontmatter_Key2", "no value") is False
|
||||
|
||||
assert frontmatter.contains(r"\d$", is_regex=True) is True
|
||||
assert frontmatter.contains(r"^\d", is_regex=True) is False
|
||||
assert frontmatter.contains("key", r"_\d", is_regex=True) is False
|
||||
assert frontmatter.contains("key", r"\w\d_", is_regex=True) is True
|
||||
|
||||
|
||||
def test_frontmatter_add() -> None:
|
||||
"""Test frontmatter add."""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
|
||||
assert frontmatter.add("frontmatter_Key1") is False
|
||||
assert frontmatter.add("added_key") is True
|
||||
assert frontmatter.dict == {
|
||||
"added_key": [],
|
||||
"frontmatter_Key1": ["frontmatter_Key1_value"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"tags": ["tag_1", "tag_2", "📅/tag_3"],
|
||||
}
|
||||
|
||||
assert frontmatter.add("added_key", "added_value") is True
|
||||
assert frontmatter.dict == {
|
||||
"added_key": ["added_value"],
|
||||
"frontmatter_Key1": ["frontmatter_Key1_value"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"tags": ["tag_1", "tag_2", "📅/tag_3"],
|
||||
}
|
||||
|
||||
assert frontmatter.add("added_key", "added_value_2") is True
|
||||
assert frontmatter.dict == {
|
||||
"added_key": ["added_value", "added_value_2"],
|
||||
"frontmatter_Key1": ["frontmatter_Key1_value"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"tags": ["tag_1", "tag_2", "📅/tag_3"],
|
||||
}
|
||||
|
||||
assert frontmatter.add("added_key", ["added_value_3", "added_value_4"]) is True
|
||||
assert frontmatter.dict == {
|
||||
"added_key": ["added_value", "added_value_2", "added_value_3", "added_value_4"],
|
||||
"frontmatter_Key1": ["frontmatter_Key1_value"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"tags": ["tag_1", "tag_2", "📅/tag_3"],
|
||||
}
|
||||
|
||||
assert frontmatter.add("added_key2", ["added_value_1", "added_value_2"]) is True
|
||||
assert frontmatter.dict == {
|
||||
"added_key": ["added_value", "added_value_2", "added_value_3", "added_value_4"],
|
||||
"added_key2": ["added_value_1", "added_value_2"],
|
||||
"frontmatter_Key1": ["frontmatter_Key1_value"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"tags": ["tag_1", "tag_2", "📅/tag_3"],
|
||||
}
|
||||
|
||||
assert frontmatter.add("added_key3", "added_value_1") is True
|
||||
assert frontmatter.dict == {
|
||||
"added_key": ["added_value", "added_value_2", "added_value_3", "added_value_4"],
|
||||
"added_key2": ["added_value_1", "added_value_2"],
|
||||
"added_key3": ["added_value_1"],
|
||||
"frontmatter_Key1": ["frontmatter_Key1_value"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"tags": ["tag_1", "tag_2", "📅/tag_3"],
|
||||
}
|
||||
|
||||
assert frontmatter.add("added_key3", "added_value_1") is False
|
||||
|
||||
|
||||
def test_frontmatter_rename() -> None:
|
||||
"""Test frontmatter rename."""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.dict == {
|
||||
"frontmatter_Key1": ["frontmatter_Key1_value"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"tags": ["tag_1", "tag_2", "📅/tag_3"],
|
||||
}
|
||||
|
||||
assert frontmatter.rename("no key", "new key") is False
|
||||
assert frontmatter.rename("tags", "no tag", "new key") is False
|
||||
|
||||
assert frontmatter.has_changes() is False
|
||||
assert frontmatter.rename("tags", "tag_2", "new tag") is True
|
||||
|
||||
assert frontmatter.dict["tags"] == ["new tag", "tag_1", "📅/tag_3"]
|
||||
assert frontmatter.rename("tags", "old_tags") is True
|
||||
assert frontmatter.dict["old_tags"] == ["new tag", "tag_1", "📅/tag_3"]
|
||||
assert "tags" not in frontmatter.dict
|
||||
|
||||
assert frontmatter.has_changes() is True
|
||||
|
||||
|
||||
def test_frontmatter_delete() -> None:
|
||||
"""Test Frontmatter delete method."""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.dict == {
|
||||
"frontmatter_Key1": ["frontmatter_Key1_value"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"tags": ["tag_1", "tag_2", "📅/tag_3"],
|
||||
}
|
||||
|
||||
assert frontmatter.delete("no key") is False
|
||||
assert frontmatter.delete("tags", "no value") is False
|
||||
assert frontmatter.delete(r"\d{3}") is False
|
||||
assert frontmatter.has_changes() is False
|
||||
assert frontmatter.delete("tags", "tag_2") is True
|
||||
assert frontmatter.dict["tags"] == ["tag_1", "📅/tag_3"]
|
||||
assert frontmatter.delete("tags") is True
|
||||
assert "tags" not in frontmatter.dict
|
||||
assert frontmatter.has_changes() is True
|
||||
assert frontmatter.delete("shared_key1", r"\w+") is True
|
||||
assert frontmatter.dict["shared_key1"] == []
|
||||
assert frontmatter.delete(r"\w.tter") is True
|
||||
assert frontmatter.dict == {"shared_key1": []}
|
||||
|
||||
|
||||
def test_frontmatter_yaml_conversion():
|
||||
"""Test Frontmatter to_yaml method."""
|
||||
new_frontmatter: str = """\
|
||||
tags:
|
||||
- tag_1
|
||||
- tag_2
|
||||
- 📅/tag_3
|
||||
frontmatter_Key1: frontmatter_Key1_value
|
||||
frontmatter_Key2:
|
||||
- article
|
||||
- note
|
||||
shared_key1: shared_key1_value
|
||||
"""
|
||||
new_frontmatter_sorted: str = """\
|
||||
frontmatter_Key1: frontmatter_Key1_value
|
||||
frontmatter_Key2:
|
||||
- article
|
||||
- note
|
||||
shared_key1: shared_key1_value
|
||||
tags:
|
||||
- tag_1
|
||||
- tag_2
|
||||
- 📅/tag_3
|
||||
"""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
assert frontmatter.to_yaml() == new_frontmatter
|
||||
assert frontmatter.to_yaml(sort_keys=True) == new_frontmatter_sorted
|
||||
|
||||
|
||||
def test_inline_metadata_add() -> None:
|
||||
"""Test inline add."""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
|
||||
assert inline.add("bold_key1") is False
|
||||
assert inline.add("bold_key1", "bold_key1_value") is False
|
||||
assert inline.add("added_key") is True
|
||||
assert inline.dict == {
|
||||
"added_key": [],
|
||||
"bold_key1": ["bold_key1_value"],
|
||||
"bold_key2": ["bold_key2_value"],
|
||||
"emoji_📅_key": ["emoji_📅_key_value"],
|
||||
"in_text_key1": ["in_text_key1_value"],
|
||||
"in_text_key2": ["in_text_key2_value"],
|
||||
"link_key": ["link_key_value"],
|
||||
"repeated_key": ["repeated_key_value1", "repeated_key_value2"],
|
||||
"tag_key": ["tag_key_value"],
|
||||
}
|
||||
|
||||
assert inline.add("added_key1", "added_value") is True
|
||||
assert inline.dict == {
|
||||
"added_key": [],
|
||||
"added_key1": ["added_value"],
|
||||
"bold_key1": ["bold_key1_value"],
|
||||
"bold_key2": ["bold_key2_value"],
|
||||
"emoji_📅_key": ["emoji_📅_key_value"],
|
||||
"in_text_key1": ["in_text_key1_value"],
|
||||
"in_text_key2": ["in_text_key2_value"],
|
||||
"link_key": ["link_key_value"],
|
||||
"repeated_key": ["repeated_key_value1", "repeated_key_value2"],
|
||||
"tag_key": ["tag_key_value"],
|
||||
}
|
||||
|
||||
assert inline.dict == {
|
||||
"added_key": [],
|
||||
"added_key1": ["added_value"],
|
||||
"bold_key1": ["bold_key1_value"],
|
||||
"bold_key2": ["bold_key2_value"],
|
||||
"emoji_📅_key": ["emoji_📅_key_value"],
|
||||
"in_text_key1": ["in_text_key1_value"],
|
||||
"in_text_key2": ["in_text_key2_value"],
|
||||
"link_key": ["link_key_value"],
|
||||
"repeated_key": ["repeated_key_value1", "repeated_key_value2"],
|
||||
"tag_key": ["tag_key_value"],
|
||||
}
|
||||
|
||||
assert inline.add("added_key", "added_value")
|
||||
assert inline.dict == {
|
||||
"added_key": ["added_value"],
|
||||
"added_key1": ["added_value"],
|
||||
"bold_key1": ["bold_key1_value"],
|
||||
"bold_key2": ["bold_key2_value"],
|
||||
"emoji_📅_key": ["emoji_📅_key_value"],
|
||||
"in_text_key1": ["in_text_key1_value"],
|
||||
"in_text_key2": ["in_text_key2_value"],
|
||||
"link_key": ["link_key_value"],
|
||||
"repeated_key": ["repeated_key_value1", "repeated_key_value2"],
|
||||
"tag_key": ["tag_key_value"],
|
||||
}
|
||||
assert inline.add("repeated_key", "repeated_key_value1") is False
|
||||
assert inline.add("repeated_key", "new_value") is True
|
||||
|
||||
|
||||
def test_inline_metadata_contains() -> None:
|
||||
"""Test inline metadata contains method."""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
|
||||
assert inline.contains("bold_key1") is True
|
||||
assert inline.contains("bold_key2", "bold_key2_value") is True
|
||||
assert inline.contains("bold_key3") is False
|
||||
assert inline.contains("bold_key2", "no value") is False
|
||||
|
||||
assert inline.contains(r"\w{4}_key", is_regex=True) is True
|
||||
assert inline.contains(r"^\d", is_regex=True) is False
|
||||
assert inline.contains("1$", r"\d_value", is_regex=True) is True
|
||||
assert inline.contains("key", r"^\d_value", is_regex=True) is False
|
||||
|
||||
|
||||
def test_inline_metadata_create() -> None:
|
||||
"""Test inline metadata creation."""
|
||||
inline = InlineMetadata(FRONTMATTER_CONTENT)
|
||||
assert inline.dict == {}
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.dict == {
|
||||
"bold_key1": ["bold_key1_value"],
|
||||
"bold_key2": ["bold_key2_value"],
|
||||
"emoji_📅_key": ["emoji_📅_key_value"],
|
||||
"in_text_key1": ["in_text_key1_value"],
|
||||
"in_text_key2": ["in_text_key2_value"],
|
||||
"link_key": ["link_key_value"],
|
||||
"repeated_key": ["repeated_key_value1", "repeated_key_value2"],
|
||||
"tag_key": ["tag_key_value"],
|
||||
}
|
||||
assert inline.dict_original == {
|
||||
"bold_key1": ["bold_key1_value"],
|
||||
"bold_key2": ["bold_key2_value"],
|
||||
"emoji_📅_key": ["emoji_📅_key_value"],
|
||||
"in_text_key1": ["in_text_key1_value"],
|
||||
"in_text_key2": ["in_text_key2_value"],
|
||||
"link_key": ["link_key_value"],
|
||||
"repeated_key": ["repeated_key_value1", "repeated_key_value2"],
|
||||
"tag_key": ["tag_key_value"],
|
||||
}
|
||||
|
||||
|
||||
def test_inline_metadata_delete() -> None:
|
||||
"""Test inline metadata delete."""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.dict == {
|
||||
"bold_key1": ["bold_key1_value"],
|
||||
"bold_key2": ["bold_key2_value"],
|
||||
"emoji_📅_key": ["emoji_📅_key_value"],
|
||||
"in_text_key1": ["in_text_key1_value"],
|
||||
"in_text_key2": ["in_text_key2_value"],
|
||||
"link_key": ["link_key_value"],
|
||||
"repeated_key": ["repeated_key_value1", "repeated_key_value2"],
|
||||
"tag_key": ["tag_key_value"],
|
||||
}
|
||||
|
||||
assert inline.delete("no key") is False
|
||||
assert inline.delete("repeated_key", "no value") is False
|
||||
assert inline.has_changes() is False
|
||||
assert inline.delete("repeated_key", "repeated_key_value1") is True
|
||||
assert inline.dict["repeated_key"] == ["repeated_key_value2"]
|
||||
assert inline.delete("repeated_key") is True
|
||||
assert "repeated_key" not in inline.dict
|
||||
assert inline.has_changes() is True
|
||||
assert inline.delete(r"\d{3}") is False
|
||||
assert inline.delete(r"bold_key\d") is True
|
||||
assert inline.dict == {
|
||||
"emoji_📅_key": ["emoji_📅_key_value"],
|
||||
"in_text_key1": ["in_text_key1_value"],
|
||||
"in_text_key2": ["in_text_key2_value"],
|
||||
"link_key": ["link_key_value"],
|
||||
"tag_key": ["tag_key_value"],
|
||||
}
|
||||
assert inline.delete("emoji_📅_key", ".*📅.*") is True
|
||||
assert inline.dict == {
|
||||
"emoji_📅_key": [],
|
||||
"in_text_key1": ["in_text_key1_value"],
|
||||
"in_text_key2": ["in_text_key2_value"],
|
||||
"link_key": ["link_key_value"],
|
||||
"tag_key": ["tag_key_value"],
|
||||
}
|
||||
|
||||
|
||||
def test_inline_metadata_rename() -> None:
|
||||
"""Test inline metadata rename."""
|
||||
inline = InlineMetadata(INLINE_CONTENT)
|
||||
assert inline.dict == {
|
||||
"bold_key1": ["bold_key1_value"],
|
||||
"bold_key2": ["bold_key2_value"],
|
||||
"emoji_📅_key": ["emoji_📅_key_value"],
|
||||
"in_text_key1": ["in_text_key1_value"],
|
||||
"in_text_key2": ["in_text_key2_value"],
|
||||
"link_key": ["link_key_value"],
|
||||
"repeated_key": ["repeated_key_value1", "repeated_key_value2"],
|
||||
"tag_key": ["tag_key_value"],
|
||||
}
|
||||
|
||||
assert inline.rename("no key", "new key") is False
|
||||
assert inline.rename("repeated_key", "no value", "new key") is False
|
||||
assert inline.has_changes() is False
|
||||
assert inline.rename("repeated_key", "repeated_key_value1", "new value") is True
|
||||
assert inline.dict["repeated_key"] == ["new value", "repeated_key_value2"]
|
||||
assert inline.rename("repeated_key", "old_key") is True
|
||||
assert inline.dict["old_key"] == ["new value", "repeated_key_value2"]
|
||||
assert "repeated_key" not in inline.dict
|
||||
assert inline.has_changes() is True
|
||||
|
||||
|
||||
def test_inline_tags_add() -> None:
|
||||
"""Test inline tags add."""
|
||||
tags = InlineTags(INLINE_CONTENT)
|
||||
|
||||
assert tags.add("bold_tag") is False
|
||||
assert tags.add("new_tag") is True
|
||||
assert tags.list == [
|
||||
"bold_tag",
|
||||
"in_text_tag",
|
||||
"inline_tag_top1",
|
||||
"inline_tag_top2",
|
||||
"new_tag",
|
||||
"tag_key_value",
|
||||
]
|
||||
|
||||
|
||||
def test_inline_tags_contains() -> None:
|
||||
"""Test inline tags contains."""
|
||||
tags = InlineTags(INLINE_CONTENT)
|
||||
assert tags.contains("bold_tag") is True
|
||||
assert tags.contains("no tag") is False
|
||||
|
||||
assert tags.contains(r"\w_\w", is_regex=True) is True
|
||||
assert tags.contains(r"\d_\d", is_regex=True) is False
|
||||
|
||||
|
||||
def test_inline_tags_create() -> None:
|
||||
"""Test inline tags creation."""
|
||||
tags = InlineTags(FRONTMATTER_CONTENT)
|
||||
tags.metadata_key
|
||||
assert tags.list == []
|
||||
|
||||
tags = InlineTags(INLINE_CONTENT)
|
||||
assert tags.list == [
|
||||
"bold_tag",
|
||||
"in_text_tag",
|
||||
"inline_tag_top1",
|
||||
"inline_tag_top2",
|
||||
"tag_key_value",
|
||||
]
|
||||
assert tags.list_original == [
|
||||
"bold_tag",
|
||||
"in_text_tag",
|
||||
"inline_tag_top1",
|
||||
"inline_tag_top2",
|
||||
"tag_key_value",
|
||||
]
|
||||
|
||||
|
||||
def test_inline_tags_delete() -> None:
|
||||
"""Test inline tags delete."""
|
||||
tags = InlineTags(INLINE_CONTENT)
|
||||
assert tags.list == [
|
||||
"bold_tag",
|
||||
"in_text_tag",
|
||||
"inline_tag_top1",
|
||||
"inline_tag_top2",
|
||||
"tag_key_value",
|
||||
]
|
||||
|
||||
assert tags.delete("no tag") is False
|
||||
assert tags.has_changes() is False
|
||||
assert tags.delete("bold_tag") is True
|
||||
assert tags.list == [
|
||||
"in_text_tag",
|
||||
"inline_tag_top1",
|
||||
"inline_tag_top2",
|
||||
"tag_key_value",
|
||||
]
|
||||
assert tags.has_changes() is True
|
||||
assert tags.delete(r"\d{3}") is False
|
||||
assert tags.delete(r"inline_tag_top\d") is True
|
||||
assert tags.list == ["in_text_tag", "tag_key_value"]
|
||||
|
||||
|
||||
def test_inline_tags_rename() -> None:
|
||||
"""Test inline tags rename."""
|
||||
tags = InlineTags(INLINE_CONTENT)
|
||||
assert tags.list == [
|
||||
"bold_tag",
|
||||
"in_text_tag",
|
||||
"inline_tag_top1",
|
||||
"inline_tag_top2",
|
||||
"tag_key_value",
|
||||
]
|
||||
|
||||
assert tags.rename("no tag", "new tag") is False
|
||||
assert tags.has_changes() is False
|
||||
assert tags.rename("bold_tag", "new tag") is True
|
||||
assert tags.list == [
|
||||
"in_text_tag",
|
||||
"inline_tag_top1",
|
||||
"inline_tag_top2",
|
||||
"new tag",
|
||||
"tag_key_value",
|
||||
]
|
||||
assert tags.has_changes() is True
|
||||
|
||||
|
||||
def test_vault_metadata() -> None:
|
||||
"""Test VaultMetadata class."""
|
||||
vm = VaultMetadata()
|
||||
assert vm.dict == {}
|
||||
|
||||
vm.index_metadata(area=MetadataType.FRONTMATTER, metadata=METADATA)
|
||||
vm.index_metadata(area=MetadataType.INLINE, metadata=METADATA_2)
|
||||
vm.index_metadata(area=MetadataType.TAGS, metadata=TAG_LIST)
|
||||
assert vm.dict == {
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"intext_key": ["intext_key_value"],
|
||||
"key1": ["value1"],
|
||||
"key2": ["value2", "value3"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"shared_key2": ["shared_key2_value"],
|
||||
"tags": ["tag 1", "tag 2", "tag 3"],
|
||||
"top_key1": ["top_key1_value"],
|
||||
"top_key2": ["top_key2_value"],
|
||||
"top_key3": ["top_key3_value"],
|
||||
}
|
||||
assert vm.frontmatter == {
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"intext_key": ["intext_key_value"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"shared_key2": ["shared_key2_value"],
|
||||
"tags": ["tag 1", "tag 2", "tag 3"],
|
||||
"top_key1": ["top_key1_value"],
|
||||
"top_key2": ["top_key2_value"],
|
||||
"top_key3": ["top_key3_value"],
|
||||
}
|
||||
assert vm.inline_metadata == {"key1": ["value1"], "key2": ["value2", "value3"]}
|
||||
assert vm.tags == ["tag 1", "tag 2", "tag 3"]
|
||||
|
||||
new_metadata = {"added_key": ["added_value"], "frontmatter_Key2": ["new_value"]}
|
||||
new_tags = ["tag 4", "tag 5"]
|
||||
vm.index_metadata(area=MetadataType.FRONTMATTER, metadata=new_metadata)
|
||||
vm.index_metadata(area=MetadataType.TAGS, metadata=new_tags)
|
||||
assert vm.dict == {
|
||||
"added_key": ["added_value"],
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["article", "new_value", "note"],
|
||||
"intext_key": ["intext_key_value"],
|
||||
"key1": ["value1"],
|
||||
"key2": ["value2", "value3"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"shared_key2": ["shared_key2_value"],
|
||||
"tags": ["tag 1", "tag 2", "tag 3"],
|
||||
"top_key1": ["top_key1_value"],
|
||||
"top_key2": ["top_key2_value"],
|
||||
"top_key3": ["top_key3_value"],
|
||||
}
|
||||
assert vm.frontmatter == {
|
||||
"added_key": ["added_value"],
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["article", "new_value", "note"],
|
||||
"intext_key": ["intext_key_value"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"shared_key2": ["shared_key2_value"],
|
||||
"tags": ["tag 1", "tag 2", "tag 3"],
|
||||
"top_key1": ["top_key1_value"],
|
||||
"top_key2": ["top_key2_value"],
|
||||
"top_key3": ["top_key3_value"],
|
||||
}
|
||||
assert vm.inline_metadata == {"key1": ["value1"], "key2": ["value2", "value3"]}
|
||||
assert vm.tags == ["tag 1", "tag 2", "tag 3", "tag 4", "tag 5"]
|
||||
|
||||
|
||||
def test_vault_metadata_print(capsys) -> None:
|
||||
"""Test print_metadata method."""
|
||||
vm = VaultMetadata()
|
||||
vm.index_metadata(area=MetadataType.FRONTMATTER, metadata=METADATA)
|
||||
vm.index_metadata(area=MetadataType.INLINE, metadata=METADATA_2)
|
||||
vm.index_metadata(area=MetadataType.TAGS, metadata=TAG_LIST)
|
||||
|
||||
vm.print_metadata(area=MetadataType.ALL)
|
||||
captured = 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 = 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 = 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 = 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 = 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:
|
||||
"""Test contains method."""
|
||||
vm = VaultMetadata()
|
||||
vm.index_metadata(area=MetadataType.FRONTMATTER, metadata=METADATA)
|
||||
vm.index_metadata(area=MetadataType.INLINE, metadata=METADATA_2)
|
||||
vm.index_metadata(area=MetadataType.TAGS, metadata=TAG_LIST)
|
||||
assert vm.dict == {
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"intext_key": ["intext_key_value"],
|
||||
"key1": ["value1"],
|
||||
"key2": ["value2", "value3"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"shared_key2": ["shared_key2_value"],
|
||||
"tags": ["tag 1", "tag 2", "tag 3"],
|
||||
"top_key1": ["top_key1_value"],
|
||||
"top_key2": ["top_key2_value"],
|
||||
"top_key3": ["top_key3_value"],
|
||||
}
|
||||
assert vm.frontmatter == {
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"intext_key": ["intext_key_value"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"shared_key2": ["shared_key2_value"],
|
||||
"tags": ["tag 1", "tag 2", "tag 3"],
|
||||
"top_key1": ["top_key1_value"],
|
||||
"top_key2": ["top_key2_value"],
|
||||
"top_key3": ["top_key3_value"],
|
||||
}
|
||||
assert vm.inline_metadata == {"key1": ["value1"], "key2": ["value2", "value3"]}
|
||||
assert vm.tags == ["tag 1", "tag 2", "tag 3"]
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
vm.contains(area=MetadataType.ALL, value="key1")
|
||||
|
||||
assert vm.contains(area=MetadataType.ALL, key="no_key") is False
|
||||
assert vm.contains(area=MetadataType.ALL, key="key1") is True
|
||||
assert vm.contains(area=MetadataType.ALL, key="frontmatter_Key2", value="article") is True
|
||||
assert vm.contains(area=MetadataType.ALL, key="frontmatter_Key2", value="none") is False
|
||||
assert vm.contains(area=MetadataType.ALL, key="1$", is_regex=True) is True
|
||||
assert vm.contains(area=MetadataType.ALL, key=r"\d\d", is_regex=True) is False
|
||||
|
||||
assert vm.contains(area=MetadataType.FRONTMATTER, key="no_key") is False
|
||||
assert vm.contains(area=MetadataType.FRONTMATTER, key="frontmatter_Key1") is True
|
||||
assert (
|
||||
vm.contains(area=MetadataType.FRONTMATTER, key="frontmatter_Key2", value="article") is True
|
||||
)
|
||||
assert vm.contains(area=MetadataType.FRONTMATTER, key="frontmatter_Key2", value="none") is False
|
||||
assert vm.contains(area=MetadataType.FRONTMATTER, key="1$", is_regex=True) is True
|
||||
assert vm.contains(area=MetadataType.FRONTMATTER, key=r"\d\d", is_regex=True) is False
|
||||
|
||||
assert vm.contains(area=MetadataType.INLINE, key="no_key") is False
|
||||
assert vm.contains(area=MetadataType.INLINE, key="key1") is True
|
||||
assert vm.contains(area=MetadataType.INLINE, key="key2", value="value3") is True
|
||||
assert vm.contains(area=MetadataType.INLINE, key="key2", value="none") is False
|
||||
assert vm.contains(area=MetadataType.INLINE, key="1$", is_regex=True) is True
|
||||
assert vm.contains(area=MetadataType.INLINE, key=r"\d\d", is_regex=True) is False
|
||||
|
||||
assert vm.contains(area=MetadataType.TAGS, value="no_tag") is False
|
||||
assert vm.contains(area=MetadataType.TAGS, value="tag 1") is True
|
||||
assert vm.contains(area=MetadataType.TAGS, value=r"\w+ \d$", is_regex=True) is True
|
||||
assert vm.contains(area=MetadataType.TAGS, value=r"\w+ \d\d$", is_regex=True) is False
|
||||
with pytest.raises(ValueError):
|
||||
vm.contains(area=MetadataType.TAGS, key="key1")
|
||||
|
||||
|
||||
def test_vault_metadata_delete() -> None:
|
||||
"""Test delete method."""
|
||||
vm = VaultMetadata()
|
||||
vm.index_metadata(area=MetadataType.FRONTMATTER, metadata=METADATA)
|
||||
assert vm.dict == {
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"intext_key": ["intext_key_value"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"shared_key2": ["shared_key2_value"],
|
||||
"tags": ["tag 1", "tag 2", "tag 3"],
|
||||
"top_key1": ["top_key1_value"],
|
||||
"top_key2": ["top_key2_value"],
|
||||
"top_key3": ["top_key3_value"],
|
||||
}
|
||||
|
||||
assert vm.delete("no key") is False
|
||||
assert vm.delete("tags", "no value") is False
|
||||
assert vm.delete("tags", "tag 2") is True
|
||||
assert vm.dict["tags"] == ["tag 1", "tag 3"]
|
||||
assert vm.delete("tags") is True
|
||||
assert "tags" not in vm.dict
|
||||
|
||||
|
||||
def test_vault_metadata_rename() -> None:
|
||||
"""Test rename method."""
|
||||
vm = VaultMetadata()
|
||||
vm.index_metadata(area=MetadataType.FRONTMATTER, metadata=METADATA)
|
||||
assert vm.dict == {
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"intext_key": ["intext_key_value"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"shared_key2": ["shared_key2_value"],
|
||||
"tags": ["tag 1", "tag 2", "tag 3"],
|
||||
"top_key1": ["top_key1_value"],
|
||||
"top_key2": ["top_key2_value"],
|
||||
"top_key3": ["top_key3_value"],
|
||||
}
|
||||
|
||||
assert vm.rename("no key", "new key") is False
|
||||
assert vm.rename("tags", "no tag", "new key") is False
|
||||
assert vm.rename("tags", "tag 2", "new tag") is True
|
||||
assert vm.dict["tags"] == ["new tag", "tag 1", "tag 3"]
|
||||
assert vm.rename("tags", "old_tags") is True
|
||||
assert vm.dict["old_tags"] == ["new tag", "tag 1", "tag 3"]
|
||||
assert "tags" not in vm.dict
|
||||
814
tests/metadata_vault_test.py
Normal file
814
tests/metadata_vault_test.py
Normal file
@@ -0,0 +1,814 @@
|
||||
# type: ignore
|
||||
"""Test VaultMetadata object from metadata.py."""
|
||||
import pytest
|
||||
|
||||
from obsidian_metadata.models.enums import MetadataType
|
||||
from obsidian_metadata.models.metadata import (
|
||||
VaultMetadata,
|
||||
)
|
||||
from tests.helpers import Regex, remove_ansi
|
||||
|
||||
|
||||
def test_vault_metadata__init_1() -> None:
|
||||
"""Test VaultMetadata class."""
|
||||
vm = VaultMetadata()
|
||||
assert vm.dict == {}
|
||||
assert vm.frontmatter == {}
|
||||
assert vm.inline_metadata == {}
|
||||
assert vm.tags == []
|
||||
|
||||
|
||||
def test_index_metadata_1():
|
||||
"""Test index_metadata() method.
|
||||
|
||||
GIVEN a dictionary to add
|
||||
WHEN the target area is FRONTMATTER and the old dictionary is empty
|
||||
THEN the new dictionary is added to the target area
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
new_dict = {"key1": ["value1"], "key2": ["value2", "value3"]}
|
||||
vm.index_metadata(area=MetadataType.FRONTMATTER, metadata=new_dict)
|
||||
assert vm.dict == new_dict
|
||||
assert vm.frontmatter == new_dict
|
||||
|
||||
|
||||
def test_index_metadata_2():
|
||||
"""Test index_metadata() method.
|
||||
|
||||
GIVEN a dictionary to add
|
||||
WHEN the target area is FRONTMATTER and the old dictionary is not empty
|
||||
THEN the new dictionary is merged with the old dictionary
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {"key1": ["value1"], "key2": ["value1", "value2"], "other_key": ["value1"]}
|
||||
vm.frontmatter = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
|
||||
new_dict = {"key1": ["value1"], "key2": ["value1", "value3"], "key3": ["value1"]}
|
||||
|
||||
vm.index_metadata(area=MetadataType.FRONTMATTER, metadata=new_dict)
|
||||
assert vm.dict == {
|
||||
"key1": ["value1"],
|
||||
"key2": ["value1", "value2", "value3"],
|
||||
"key3": ["value1"],
|
||||
"other_key": ["value1"],
|
||||
}
|
||||
assert vm.frontmatter == {
|
||||
"key1": ["value1"],
|
||||
"key2": ["value1", "value2", "value3"],
|
||||
"key3": ["value1"],
|
||||
}
|
||||
|
||||
|
||||
def test_index_metadata_3():
|
||||
"""Test index_metadata() method.
|
||||
|
||||
GIVEN a dictionary to add
|
||||
WHEN the target area is INLINE and the old dictionary is empty
|
||||
THEN the new dictionary is added to the target area
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
new_dict = {"key1": ["value1"], "key2": ["value2", "value3"]}
|
||||
vm.index_metadata(area=MetadataType.INLINE, metadata=new_dict)
|
||||
assert vm.dict == new_dict
|
||||
assert vm.inline_metadata == new_dict
|
||||
|
||||
|
||||
def test_index_metadata_4():
|
||||
"""Test index_metadata() method.
|
||||
|
||||
GIVEN a dictionary to add
|
||||
WHEN the target area is INLINE and the old dictionary is not empty
|
||||
THEN the new dictionary is merged with the old dictionary
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {"key1": ["value1"], "key2": ["value1", "value2"], "other_key": ["value1"]}
|
||||
vm.inline_metadata = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
|
||||
new_dict = {"key1": ["value1"], "key2": ["value1", "value3"], "key3": ["value1"]}
|
||||
|
||||
vm.index_metadata(area=MetadataType.INLINE, metadata=new_dict)
|
||||
assert vm.dict == {
|
||||
"key1": ["value1"],
|
||||
"key2": ["value1", "value2", "value3"],
|
||||
"key3": ["value1"],
|
||||
"other_key": ["value1"],
|
||||
}
|
||||
assert vm.inline_metadata == {
|
||||
"key1": ["value1"],
|
||||
"key2": ["value1", "value2", "value3"],
|
||||
"key3": ["value1"],
|
||||
}
|
||||
|
||||
|
||||
def test_index_metadata_5():
|
||||
"""Test index_metadata() method.
|
||||
|
||||
GIVEN a dictionary to add
|
||||
WHEN the target area is TAGS and the old list is empty
|
||||
THEN the new list is added to the target area
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
new_list = ["tag1", "tag2", "tag3"]
|
||||
vm.index_metadata(area=MetadataType.TAGS, metadata=new_list)
|
||||
assert vm.dict == {}
|
||||
assert vm.tags == new_list
|
||||
|
||||
|
||||
def test_index_metadata_6():
|
||||
"""Test index_metadata() method.
|
||||
|
||||
GIVEN a dictionary to add
|
||||
WHEN the target area is TAGS and the old list is not empty
|
||||
THEN the new list is merged with the old list
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.tags = ["tag1", "tag2", "tag3"]
|
||||
new_list = ["tag1", "tag2", "tag4", "tag5"]
|
||||
|
||||
vm.index_metadata(area=MetadataType.TAGS, metadata=new_list)
|
||||
assert vm.dict == {}
|
||||
assert vm.tags == ["tag1", "tag2", "tag3", "tag4", "tag5"]
|
||||
|
||||
|
||||
def test_contains_1():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN FRONTMATTER is checked for a key that exists
|
||||
THEN True is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.frontmatter = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.FRONTMATTER, key="key1") is True
|
||||
|
||||
|
||||
def test_contains_2():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN FRONTMATTER is checked for a key that does not exist
|
||||
THEN False is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.frontmatter = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.FRONTMATTER, key="key3") is False
|
||||
|
||||
|
||||
def test_contains_3():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN FRONTMATTER is checked for a key and value that exists
|
||||
THEN True is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.frontmatter = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.FRONTMATTER, key="key2", value="value1") is True
|
||||
|
||||
|
||||
def test_contains_4():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN FRONTMATTER is checked for a key and value that does not exist
|
||||
THEN False is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.frontmatter = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.FRONTMATTER, key="key2", value="value3") is False
|
||||
|
||||
|
||||
def test_contains_5():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN FRONTMATTER is checked for a key that exists with regex
|
||||
THEN True is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.frontmatter = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.FRONTMATTER, key=r"\w+\d", is_regex=True) is True
|
||||
|
||||
|
||||
def test_contains_6():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN FRONTMATTER is checked for a key that does not exist with regex
|
||||
THEN False is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.frontmatter = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.FRONTMATTER, key=r"^\d", is_regex=True) is False
|
||||
|
||||
|
||||
def test_contains_7():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN FRONTMATTER is checked for a key and value that exists with regex
|
||||
THEN True is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.frontmatter = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert (
|
||||
vm.contains(area=MetadataType.FRONTMATTER, key="key2", value=r"\w\d", is_regex=True) is True
|
||||
)
|
||||
|
||||
|
||||
def test_contains_8():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN FRONTMATTER is checked for a key and value that does not exist with regex
|
||||
THEN False is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.frontmatter = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert (
|
||||
vm.contains(area=MetadataType.FRONTMATTER, key="key2", value=r"^\d", is_regex=True) is False
|
||||
)
|
||||
|
||||
|
||||
def test_contains_9():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN FRONTMATTER is checked with a key is None
|
||||
THEN raise a ValueError
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.frontmatter = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
with pytest.raises(ValueError, match="Key must be provided"):
|
||||
vm.contains(area=MetadataType.FRONTMATTER, value="value1")
|
||||
|
||||
|
||||
def test_contains_10():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN INLINE is checked for a key that exists
|
||||
THEN True is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.inline_metadata = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.INLINE, key="key1") is True
|
||||
|
||||
|
||||
def test_contains_11():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN INLINE is checked for a key that does not exist
|
||||
THEN False is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.inline_metadata = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.INLINE, key="key3") is False
|
||||
|
||||
|
||||
def test_contains_12():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN INLINE is checked for a key and value that exists
|
||||
THEN True is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.inline_metadata = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.INLINE, key="key2", value="value1") is True
|
||||
|
||||
|
||||
def test_contains_13():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN INLINE is checked for a key and value that does not exist
|
||||
THEN False is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.inline_metadata = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.INLINE, key="key2", value="value3") is False
|
||||
|
||||
|
||||
def test_contains_14():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN INLINE is checked for a key that exists with regex
|
||||
THEN True is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.inline_metadata = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.INLINE, key=r"\w+\d", is_regex=True) is True
|
||||
|
||||
|
||||
def test_contains_15():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN INLINE is checked for a key that does not exist with regex
|
||||
THEN False is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.inline_metadata = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.INLINE, key=r"^\d", is_regex=True) is False
|
||||
|
||||
|
||||
def test_contains_16():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN INLINE is checked for a key and value that exists with regex
|
||||
THEN True is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.inline_metadata = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.INLINE, key="key2", value=r"\w\d", is_regex=True) is True
|
||||
|
||||
|
||||
def test_contains_17():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN INLINE is checked for a key and value that does not exist with regex
|
||||
THEN False is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.inline_metadata = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.INLINE, key="key2", value=r"^\d", is_regex=True) is False
|
||||
|
||||
|
||||
def test_contains_18():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN INLINE is checked with a key is None
|
||||
THEN raise a ValueError
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.inline_metadata = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
with pytest.raises(ValueError, match="Key must be provided"):
|
||||
vm.contains(area=MetadataType.INLINE, value="value1")
|
||||
|
||||
|
||||
def test_contains_19():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN TAGS is checked for a key but not a value
|
||||
THEN raise a ValueError
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.tags = ["tag1", "tag2", "tag3"]
|
||||
with pytest.raises(ValueError, match="Value must be provided"):
|
||||
vm.contains(area=MetadataType.TAGS, key="key1")
|
||||
|
||||
|
||||
def test_contains_20():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN TAGS is checked for a value that exists
|
||||
THEN True is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.tags = ["tag1", "tag2", "tag3"]
|
||||
assert vm.contains(area=MetadataType.TAGS, value="tag1") is True
|
||||
|
||||
|
||||
def test_contains_21():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN TAGS is checked for a value that does not exist
|
||||
THEN False is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.tags = ["tag1", "tag2", "tag3"]
|
||||
assert vm.contains(area=MetadataType.TAGS, value="value1") is False
|
||||
|
||||
|
||||
def test_contains_22():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN TAGS is checked for a key regex but no value
|
||||
THEN True is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.tags = ["tag1", "tag2", "tag3"]
|
||||
with pytest.raises(ValueError, match="Value must be provided"):
|
||||
vm.contains(area=MetadataType.TAGS, key=r"\w", is_regex=True)
|
||||
|
||||
|
||||
def test_contains_23():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN TAGS is checked for a value that does not exist with regex
|
||||
THEN False is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.tags = ["tag1", "tag2", "tag3"]
|
||||
assert vm.contains(area=MetadataType.TAGS, value=r"^\d", is_regex=True) is False
|
||||
|
||||
|
||||
def test_contains_24():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN TAGS is checked for a value that exists with regex
|
||||
THEN True is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.tags = ["tag1", "tag2", "tag3"]
|
||||
assert vm.contains(area=MetadataType.TAGS, value=r"^tag\d", is_regex=True) is True
|
||||
|
||||
|
||||
def test_contains_25():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN ALL is checked for a key that exists
|
||||
THEN True is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.ALL, key="key1") is True
|
||||
|
||||
|
||||
def test_contains_26():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN ALL is checked for a key that does not exist
|
||||
THEN False is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.ALL, key="key3") is False
|
||||
|
||||
|
||||
def test_contains_27():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN ALL is checked for a key and value that exists
|
||||
THEN True is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.ALL, key="key2", value="value1") is True
|
||||
|
||||
|
||||
def test_contains_28():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN ALL is checked for a key and value that does not exist
|
||||
THEN False is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.ALL, key="key2", value="value3") is False
|
||||
|
||||
|
||||
def test_contains_29():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN ALL is checked for a key that exists with regex
|
||||
THEN True is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.ALL, key=r"\w+\d", is_regex=True) is True
|
||||
|
||||
|
||||
def test_contains_30():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN ALL is checked for a key that does not exist with regex
|
||||
THEN False is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.ALL, key=r"^\d", is_regex=True) is False
|
||||
|
||||
|
||||
def test_contains_31():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN ALL is checked for a key and value that exists with regex
|
||||
THEN True is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.ALL, key="key2", value=r"\w\d", is_regex=True) is True
|
||||
|
||||
|
||||
def test_contains_32():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN ALL is checked for a key and value that does not exist with regex
|
||||
THEN False is returned
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.contains(area=MetadataType.ALL, key="key2", value=r"^\d", is_regex=True) is False
|
||||
|
||||
|
||||
def test_contains_33():
|
||||
"""Test contains() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN ALL is checked with a key is None
|
||||
THEN raise a ValueError
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
with pytest.raises(ValueError, match="Key must be provided"):
|
||||
vm.contains(area=MetadataType.ALL, value="value1")
|
||||
|
||||
|
||||
def test_delete_1():
|
||||
"""Test delete() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN a key is deleted
|
||||
THEN return True and the key is removed
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.delete(key="key1") is True
|
||||
assert vm.dict == {"key2": ["value1", "value2"]}
|
||||
|
||||
|
||||
def test_delete_2():
|
||||
"""Test delete() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN a key is deleted that does not exist
|
||||
THEN return False and the key is not removed
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.delete(key="key3") is False
|
||||
assert vm.dict == {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
|
||||
|
||||
def test_delete_3():
|
||||
"""Test delete() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN a key and value are specified
|
||||
THEN return True and remove the value
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.delete(key="key2", value_to_delete="value1") is True
|
||||
assert vm.dict == {"key1": ["value1"], "key2": ["value2"]}
|
||||
|
||||
|
||||
def test_delete_4():
|
||||
"""Test delete() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN a key and nonexistent value are specified
|
||||
THEN return False
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.delete(key="key2", value_to_delete="value11") is False
|
||||
assert vm.dict == {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
|
||||
|
||||
def test_rename_1():
|
||||
"""Test VaultMetadata rename() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN the rename() method is called with a key
|
||||
THEN return False if the key is not found
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.rename("no key", "new key") is False
|
||||
|
||||
|
||||
def test_rename_2():
|
||||
"""Test VaultMetadata rename() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN the rename() method is called with an existing key and non-existing value
|
||||
THEN return False
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.rename("key1", "no value", "new value") is False
|
||||
|
||||
|
||||
def test_rename_3():
|
||||
"""Test VaultMetadata rename() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN the rename() method is called with an existing key
|
||||
THEN return True and rename the key
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.rename("key1", "new key") is True
|
||||
assert vm.dict == {"key2": ["value1", "value2"], "new key": ["value1"]}
|
||||
|
||||
|
||||
def test_rename_4():
|
||||
"""Test VaultMetadata rename() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN the rename() method is called with an existing key and value
|
||||
THEN return True and rename the value
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.rename("key1", "value1", "new value") is True
|
||||
assert vm.dict == {"key1": ["new value"], "key2": ["value1", "value2"]}
|
||||
|
||||
|
||||
def test_rename_5():
|
||||
"""Test VaultMetadata rename() method.
|
||||
|
||||
GIVEN a VaultMetadata object
|
||||
WHEN the rename() method is called with an existing key and value and the new value already exists
|
||||
THEN return True and remove the old value leaving one instance of the new value
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
assert vm.rename("key2", "value1", "value2") is True
|
||||
assert vm.dict == {"key1": ["value1"], "key2": ["value2"]}
|
||||
|
||||
|
||||
def test_print_metadata_1(capsys):
|
||||
"""Test print_metadata() method.
|
||||
|
||||
GIVEN calling print_metadata() with a VaultMetadata object
|
||||
WHEN ALL is specified
|
||||
THEN print all the metadata
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {
|
||||
"key1": ["value1", "value2"],
|
||||
"key2": ["value1", "value2"],
|
||||
"key3": ["value1"],
|
||||
"key4": ["value1", "value2"],
|
||||
}
|
||||
vm.frontmatter = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
vm.inline_metadata = {
|
||||
"key1": ["value1", "value2"],
|
||||
"key3": ["value1"],
|
||||
"key4": ["value1", "value2"],
|
||||
}
|
||||
vm.tags = ["tag1", "tag2", "tag3"]
|
||||
|
||||
vm.print_metadata(area=MetadataType.ALL)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert "All metadata" in captured
|
||||
assert captured == Regex("┃ Keys +┃ Values +┃")
|
||||
assert captured == Regex("│ key1 +│ value1 +│")
|
||||
assert captured == Regex("│ key2 +│ value1 +│")
|
||||
assert captured == Regex("│ key4 +│ value1 +│")
|
||||
assert "All inline tags" in captured
|
||||
assert captured == Regex("#tag1 +#tag2")
|
||||
|
||||
|
||||
def test_print_metadata_2(capsys):
|
||||
"""Test print_metadata() method.
|
||||
|
||||
GIVEN calling print_metadata() with a VaultMetadata object
|
||||
WHEN FRONTMATTER is specified
|
||||
THEN print all the metadata
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {
|
||||
"key1": ["value1", "value2"],
|
||||
"key2": ["value1", "value2"],
|
||||
"key3": ["value1"],
|
||||
"key4": ["value1", "value2"],
|
||||
}
|
||||
vm.frontmatter = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
vm.inline_metadata = {
|
||||
"key1": ["value1", "value2"],
|
||||
"key3": ["value1"],
|
||||
"key4": ["value1", "value2"],
|
||||
}
|
||||
vm.tags = ["tag1", "tag2", "tag3"]
|
||||
|
||||
vm.print_metadata(area=MetadataType.FRONTMATTER)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert "All frontmatter" in captured
|
||||
assert captured == Regex("┃ Keys +┃ Values +┃")
|
||||
assert captured == Regex("│ key1 +│ value1 +│")
|
||||
assert captured == Regex("│ key2 +│ value1 +│")
|
||||
assert captured != Regex("│ key4 +│ value1 +│")
|
||||
assert "All inline tags" not in captured
|
||||
assert captured != Regex("#tag1 +#tag2")
|
||||
|
||||
|
||||
def test_print_metadata_3(capsys):
|
||||
"""Test print_metadata() method.
|
||||
|
||||
GIVEN calling print_metadata() with a VaultMetadata object
|
||||
WHEN INLINE is specified
|
||||
THEN print all the metadata
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {
|
||||
"key1": ["value1", "value2"],
|
||||
"key2": ["value1", "value2"],
|
||||
"key3": ["value1"],
|
||||
"key4": ["value1", "value2"],
|
||||
}
|
||||
vm.frontmatter = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
vm.inline_metadata = {
|
||||
"key1": ["value1", "value2"],
|
||||
"key3": ["value1"],
|
||||
"key4": ["value1", "value2"],
|
||||
}
|
||||
vm.tags = ["tag1", "tag2", "tag3"]
|
||||
|
||||
vm.print_metadata(area=MetadataType.INLINE)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert "All inline" in captured
|
||||
assert captured == Regex("┃ Keys +┃ Values +┃")
|
||||
assert captured == Regex("│ key1 +│ value1 +│")
|
||||
assert captured != Regex("│ key2 +│ value1 +│")
|
||||
assert captured == Regex("│ key4 +│ value1 +│")
|
||||
assert "All inline tags" not in captured
|
||||
assert captured != Regex("#tag1 +#tag2")
|
||||
|
||||
|
||||
def test_print_metadata_4(capsys):
|
||||
"""Test print_metadata() method.
|
||||
|
||||
GIVEN calling print_metadata() with a VaultMetadata object
|
||||
WHEN TAGS is specified
|
||||
THEN print all the tags
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {
|
||||
"key1": ["value1", "value2"],
|
||||
"key2": ["value1", "value2"],
|
||||
"key3": ["value1"],
|
||||
"key4": ["value1", "value2"],
|
||||
}
|
||||
vm.frontmatter = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
vm.inline_metadata = {
|
||||
"key1": ["value1", "value2"],
|
||||
"key3": ["value1"],
|
||||
"key4": ["value1", "value2"],
|
||||
}
|
||||
vm.tags = ["tag1", "tag2", "tag3"]
|
||||
|
||||
vm.print_metadata(area=MetadataType.TAGS)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert "All inline tags" in captured
|
||||
assert captured != Regex("┃ Keys +┃ Values +┃")
|
||||
assert captured != Regex("│ key1 +│ value1 +│")
|
||||
assert captured != Regex("│ key2 +│ value1 +│")
|
||||
assert captured != Regex("│ key4 +│ value1 +│")
|
||||
assert captured == Regex("#tag1 +#tag2 +#tag3")
|
||||
|
||||
|
||||
def test_print_metadata_5(capsys):
|
||||
"""Test print_metadata() method.
|
||||
|
||||
GIVEN calling print_metadata() with a VaultMetadata object
|
||||
WHEN KEYS is specified
|
||||
THEN print all the tags
|
||||
"""
|
||||
vm = VaultMetadata()
|
||||
vm.dict = {
|
||||
"key1": ["value1", "value2"],
|
||||
"key2": ["value1", "value2"],
|
||||
"key3": ["value1"],
|
||||
"key4": ["value1", "value2"],
|
||||
}
|
||||
vm.frontmatter = {"key1": ["value1"], "key2": ["value1", "value2"]}
|
||||
vm.inline_metadata = {
|
||||
"key1": ["value1", "value2"],
|
||||
"key3": ["value1"],
|
||||
"key4": ["value1", "value2"],
|
||||
}
|
||||
vm.tags = ["tag1", "tag2", "tag3"]
|
||||
|
||||
vm.print_metadata(area=MetadataType.KEYS)
|
||||
captured = remove_ansi(capsys.readouterr().out)
|
||||
assert "All Keys" in captured
|
||||
assert captured != Regex("┃ Keys +┃ Values +┃")
|
||||
assert captured != Regex("│ key1 +│ value1 +│")
|
||||
assert captured != Regex("│ key2 +│ value1 +│")
|
||||
assert captured != Regex("│ key4 +│ value1 +│")
|
||||
assert captured != Regex("#tag1 +#tag2 +#tag3")
|
||||
assert captured == Regex("key1 +key2 +key3 +key4")
|
||||
@@ -48,7 +48,7 @@ def test_create_note_1(sample_note):
|
||||
],
|
||||
}
|
||||
|
||||
assert note.inline_tags.list == [
|
||||
assert note.tags.list == [
|
||||
"inline_tag_bottom1",
|
||||
"inline_tag_bottom2",
|
||||
"inline_tag_top1",
|
||||
@@ -229,16 +229,17 @@ def test_add_metadata_method_10(sample_note):
|
||||
"""Test add_metadata() method.
|
||||
|
||||
GIVEN a note object
|
||||
WHEN add_metadata() is with a new tag
|
||||
WHEN add_metadata() is called 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 "new_tag2" not in note.tags.list
|
||||
assert (
|
||||
note.add_metadata(MetadataType.TAGS, value="new_tag", location=InsertLocation.TOP) is True
|
||||
note.add_metadata(MetadataType.TAGS, value="new_tag2", location=InsertLocation.BOTTOM)
|
||||
is True
|
||||
)
|
||||
assert "new_tag" in note.inline_tags.list
|
||||
assert "#new_tag" in note.file_content
|
||||
assert "new_tag2" in note.tags.list
|
||||
assert "#new_tag2" in note.file_content
|
||||
|
||||
|
||||
def test_commit_1(sample_note, tmp_path) -> None:
|
||||
@@ -263,7 +264,7 @@ def test_commit_1(sample_note, tmp_path) -> None:
|
||||
assert "Heading 1" not in note2.file_content
|
||||
|
||||
|
||||
def test_commit_2(sample_note, tmp_path) -> None:
|
||||
def test_commit_2(sample_note) -> None:
|
||||
"""Test commit() method.
|
||||
|
||||
GIVEN a note object with commit() called
|
||||
@@ -278,19 +279,19 @@ def test_commit_2(sample_note, tmp_path) -> None:
|
||||
assert "Heading 1" in note.file_content
|
||||
|
||||
|
||||
def test_contains_inline_tag(sample_note) -> None:
|
||||
"""Test contains_inline_tag method.
|
||||
def test_contains_tag(sample_note) -> None:
|
||||
"""Test contains_tag method.
|
||||
|
||||
GIVEN a note object
|
||||
WHEN contains_inline_tag() is called
|
||||
WHEN contains_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
|
||||
assert note.contains_inline_tag(r"\d$", is_regex=True) is True
|
||||
assert note.contains_inline_tag(r"^\d", is_regex=True) is False
|
||||
assert note.contains_tag("intext_tag1") is True
|
||||
assert note.contains_tag("nonexistent_tag") is False
|
||||
assert note.contains_tag(r"\d$", is_regex=True) is True
|
||||
assert note.contains_tag(r"^\d", is_regex=True) is False
|
||||
|
||||
|
||||
def test_contains_metadata(sample_note) -> None:
|
||||
@@ -313,17 +314,35 @@ 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_tag(sample_note) -> None:
|
||||
"""Test delete_inline_tag method.
|
||||
def test_delete_all_metadata(sample_note):
|
||||
"""Test delete_all_metadata() method.
|
||||
|
||||
GIVEN a note object
|
||||
WHEN delete_inline_tag() is called
|
||||
WHEN delete_all_metadata() is called
|
||||
THEN all tags, frontmatter, and inline metadata are deleted
|
||||
"""
|
||||
note = Note(note_path=sample_note)
|
||||
note.delete_all_metadata()
|
||||
assert note.tags.list == []
|
||||
assert note.frontmatter.dict == {}
|
||||
assert note.inline_metadata.dict == {}
|
||||
assert note.file_content == Regex("consequat. Duis")
|
||||
assert "codeblock_key:: some text" in note.file_content
|
||||
assert "#ffffff" in note.file_content
|
||||
assert "---" not in note.file_content
|
||||
|
||||
|
||||
def test_delete_tag(sample_note) -> None:
|
||||
"""Test delete_tag method.
|
||||
|
||||
GIVEN a note object
|
||||
WHEN delete_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.delete_tag("not_a_tag") is False
|
||||
assert note.delete_tag("intext_tag[1]") is True
|
||||
assert "intext_tag1" not in note.tags.list
|
||||
assert note.file_content == Regex("consequat. Duis")
|
||||
|
||||
|
||||
@@ -435,7 +454,7 @@ def test_has_changes(sample_note) -> None:
|
||||
|
||||
note = Note(note_path=sample_note)
|
||||
assert note.has_changes() is False
|
||||
note.delete_inline_tag("intext_tag1")
|
||||
note.delete_tag("intext_tag1")
|
||||
assert note.has_changes() is True
|
||||
|
||||
|
||||
@@ -475,29 +494,29 @@ def test_print_note(sample_note, capsys) -> None:
|
||||
assert "#shared_tag" in captured.out
|
||||
|
||||
|
||||
def test_rename_inline_tag_1(sample_note) -> None:
|
||||
"""Test rename_inline_tag() method.
|
||||
def test_rename_tag_1(sample_note) -> None:
|
||||
"""Test rename_tag() method.
|
||||
|
||||
GIVEN a note object
|
||||
WHEN rename_inline_tag() is called with a tag that does not exist
|
||||
WHEN rename_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
|
||||
assert note.rename_tag("no_note_tag", "intext_tag2") is False
|
||||
|
||||
|
||||
def test_rename_inline_tag_2(sample_note) -> None:
|
||||
"""Test rename_inline_tag() method.
|
||||
def test_rename_tag_2(sample_note) -> None:
|
||||
"""Test rename_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
|
||||
WHEN rename_tag() is called with a tag exists
|
||||
THEN the tag is renamed in the InlineTag 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 "intext_tag1" in note.tags.list
|
||||
assert note.rename_tag("intext_tag1", "intext_tag26") is True
|
||||
assert "intext_tag1" not in note.tags.list
|
||||
assert "intext_tag26" in note.tags.list
|
||||
assert note.file_content == Regex(r"#intext_tag26")
|
||||
assert note.file_content != Regex(r"#intext_tag1")
|
||||
|
||||
@@ -696,10 +715,9 @@ def test_transpose_metadata_4(sample_note):
|
||||
"intext_key": ["intext_value"],
|
||||
"key📅": ["📅_key_value"],
|
||||
"shared_key1": [
|
||||
"shared_key1_value",
|
||||
"shared_key1_value3",
|
||||
"shared_key1_value",
|
||||
"shared_key1_value2",
|
||||
"shared_key1_value3",
|
||||
],
|
||||
"shared_key2": ["shared_key2_value1", "shared_key2_value2"],
|
||||
"tags": [
|
||||
@@ -828,7 +846,7 @@ def test_write_delete_inline_metadata_2(sample_note) -> None:
|
||||
|
||||
"""
|
||||
note = Note(note_path=sample_note)
|
||||
note.write_delete_inline_metadata("intext_key")
|
||||
note.write_delete_inline_metadata("intext_key", is_regex=False)
|
||||
assert note.file_content == Regex(r"dolore eu fugiat", re.DOTALL)
|
||||
|
||||
|
||||
@@ -840,7 +858,7 @@ def test_write_delete_inline_metadata_3(sample_note) -> None:
|
||||
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")
|
||||
note.write_delete_inline_metadata("bottom_key2", "bottom_key2_value", is_regex=False)
|
||||
assert note.file_content != Regex(r"bottom_key2_value")
|
||||
assert note.file_content == Regex(r"bottom_key2::")
|
||||
note.write_delete_inline_metadata("bottom_key1")
|
||||
|
||||
@@ -6,17 +6,7 @@ import pytest
|
||||
from obsidian_metadata.models.patterns import Patterns
|
||||
|
||||
TAG_CONTENT: str = "#1 #2 **#3** [[#4]] [[#5|test]] #6#notag #7_8 #9/10 #11-12 #13; #14, #15. #16: #17* #18(#19) #20[#21] #22\\ #23& #24# #25 **#26** #📅/tag [link](#no_tag) https://example.com/somepage.html_#no_url_tags"
|
||||
INLINE_METADATA: str = """
|
||||
**1:: 1**
|
||||
2_2:: [[2_2]] | 2
|
||||
asdfasdf [3:: 3] asdfasdf [7::7] asdf
|
||||
[4:: 4] [5:: 5]
|
||||
> 6:: 6
|
||||
**8**:: **8**
|
||||
10::
|
||||
📅11:: 11/📅/11
|
||||
emoji_📅_key:: 📅emoji_📅_key_value
|
||||
"""
|
||||
|
||||
FRONTMATTER_CONTENT: str = """
|
||||
---
|
||||
tags:
|
||||
@@ -150,17 +140,64 @@ def test_find_inline_tags():
|
||||
def test_find_inline_metadata():
|
||||
"""Test find_inline_metadata regex."""
|
||||
pattern = Patterns()
|
||||
content = """
|
||||
**1:: 1**
|
||||
2_2:: [[2_2]] | 2
|
||||
asdfasdf [3:: 3] asdfasdf [7::7] asdf
|
||||
[4:: 4] [5:: 5]
|
||||
> 6:: 6
|
||||
**8**:: **8**
|
||||
10::
|
||||
📅11:: 11/📅/11
|
||||
emoji_📅_key::emoji_📅_key_value
|
||||
key1:: value1
|
||||
key1:: value2
|
||||
key1:: value3
|
||||
indented_key:: value1
|
||||
Paragraph of text with an [inline_key:: value1] and [inline_key:: value2] and [inline_key:: value3] which should do it.
|
||||
> blockquote_key:: value1
|
||||
> blockquote_key:: value2
|
||||
|
||||
result = pattern.find_inline_metadata.findall(INLINE_METADATA)
|
||||
- list_key:: value1
|
||||
- list_key:: [[value2]]
|
||||
|
||||
1. list_key:: value1
|
||||
2. list_key:: value2
|
||||
|
||||
| table_key:: value1 | table_key:: value2 |
|
||||
---
|
||||
frontmatter_key1: frontmatter_key1_value
|
||||
---
|
||||
not_a_key: not_a_value
|
||||
paragraph metadata:: key in text
|
||||
"""
|
||||
|
||||
result = pattern.find_inline_metadata.findall(content)
|
||||
assert result == [
|
||||
("", "", "1", "1**"),
|
||||
("", "", "2_2", "[[2_2]] | 2"),
|
||||
("3", "3", "", ""),
|
||||
("7", "7", "", ""),
|
||||
("", "", "4", "4] [5:: 5]"),
|
||||
("", "", "6", "6"),
|
||||
("", "", "8**", "**8**"),
|
||||
("", "", "11", "11/📅/11"),
|
||||
("", "", "emoji_📅_key", "📅emoji_📅_key_value"),
|
||||
("", "", "emoji_📅_key", "emoji_📅_key_value"),
|
||||
("", "", "key1", "value1"),
|
||||
("", "", "key1", "value2"),
|
||||
("", "", "key1", "value3"),
|
||||
("", "", "indented_key", "value1"),
|
||||
("inline_key", "value1", "", ""),
|
||||
("inline_key", "value2", "", ""),
|
||||
("inline_key", "value3", "", ""),
|
||||
("", "", "blockquote_key", "value1"),
|
||||
("", "", "blockquote_key", "value2"),
|
||||
("", "", "list_key", "value1"),
|
||||
("", "", "list_key", "[[value2]]"),
|
||||
("", "", "list_key", "value1"),
|
||||
("", "", "list_key", "value2"),
|
||||
("", "", "table_key", "value1 | table_key:: value2 |"),
|
||||
("", "", "metadata", "key in text"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -68,12 +68,12 @@ def test_validate_number() -> None:
|
||||
assert questions._validate_number("1") is True
|
||||
|
||||
|
||||
def test_validate_existing_inline_tag() -> None:
|
||||
def test_validate_existing_tag() -> None:
|
||||
"""Test existing tag validation."""
|
||||
questions = Questions(vault=VAULT)
|
||||
assert "Tag cannot be empty" in questions._validate_existing_inline_tag("")
|
||||
assert "'test' does not exist" in questions._validate_existing_inline_tag("test")
|
||||
assert questions._validate_existing_inline_tag("shared_tag") is True
|
||||
assert "Tag cannot be empty" in questions._validate_existing_tag("")
|
||||
assert "'test' does not exist" in questions._validate_existing_tag("test")
|
||||
assert questions._validate_existing_tag("shared_tag") is True
|
||||
|
||||
|
||||
def test_validate_key_exists_regex() -> None:
|
||||
|
||||
@@ -1,13 +1,177 @@
|
||||
# type: ignore
|
||||
"""Test the utilities module."""
|
||||
|
||||
import pytest
|
||||
import typer
|
||||
|
||||
from obsidian_metadata._utils import (
|
||||
clean_dictionary,
|
||||
delete_from_dict,
|
||||
dict_contains,
|
||||
dict_keys_to_lower,
|
||||
dict_values_to_lists_strings,
|
||||
remove_markdown_sections,
|
||||
rename_in_dict,
|
||||
validate_csv_bulk_imports,
|
||||
)
|
||||
from tests.helpers import Regex, remove_ansi
|
||||
|
||||
|
||||
def test_delete_from_dict_1():
|
||||
"""Test delete_from_dict() function.
|
||||
|
||||
GIVEN a dictionary with values
|
||||
WHEN the delete_from_dict() function is called with a key that exists
|
||||
THEN the key should be deleted from the dictionary and the original dictionary should not be modified
|
||||
"""
|
||||
test_dict = {"key1": ["value1"], "key2": ["value2", "value3"], "key3": "value4"}
|
||||
|
||||
assert delete_from_dict(dictionary=test_dict, key="key1") == {
|
||||
"key2": ["value2", "value3"],
|
||||
"key3": "value4",
|
||||
}
|
||||
assert test_dict == {"key1": ["value1"], "key2": ["value2", "value3"], "key3": "value4"}
|
||||
|
||||
|
||||
def test_delete_from_dict_2():
|
||||
"""Test delete_from_dict() function.
|
||||
|
||||
GIVEN a dictionary with values
|
||||
WHEN the delete_from_dict() function is called with a key that does not exist
|
||||
THEN the dictionary should not be modified
|
||||
"""
|
||||
test_dict = {"key1": ["value1"], "key2": ["value2", "value3"], "key3": "value4"}
|
||||
|
||||
assert delete_from_dict(dictionary=test_dict, key="key5") == test_dict
|
||||
|
||||
|
||||
def test_delete_from_dict_3():
|
||||
"""Test delete_from_dict() function.
|
||||
|
||||
GIVEN a dictionary with values in a list
|
||||
WHEN the delete_from_dict() function is called with a key and value that exists
|
||||
THEN the value should be deleted from the specified key in dictionary
|
||||
"""
|
||||
test_dict = {"key1": ["value1"], "key2": ["value2", "value3"], "key3": "value4"}
|
||||
|
||||
assert delete_from_dict(dictionary=test_dict, key="key2", value="value3") == {
|
||||
"key1": ["value1"],
|
||||
"key2": ["value2"],
|
||||
"key3": "value4",
|
||||
}
|
||||
|
||||
|
||||
def test_delete_from_dict_4():
|
||||
"""Test delete_from_dict() function.
|
||||
|
||||
GIVEN a dictionary with values as strings
|
||||
WHEN the delete_from_dict() function is called with a key and value that exists
|
||||
THEN the value and key should be deleted from the dictionary
|
||||
"""
|
||||
test_dict = {"key1": ["value1"], "key2": ["value2", "value3"], "key3": "value4"}
|
||||
|
||||
assert delete_from_dict(dictionary=test_dict, key="key3", value="value4") == {
|
||||
"key1": ["value1"],
|
||||
"key2": ["value2", "value3"],
|
||||
}
|
||||
|
||||
|
||||
def test_delete_from_dict_5():
|
||||
"""Test delete_from_dict() function.
|
||||
|
||||
GIVEN a dictionary with values as strings
|
||||
WHEN the delete_from_dict() function is called with a key and value that does not exist
|
||||
THEN the dictionary should not be modified
|
||||
"""
|
||||
test_dict = {"key1": ["value1"], "key2": ["value2", "value3"], "key3": "value4"}
|
||||
|
||||
assert delete_from_dict(dictionary=test_dict, key="key3", value="value5") == test_dict
|
||||
|
||||
|
||||
def test_delete_from_dict_6():
|
||||
"""Test delete_from_dict() function.
|
||||
|
||||
GIVEN a dictionary with values as strings
|
||||
WHEN the delete_from_dict() function is called with a key regex that matches
|
||||
THEN the matching keys should be deleted from the dictionary
|
||||
"""
|
||||
test_dict = {"key1": ["value1"], "key2": ["value2", "value3"], "key3": "value4"}
|
||||
|
||||
assert delete_from_dict(dictionary=test_dict, key="key[23]", is_regex=True) == {
|
||||
"key1": ["value1"]
|
||||
}
|
||||
|
||||
|
||||
def test_delete_from_dict_7():
|
||||
"""Test delete_from_dict() function.
|
||||
|
||||
GIVEN a dictionary with values as strings
|
||||
WHEN the delete_from_dict() function is called with a key regex that does not match
|
||||
THEN no keys should be deleted from the dictionary
|
||||
"""
|
||||
test_dict = {"key1": ["value1"], "key2": ["value2", "value3"], "key3": "value4"}
|
||||
|
||||
assert delete_from_dict(dictionary=test_dict, key=r"key\d\d", is_regex=True) == test_dict
|
||||
|
||||
|
||||
def test_delete_from_dict_8():
|
||||
"""Test delete_from_dict() function.
|
||||
|
||||
GIVEN a dictionary with values as strings
|
||||
WHEN the delete_from_dict() function is called with a key and value regex that matches
|
||||
THEN the matching keys should be deleted from the dictionary
|
||||
"""
|
||||
test_dict = {"key1": ["value1"], "key2": ["value2", "value3"], "key3": "value4"}
|
||||
|
||||
assert delete_from_dict(dictionary=test_dict, key="key2", value=r"\w+", is_regex=True) == {
|
||||
"key1": ["value1"],
|
||||
"key2": [],
|
||||
"key3": "value4",
|
||||
}
|
||||
|
||||
|
||||
def test_delete_from_dict_9():
|
||||
"""Test delete_from_dict() function.
|
||||
|
||||
GIVEN a dictionary with values as strings
|
||||
WHEN the delete_from_dict() function is called with a key and value regex that does not match
|
||||
THEN no keys should be deleted from the dictionary
|
||||
"""
|
||||
test_dict = {"key1": ["value1"], "key2": ["value2", "value3"], "key3": "value4"}
|
||||
|
||||
assert (
|
||||
delete_from_dict(dictionary=test_dict, key=r"key2", value=r"^\d", is_regex=True)
|
||||
== test_dict
|
||||
)
|
||||
|
||||
|
||||
def test_delete_from_dict_10():
|
||||
"""Test delete_from_dict() function.
|
||||
|
||||
GIVEN a dictionary with values as strings
|
||||
WHEN the delete_from_dict() function is called with a key and value regex that matches
|
||||
THEN the matching keys should be deleted from the dictionary
|
||||
"""
|
||||
test_dict = {"key1": ["value1"], "key2": ["value2", "value3"], "key3": "value4"}
|
||||
|
||||
assert delete_from_dict(dictionary=test_dict, key="key3", value=r"\w+", is_regex=True) == {
|
||||
"key1": ["value1"],
|
||||
"key2": ["value2", "value3"],
|
||||
}
|
||||
|
||||
|
||||
def test_delete_from_dict_11():
|
||||
"""Test delete_from_dict() function.
|
||||
|
||||
GIVEN a dictionary with values as strings
|
||||
WHEN the delete_from_dict() function is called with a key regex that matches multiple and values that match
|
||||
THEN the values matching the associated keys should be deleted from the dictionary
|
||||
"""
|
||||
test_dict = {"key1": ["value1"], "key2": ["value2", "value3"], "key3": "value4"}
|
||||
|
||||
assert delete_from_dict(
|
||||
dictionary=test_dict, key=r"key[23]", value=r"\w+[34]$", is_regex=True
|
||||
) == {"key1": ["value1"], "key2": ["value2"]}
|
||||
|
||||
|
||||
def test_dict_contains() -> None:
|
||||
@@ -25,6 +189,17 @@ def test_dict_contains() -> None:
|
||||
assert dict_contains(d, r"key\d", "value5", is_regex=True) is True
|
||||
|
||||
|
||||
def test_dict_keys_to_lower() -> None:
|
||||
"""Test the dict_keys_to_lower() function.
|
||||
|
||||
GIVEN a dictionary with mixed case keys
|
||||
WHEN the dict_keys_to_lower() function is called
|
||||
THEN the dictionary keys should be converted to lowercase
|
||||
"""
|
||||
test_dict = {"Key1": "Value1", "KEY2": "Value2", "key3": "Value3"}
|
||||
assert dict_keys_to_lower(test_dict) == {"key1": "Value1", "key2": "Value2", "key3": "Value3"}
|
||||
|
||||
|
||||
def test_dict_values_to_lists_strings():
|
||||
"""Test converting dictionary values to lists of strings."""
|
||||
dictionary = {
|
||||
@@ -66,6 +241,78 @@ def test_dict_values_to_lists_strings():
|
||||
}
|
||||
|
||||
|
||||
def test_rename_in_dict_1():
|
||||
"""Test rename_in_dict() function.
|
||||
|
||||
GIVEN a dictionary with values as a list
|
||||
WHEN the rename_in_dict() function is called with a key that does not exist
|
||||
THEN no keys should be renamed in the dictionary
|
||||
"""
|
||||
test_dict = {"key1": ["value1"], "key2": ["value2", "value3"]}
|
||||
|
||||
assert rename_in_dict(dictionary=test_dict, key="key4", value_1="key5") == test_dict
|
||||
|
||||
|
||||
def test_rename_in_dict_2():
|
||||
"""Test rename_in_dict() function.
|
||||
|
||||
GIVEN a dictionary with values as a list
|
||||
WHEN the rename_in_dict() function is called with a key that exists and a new value for the key
|
||||
THEN the key should be renamed in the returned dictionary and the original dictionary should not be modified
|
||||
"""
|
||||
test_dict = {"key1": ["value1"], "key2": ["value2", "value3"]}
|
||||
|
||||
assert rename_in_dict(dictionary=test_dict, key="key2", value_1="new_key") == {
|
||||
"key1": ["value1"],
|
||||
"new_key": ["value2", "value3"],
|
||||
}
|
||||
assert test_dict == {"key1": ["value1"], "key2": ["value2", "value3"]}
|
||||
|
||||
|
||||
def test_rename_in_dict_3():
|
||||
"""Test rename_in_dict() function.
|
||||
|
||||
GIVEN a dictionary with values as a list
|
||||
WHEN the rename_in_dict() function is called with a key that exists value that does not exist
|
||||
THEN the dictionary should not be modified
|
||||
"""
|
||||
test_dict = {"key1": ["value1"], "key2": ["value2", "value3"]}
|
||||
|
||||
assert (
|
||||
rename_in_dict(dictionary=test_dict, key="key2", value_1="no_value", value_2="new_value")
|
||||
== test_dict
|
||||
)
|
||||
|
||||
|
||||
def test_rename_in_dict_4():
|
||||
"""Test rename_in_dict() function.
|
||||
|
||||
GIVEN a dictionary with values as a list
|
||||
WHEN the rename_in_dict() function is called with a key that exists and a new value for a value
|
||||
THEN update the specified value in the dictionary
|
||||
"""
|
||||
test_dict = {"key1": ["value1"], "key2": ["value2", "value3"]}
|
||||
|
||||
assert rename_in_dict(
|
||||
dictionary=test_dict, key="key2", value_1="value2", value_2="new_value"
|
||||
) == {"key1": ["value1"], "key2": ["new_value", "value3"]}
|
||||
|
||||
|
||||
def test_rename_in_dict_5():
|
||||
"""Test rename_in_dict() function.
|
||||
|
||||
GIVEN a dictionary with values as a list
|
||||
WHEN the rename_in_dict() function is called with a key that exists and a an existing value for a renamed value
|
||||
THEN only one instance of the new value should be in the key
|
||||
"""
|
||||
test_dict = {"key1": ["value1"], "key2": ["value2", "value3"]}
|
||||
|
||||
assert rename_in_dict(dictionary=test_dict, key="key2", value_1="value2", value_2="value3") == {
|
||||
"key1": ["value1"],
|
||||
"key2": ["value3"],
|
||||
}
|
||||
|
||||
|
||||
def test_remove_markdown_sections():
|
||||
"""Test removing markdown sections."""
|
||||
text: str = """
|
||||
@@ -106,3 +353,172 @@ def test_clean_dictionary():
|
||||
|
||||
new_dict = clean_dictionary(dictionary)
|
||||
assert new_dict == {"key": ["value", "value2", "value3"]}
|
||||
|
||||
|
||||
def test_validate_csv_bulk_imports_1(tmp_path):
|
||||
"""Test the validate_csv_bulk_imports function.
|
||||
|
||||
GIVEN a csv file missing the `path` column
|
||||
WHEN the validate_csv_bulk_imports function is called
|
||||
THEN an exception should be raised
|
||||
"""
|
||||
csv_path = tmp_path / "test.csv"
|
||||
csv_content = """\
|
||||
PATH,type,key,value
|
||||
note1.md,frontmatter,key,value"""
|
||||
csv_path.write_text(csv_content)
|
||||
|
||||
with pytest.raises(typer.BadParameter):
|
||||
validate_csv_bulk_imports(csv_path=csv_path, note_paths=[])
|
||||
|
||||
|
||||
def test_validate_csv_bulk_imports_2(tmp_path):
|
||||
"""Test the validate_csv_bulk_imports function.
|
||||
|
||||
GIVEN a csv file missing the `type` column
|
||||
WHEN the validate_csv_bulk_imports function is called
|
||||
THEN an exception should be raised
|
||||
"""
|
||||
csv_path = tmp_path / "test.csv"
|
||||
csv_content = """\
|
||||
path,Type,key,value
|
||||
note1.md,frontmatter,key,value"""
|
||||
csv_path.write_text(csv_content)
|
||||
|
||||
with pytest.raises(typer.BadParameter):
|
||||
validate_csv_bulk_imports(csv_path=csv_path, note_paths=[])
|
||||
|
||||
|
||||
def test_validate_csv_bulk_imports_3(tmp_path):
|
||||
"""Test the validate_csv_bulk_imports function.
|
||||
|
||||
GIVEN a csv file missing the `key` column
|
||||
WHEN the validate_csv_bulk_imports function is called
|
||||
THEN an exception should be raised
|
||||
"""
|
||||
csv_path = tmp_path / "test.csv"
|
||||
csv_content = """\
|
||||
path,type,value
|
||||
note1.md,frontmatter,key,value"""
|
||||
csv_path.write_text(csv_content)
|
||||
|
||||
with pytest.raises(typer.BadParameter):
|
||||
validate_csv_bulk_imports(csv_path=csv_path, note_paths=[])
|
||||
|
||||
|
||||
def test_validate_csv_bulk_imports_4(tmp_path):
|
||||
"""Test the validate_csv_bulk_imports function.
|
||||
|
||||
GIVEN a csv file missing the `value` column
|
||||
WHEN the validate_csv_bulk_imports function is called
|
||||
THEN an exception should be raised
|
||||
"""
|
||||
csv_path = tmp_path / "test.csv"
|
||||
csv_content = """\
|
||||
path,type,key,values
|
||||
note1.md,frontmatter,key,value"""
|
||||
csv_path.write_text(csv_content)
|
||||
|
||||
with pytest.raises(typer.BadParameter):
|
||||
validate_csv_bulk_imports(csv_path=csv_path, note_paths=[])
|
||||
|
||||
|
||||
def test_validate_csv_bulk_imports_5(tmp_path):
|
||||
"""Test the validate_csv_bulk_imports function.
|
||||
|
||||
GIVEN a csv file with only headers
|
||||
WHEN the validate_csv_bulk_imports function is called
|
||||
THEN an exception should be raised
|
||||
"""
|
||||
csv_path = tmp_path / "test.csv"
|
||||
csv_content = "path,type,key,value"
|
||||
csv_path.write_text(csv_content)
|
||||
|
||||
with pytest.raises(typer.BadParameter):
|
||||
validate_csv_bulk_imports(csv_path=csv_path, note_paths=[])
|
||||
|
||||
|
||||
def test_validate_csv_bulk_imports_6(tmp_path):
|
||||
"""Test the validate_csv_bulk_imports function.
|
||||
|
||||
GIVEN a valid csv file
|
||||
WHEN a path is given that does not exist in the vault
|
||||
THEN show the user a warning
|
||||
"""
|
||||
csv_path = tmp_path / "test.csv"
|
||||
csv_content = """\
|
||||
path,type,key,value
|
||||
note1.md,frontmatter,key,value
|
||||
note1.md,tag,key,value
|
||||
note1.md,inline_metadata,key,value
|
||||
note1.md,inline_metadata,key2,value
|
||||
note1.md,inline_metadata,key2,value2
|
||||
note2.md,frontmatter,key,value
|
||||
note2.md,tag,key,value
|
||||
note2.md,inline_metadata,key,value
|
||||
note2.md,inline_metadata,key2,value
|
||||
note2.md,inline_metadata,key2,value2
|
||||
"""
|
||||
csv_path.write_text(csv_content)
|
||||
|
||||
with pytest.raises(typer.BadParameter):
|
||||
validate_csv_bulk_imports(csv_path=csv_path, note_paths=["note1.md"])
|
||||
|
||||
|
||||
def test_validate_csv_bulk_imports_7(tmp_path):
|
||||
"""Test the validate_csv_bulk_imports function.
|
||||
|
||||
GIVEN a valid csv file
|
||||
WHEN if a type is not 'frontmatter' or 'inline_metadata', 'tag'
|
||||
THEN exit the program
|
||||
"""
|
||||
csv_path = tmp_path / "test.csv"
|
||||
csv_content = """\
|
||||
path,type,key,value
|
||||
note1.md,frontmatter,key,value
|
||||
note2.md,notvalid,key,value
|
||||
"""
|
||||
csv_path.write_text(csv_content)
|
||||
with pytest.raises(typer.BadParameter):
|
||||
validate_csv_bulk_imports(csv_path=csv_path, note_paths=["note1.md", "note2.md"])
|
||||
|
||||
|
||||
def test_validate_csv_bulk_imports_8(tmp_path):
|
||||
"""Test the validate_csv_bulk_imports function.
|
||||
|
||||
GIVEN a valid csv file
|
||||
WHEN more than one row has the same path
|
||||
THEN add the row to the list of rows for that path
|
||||
"""
|
||||
csv_path = tmp_path / "test.csv"
|
||||
csv_content = """\
|
||||
path,type,key,value
|
||||
note1.md,frontmatter,key,value
|
||||
note1.md,tag,key,value
|
||||
note1.md,inline_metadata,key,value
|
||||
note1.md,inline_metadata,key2,value
|
||||
note1.md,inline_metadata,key2,value2
|
||||
note2.md,frontmatter,key,value
|
||||
note2.md,tag,key,value
|
||||
note2.md,inline_metadata,key,value
|
||||
note2.md,inline_metadata,key2,value
|
||||
note2.md,inline_metadata,key2,value2
|
||||
"""
|
||||
csv_path.write_text(csv_content)
|
||||
csv_dict = validate_csv_bulk_imports(csv_path=csv_path, note_paths=["note1.md", "note2.md"])
|
||||
assert csv_dict == {
|
||||
"note1.md": [
|
||||
{"key": "key", "type": "frontmatter", "value": "value"},
|
||||
{"key": "key", "type": "tag", "value": "value"},
|
||||
{"key": "key", "type": "inline_metadata", "value": "value"},
|
||||
{"key": "key2", "type": "inline_metadata", "value": "value"},
|
||||
{"key": "key2", "type": "inline_metadata", "value": "value2"},
|
||||
],
|
||||
"note2.md": [
|
||||
{"key": "key", "type": "frontmatter", "value": "value"},
|
||||
{"key": "key", "type": "tag", "value": "value"},
|
||||
{"key": "key", "type": "inline_metadata", "value": "value"},
|
||||
{"key": "key2", "type": "inline_metadata", "value": "value"},
|
||||
{"key": "key2", "type": "inline_metadata", "value": "value2"},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ def test_commit_changes_2(test_vault, tmp_path):
|
||||
assert "new_key: new_key_value" not in committed_content
|
||||
|
||||
|
||||
def test_backup_1(test_vault, tmp_path, capsys):
|
||||
def test_backup_1(test_vault, capsys):
|
||||
"""Test the backup method.
|
||||
|
||||
GIVEN a vault object
|
||||
@@ -315,16 +315,16 @@ def test_delete_backup_2(test_vault, capsys):
|
||||
assert vault.backup_path.exists() is True
|
||||
|
||||
|
||||
def test_delete_inline_tag_1(test_vault) -> None:
|
||||
"""Test delete_inline_tag() method.
|
||||
def test_delete_tag_1(test_vault) -> None:
|
||||
"""Test delete_tag() method.
|
||||
|
||||
GIVEN a vault object
|
||||
WHEN the delete_inline_tag method is called
|
||||
WHEN the delete_tag method is called
|
||||
THEN the inline tag is deleted
|
||||
"""
|
||||
vault = Vault(config=test_vault)
|
||||
|
||||
assert vault.delete_inline_tag("intext_tag2") == 1
|
||||
assert vault.delete_tag("intext_tag2") == 1
|
||||
assert vault.metadata.tags == [
|
||||
"inline_tag_bottom1",
|
||||
"inline_tag_bottom2",
|
||||
@@ -335,16 +335,16 @@ def test_delete_inline_tag_1(test_vault) -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_delete_inline_tag_2(test_vault) -> None:
|
||||
"""Test delete_inline_tag() method.
|
||||
def test_delete_tag_2(test_vault) -> None:
|
||||
"""Test delete_tag() method.
|
||||
|
||||
GIVEN a vault object
|
||||
WHEN the delete_inline_tag method is called with a tag that does not exist
|
||||
WHEN the delete_tag method is called with a tag that does not exist
|
||||
THEN no changes are made
|
||||
"""
|
||||
vault = Vault(config=test_vault)
|
||||
|
||||
assert vault.delete_inline_tag("no tag") == 0
|
||||
assert vault.delete_tag("no tag") == 0
|
||||
|
||||
|
||||
def test_delete_metadata_1(test_vault) -> None:
|
||||
@@ -431,6 +431,38 @@ def test_export_json(tmp_path, test_vault):
|
||||
assert '"frontmatter": {' in export_file.read_text()
|
||||
|
||||
|
||||
def test_export_notes_to_csv_1(tmp_path, test_vault):
|
||||
"""Test export_notes_to_csv() method.
|
||||
|
||||
GIVEN a vault object
|
||||
WHEN the export_notes_to_csv method is called with a path
|
||||
THEN the notes are exported to a CSV file
|
||||
"""
|
||||
vault = Vault(config=test_vault)
|
||||
export_file = Path(f"{tmp_path}/export.csv")
|
||||
vault.export_notes_to_csv(path=export_file)
|
||||
assert export_file.exists() is True
|
||||
assert "path,type,key,value" in export_file.read_text()
|
||||
assert "test1.md,frontmatter,shared_key1,shared_key1_value" in export_file.read_text()
|
||||
assert "test1.md,inline_metadata,shared_key1,shared_key1_value" in export_file.read_text()
|
||||
assert "test1.md,tag,,shared_tag" in export_file.read_text()
|
||||
assert "test1.md,frontmatter,tags,📅/frontmatter_tag3" in export_file.read_text()
|
||||
assert "test1.md,inline_metadata,key📅,📅_key_value" in export_file.read_text()
|
||||
|
||||
|
||||
def test_export_notes_to_csv_2(test_vault):
|
||||
"""Test export_notes_to_csv() method.
|
||||
|
||||
GIVEN a vault object
|
||||
WHEN the export_notes_to_csv method is called with a path where the parent directory does not exist
|
||||
THEN an error is raised
|
||||
"""
|
||||
vault = Vault(config=test_vault)
|
||||
export_file = Path("/I/do/not/exist/export.csv")
|
||||
with pytest.raises(typer.Exit):
|
||||
vault.export_notes_to_csv(path=export_file)
|
||||
|
||||
|
||||
def test_get_filtered_notes_1(sample_vault) -> None:
|
||||
"""Test filtering notes.
|
||||
|
||||
@@ -562,16 +594,16 @@ def test_move_inline_metadata_1(test_vault) -> None:
|
||||
assert vault.move_inline_metadata(location=InsertLocation.TOP) == 1
|
||||
|
||||
|
||||
def test_rename_inline_tag_1(test_vault) -> None:
|
||||
"""Test rename_inline_tag() method.
|
||||
def test_rename_tag_1(test_vault) -> None:
|
||||
"""Test rename_tag() method.
|
||||
|
||||
GIVEN a vault object
|
||||
WHEN the rename_inline_tag() method is called with a tag that is found
|
||||
WHEN the rename_tag() method is called with a tag that is found
|
||||
THEN the inline tag is renamed
|
||||
"""
|
||||
vault = Vault(config=test_vault)
|
||||
|
||||
assert vault.rename_inline_tag("intext_tag2", "new_tag") == 1
|
||||
assert vault.rename_tag("intext_tag2", "new_tag") == 1
|
||||
assert vault.metadata.tags == [
|
||||
"inline_tag_bottom1",
|
||||
"inline_tag_bottom2",
|
||||
@@ -583,16 +615,16 @@ def test_rename_inline_tag_1(test_vault) -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_rename_inline_tag_2(test_vault) -> None:
|
||||
"""Test rename_inline_tag() method.
|
||||
def test_rename_tag_2(test_vault) -> None:
|
||||
"""Test rename_tag() method.
|
||||
|
||||
GIVEN a vault object
|
||||
WHEN the rename_inline_tag() method is called with a tag that is not found
|
||||
WHEN the rename_tag() method is called with a tag that is not found
|
||||
THEN the inline tag is not renamed
|
||||
"""
|
||||
vault = Vault(config=test_vault)
|
||||
|
||||
assert vault.rename_inline_tag("no tag", "new_tag") == 0
|
||||
assert vault.rename_tag("no tag", "new_tag") == 0
|
||||
|
||||
|
||||
def test_rename_metadata_1(test_vault) -> None:
|
||||
@@ -688,3 +720,60 @@ def test_transpose_metadata(test_vault) -> None:
|
||||
)
|
||||
== 0
|
||||
)
|
||||
|
||||
|
||||
def test_update_from_dict_1(test_vault):
|
||||
"""Test update_from_dict() method.
|
||||
|
||||
GIVEN a vault object and an update dictionary
|
||||
WHEN no dictionary keys match paths in the vault
|
||||
THEN no notes are updated and 0 is returned
|
||||
"""
|
||||
vault = Vault(config=test_vault)
|
||||
update_dict = {
|
||||
"path1": {"type": "frontmatter", "key": "new_key", "value": "new_value"},
|
||||
"path2": {"type": "frontmatter", "key": "new_key", "value": "new_value"},
|
||||
}
|
||||
|
||||
assert vault.update_from_dict(update_dict) == 0
|
||||
assert vault.get_changed_notes() == []
|
||||
|
||||
|
||||
def test_update_from_dict_2(test_vault):
|
||||
"""Test update_from_dict() method.
|
||||
|
||||
GIVEN a vault object and an update dictionary
|
||||
WHEN the dictionary is empty
|
||||
THEN no notes are updated and 0 is returned
|
||||
"""
|
||||
vault = Vault(config=test_vault)
|
||||
update_dict = {}
|
||||
|
||||
assert vault.update_from_dict(update_dict) == 0
|
||||
assert vault.get_changed_notes() == []
|
||||
|
||||
|
||||
def test_update_from_dict_3(test_vault):
|
||||
"""Test update_from_dict() method.
|
||||
|
||||
GIVEN a vault object and an update dictionary
|
||||
WHEN a dictionary key matches a path in the vault
|
||||
THEN the note is updated to match the dictionary values
|
||||
"""
|
||||
vault = Vault(config=test_vault)
|
||||
|
||||
update_dict = {
|
||||
"test1.md": [
|
||||
{"type": "frontmatter", "key": "new_key", "value": "new_value"},
|
||||
{"type": "inline_metadata", "key": "new_key2", "value": "new_value"},
|
||||
{"type": "tag", "key": "", "value": "new_tag"},
|
||||
]
|
||||
}
|
||||
assert vault.update_from_dict(update_dict) == 1
|
||||
assert vault.get_changed_notes()[0].note_path.name == "test1.md"
|
||||
assert vault.get_changed_notes()[0].frontmatter.dict == {"new_key": ["new_value"]}
|
||||
assert vault.get_changed_notes()[0].inline_metadata.dict == {"new_key2": ["new_value"]}
|
||||
assert vault.get_changed_notes()[0].tags.list == ["new_tag"]
|
||||
assert vault.metadata.frontmatter == {"new_key": ["new_value"]}
|
||||
assert vault.metadata.inline_metadata == {"new_key2": ["new_value"]}
|
||||
assert vault.metadata.tags == ["new_tag"]
|
||||
|
||||
Reference in New Issue
Block a user