Skip to content

Commit b7e3e58

Browse files
Merge pull request #48 from insightindustry/develop
Merging v.1.3.8 into master in preparation for release.
2 parents 36ce607 + d474b27 commit b7e3e58

14 files changed

+136
-23
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ venv/
9595
.clean
9696
ENV/
9797
.activate.ps1
98+
.pyinstaller27/
99+
.pyinstaller36/
98100

99101
# Spyder project settings
100102
.spyderproject

CHANGES.rst

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
-----------
22

3+
Release 1.3.8 (released December 7, 2019)
4+
============================================
5+
6+
* #43: Modified how ``__init__.py`` reads version information.
7+
* #45: Fixed false-positives and false-negatives in iterable validation.
8+
9+
-----------
10+
311
Release 1.3.7 (released September 7, 2019)
412
============================================
513

README.rst

+5-5
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,18 @@ Validator Collection
2626
:target: http://validator-collection.readthedocs.io/en/latest/?badge=latest
2727
:alt: Documentation Status (ReadTheDocs)
2828

29-
* - `v. 1.3 <https://github.com/insightindustry/validator-collection/tree/v.1.3.7>`_
29+
* - `v. 1.3 <https://github.com/insightindustry/validator-collection/tree/v.1.3.8>`_
3030
-
31-
.. image:: https://travis-ci.org/insightindustry/validator-collection.svg?branch=v.1.3.7
31+
.. image:: https://travis-ci.org/insightindustry/validator-collection.svg?branch=v.1.3.8
3232
:target: https://travis-ci.org/insightindustry/validator-collection
3333
:alt: Build Status (Travis CI)
3434

35-
.. image:: https://codecov.io/gh/insightindustry/validator-collection/branch/v.1.3.7/graph/badge.svg
35+
.. image:: https://codecov.io/gh/insightindustry/validator-collection/branch/v.1.3.8/graph/badge.svg
3636
:target: https://codecov.io/gh/insightindustry/validator-collection
3737
:alt: Code Coverage Status (Codecov)
3838

39-
.. image:: https://readthedocs.org/projects/validator-collection/badge/?version=v.1.3.7
40-
:target: http://validator-collection.readthedocs.io/en/latest/?badge=v.1.3.7
39+
.. image:: https://readthedocs.org/projects/validator-collection/badge/?version=v.1.3.8
40+
:target: http://validator-collection.readthedocs.io/en/latest/?badge=v.1.3.8
4141
:alt: Documentation Status (ReadTheDocs)
4242

4343
* - `v. 1.2 <https://github.com/insightindustry/validator-collection/tree/v.1.2.0>`_

docs/_contributors.rst

+2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
* Chris Modzelewski (`@insightindustry <https://github.com/insightindustry/>`_)
22
* Jordan Reiter (`@JordanReiter <https://github.com/JordanReiter/>`_)
3+
* Ihor Liubymov (`@mastak <https://github.com/mastak/>`_)
4+
* Alon Bar Tzlil (`@aivrit <https://github.com/aivrit/>`_)

docs/_unit_tests_code_coverage.rst

+5-5
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,18 @@
1818
:target: http://validator-collection.readthedocs.io/en/latest/?badge=latest
1919
:alt: Documentation Status (ReadTheDocs)
2020

21-
* - `v. 1.3 <https://github.com/insightindustry/validator-collection/tree/v.1.3.7>`_
21+
* - `v. 1.3 <https://github.com/insightindustry/validator-collection/tree/v.1.3.8>`_
2222
-
23-
.. image:: https://travis-ci.org/insightindustry/validator-collection.svg?branch=v.1.3.7
23+
.. image:: https://travis-ci.org/insightindustry/validator-collection.svg?branch=v.1.3.8
2424
:target: https://travis-ci.org/insightindustry/validator-collection
2525
:alt: Build Status (Travis CI)
2626

27-
.. image:: https://codecov.io/gh/insightindustry/validator-collection/branch/v.1.3.7/graph/badge.svg
27+
.. image:: https://codecov.io/gh/insightindustry/validator-collection/branch/v.1.3.8/graph/badge.svg
2828
:target: https://codecov.io/gh/insightindustry/validator-collection
2929
:alt: Code Coverage Status (Codecov)
3030

31-
.. image:: https://readthedocs.org/projects/validator-collection/badge/?version=v.1.3.7
32-
:target: http://validator-collection.readthedocs.io/en/latest/?badge=v.1.3.7
31+
.. image:: https://readthedocs.org/projects/validator-collection/badge/?version=v.1.3.8
32+
:target: http://validator-collection.readthedocs.io/en/latest/?badge=v.1.3.8
3333
:alt: Documentation Status (ReadTheDocs)
3434

3535
* - `v. 1.2 <https://github.com/insightindustry/validator-collection/tree/v.1.2.0>`_

docs/errors.rst

+5
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,11 @@ NotAnIterableError (from :class:`CannotCoerceError <validator_collection.errors.
225225

226226
.. autoclass:: NotAnIterableError
227227

228+
IterationFailedError (from :class:`NotAnIterableError <validator_collection.errors.NotAnIterableError>`)
229+
---------------------------------------------------------------------------------------------------------
230+
231+
.. autoclass:: IterationFailedError
232+
228233
NotCallableError (from :class:`ValueError <python:ValueError>`)
229234
------------------------------------------------------------------
230235

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
# Get the version number from the VERSION file
2323
version_dict = {}
24-
with open('./validator_collection/_version.py') as version_file:
24+
with open(path.join(here, 'validator_collection', '_version.py')) as version_file:
2525
exec(version_file.read(), version_dict) # pylint: disable=W0122
2626

2727
version = version_dict.get('__version__')

tests/conftest.py

+49
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,16 @@
1010
"""
1111

1212
import abc
13+
import collections
1314
import os
1415

1516
import pytest
17+
from validator_collection._compat import is_py2
18+
19+
if is_py2:
20+
Iterable = collections.Iterable
21+
else:
22+
Iterable = collections.abc.Iterable
1623

1724

1825
def pytest_addoption(parser):
@@ -91,3 +98,45 @@ class MetaClassParentType(object):
9198
class MetaClassType(MetaClassParentType):
9299
"""The child meta class type."""
93100
pass
101+
102+
103+
class GetItemIterable(object):
104+
"""A class that does not have a ``__iter__`` method, but does use
105+
``__getitem__``."""
106+
107+
items = (1, 2, 3, 4)
108+
109+
def __getitem__(self, key):
110+
return self.items[key]
111+
112+
113+
class IterIterable(object):
114+
"""A class that has an ``__iter__`` method."""
115+
116+
def __iter__(self):
117+
return self
118+
119+
def __next__(self):
120+
raise StopIteration()
121+
122+
def next(self):
123+
return self.__next__()
124+
125+
class IterableIterable(Iterable):
126+
"""A class that inherits from ``Iterable``."""
127+
128+
def __iter__(self):
129+
return self
130+
131+
def __next__(self):
132+
raise StopIteration()
133+
134+
def next(self):
135+
return self.__next__()
136+
137+
138+
class FalseIterable(Iterable):
139+
"""A class that inherits from ``Iterable``."""
140+
141+
def __iter__(self):
142+
raise AttributeError('this simulates an AttributeError')

tests/test_checkers.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626

2727
from validator_collection._compat import TimeZone
2828

29-
from tests.conftest import MetaClassParentType, MetaClassType
29+
from tests.conftest import MetaClassParentType, MetaClassType, GetItemIterable, \
30+
IterIterable, IterableIterable, FalseIterable
3031

3132

3233
## CORE
@@ -52,6 +53,12 @@
5253
5354
([1, 2, 3, 4], {'allow_empty': True}, SyntaxError),
5455
56+
(GetItemIterable(), None, True),
57+
(IterIterable(), None, True),
58+
(IterableIterable(), None, True),
59+
60+
(FalseIterable(), None, True),
61+
5562
])
5663
def test_is_iterable(value, kwargs, expects):
5764
if kwargs and isinstance(expects, bool):
@@ -278,9 +285,18 @@ def test_is_string(value, expects, coerce_value, minimum_length, maximum_length,
278285
(['test', 123], True, False, None, 1),
279286
280287
(str, True, False, None, None),
288+
289+
(GetItemIterable(), False, False, None, None),
290+
(IterIterable(), False, False, None, None),
291+
(IterableIterable(), False, False, None, None),
292+
293+
(FalseIterable(), True, False, None, None),
294+
281295
])
282296
def test_is_iterable(value, fails, allow_empty, minimum_length, maximum_length):
283297
expects = not fails
298+
if not fails:
299+
iter(value)
284300
result = checkers.is_iterable(value,
285301
minimum_length = minimum_length,
286302
maximum_length = maximum_length)

tests/test_validators.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import validator_collection.validators as validators
2525
from validator_collection._compat import numeric_types, basestring
26+
from tests.conftest import GetItemIterable, IterIterable, IterableIterable, FalseIterable
2627

2728

2829
## CORE
@@ -248,6 +249,12 @@ def test_uuid(value, fails, allow_empty):
248249
(['test', 123], True, False, 3, None),
249250
(['test', 123], True, False, None, 1),
250251
252+
(GetItemIterable(), False, False, None, None),
253+
(IterIterable(), False, False, None, None),
254+
(IterableIterable(), False, False, None, None),
255+
256+
(FalseIterable(), True, False, None, None),
257+
251258
])
252259
def test_iterable(value, fails, allow_empty, minimum_length, maximum_length):
253260
"""Test the string validator."""
@@ -258,7 +265,7 @@ def test_iterable(value, fails, allow_empty, minimum_length, maximum_length):
258265
maximum_length = maximum_length)
259266
if value is not None:
260267
assert validated is not None
261-
assert hasattr(validated, '__iter__')
268+
iter(validated)
262269
else:
263270
assert validated is None
264271
else:

validator_collection/__init__.py

+1-8
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,7 @@
99
"""
1010

1111
import os
12-
13-
# Get the version number from the _version.py file
14-
version_dict = {}
15-
with open(os.path.join(os.path.dirname(__file__), '_version.py')) as version_file:
16-
exec(version_file.read(), version_dict) # pylint: disable=W0122
17-
18-
__version__ = version_dict.get('__version__')
19-
12+
from validator_collection._version import __version__
2013

2114
from validator_collection.validators import bytesIO, date, dict, decimal, \
2215
directory_exists, datetime, email, float, fraction, file_exists, ip_address, \

validator_collection/_version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# -*- coding: utf-8 -*-
2-
__version__ = '1.3.7'
2+
__version__ = '1.3.8'

validator_collection/errors.py

+10
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,16 @@ class NotAnIterableError(CannotCoerceError):
165165
"""
166166
pass
167167

168+
class IterationFailedError(NotAnIterableError):
169+
"""Exception raised when a value conforms to one of Python's supported
170+
iterable protocols, but iterating across the object produced an unexpected
171+
Exception.
172+
173+
**INHERITS FROM:** :class:`TypeError <python:TypeError>` -> :class:`CannotCoerceError <validator_collection.errors.CannotCoerceError>` -> :class:`NotAnIterableError <validator_collection.errors.NotAnIterableError>`
174+
175+
"""
176+
pass
177+
168178

169179
class MaximumLengthError(ValueError):
170180
"""Exception raised when a value exceeds a maximum allowed length.

validator_collection/validators.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,18 @@ def iterable(value,
259259
**kwargs):
260260
"""Validate that ``value`` is a valid iterable.
261261
262+
.. hint::
263+
264+
This validator checks to ensure that ``value`` supports iteration using
265+
any of Python's three iteration protocols: the ``__getitem__`` protocol,
266+
the ``__iter__`` / ``next()`` protocol, or the inheritance from Python's
267+
`Iterable` abstract base class.
268+
269+
If ``value`` supports any of these three iteration protocols, it will be
270+
validated. However, if iteration across ``value`` raises an unsupported
271+
exception, this function will raise an
272+
:exc:`IterationFailedError <validator_collection.errors.IterationFailedError>`
273+
262274
:param value: The value to validate.
263275
264276
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if ``value``
@@ -286,6 +298,8 @@ def iterable(value,
286298
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
287299
:raises NotAnIterableError: if ``value`` is not a valid iterable or
288300
:obj:`None <python:None>`
301+
:raises IterationFailedError: if ``value`` is a valid iterable, but iteration
302+
fails for some unexpected exception
289303
:raises MinimumLengthError: if ``minimum_length`` is supplied and the length of
290304
``value`` is less than ``minimum_length`` and ``whitespace_padding`` is
291305
``False``
@@ -300,9 +314,16 @@ def iterable(value,
300314
minimum_length = integer(minimum_length, allow_empty = True, force_run = True) # pylint: disable=E1123
301315
maximum_length = integer(maximum_length, allow_empty = True, force_run = True) # pylint: disable=E1123
302316

303-
if isinstance(value, forbid_literals) or not hasattr(value, '__iter__'):
317+
if isinstance(value, forbid_literals):
304318
raise errors.NotAnIterableError('value type (%s) not iterable' % type(value))
305319

320+
try:
321+
iter(value)
322+
except TypeError:
323+
raise errors.NotAnIterableError('value type (%s) not iterable' % type(value))
324+
except Exception as error:
325+
raise errors.IterationFailedError('iterating across value raised an unexpected Exception: "%s"' % error)
326+
306327
if value and minimum_length is not None and len(value) < minimum_length:
307328
raise errors.MinimumLengthError(
308329
'value has fewer items than the minimum length %s' % minimum_length

0 commit comments

Comments
 (0)