Repository: saulpw/visidata

Issue:
vds: can't load a .vds file from stdin
**Small description**

`cat test.vds | vd --filetype vds` results in: `AttributeError: 'RepeatFile' object has no attribute 'readline'`.

It should instead just work(tm).

**Additional context**

Problem is present on v2.4 and on the develop branch (commit 3350d9fd8c9e64ebf409deae4b31085d12efeb7f)
vds: can't load a .vds file from stdin
**Small description**

`cat test.vds | vd --filetype vds` results in: `AttributeError: 'RepeatFile' object has no attribute 'readline'`.

It should instead just work(tm).

**Additional context**

Problem is present on v2.4 and on the develop branch (commit 3350d9fd8c9e64ebf409deae4b31085d12efeb7f)


Discussion:

Resolving patch:
diff --git a/visidata/loaders/http.py b/visidata/loaders/http.py
--- a/visidata/loaders/http.py
+++ b/visidata/loaders/http.py
@@ -62,7 +62,7 @@ def _iter_lines(path=path, response=response, max_next=options.http_max_next):
             response = requests.get(src, stream=True)
 
     # add resettable iterator over contents as an already-open fp
-    path.fp = RepeatFile(iter_lines=_iter_lines())
+    path.fp = RepeatFile(_iter_lines())
 
     return vd.openSource(path, filetype=filetype)
 
diff --git a/visidata/path.py b/visidata/path.py
--- a/visidata/path.py
+++ b/visidata/path.py
@@ -147,7 +147,7 @@ def open_text(self, mode='rt'):
             return self.rfile
 
         if self.fp:
-            self.rfile = RepeatFile(fp=self.fp)
+            self.rfile = RepeatFile(self.fp)
             return self.rfile
 
         if 't' not in mode:
@@ -174,7 +174,7 @@ def read_text(self, *args, **kwargs):
             kwargs['errors'] = kwargs.get('encoding_errors', options.encoding_errors)
 
         if self.lines:
-            return RepeatFile(iter_lines=self.lines).read()
+            return RepeatFile(self.lines).read()
         elif self.fp:
             return self.fp.read()
         else:
@@ -260,16 +260,16 @@ def with_name(self, name):
 
 
 class RepeatFile:
-    def __init__(self, *, fp=None, iter_lines=None):
-        'Provide either fp or iter_lines, and lines will be filled from it.'
-        self.fp = fp
+    '''Lazy file-like object that can be read and line-seeked more than once from memory.'''
+
+    def __init__(self, iter_lines, lines=None):
         self.iter_lines = iter_lines
-        self.lines = []
+        self.lines = lines if lines is not None else []
         self.iter = RepeatFileIter(self)
 
     def __enter__(self):
-        self.iter = RepeatFileIter(self)
-        return self
+        '''Returns a new independent file-like object, sharing the same line cache.'''
+        return RepeatFile(self.iter_lines, lines=self.lines)
 
     def __exit__(self, a,b,c):
         pass
@@ -286,10 +286,21 @@ def read(self, n=None):
                 break  # end of file
         return r
 
+    def tell(self):
+        '''Tells the current position as an opaque line marker.'''
+        return self.iter.nextIndex
+
     def seek(self, n):
-        if n != 0:
-            vd.error('RepeatFile can only seek to beginning')
-        self.iter = RepeatFileIter(self)
+        '''Seek to an already seen opaque line position marker only.'''
+        self.iter.nextIndex = n
+
+    def readline(self, size=-1):
+        if size != -1:
+            vd.error('RepeatFile does not support limited line length')
+        try:
+            return next(self.iter)
+        except StopIteration:
+            return ''
 
     def __iter__(self):
         return RepeatFileIter(self)
@@ -319,13 +330,6 @@ def __next__(self):
             except StopIteration:
                 self.rf.iter_lines = None
                 raise
-        elif self.rf.fp:
-            try:
-                r = next(self.rf.fp)
-                self.rf.lines.append(r)
-            except StopIteration:
-                self.rf.fp = None
-                raise
         else:
             raise StopIteration()
 


Test patch: