Skip to content

Commit e02d2e4

Browse files
authored
Add boto dependency as s3 extra (#46)
* Gracefully fail if boto is not present * Add s3 extra * Aggregate coverage runs * Version bump
1 parent fa5faeb commit e02d2e4

File tree

6 files changed

+62
-10
lines changed

6 files changed

+62
-10
lines changed

README.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,8 +293,11 @@ Vladiate comes with the following input types:
293293

294294
*class* ``S3File``
295295

296-
Read from a file in S3. Uses the `boto <https://github.com/boto/boto>`_
297-
library. Optionally can specify either a full path, or a bucket/key pair.
296+
Read from a file in S3. Optionally can specify either a full path, or a
297+
bucket/key pair.
298+
299+
Requires the `boto <https://github.com/boto/boto>`_ library, which should be
300+
installed via ``pip install vladiate[s3]``.
298301

299302
:``path=None``:
300303
A full S3 filepath (e.g., ``s3://foo.bar/path/to/file.csv``)

setup.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from setuptools import setup, find_packages
55
from setuptools.command.test import test as TestCommand
66

7-
__version__ = '0.0.18'
7+
__version__ = '0.0.19'
88

99

1010
class PyTest(TestCommand):
@@ -60,7 +60,8 @@ def readme():
6060
packages=find_packages(exclude=['examples', 'tests']),
6161
include_package_data=True,
6262
zip_safe=False,
63-
install_requires=['boto'],
63+
install_requires=[],
64+
extras_require={'s3': ['boto']},
6465
tests_require=['pretend', 'pytest', 'flake8'],
6566
cmdclass={'test': PyTest},
6667
entry_points={

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ basepython = python3.6
1919
commands = flake8
2020

2121
[testenv]
22-
commands = coverage run --source=vladiate setup.py test
22+
commands = coverage run --append --source=vladiate setup.py test
2323
deps =
2424
pytest
2525
coverage

vladiate/exceptions.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,12 @@ class BadValidatorException(Exception):
88
def __init__(self, extra):
99
message = 'Row does not contain the following unique_with fields: {}'
1010
super(BadValidatorException, self).__init__(message.format(extra))
11+
12+
13+
class MissingExtraException(Exception):
14+
''' Thrown when an extra dependency is missing '''
15+
def __init__(self):
16+
super(MissingExtraException, self).__init__(
17+
'The `s3` extra is required to use the `S3File` class. Install'
18+
' it via `pip install vladiate[s3]`.'
19+
)

vladiate/inputs.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import io
2-
import boto
32
try:
43
from urlparse import urlparse
54
except:
@@ -9,6 +8,8 @@
98
except:
109
from io import StringIO
1110

11+
from vladiate.exceptions import MissingExtraException
12+
1213

1314
class VladInput(object):
1415
''' A generic input class '''
@@ -41,6 +42,15 @@ class S3File(VladInput):
4142
''' Read from a file in S3 '''
4243

4344
def __init__(self, path=None, bucket=None, key=None):
45+
try:
46+
import boto # noqa
47+
self.boto = boto
48+
except:
49+
# 2.7 workaround, should just be `raise Exception() from None`
50+
exc = MissingExtraException()
51+
exc.__context__ = None
52+
raise exc
53+
4454
if path and not any((bucket, key)):
4555
self.path = path
4656
parse_result = urlparse(path)
@@ -57,7 +67,7 @@ def __init__(self, path=None, bucket=None, key=None):
5767
)
5868

5969
def open(self):
60-
s3 = boto.connect_s3()
70+
s3 = self.boto.connect_s3()
6171
bucket = s3.get_bucket(self.bucket)
6272
key = bucket.new_key(self.key)
6373
contents = key.get_contents_as_string()

vladiate/test/test_inputs.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,32 @@
11
import pytest
22
from pretend import stub, call, call_recorder
33

4+
from ..exceptions import MissingExtraException
45
from ..inputs import S3File, StringIO, String, VladInput
56
from ..vlad import Vlad
67

78

9+
def mock_boto(result):
10+
try:
11+
import builtins
12+
except:
13+
import __builtin__ as builtins
14+
realimport = builtins.__import__
15+
16+
def badimport(name, *args, **kwargs):
17+
if name == 'boto':
18+
return result()
19+
return realimport(name, *args, **kwargs)
20+
21+
builtins.__import__ = badimport
22+
23+
824
@pytest.mark.parametrize('kwargs', [
925
({'path': 's3://some.bucket/some/s3/key.csv'}),
1026
({'bucket': 'some.bucket', 'key': '/some/s3/key.csv'}),
1127
])
1228
def test_s3_input_works(kwargs):
29+
mock_boto(lambda: stub())
1330
S3File(**kwargs)
1431

1532

@@ -21,6 +38,7 @@ def test_s3_input_works(kwargs):
2138
({'key': '/some/s3/key.csv'}),
2239
])
2340
def test_s3_input_fails(kwargs):
41+
mock_boto(lambda: stub())
2442
with pytest.raises(ValueError):
2543
S3File(**kwargs)
2644

@@ -35,7 +53,7 @@ def test_string_input_works(kwargs):
3553
assert Vlad(source=source, validators=validators).validate()
3654

3755

38-
def test_open_s3file(monkeypatch):
56+
def test_open_s3file():
3957
new_key = call_recorder(lambda *args, **kwargs: stub(
4058
get_contents_as_string=lambda: 'contents'.encode()
4159
))
@@ -44,9 +62,10 @@ def test_open_s3file(monkeypatch):
4462

4563
mock_boto = stub(connect_s3=lambda: stub(get_bucket=get_bucket))
4664

47-
monkeypatch.setattr('vladiate.inputs.boto', mock_boto)
65+
s3file = S3File('s3://some.bucket/some/s3/key.csv')
66+
s3file.boto = mock_boto
4867

49-
result = S3File('s3://some.bucket/some/s3/key.csv').open()
68+
result = s3file.open()
5069

5170
assert get_bucket.calls == [
5271
call('some.bucket')
@@ -77,3 +96,13 @@ def __init__(self):
7796

7897
with pytest.raises(NotImplementedError):
7998
repr(PartiallyImplemented())
99+
100+
101+
def test_s3file_raises_when_no_boto():
102+
def import_result():
103+
raise ImportError
104+
105+
mock_boto(import_result)
106+
107+
with pytest.raises(MissingExtraException):
108+
S3File()

0 commit comments

Comments
 (0)