Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Unreleased
----------

- feat: Suggest :code:`--snifflimit 0` when standard input is not seekable.
- feat: :doc:`/scripts/csvcut` adds an :code:`--ignore-unknown-columns` option to skip identifiers in :code:`-c/--columns` that do not match a column in the input.
- feat: :doc:`/scripts/csvclean` adds a :code:`--remove-empty-columns` option to remove empty columns from standard output.
- feat: :doc:`/scripts/in2csv` guesses the ``ndjson`` format for files with :code:`.ndjson`, :code:`.jsonl` and :code:`.jl` extensions.
Expand Down
3 changes: 3 additions & 0 deletions csvkit/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import datetime
import decimal
import gzip
import io
import itertools
import lzma
import os
Expand Down Expand Up @@ -346,6 +347,8 @@ def handler(t, value, traceback):
)
else:
sys.stderr.write(f'{t.__name__}: {str(value)}\n')
if t == io.UnsupportedOperation and str(value) == 'underlying stream is not seekable':
sys.stderr.write('Try setting --snifflimit 0.\n')

sys.excepthook = handler

Expand Down
50 changes: 49 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,61 @@
import io
import sys
import unittest
from types import SimpleNamespace
from unittest import mock

from csvkit.cli import ColumnIdentifierError, match_column_identifier, parse_column_identifiers
from csvkit.cli import ColumnIdentifierError, CSVKitUtility, match_column_identifier, parse_column_identifiers


class TestCli(unittest.TestCase):

def setUp(self):
self.headers = ['id', 'name', 'i_work_here', '1', 'more-header-values', 'stuff', 'blueberry']

def install_exception_handler(self, verbose=False):
original = sys.excepthook
self.addCleanup(setattr, sys, 'excepthook', original)
utility = object.__new__(CSVKitUtility)
utility.args = SimpleNamespace(verbose=verbose, encoding='utf-8')
utility._install_exception_handler()
return sys.excepthook

def test_exception_handler_suggests_snifflimit_for_non_seekable_stream(self):
handler = self.install_exception_handler()
with mock.patch('sys.stderr', new_callable=io.StringIO) as stderr:
handler(io.UnsupportedOperation, io.UnsupportedOperation('underlying stream is not seekable'), None)

self.assertIn('UnsupportedOperation: underlying stream is not seekable', stderr.getvalue())
self.assertIn('--snifflimit 0', stderr.getvalue())

def test_exception_handler_does_not_suggest_snifflimit_for_other_unsupported_operation(self):
handler = self.install_exception_handler()
with mock.patch('sys.stderr', new_callable=io.StringIO) as stderr:
handler(io.UnsupportedOperation, io.UnsupportedOperation('other operation'), None)

self.assertEqual('UnsupportedOperation: other operation\n', stderr.getvalue())

def test_exception_handler_verbose_uses_default_hook(self):
handler = self.install_exception_handler(verbose=True)
traceback = object()
error = io.UnsupportedOperation('underlying stream is not seekable')
with mock.patch.object(sys, '__excepthook__') as default_hook:
handler(io.UnsupportedOperation, error, traceback)

default_hook.assert_called_once_with(io.UnsupportedOperation, error, traceback)

def test_exception_handler_preserves_unicode_decode_message(self):
handler = self.install_exception_handler()
error = UnicodeDecodeError('utf-8', b'\xff', 0, 1, 'invalid start byte')
with mock.patch('sys.stderr', new_callable=io.StringIO) as stderr:
handler(UnicodeDecodeError, error, None)

self.assertEqual(
'Your file is not "utf-8" encoded. Please specify the correct encoding with the --encoding flag.'
' Use the -v flag to see the complete error.\n',
stderr.getvalue(),
)

def test_match_column_identifier_string(self):
self.assertEqual(2, match_column_identifier(self.headers, 'i_work_here'))
self.assertEqual(2, match_column_identifier(self.headers, 'i_work_here', column_offset=0))
Expand Down