Skip to content

Commit 60670cd

Browse files
committed
Add open() to CSVReader interface
This allows subclasses to customize raw file reading.
1 parent 1f315a6 commit 60670cd

2 files changed

Lines changed: 74 additions & 28 deletions

File tree

beangulp/importers/csvbase.py

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,25 @@ def __init__(self):
276276
# warnings.warn('skiplines is deprecated, use header instead', DeprecationWarning)
277277
self.header = self.skiplines
278278

279+
def open(self, filepath, encoding):
280+
"""Open filepath and return an iterable yielding lines of CSV-formatted text.
281+
282+
This method can be overridden in subclasses to customize file reading, for
283+
example to pre-proceess text lines before import. To simply skip a fixed
284+
number of lines at the beginning or end, consider setting the ``header``
285+
or ``footer`` class variables instead.
286+
287+
Args:
288+
filepath: Filesystem path to the input file.
289+
encoding: File encoding as provided to the ``open()`` built-in
290+
function.
291+
292+
Returns:
293+
An iterable providing lines of CSV-formatted text.
294+
"""
295+
with open(filepath, encoding=encoding) as fd:
296+
yield from fd
297+
279298
def read(self, filepath):
280299
"""Read CSV file according to class defined columns specification.
281300
@@ -292,34 +311,35 @@ def read(self, filepath):
292311
293312
"""
294313

295-
with open(filepath, encoding=self.encoding) as fd:
296-
# Skip header and footer lines.
297-
lines = _chomp(fd, self.header, self.footer)
298-
299-
# Filter out comment lines.
300-
if self.comments:
301-
lines = filter(lambda x: not x.startswith(self.comments), lines)
302-
303-
reader = csv.reader(lines, dialect=self.dialect)
304-
305-
# Map column names to column indices.
306-
names = None
307-
if self.names:
308-
headers = next(reader, None)
309-
if headers is None:
310-
raise IndexError("The input file does not contain an header line")
311-
names = {name.strip(): index for index, name in enumerate(headers)}
312-
313-
# Construct a class with attribute accessors for the
314-
# configured columns that works similarly to a namedtuple.
315-
attrs = {}
316-
for name, column in self.columns.items():
317-
attrs[name] = property(column.getter(names))
318-
row = type("Row", (tuple,), attrs)
319-
320-
# Return data rows.
321-
for x in reader:
322-
yield row(x)
314+
lines = self.open(filepath, self.encoding)
315+
316+
# Skip header and footer lines.
317+
lines = _chomp(lines, self.header, self.footer)
318+
319+
# Filter out comment lines.
320+
if self.comments:
321+
lines = filter(lambda x: not x.startswith(self.comments), lines)
322+
323+
reader = csv.reader(lines, dialect=self.dialect)
324+
325+
# Map column names to column indices.
326+
names = None
327+
if self.names:
328+
headers = next(reader, None)
329+
if headers is None:
330+
raise IndexError("The input file does not contain an header line")
331+
names = {name.strip(): index for index, name in enumerate(headers)}
332+
333+
# Construct a class with attribute accessors for the
334+
# configured columns that works similarly to a namedtuple.
335+
attrs = {}
336+
for name, column in self.columns.items():
337+
attrs[name] = property(column.getter(names))
338+
row = type("Row", (tuple,), attrs)
339+
340+
# Return data rows.
341+
for x in reader:
342+
yield row(x)
323343

324344

325345
class Importer(beangulp.Importer, CSVReader):

beangulp/importers/csvbase_test.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import decimal
33
import re
44
import unittest
5+
from itertools import dropwhile
56

67
from beancount.core import data
78
from beancount.parser import cmptest
@@ -451,6 +452,31 @@ class Reader(CSVReader):
451452
self.assertEqual(len(rows), 1)
452453
self.assertEqual(rows[0][0], "a")
453454

455+
@docfile
456+
def test_custom_open(self, filename):
457+
"""\
458+
Skip this line
459+
Skip this too
460+
First, Second
461+
a, b
462+
c, d
463+
"""
464+
465+
class Reader(CSVReader):
466+
first = Column("First")
467+
second = Column("Second")
468+
469+
def open(self, filepath):
470+
"""Skip lines until we find the column headers."""
471+
lines = super().open(filepath)
472+
return dropwhile(lambda line: "First" not in line, lines)
473+
474+
reader = Reader()
475+
rows = list(reader.read(filename))
476+
self.assertEqual(len(rows), 2)
477+
self.assertEqual(rows[0].first, "a")
478+
self.assertEqual(rows[1].second, "d")
479+
454480

455481
class Base(Importer):
456482
def identify(self, filepath):

0 commit comments

Comments
 (0)