Source

say / say / text.py

Full commit
"""
A text object that uses say to aggregate lines, say in a file or script.
"""

from say.core import fmt
import inspect
import sys, os

_PY3 = sys.version_info[0] > 2

class Text(object):
    def __init__(self, data=None, interpolate=True, dedent=True):
        self._lines = []
        if data:
            lines = self._data_to_lines(data)
            callframe = inspect.currentframe().f_back
            self.extend(lines, callframe, interpolate, dedent)
        
    def _dedent_lines(self, lines):
        """
        Given a list of lines, remove a leading or trailing blank line (if any),
        as well as common leading blank prefixes.
        """
        if lines and lines[0].strip() == "":
            lines.pop(0)
        if lines and lines[-1].strip() == "":
            lines.pop()
        if lines:
            nonblanklines = [ line for line in lines if line.strip() != "" ]
            # piggyback os.path function to compute longest common prefix
            prefix = os.path.commonprefix(nonblanklines)
            # but then determine how much of that prefix is blank
            prelen, maxprelen = 0, len(prefix)
            while prelen < maxprelen and prefix[prelen] == ' ':
                prelen += 1
            if prelen:
                lines = [ line[prelen:] for line in lines ]
        return lines

    def __iadd__(self, data):
        """
        In-place add the text or lines contained in data, with auto-dedent.
        """
        lines = self._data_to_lines(data)
        callframe = inspect.currentframe().f_back
        self.extend(lines, callframe, interpolate=True, dedent=True)
        return self
    
    def __ior__(self, data):
        """
        In-place add the text or lines contained in data, with NO auto-dedent.
        """
        lines = self._data_to_lines(data)
        callframe = inspect.currentframe().f_back
        self.extend(lines, callframe, 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.
        """
        lines = self._data_to_lines(data)
        self._lines.extend(lines)
        return self

    def append(self, line, callframe=None, interpolate=True):
        if interpolate:
            callframe = callframe or inspect.currentframe().f_back
            line = fmt(line, _callframe=callframe)
        self._lines.append(line)
    
    def extend(self, lines, callframe=None, interpolate=True, dedent=True):
        if dedent:
            lines = self._dedent_lines(lines)
        if interpolate:
            callframe = callframe or inspect.currentframe().f_back
        for line in lines:
            self.append(line, callframe, interpolate)
    
    def insert(self, i, data, callframe=None, interpolate=True, dedent=True):
        lines = self._data_to_lines(data)
        if dedent:
            lines = self._dedent_lines(lines)
        if interpolate:
            callframe = callframe or inspect.currentframe().f_back
            lines = [ fmt(line, _callframe=callframe) for line in lines ]
        for line in reversed(lines):
            self._lines.insert(i, line)

    def __getitem__(self, n):
        return self._lines[n]

    def __setitem__(self, n, value):
        self._lines[n] = value.rstrip('\n')
    
    def __len__(self):
        return len(self._lines)
    
    def __iter__(self):
        return iter(self._lines)
    
    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._lines)

    @text.setter
    def text(self, data):
        self._lines = self._data_to_lines(data)
            
    @property
    def lines(self):
        return self._lines[:]
    
    @lines.setter
    def lines(self, somelines):
        self._lines = [ line.rstrip('\n') for line in somelines ]
        
    def __str__(self):
        return self.text
    
    def __repr__(self):
        return 'Text({0}, {1} lines)'.format(id(self), len(self._lines))
    
    def copy(self):
        """
        Make a copy.
        """
        newt = Text()
        newt._lines = self._lines[:]
        return newt
    
    def read_from(self, filepath, interpolate=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.
        """
        lines = open(filepath).read().splitlines()
        self.extend(lines, inspect.currentframe().f_back, interpolate=interpolate)
        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))
            
    # TODO: add a backup feature
    
# TODO: Consider additional `list` and `str` methods
# TODO: Possibly add in-place replacement, modification, etc.
# TODO: Extend tests