@@ -657,6 +657,175 @@ def testBatches(self):
657657 actual = list (FrameSet (case .input ).batches (case .batch , frames = False ))
658658 self .assertListEqual (expect , actual , msg = str (case ))
659659
660+ def testWhitespaceHandling (self ):
661+ """
662+ Issue #137: Whitespace tolerance in frame range parsing.
663+
664+ Test that FrameSet correctly handles whitespace in various positions:
665+ - Spaces after commas
666+ - Spaces around range operators
667+ - Leading/trailing spaces
668+ - Spaces with modifiers (x, y, :)
669+ - Tabs and newlines
670+ """
671+ @dataclasses .dataclass
672+ class Case :
673+ input : str
674+ expected_frange : str
675+ expected_frames : list [int ]
676+ description : str
677+
678+ table = [
679+ # Basic whitespace after commas
680+ Case (
681+ input = '1, 2, 3' ,
682+ expected_frange = '1,2,3' ,
683+ expected_frames = [1 , 2 , 3 ],
684+ description = 'spaces after commas'
685+ ),
686+ Case (
687+ input = '1 , 2 , 3' ,
688+ expected_frange = '1,2,3' ,
689+ expected_frames = [1 , 2 , 3 ],
690+ description = 'spaces before and after commas'
691+ ),
692+
693+ # Whitespace around range operators
694+ Case (
695+ input = '1 - 10' ,
696+ expected_frange = '1-10' ,
697+ expected_frames = [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ],
698+ description = 'spaces around dash'
699+ ),
700+ Case (
701+ input = '1 - 10' ,
702+ expected_frange = '1-10' ,
703+ expected_frames = [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ],
704+ description = 'multiple spaces around dash'
705+ ),
706+
707+ # Leading and trailing whitespace
708+ Case (
709+ input = ' 1-5 ' ,
710+ expected_frange = '1-5' ,
711+ expected_frames = [1 , 2 , 3 , 4 , 5 ],
712+ description = 'leading and trailing spaces'
713+ ),
714+ Case (
715+ input = ' 1 ' ,
716+ expected_frange = '1' ,
717+ expected_frames = [1 ],
718+ description = 'spaces around single frame'
719+ ),
720+
721+ # Whitespace with x modifier (step)
722+ Case (
723+ input = '1-10 x 2' ,
724+ expected_frange = '1-10x2' ,
725+ expected_frames = [1 , 3 , 5 , 7 , 9 ],
726+ description = 'spaces with x modifier'
727+ ),
728+ Case (
729+ input = '1 - 10 x 2' ,
730+ expected_frange = '1-10x2' ,
731+ expected_frames = [1 , 3 , 5 , 7 , 9 ],
732+ description = 'spaces in range and x modifier'
733+ ),
734+
735+ # Whitespace with y modifier (fill)
736+ Case (
737+ input = '1-10 y 2' ,
738+ expected_frange = '1-10y2' ,
739+ expected_frames = [2 , 4 , 6 , 8 , 10 ],
740+ description = 'spaces with y modifier'
741+ ),
742+
743+ # Whitespace with : modifier (stagger)
744+ Case (
745+ input = '1-6 : 3' ,
746+ expected_frange = '1-6:3' ,
747+ expected_frames = [1 , 4 , 3 , 5 , 2 , 6 ],
748+ description = 'spaces with : modifier'
749+ ),
750+
751+ # Complex case with multiple comma-separated ranges
752+ Case (
753+ input = '1 , 2 , 3 , 5-10 , 20-30' ,
754+ expected_frange = '1,2,3,5-10,20-30' ,
755+ expected_frames = [1 , 2 , 3 , 5 , 6 , 7 , 8 , 9 , 10 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 ],
756+ description = 'complex multi-range with spaces'
757+ ),
758+ Case (
759+ input = ' 1 , 5-10 , 20 ' ,
760+ expected_frange = '1,5-10,20' ,
761+ expected_frames = [1 , 5 , 6 , 7 , 8 , 9 , 10 , 20 ],
762+ description = 'multiple spaces in complex range'
763+ ),
764+
765+ # Tabs (should also be removed)
766+ Case (
767+ input = '1\t ,\t 2\t ,\t 3' ,
768+ expected_frange = '1,2,3' ,
769+ expected_frames = [1 , 2 , 3 ],
770+ description = 'tabs instead of spaces'
771+ ),
772+ Case (
773+ input = '1\t -\t 10' ,
774+ expected_frange = '1-10' ,
775+ expected_frames = [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ],
776+ description = 'tabs around dash'
777+ ),
778+
779+ # Newlines (should also be removed)
780+ Case (
781+ input = '1\n -\n 10' ,
782+ expected_frange = '1-10' ,
783+ expected_frames = [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ],
784+ description = 'newlines in range'
785+ ),
786+
787+ # Mixed whitespace characters
788+ Case (
789+ input = ' \t 1\n ,\r 2 , 3\t ' ,
790+ expected_frange = '1,2,3' ,
791+ expected_frames = [1 , 2 , 3 ],
792+ description = 'mixed whitespace types'
793+ ),
794+
795+ # Edge case: all whitespace
796+ Case (
797+ input = ' ' ,
798+ expected_frange = '' ,
799+ expected_frames = [],
800+ description = 'only whitespace (empty range)'
801+ ),
802+
803+ # Negative frames with whitespace
804+ Case (
805+ input = ' -10 - -5 ' ,
806+ expected_frange = '-10--5' ,
807+ expected_frames = [- 10 , - 9 , - 8 , - 7 , - 6 , - 5 ],
808+ description = 'negative frames with spaces'
809+ ),
810+ ]
811+
812+ for case in table :
813+ with self .subTest (case .description ):
814+ fs = FrameSet (case .input )
815+
816+ # Check normalized frange string
817+ self .assertEqual (fs .frange , case .expected_frange ,
818+ f"frange mismatch for { case .description } " )
819+
820+ # Check actual frame values
821+ actual_frames = list (fs )
822+ self .assertEqual (actual_frames , case .expected_frames ,
823+ f"frames mismatch for { case .description } " )
824+
825+ # Check length
826+ self .assertEqual (len (fs ), len (case .expected_frames ),
827+ f"length mismatch for { case .description } " )
828+
660829class TestBase (unittest .TestCase ):
661830 RX_PATHSEP = re .compile (r'[/\\]' )
662831
0 commit comments