diff --git a/README.md b/README.md index 10cbf66b..5b1ca4d6 100644 --- a/README.md +++ b/README.md @@ -791,8 +791,8 @@ example "{title}" which would resolve to the title of the photo. Template statements may contain one or more modifiers. The full syntax is: -"pretext{delim+template_field:subfield|filter(path_sep)[find,replace]?bool_value -,default}posttext" +"pretext{delim+template_field:subfield|filter(path_sep)[find,replace] +conditional?bool_value,default}posttext" Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement. @@ -865,13 +865,68 @@ another find|replace pair. e.g. to replace both "/" and ":" in album name: "{album[/,-|:,-]}". find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair. -?bool_value: Template fields may be evaluated as boolean by appending "?" after -the field name (and following "(path_sep)" or "[find/replace]". If a field is -True (e.g. photo is HDR and field is "{hdr}") or has any value, the value -following the "?" will be used to render the template instead of the actual -field value. If the template field evaluates to False (e.g. in above example, -photo is not HDR) or has no value (e.g. photo has no title and field is -"{title}") then the default value following a "," will be used. +conditional: optional conditional expression that is evaluated as boolean +(True/False) for use with the ?bool_value modifier. Conditional expressions +take the form ' not operator value' where not is an optional modifier that +negates the operator. Note: the space before the conditional expression is +required if you use a conditional expression. Valid comparison operators are: + + • contains: template field contains value, similar to python's in + • matches: template field contains exactly value, unlike contains: does not + match partial matches + • startswith: template field starts with value + • endswith: template field ends with value + • <=: template field is less than or equal to value + • >=: template field is greater than or equal to value + • <: template field is less than value + • >: template field is greater than value + • ==: template field equals value + • !=: template field does not equal value + +The value part of the conditional expression is treated as a bare (unquoted) +word/phrase. Multiple values may be separated by '|' (the pipe symbol). value +is itself a template statement so you can use one or more template fields in +value which will be resolved before the comparison occurs. + +For example: + + • {keyword matches Beach} resolves to True if 'Beach' is a keyword. It would + not match keyword 'BeachDay'. + • {keyword contains Beach} resolves to True if any keyword contains the word + 'Beach' so it would match both 'Beach' and 'BeachDay'. + • {photo.score.overall > 0.7} resolves to True if the photo's overall aesthetic + score is greater than 0.7. + • {keyword|lower contains beach} uses the lower case filter to do + case-insensitive matching to match any keyword that contains the word + 'beach'. + • {keyword|lower not contains beach} uses the not modifier to negate the + comparison so this resolves to True if there is no keyword that matches + 'beach'. + +Examples: to export photos that contain certain keywords with the osxphotos +export command's --directory option: + +--directory "{keyword|lower matches +travel|vacation?Travel-Photos,Not-Travel-Photos}" + +This exports any photo that has keywords 'travel' or 'vacation' into a directory +'Travel-Photos' and all other photos into directory 'Not-Travel-Photos'. + +This can be used to rename files as well, for example: --filename +"{favorite?Favorite-{original_name},{original_name}}" + +This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where +'ImageName.jpg' is the original name of the photo) and all other photos with the +unmodified original name. + +?bool_value: Template fields may be evaluated as boolean (True/False) by +appending "?" after the field name (and following "(path_sep)" or +"[find/replace]". If a field is True (e.g. photo is HDR and field is "{hdr}") +or has any value, the value following the "?" will be used to render the +template instead of the actual field value. If the template field evaluates to +False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has +no title and field is "{title}") then the default value following a "," will be +used. e.g. if photo is an HDR image, @@ -1156,6 +1211,7 @@ Substitution Description {comma} A comma: ',' {semicolon} A semicolon: ';' +{questionmark} A question mark: '?' {pipe} A vertical pipe: '|' {openbrace} An open brace: '{' {closebrace} A close brace: '}' @@ -2101,7 +2157,7 @@ In its simplest form, a template statement has the form: `"{template_field}"`, f Template statements may contain one or more modifiers. The full syntax is: -`"pretext{delim+template_field:subfield|filter(path_sep)[find,replace]?bool_value,default}posttext"` +`"pretext{delim+template_field:subfield|filter(path_sep)[find,replace] conditional?bool_value,default}posttext"` Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement. @@ -2159,7 +2215,41 @@ e.g. If Photo is in `Album1` in `Folder1`: `[find|replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair. -`?bool_value`: Template fields may be evaluated as boolean by appending "?" after the field name (and following "(path_sep)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is `"{hdr}"`) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is `"{title}"`) then the default value following a "," will be used. +`conditional`: optional conditional expression that is evaluated as boolean (True/False) for use with the `?bool_value` modifier. Conditional expressions take the form '` not operator value`' where `not` is an optional modifier that negates the `operator`. Note: the space before the conditional expression is required if you use a conditional expression. Valid comparison operators are: + +- `contains`: template field contains value, similar to python's `in` +- `matches`: template field contains exactly value, unlike `contains`: does not match partial matches +- `startswith`: template field starts with value +- `endswith`: template field ends with value +- `<=`: template field is less than or equal to value +- `>=`: template field is greater than or equal to value +- `<`: template field is less than value +- `>`: template field is greater than value +- `==`: template field equals value +- `!=`: template field does not equal value + +The `value` part of the conditional expression is treated as a bare (unquoted) word/phrase. Multiple values may be separated by '|' (the pipe symbol). `value` is itself a template statement so you can use one or more template fields in `value` which will be resolved before the comparison occurs. + +For example: + +- `{keyword matches Beach}` resolves to True if 'Beach' is a keyword. It would not match keyword 'BeachDay'. +- `{keyword contains Beach}` resolves to True if any keyword contains the word 'Beach' so it would match both 'Beach' and 'BeachDay'. +- `{photo.score.overall > 0.7}` resolves to True if the photo's overall aesthetic score is greater than 0.7. +- `{keyword|lower contains beach}` uses the lower case filter to do case-insensitive matching to match any keyword that contains the word 'beach'. +- `{keyword|lower not contains beach}` uses the `not` modifier to negate the comparison so this resolves to True if there is no keyword that matches 'beach'. + +Examples: to export photos that contain certain keywords with the `osxphotos export` command's `--directory` option: + +`--directory "{keyword|lower matches travel|vacation?Travel-Photos,Not-Travel-Photos}"` + +This exports any photo that has keywords 'travel' or 'vacation' into a directory 'Travel-Photos' and all other photos into directory 'Not-Travel-Photos'. + +This can be used to rename files as well, for example: +`--filename "{favorite?Favorite-{original_name},{original_name}}"` + +This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where 'ImageName.jpg' is the original name of the photo) and all other photos with the unmodified original name. + +`?bool_value`: Template fields may be evaluated as boolean (True/False) by appending "?" after the field name (and following "(path_sep)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is `"{hdr}"`) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is `"{title}"`) then the default value following a "," will be used. e.g. if photo is an HDR image, @@ -2781,6 +2871,7 @@ The following template field substitutions are availabe for use with `PhotoInfo. |{uuid}|Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'| |{comma}|A comma: ','| |{semicolon}|A semicolon: ';'| +|{questionmark}|A question mark: '?'| |{pipe}|A vertical pipe: '|'| |{openbrace}|An open brace: '{'| |{closebrace}|A close brace: '}'| diff --git a/docs/.buildinfo b/docs/.buildinfo index 987087a8..7e81afe2 100644 --- a/docs/.buildinfo +++ b/docs/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 0179796ca3088aa7a0ec190387bfe684 +config: 3a4409f9ef528dee2e4ce25074f0e795 tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/_modules/index.html b/docs/_modules/index.html index f13c30be..4a261be2 100644 --- a/docs/_modules/index.html +++ b/docs/_modules/index.html @@ -5,7 +5,7 @@ - Overview: module code — osxphotos 0.41.10 documentation + Overview: module code — osxphotos 0.42.00 documentation diff --git a/docs/_static/documentation_options.js b/docs/_static/documentation_options.js index 4e70fb3f..e808a3d7 100644 --- a/docs/_static/documentation_options.js +++ b/docs/_static/documentation_options.js @@ -1,6 +1,6 @@ var DOCUMENTATION_OPTIONS = { URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), - VERSION: '0.41.10', + VERSION: '0.42.00', LANGUAGE: 'None', COLLAPSE_INDEX: false, BUILDER: 'html', diff --git a/docs/cli.html b/docs/cli.html index db7a47ee..774e3596 100644 --- a/docs/cli.html +++ b/docs/cli.html @@ -5,7 +5,7 @@ - osxphotos command line interface (CLI) — osxphotos 0.41.10 documentation + osxphotos command line interface (CLI) — osxphotos 0.42.00 documentation diff --git a/docs/genindex.html b/docs/genindex.html index 16fc78e5..7cf1dad5 100644 --- a/docs/genindex.html +++ b/docs/genindex.html @@ -5,7 +5,7 @@ - Index — osxphotos 0.41.10 documentation + Index — osxphotos 0.42.00 documentation diff --git a/docs/index.html b/docs/index.html index f1a956dd..faa0a11f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -5,7 +5,7 @@ - Welcome to osxphotos’s documentation! — osxphotos 0.41.10 documentation + Welcome to osxphotos’s documentation! — osxphotos 0.42.00 documentation diff --git a/docs/modules.html b/docs/modules.html index ed73cff1..15676719 100644 --- a/docs/modules.html +++ b/docs/modules.html @@ -5,7 +5,7 @@ - osxphotos — osxphotos 0.41.10 documentation + osxphotos — osxphotos 0.42.00 documentation diff --git a/docs/osxphotos.pdf b/docs/osxphotos.pdf index 0b3634a6..7e728e36 100644 Binary files a/docs/osxphotos.pdf and b/docs/osxphotos.pdf differ diff --git a/docs/reference.html b/docs/reference.html index 7ac9c831..1a0580a8 100644 --- a/docs/reference.html +++ b/docs/reference.html @@ -5,7 +5,7 @@ - osxphotos package — osxphotos 0.41.10 documentation + osxphotos package — osxphotos 0.42.00 documentation diff --git a/docs/search.html b/docs/search.html index 7ea5b33e..db623957 100644 --- a/docs/search.html +++ b/docs/search.html @@ -5,7 +5,7 @@ - Search — osxphotos 0.41.10 documentation + Search — osxphotos 0.42.00 documentation diff --git a/osxphotos/_version.py b/osxphotos/_version.py index 9d35c4ab..da54677a 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,3 +1,3 @@ """ version info """ -__version__ = "0.41.11" +__version__ = "0.42.00" diff --git a/osxphotos/phototemplate.md b/osxphotos/phototemplate.md index b464c8c5..af41db26 100644 --- a/osxphotos/phototemplate.md +++ b/osxphotos/phototemplate.md @@ -4,7 +4,7 @@ In its simplest form, a template statement has the form: `"{template_field}"`, f Template statements may contain one or more modifiers. The full syntax is: -`"pretext{delim+template_field:subfield|filter(path_sep)[find,replace]?bool_value,default}posttext"` +`"pretext{delim+template_field:subfield|filter(path_sep)[find,replace] conditional?bool_value,default}posttext"` Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement. @@ -62,7 +62,41 @@ e.g. If Photo is in `Album1` in `Folder1`: `[find|replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair. -`?bool_value`: Template fields may be evaluated as boolean by appending "?" after the field name (and following "(path_sep)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is `"{hdr}"`) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is `"{title}"`) then the default value following a "," will be used. +`conditional`: optional conditional expression that is evaluated as boolean (True/False) for use with the `?bool_value` modifier. Conditional expressions take the form '` not operator value`' where `not` is an optional modifier that negates the `operator`. Note: the space before the conditional expression is required if you use a conditional expression. Valid comparison operators are: + +- `contains`: template field contains value, similar to python's `in` +- `matches`: template field contains exactly value, unlike `contains`: does not match partial matches +- `startswith`: template field starts with value +- `endswith`: template field ends with value +- `<=`: template field is less than or equal to value +- `>=`: template field is greater than or equal to value +- `<`: template field is less than value +- `>`: template field is greater than value +- `==`: template field equals value +- `!=`: template field does not equal value + +The `value` part of the conditional expression is treated as a bare (unquoted) word/phrase. Multiple values may be separated by '|' (the pipe symbol). `value` is itself a template statement so you can use one or more template fields in `value` which will be resolved before the comparison occurs. + +For example: + +- `{keyword matches Beach}` resolves to True if 'Beach' is a keyword. It would not match keyword 'BeachDay'. +- `{keyword contains Beach}` resolves to True if any keyword contains the word 'Beach' so it would match both 'Beach' and 'BeachDay'. +- `{photo.score.overall > 0.7}` resolves to True if the photo's overall aesthetic score is greater than 0.7. +- `{keyword|lower contains beach}` uses the lower case filter to do case-insensitive matching to match any keyword that contains the word 'beach'. +- `{keyword|lower not contains beach}` uses the `not` modifier to negate the comparison so this resolves to True if there is no keyword that matches 'beach'. + +Examples: to export photos that contain certain keywords with the `osxphotos export` command's `--directory` option: + +`--directory "{keyword|lower matches travel|vacation?Travel-Photos,Not-Travel-Photos}"` + +This exports any photo that has keywords 'travel' or 'vacation' into a directory 'Travel-Photos' and all other photos into directory 'Not-Travel-Photos'. + +This can be used to rename files as well, for example: +`--filename "{favorite?Favorite-{original_name},{original_name}}"` + +This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where 'ImageName.jpg' is the original name of the photo) and all other photos with the unmodified original name. + +`?bool_value`: Template fields may be evaluated as boolean (True/False) by appending "?" after the field name (and following "(path_sep)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is `"{hdr}"`) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is `"{title}"`) then the default value following a "," will be used. e.g. if photo is an HDR image, diff --git a/osxphotos/phototemplate.py b/osxphotos/phototemplate.py index de55c75a..bf523cc6 100644 --- a/osxphotos/phototemplate.py +++ b/osxphotos/phototemplate.py @@ -120,6 +120,7 @@ TEMPLATE_SUBSTITUTIONS = { "{uuid}": "Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'", "{comma}": "A comma: ','", "{semicolon}": "A semicolon: ';'", + "{questionmark}": "A question mark: '?'", "{pipe}": "A vertical pipe: '|'", "{openbrace}": "An open brace: '{'", "{closebrace}": "A close brace: '}'", @@ -191,6 +192,7 @@ PUNCTUATION = { "closeparens": ")", "openbracket": "[", "closebracket": "]", + "questionmark": "?", } @@ -323,17 +325,6 @@ class PhotoTemplate: unmatched=unmatched, ) - # process find/replace - if ts.template and ts.template.findreplace: - new_results = [] - for result in results: - for pair in ts.template.findreplace.pairs: - find = pair.find or "" - repl = pair.replace or "" - result = result.replace(find, repl) - new_results.append(result) - results = new_results - rendered_strings = results if filename: @@ -430,6 +421,30 @@ class PhotoTemplate: else: default = [] + # process conditional + if ts.template.conditional is not None: + operator = ts.template.conditional.operator + negation = ts.template.conditional.negation + if ts.template.conditional.value is not None: + # conditional value is also a TemplateString + conditional_value, u = self._render_statement( + ts.template.conditional.value, + none_str=none_str, + path_sep=path_sep, + expand_inplace=expand_inplace, + inplace_sep=inplace_sep, + filename=filename, + dirname=dirname, + ) + unmatched.extend(u) + else: + # this shouldn't happen + conditional_value = [""] + else: + operator = None + negation = None + conditional_value = [] + vals = [] if field in SINGLE_VALUE_SUBSTITUTIONS: vals = self.get_template_value( @@ -458,14 +473,6 @@ class PhotoTemplate: vals = [val for val in vals if val is not None] - if is_bool: - if not vals: - vals = default - else: - vals = bool_val - elif not vals: - vals = default or [none_str] - if expand_inplace or delim is not None: sep = delim if delim is not None else inplace_sep vals = [sep.join(sorted(vals))] @@ -473,6 +480,99 @@ class PhotoTemplate: for filter_ in filters: vals = self.get_template_value_filter(filter_, vals) + # process find/replace + if ts.template.findreplace: + new_vals = [] + for val in vals: + for pair in ts.template.findreplace.pairs: + find = pair.find or "" + repl = pair.replace or "" + val = val.replace(find, repl) + new_vals.append(val) + vals = new_vals + + if operator: + # have a conditional operator + + def string_test(test_function): + """ Perform string comparison using test_function; closure to capture conditional_value, vals, negation """ + match = False + for c in conditional_value: + for v in vals: + if test_function(v, c): + match = True + break + if match: + break + if (match and not negation) or (negation and not match): + return ["True"] + else: + return [] + + def comparison_test(test_function): + """ Perform numerical comparisons using test_function; closure to capture conditional_val, vals, negation """ + if len(vals) != 1 or len(conditional_value) != 1: + raise ValueError( + f"comparison operators may only be used with a single value: {vals} {conditional_value}" + ) + try: + match = ( + True + if test_function( + float(vals[0]), float(conditional_value[0]) + ) + else False + ) + if (match and not negation) or (negation and not match): + return ["True"] + else: + return [] + except ValueError as e: + raise ValueError( + f"comparison operators may only be used with values that can be converted to numbers: {vals} {conditional_value}" + ) + + if operator in ["contains", "matches", "startswith", "endswith"]: + # process any "or" values separated by "|" + temp_values = [] + for c in conditional_value: + temp_values.extend(c.split("|")) + conditional_value = temp_values + + if operator == "contains": + vals = string_test(lambda v, c: c in v) + elif operator == "matches": + vals = string_test(lambda v, c: v == c) + elif operator == "startswith": + vals = string_test(lambda v, c: v.startswith(c)) + elif operator == "endswith": + vals = string_test(lambda v, c: v.endswith(c)) + elif operator == "==": + match = sorted(vals) == sorted(conditional_value) + if (match and not negation) or (negation and not match): + vals = ["True"] + else: + vals = [] + elif operator == "!=": + match = sorted(vals) != sorted(conditional_value) + if (match and not negation) or (negation and not match): + vals = ["True"] + else: + vals = [] + elif operator == "<": + vals = comparison_test(lambda v, c: v < c) + elif operator == "<=": + vals = comparison_test(lambda v, c: v <= c) + elif operator == ">": + vals = comparison_test(lambda v, c: v > c) + elif operator == ">=": + vals = comparison_test(lambda v, c: v >= c) + + if is_bool: + vals = default if not vals else bool_val + elif not vals: + vals = default or [none_str] + pre = ts.pre or "" post = ts.post or "" diff --git a/osxphotos/phototemplate.tx b/osxphotos/phototemplate.tx index 4a9fb89b..3914d975 100644 --- a/osxphotos/phototemplate.tx +++ b/osxphotos/phototemplate.tx @@ -1,6 +1,6 @@ // OSXPhotos Template Language (OTL) // a TemplateString has format: -// pre{delim+template_field:subfield|filter(path_sep)[find,replace]?bool_value,default}post +// pre{delim+template_field:subfield|filter(path_sep)[find,replace] conditional?bool_value,default}post // a TemplateStatement may contain zero or more TemplateStrings // The pre and post are optional strings // The template itself (inside the {}) is also optional but if present @@ -25,6 +25,7 @@ Template: filter=Filter pathsep=PathSep findreplace=FindReplace + conditional=Conditional bool=Boolean default=Default "}" @@ -32,7 +33,7 @@ Template: ; NON_TEMPLATE_STRING: - /[^\{\},]*/ + /[^\{\},\?]*/ ; Delim: @@ -76,6 +77,24 @@ FILTER_WORD: /[\.\w]+/ ; +Conditional: + ( + (" "+)- + (negation=NEGATION)? + (operator=OPERATOR) + (" "+)- + (value=Statement) + )? +; + +NEGATION: + "not " +; + +OPERATOR: + "contains" | "matches" | "startswith" | "endswith" | "<=" | ">=" | "<" | ">" | "==" | "!=" +; + PathSep: ( "(" diff --git a/tests/test_template.py b/tests/test_template.py index 35055964..511b6470 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -283,6 +283,50 @@ UUID_PHOTO = { "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51": {"{photo.favorite}": ["favorite"]}, } +UUID_CONDITIONAL = { + "3DD2C897-F19E-4CA6-8C22-B027D5A71907": { + "{title matches Elder Park?YES,NO}": ["YES"], + "{title matches not Elder Park?YES,NO}": ["NO"], + "{title contains Park?YES,NO}": ["YES"], + "{title not contains Park?YES,NO}": ["NO"], + "{title matches Park?YES,NO}": ["NO"], + "{title matches Elder Park?YES,NO}": ["YES"], + "{title == Elder Park?YES,NO}": ["YES"], + "{title != Elder Park?YES,NO}": ["NO"], + "{title[ ,] == ElderPark?YES,NO}": ["YES"], + "{title not != Elder Park?YES,NO}": ["YES"], + "{title not == Elder Park?YES,NO}": ["NO"], + "{title endswith Park?YES,NO}": ["YES"], + "{title endswith Elder?YES,NO}": ["NO"], + "{title startswith Elder?YES,NO}": ["YES"], + "{title endswith Elder?YES,NO}": ["NO"], + "{photo.place.name contains Adelaide?YES,NO}": ["YES"], + "{photo.place.name|lower contains adelaide?YES,NO}": ["YES"], + "{photo.place.name|lower not contains adelaide?YES,NO}": ["NO"], + "{photo.score.overall < 0.7?YES,NO}": ["YES"], + "{photo.score.overall <= 0.7?YES,NO}": ["YES"], + "{photo.score.overall > 0.7?YES,NO}": ["NO"], + "{photo.score.overall >= 0.7?YES,NO}": ["NO"], + "{photo.score.overall not < 0.7?YES,NO}": ["NO"], + "{folder_album(-) contains Folder1-SubFolder2-AlbumInFolder?YES,NO}": ["YES"], + "{folder_album(-)[In,] contains Folder1-SubFolder2-AlbumFolder?YES,NO}": [ + "YES" + ], + }, + "DC99FBDD-7A52-4100-A5BB-344131646C30": { + "{keyword == {keyword}?YES,NO}": ["YES"], + "{keyword contains England?YES,NO}": ["YES"], + "{keyword contains Eng?YES,NO}": ["YES"], + "{keyword contains Foo?YES,NO}": ["NO"], + "{keyword matches England?YES,NO}": ["YES"], + "{keyword matches Eng?YES,NO}": ["NO"], + "{keyword contains Foo|Bar|England?YES,NO}": ["YES"], + "{keyword contains Foo|Bar?YES,NO}": ["NO"], + "{keyword matches Foo|Bar|England?YES,NO}": ["YES"], + "{keyword matches Foo|Bar?YES,NO}": ["NO"], + }, +} + @pytest.fixture(scope="module") def photosdb_places(): @@ -913,3 +957,11 @@ def test_photo_template(photosdb): for template in UUID_PHOTO[uuid]: rendered, _ = photo.render_template(template) assert sorted(rendered) == sorted(UUID_PHOTO[uuid][template]) + + +def test_conditional(photosdb): + for uuid in UUID_CONDITIONAL: + photo = photosdb.get_photo(uuid) + for template in UUID_CONDITIONAL[uuid]: + rendered, _ = photo.render_template(template) + assert sorted(rendered) == sorted(UUID_CONDITIONAL[uuid][template])