From e987948937ea3b6b00e0b31b66ba260b05829458 Mon Sep 17 00:00:00 2001 From: Justin Israel Date: Sun, 24 Mar 2024 14:49:09 +1300 Subject: [PATCH] Make FileSequence perform more accurate equality test using FrameSet (refs #131) --- src/fileseq/filesequence.py | 43 ++++++++++++++----- test/test_unit.py | 86 +++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 11 deletions(-) diff --git a/src/fileseq/filesequence.py b/src/fileseq/filesequence.py index 6b17f98..48855bc 100644 --- a/src/fileseq/filesequence.py +++ b/src/fileseq/filesequence.py @@ -3,6 +3,7 @@ """ from __future__ import annotations +import dataclasses import decimal import fnmatch import functools @@ -55,6 +56,14 @@ class FileSequence: _DEFAULT_PAD_CHAR = '@' + @dataclasses.dataclass + class _Components: + dir: str + base: str + frameSet: FrameSet|str|None + pad: str|int + ext: str + def __init__(self, sequence: str, pad_style: constants._PadStyle = PAD_STYLE_DEFAULT, @@ -753,15 +762,9 @@ def __str__(self): Returns: str: """ - frameSet = utils.asString(self._frameSet or "") - parts = [ - self._dir, - self._base, - frameSet, - self._pad if frameSet else "", - self._ext, - ] - return "".join(parts) + cmpts = self.__components() + cmpts.frameSet = utils.asString(cmpts.frameSet or "") + return "".join(dataclasses.astuple(cmpts)) def __repr__(self): try: @@ -770,10 +773,19 @@ def __repr__(self): return super(self.__class__, self).__repr__() def __eq__(self, other): - return str(self) == str(other) + if not isinstance(other, FileSequence): + return str(self) == str(other) + + a = self.__components() + b = other.__components() + + a.pad = self.getPaddingNum(a.pad) + b.pad = other.getPaddingNum(b.pad) + + return a == b def __ne__(self, other): - return str(self) != str(other) + return not self.__eq__(other) def __hash__(self): # TODO: Technically we should be returning None, @@ -783,6 +795,15 @@ def __hash__(self): # For now, preserving the hashing behaviour in py3. return id(self) + def __components(self): + return self._Components( + self._dir, + self._base, + self._frameSet or "", + self._pad if self._frameSet else "", + self._ext, + ) + @classmethod def yield_sequences_in_list( cls, diff --git a/test/test_unit.py b/test/test_unit.py index cbfc301..1be769c 100755 --- a/test/test_unit.py +++ b/test/test_unit.py @@ -1,6 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +from __future__ import annotations + +import dataclasses import warnings with warnings.catch_warnings(): @@ -243,6 +246,30 @@ def _iter(): return iter([1, 2, 3, 4, 5, 6, 7]) class TestFrameSet(unittest.TestCase): + def testEqual(self): + @dataclasses.dataclass + class Case: + a: FrameSet|str|None + b: FrameSet|str|None + expect: bool + + table = [ + Case(a=FrameSet('1-5'), b=FrameSet('1-5'), expect=True), + Case(a=FrameSet('1-5'), b=FrameSet('1,2,3,4,5'), expect=True), + Case(a=FrameSet('1-3,5-8'), b=FrameSet('1,2,3,5,6-8'), expect=True), + Case(a=FrameSet('1-5'), b=FrameSet('1-4,6'), expect=False), + + # Automatic casting of other object + Case(a=FrameSet('1-5'), b='1-5', expect=True), + Case(a=FrameSet('1-5'), b='1-4', expect=False), + Case(a=FrameSet('1-5'), b=[1,2,3,4,5], expect=True), + Case(a=FrameSet('1-5'), b=[1,2,3,5], expect=False), + ] + + for case in table: + fn = self.assertEqual if case.expect else self.assertNotEqual + fn(case.a, case.b) + def testFrameValues(self): class Case: def __init__(self, src, expected=None, has_subframes=False, err=None): @@ -640,6 +667,65 @@ def __getitem__(self, item): class TestFileSequence(TestBase): + def testToStr(self): + @dataclasses.dataclass + class Case: + seq: FileSequence + expect: str + + FS = FileSequence + table = [ + Case(FS("/dir/file.1-5#.ext"), "/dir/file.1-5#.ext"), + Case(FS("/dir/file.0001.ext"), "/dir/file.0001#.ext"), + Case(FS("/dir/file.1.ext"), "/dir/file.1@.ext"), + Case(FS("/dir/file.ext"), "/dir/file.ext"), + Case(FS("/dir/file"), "/dir/file"), + Case(FS("/dir/.ext"), "/dir/.ext"), + Case(FS("file"), "file"), + ] + + for case in table: + actual = str(case.seq) + self.assertEqual(case.expect, actual) + + fs = FS("/dir/file.1.ext") + fs._frameSet = None + actual = str(fs) + self.assertEqual("/dir/file..ext", actual) + + def testEqual(self): + @dataclasses.dataclass + class Case: + a: FileSequence|str|None + b: FileSequence|str|None + expect: bool + + FS = FileSequence + table = [ + Case(a=FS('/dir/file.1-5#.ext'), b=FS('/dir/file.1-5#.ext'), expect=True), + Case(a=FS('/dir/file.1-5#.ext'), b=FS('/dir/file.1-5@@@@.ext'), expect=True), + Case(a=FS('/dir/file.1-5#.ext'), b=FS('/dir/file.1-5@.ext'), expect=False), + Case(a=FS('/dir/file.0001.ext'), b=FS('/dir/file.0001.ext'), expect=True), + Case(a=FS('file.1-5#.ext'), b=FS('file.1-5#.ext'), expect=True), + Case(a=FS('file.1-5#'), b=FS('file.1-5#'), expect=True), + Case(a=FS('file.ext'), b=FS('file.ext'), expect=True), + Case(a=FS('file'), b=FS('file'), expect=True), + + Case(a=FS('/dir/file.1-5#.ext'), b=FS('/dir/file.1,2,3,4,5#.ext'), expect=True), + Case(a=FS('/dir/file.1-5#.ext'), b=FS('/dir/file.1,2,3,4,5#.ext'), expect=True), + Case(a=FS('/dir/file.1-3,5-8#.ext'), b=FS('/dir/file.1,2,3,5,6-8#.ext'), expect=True), + Case(a=FS('/dir/file.1-5#.ext'), b=FS('/dir/file.1-4,6#.ext'), expect=False), + + # Automatic casting of other object, to string + Case(a=FS('/dir/file.1-5#.ext'), b='/dir/file.1-5#.ext', expect=True), + Case(a=FS('/dir/file.1-5#.ext'), b='/dir/file.1,2,3,4,5#.ext', expect=False), + Case(a=FS('/dir/file.1-5#.ext'), b='/dir/file.1-4#.ext', expect=False), + ] + + for case in table: + fn = self.assertEqual if case.expect else self.assertNotEqual + fn(case.a, case.b) + def testNativeStr(self): seq = FileSequence("/foo/boo.1-5#.exr") self.assertNativeStr(seq.dirname())