From 9e9266ec9c890ed6fb09d61b1a075be954bef7c1 Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Sat, 28 May 2022 22:07:31 -0700 Subject: [PATCH] Added slice, sslice filters --- osxphotos/phototemplate.md | 2 ++ osxphotos/phototemplate.py | 37 +++++++++++++++++++++++++++++++++++++ tests/test_template.py | 27 +++++++++++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/osxphotos/phototemplate.md b/osxphotos/phototemplate.md index b6f7f1f5..d38ac59e 100644 --- a/osxphotos/phototemplate.md +++ b/osxphotos/phototemplate.md @@ -55,6 +55,8 @@ Valid filters are: - `append(x)`: Append x to list of values, e.g. append(d): ['a', 'b', 'c'] => ['a', 'b', 'c', 'd']. - `prepend(x)`: Prepend x to list of values, e.g. prepend(d): ['a', 'b', 'c'] => ['d', 'a', 'b', 'c']. - `remove(x)`: Remove x from list of values, e.g. remove(b): ['a', 'b', 'c'] => ['a', 'c']. +- `slice(start:stop:step)`: Slice list using same semantics as Python's list slicing, e.g. slice(1:3): ['a', 'b', 'c', 'd'] => ['b', 'c']; slice(1:4:2): ['a', 'b', 'c', 'd'] => ['b', 'd']; slice(1:): ['a', 'b', 'c', 'd'] => ['b', 'c', 'd']; slice(:-1): ['a', 'b', 'c', 'd'] => ['a', 'b', 'c']; slice(::-1): ['a', 'b', 'c', 'd'] => ['d', 'c', 'b', 'a']. See also sslice(). +- `sslice(start:stop:step)`: [s(tring) slice] Slice values in a list using same semantics as Python's string slicing, e.g. sslice(1:3):'abcd => 'bc'; sslice(1:4:2): 'abcd' => 'bd', etc. See also slice(). e.g. if Photo keywords are `["FOO","bar"]`: diff --git a/osxphotos/phototemplate.py b/osxphotos/phototemplate.py index 4992c8a9..92804547 100644 --- a/osxphotos/phototemplate.py +++ b/osxphotos/phototemplate.py @@ -260,6 +260,12 @@ FILTER_VALUES = { "append(x)": "Append x to list of values, e.g. append(d): ['a', 'b', 'c'] => ['a', 'b', 'c', 'd'].", "prepend(x)": "Prepend x to list of values, e.g. prepend(d): ['a', 'b', 'c'] => ['d', 'a', 'b', 'c'].", "remove(x)": "Remove x from list of values, e.g. remove(b): ['a', 'b', 'c'] => ['a', 'c'].", + "slice(start:stop:step)": "Slice list using same semantics as Python's list slicing, " + + "e.g. slice(1:3): ['a', 'b', 'c', 'd'] => ['b', 'c']; slice(1:4:2): ['a', 'b', 'c', 'd'] => ['b', 'd']; " + + "slice(1:): ['a', 'b', 'c', 'd'] => ['b', 'c', 'd']; slice(:-1): ['a', 'b', 'c', 'd'] => ['a', 'b', 'c']; " + + "slice(::-1): ['a', 'b', 'c', 'd'] => ['d', 'c', 'b', 'a']. See also sslice().", + "sslice(start:stop:step)": "[s(tring) slice] Slice values in a list using same semantics as Python's string slicing, " + + "e.g. sslice(1:3):'abcd => 'bc'; sslice(1:4:2): 'abcd' => 'bd', etc. See also slice().", } # Just the substitutions without the braces @@ -1166,6 +1172,8 @@ class PhotoTemplate: "append", "prepend", "remove", + "slice", + "sslice", ] and (args is None or not len(args)): raise SyntaxError(f"{filter_} requires arguments") @@ -1247,6 +1255,13 @@ class PhotoTemplate: elif filter_ == "remove": # remove value from list value = [v for v in values if v != args] + elif filter_ == "slice": + # slice list of values + value = values[create_slice(args)] + elif filter_ == "sslice": + # slice each value in a list + slice_ = create_slice(args) + value = [v[slice_] for v in values] elif filter_.startswith("function:"): value = self.get_template_value_filter_function(filter_, args, values) else: @@ -1671,3 +1686,25 @@ def _get_detected_text(photo, confidence=TEXT_DETECTION_CONFIDENCE_THRESHOLD): # so the first time this gets called is slow but repeated accesses are fast detected_text = photo._detected_text() return [text for text, conf in detected_text if conf >= confidence] + + +def create_slice(args): + """Create a slice object from a string of args in form "start:end:step" """ + slice_args = args.split(":") + if len(slice_args) == 1: + start = int(slice_args[0] or 0) + end = None + step = None + elif len(slice_args) == 2: + start, end = slice_args + start = int(start) if start != "" else None + end = int(end) if end != "" else None + step = None + elif len(slice_args) == 3: + start, end, step = slice_args + start = int(start) if start != "" else None + end = int(end) if end != "" else None + step = int(step) if step != "" else None + else: + raise SyntaxError(f"Invalid slice: {args}") + return slice(start, end, step) diff --git a/tests/test_template.py b/tests/test_template.py index 8d587b42..6e3cb411 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -227,6 +227,15 @@ TEMPLATE_VALUES = { "{descr|chop(6)|autosplit|append(Restaurant)|join( )}": "Jack Rose Dining Restaurant", "{descr|chomp(4)|autosplit|prepend(Mack)|join( )}": "Mack Rose Dining Saloon", "{descr|autosplit|remove(Rose)|join( )}": "Jack Dining Saloon", + "{descr|sslice(0:3)}": "Jac", + "{descr|sslice(5:11)}": "Rose D", + "{descr|sslice(:-6)}": "Jack Rose Dining ", + "{descr|sslice(::2)}": "Jc oeDnn aon", + "{descr|autosplit|slice(1:3)|join()}": "RoseDining", + "{descr|autosplit|slice(2:)|join()}": "DiningSaloon", + "{descr|autosplit|slice(:2)|join()}": "JackRose", + "{descr|autosplit|slice(:-1)|join()}": "JackRoseDining", + "{descr|autosplit|slice(::2)|join()}": "JackDining", } @@ -1297,3 +1306,21 @@ def test_moment(photosdb): for template, value in UUID_MOMENT[uuid]["templates"].items(): rendered, _ = photo.render_template(template) assert rendered == value + + +def test_bad_slice(photosdb): + """Test invalid {|slice} filter""" + photo = photosdb.get_photo(UUID_MULTI_KEYWORDS) + # bad field raises SyntaxError + # bad function raises ValueError + with pytest.raises((SyntaxError, ValueError)): + rendered, _ = photo.render_template("{photo.original_filename|slice(1:2:3:4)}") + + +def test_bad_sslice(photosdb): + """Test invalid {|sslice} filter""" + photo = photosdb.get_photo(UUID_MULTI_KEYWORDS) + # bad field raises SyntaxError + # bad function raises ValueError + with pytest.raises((SyntaxError, ValueError)): + rendered, _ = photo.render_template("{photo.original_filename|sslice(1:2:3:4)}")