Commits

Jonathan Eunice committed 636d4f2

TextRange added and working well in basic tests

  • Participants
  • Parent commits 601336a

Comments (0)

Files changed (5)

             callframe = callframe or inspect.currentframe().f_back
         for line in lines:
             self.append(line, callframe, interpolate)
+            
+    # TODO: consider whether append and extend should be calls to insert
     
     def insert(self, i, data, callframe=None, interpolate=True, dedent=True):
         lines = self._data_to_lines(data)
         newt = Text()
         newt._lines = self._lines[:]
         return newt
+
+    def replace(self, target, replacement):
+        """
+        Replace all instances of the target string with the replacement string.
+        Works in situ, contra str.replace().
+        """
+        for i, line in enumerate(self._lines):
+            self._lines[i] = line.replace(target, replacement)
+        # TODO: should lines be recalibrated, if there are any \n in replacement?
     
-    def read_from(self, filepath, interpolate=True):
+    def read_from(self, filepath, interpolate=True, dedent=True):
         """
-        Reads lines from the designated file, adding them to the given Text.
-        If called as a class method, creates a new text. By default, interpolates
-        any {} expressions just as say and str.format do, but that can be turned off.
+        Reads lines from the designated file, appending them to the end of the
+        given Text. By default, interpolates and dedents any {}
+        expressions.
         """
         lines = open(filepath).read().splitlines()
-        self.extend(lines, inspect.currentframe().f_back, interpolate=interpolate)
+        caller = inspect.currentframe().f_back
+        self.extend(lines, caller, interpolate=interpolate, dedent=dedent)
         return self          
     
     def write_to(self, filepath, append=False, encoding='utf-8'):
+"""
+A range of say.text.Text lines that can be edited on their own,
+effecting the underlying lines.
+"""
+
+from say import Text
+import inspect
+
+class TextRange(object):
+    
+    def __init__(self, text=None, indices=None):
+        self._text = text
+        self._indices = slice(*indices)
+        
+    # TODO: Convert this constructor to take start stop rather than tuple to specify indicates - also, support open ended ranges
+
+    def __iadd__(self, data):
+        """
+        In-place add the text or lines contained in data, with auto-dedent.
+        """
+        caller = inspect.currentframe().f_back
+        self._insert(self._indices.stop, data, caller, interpolate=True, dedent=True)
+        return self
+    
+    def __ior__(self, data):
+        """
+        In-place add the text or lines contained in data, with NO auto-dedent.
+        """
+        caller = inspect.currentframe().f_back
+        self._insert(self._indices.stop, data, caller, interpolate=True, dedent=False)
+        return self
+
+    def __iand__(self, data):
+        """
+        In-place add the text or lines contained in data, with NO auto-dedent
+        and NO iterpolation.
+        """
+        self._insert(self._indices.stop, data, None, interpolate=False, dedent=False)
+        return self
+    
+    def append(self, data, caller=None, interpolate=True):
+        caller = caller or inspect.currentframe().f_back if interpolate else None
+        self._insert(self._indices.stop, data, caller=caller, interpolate=interpolate)
+    
+    def extend(self, data, caller=None, interpolate=True, dedent=True):
+        caller = caller or inspect.currentframe().f_back if interpolate else None
+        self._insert(self._indices.stop, data, caller, interpolate, dedent)
+    
+    def _base_index(self, n):
+        """
+        Check to the given index n to ensure it's within the range of lines.
+        """
+        index = self._indices.start + n
+        if self._indices.start <= index < self._indices.stop:
+            return index
+        raise IndexError('index {0} ({1} in underlying Text) out of range'.format(n, index))
+        
+    def _insert(self, n, data, caller=None, interpolate=True, dedent=True):
+        """
+        Insert into the underlying Text at a point relative to the underlying text indices.
+        """
+        newlines = self._data_to_lines(data)
+        caller = caller or inspect.currentframe().f_back if interpolate else None
+        self._text.insert(n, newlines, caller, interpolate, dedent)
+        self._adjust_indices(newlines, replacing=False)
+
+    def insert(self, n, data, caller=None, interpolate=True, dedent=True):
+        """
+        Insert into the underlying Text at a point relative to the TextRange's
+        indices.
+        """
+        caller = caller or inspect.currentframe().f_back if interpolate else None
+        self._insert(self._indices.start + n, data, caller, interpolate, dedent)
+        
+    def __getitem__(self, n):
+        index = self._base_index(n)
+        return self._text._lines[index]
+
+    def __setitem__(self, n, value):
+        index = self._base_index(n)
+        self._text._lines[index] = value.rstrip('\n')
+    
+    def __len__(self):
+        return self._indices.stop - self._indices.start
+    
+    def __iter__(self):
+        return iter(self._text._lines[self._indices])
+
+    def _data_to_lines(self, data):
+        if isinstance(data, list):
+            return [ line.rstrip('\n') for line in data ]
+        else:
+            return data.splitlines()
+
+    @property
+    def text(self):
+        return '\n'.join(self._text._lines[self._indices])
+
+    @text.setter
+    def text(self, data):
+        newlines = self._data_to_lines(data)
+        self._text._lines[self._indices] = newlines
+        self._adjust_indices(newlines)
+            
+    def _adjust_indices(self, newlines, replacing=True):
+        """
+        If a TextRange is modified, we adjust ._indices to reflect the length
+        of the new text. If replacing, assume the new lines replace the given
+        text range; if not replacing, adding to the text range.
+        """
+        newlen = len(newlines)
+        newstop = self._indices.start + newlen if replacing else self._indices.stop + newlen
+        if self._indices.stop != newstop:
+            self._indices = slice(self._indices.start, newstop)
+  
+    @property
+    def lines(self):
+        return self._text._lines[self._indices]
+    
+    @lines.setter
+    def lines(self, newlines):
+        newlines = [ line.rstrip('\n') for line in newlines ]
+        self._text._lines[self._indices] = newlines
+        self._adjust_indices(newlines)
+
+    def __str__(self):
+        return self.text
+    
+    def __repr__(self):
+        return 'TextRange({0}, {1}:{2} of {3})'.format(id(self),
+                self._indices.start, self._indices.stop, id(self._text))
+    
+    def replace(self, target, replacement):
+        """
+        Replace all instances of the target string with the replacement string.
+        Works in situ, contra str.replace().
+        """
+        for i, line in enumerate(self._text._lines[self._indices], start=self._indices.start):
+            self._text._lines[i] = line.replace(target, replacement)
+            
+        # TODO: should lines be recalibrated, if there are any \n in replacement?
+        
+    def copy(self):
+        """
+        Make a copy.
+        """
+        raise NotImplementedError('not sure what makes sense')
+    
+    def read_from(self, filepath, interpolate=True, dedent=True):
+        """
+        Reads lines from the designated file, appending them to the end of the
+        given TextRange. By default, interpolates and dedents any {}
+        expressions.
+        """
+        lines = open(filepath).read().splitlines()
+        caller = inspect.currentframe().f_back
+        self.extend(lines, caller, interpolate=interpolate, dedent=dedent)
+        return self          
+    
+    def write_to(self, filepath, append=False, encoding='utf-8'):
+        mode = "a" if append else "w"
+        with open(filepath, "w") as f:
+            f.write(self.text.encode(encoding))
 
 setup(
     name='say',
-    version=verno("0.887"),
+    version=verno("0.923"),
     author='Jonathan Eunice',
     author_email='jonathan.eunice@gmail.com',
     description='Super-simple templated printing. E.g.: say("Hello, {whoever}!", indent=1)',

test/test_text.py

     t[1] = 'would be'
     assert t.text == 'this\nwould be\nsomething'
     
+def test_replace():
+   t = Text('a\nb\nc\nd')
+   assert t.lines == 'a b c d'.split()
+   
+   t.replace('b', 'B')
+   t.replace('d', 'D')
+   assert t.lines ==  'a B c D'.split()
+    
 def test_set_text():
     t = Text()
     

test/test_textrange.py

+import six, os
+from say import Text
+from say.textrange import TextRange
+
+def test_basic():
+   t = Text('a\nb\nc\nd')
+   assert t.lines == 'a b c d'.split()
+   
+   tr = TextRange(t, (1,3))
+   assert tr.text == 'b\nc'
+   assert tr.lines == [ 'b', 'c' ]
+   
+   tr += 'hey!'
+   assert t.lines == 'a b c hey! d'.split()
+   
+   assert tr.text == 'b\nc\nhey!'
+   assert tr.lines == [ 'b', 'c', 'hey!' ]
+   
+def test_indexing():
+   
+   t = Text('a\nb\nc\nd')
+   assert t.lines == 'a b c d'.split()
+   
+   tr = TextRange(t, (1,3))
+   assert tr.text == 'b\nc'
+   assert tr.lines == [ 'b', 'c' ]
+   
+   assert tr[0] == 'b'
+   assert tr[1] == 'c'
+   
+   from pytest import raises
+   
+   with raises(IndexError):
+      assert tr[2] == 'd'
+      
+def test_replace():
+   t = Text('a\na\n\a\nb')
+   assert t.lines == 'a a a b'.split()
+   
+   tr = TextRange(t, (2,4))
+   assert tr.lines == 'a b'.split()
+   
+   tr.replace('a', 'A')
+   assert tr.lines == 'A b'.split()
+   assert t.lines == 'a a A b'.split()
+   
+      
+def test_interpolation():
+   x = 21
+   t = Text("Joe,\nthis is {x}")
+   assert t.text == 'Joe,\nthis is 21'
+   
+   tr = TextRange(t, (1, 2))
+   
+   tr += 'and {x}'
+   assert tr.text == 'this is 21\nand 21'
+   
+   tr &= 'and {x}'
+   assert tr.text == 'this is 21\nand 21\nand {x}'
+   
+   assert t.text == 'Joe,\nthis is 21\nand 21\nand {x}'
+   
+
+def test_replace():
+   t = Text('a\nb\nc\nd')
+   assert t.lines == 'a b c d'.split()
+   tr = TextRange(t, (1,3))
+   assert tr.lines == [ 'b', 'c' ]
+   
+   tr.replace('b', 'B')
+   assert tr.lines == [ 'B', 'c' ]
+
+