Skip to content

Commit

Permalink
Merge pull request #24 from mattsb42-aws/unicode
Browse files Browse the repository at this point in the history
Cleaning up string support and expanding test coverage.
  • Loading branch information
mattsb42-aws authored Nov 6, 2018
2 parents 9559dde + 1001a94 commit e11d72b
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 22 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ docs/build
.pytest_cache

# PyCharm
.idea/
.idea/
venv/
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
Changelog
*********

1.0.3 -- 2018-xx-xx
===================

* Add support for strings on input for decoding to match functionality of :class:`base64.b64decode`.
`#21 <https://github.com/aws/base64io-python/issues/21>`_
`#23 <https://github.com/aws/base64io-python/pull/23>`_
`#24 <https://github.com/aws/base64io-python/pull/24>`_

1.0.2 -- 2018-08-01
===================

Expand Down
2 changes: 1 addition & 1 deletion doc/conf.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# pylint: disable=invalid-name
"""Sphinx configuration."""
from datetime import datetime
import io
import os
import re
from datetime import datetime

VERSION_RE = re.compile(r"""__version__ = ['"]([0-9.]+)['"]""")
HERE = os.path.abspath(os.path.dirname(__file__))
Expand Down
11 changes: 11 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,14 @@ ignore =
# Doc8 Configuration
[doc8]
max-line-length = 120

[isort]
line_length = 120
# https://github.com/timothycrosley/isort#multi-line-output-modes
multi_line_output = 3
include_trailing_comma = True
force_grid_wrap = 0
combine_as_imports = True
not_skip = __init__.py
known_first_party = base64io
known_third_party =base64io,mock,pytest,setuptools
37 changes: 24 additions & 13 deletions src/base64io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,19 @@ def _py2():
file = NotImplemented # pylint: disable=invalid-name


def _to_bytes(data):
# type: (AnyStr) -> bytes
"""Convert input data from either string or bytes to bytes.
:param data: Data to convert
:returns: ``data`` converted to bytes
:rtype: bytes
"""
if isinstance(data, bytes):
return data
return data.encode("utf-8")


class Base64IO(io.IOBase):
"""Base64 stream with context manager support.
Expand Down Expand Up @@ -209,7 +222,7 @@ def writelines(self, lines):
self.write(line)

def _read_additional_data_removing_whitespace(self, data, total_bytes_to_read):
# type: (AnyStr, int) -> AnyStr
# type: (bytes, int) -> bytes
"""Read additional data from wrapped stream until we reach the desired number of bytes.
.. note::
Expand All @@ -226,20 +239,20 @@ def _read_additional_data_removing_whitespace(self, data, total_bytes_to_read):
# case the base64 module happily removes any whitespace.
return data

_data_buffer = io.BytesIO() if isinstance(data, bytes) else io.StringIO()
join_char = b'' if isinstance(data, bytes) else u''
_data_buffer.write(join_char.join(data.split())) # type: ignore
_remaining_bytes_to_read = total_bytes_to_read - _data_buffer.tell() # type: ignore
_data_buffer = io.BytesIO()

_data_buffer.write(b"".join(data.split()))
_remaining_bytes_to_read = total_bytes_to_read - _data_buffer.tell()

while _remaining_bytes_to_read > 0:
_raw_additional_data = self.__wrapped.read(_remaining_bytes_to_read)
_raw_additional_data = _to_bytes(self.__wrapped.read(_remaining_bytes_to_read))
if not _raw_additional_data:
# No more data to read from wrapped stream.
break

_data_buffer.write(join_char.join(_raw_additional_data.split())) # type: ignore
_remaining_bytes_to_read = total_bytes_to_read - _data_buffer.tell() # type: ignore
return _data_buffer.getvalue() # type: ignore
_data_buffer.write(b"".join(_raw_additional_data.split()))
_remaining_bytes_to_read = total_bytes_to_read - _data_buffer.tell()
return _data_buffer.getvalue()

def read(self, b=-1):
# type: (int) -> bytes
Expand Down Expand Up @@ -271,13 +284,11 @@ def read(self, b=-1):
_bytes_to_read += 4 - _bytes_to_read % 4

# Read encoded bytes from wrapped stream.
data = self.__wrapped.read(_bytes_to_read)
data = _to_bytes(self.__wrapped.read(_bytes_to_read))
# Remove whitespace from read data and attempt to read more data to get the desired
# number of bytes.
whitespace = string.whitespace.encode("utf-8") if isinstance(data, bytes) \
else string.whitespace # type: Union[bytes, str]

if any([char in data for char in whitespace]):
if any([char in data for char in string.whitespace.encode("utf-8")]):
data = self._read_additional_data_removing_whitespace(data, _bytes_to_read)

results = io.BytesIO()
Expand Down
12 changes: 7 additions & 5 deletions test/unit/test_base64_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
import math
import os

from mock import MagicMock, sentinel
import pytest
from mock import MagicMock, sentinel

from base64io import Base64IO

Expand Down Expand Up @@ -154,12 +154,13 @@ def test_base64io_decode(bytes_to_generate, bytes_per_round, number_of_rounds, t
assert test == plaintext_source[:total_bytes_to_expect]


@pytest.mark.parametrize("encoding", ("ascii", "utf-8"))
@pytest.mark.parametrize(
"bytes_to_generate, bytes_per_round, number_of_rounds, total_bytes_to_expect", build_test_cases()
)
def test_base64io_decode_str(bytes_to_generate, bytes_per_round, number_of_rounds, total_bytes_to_expect):
def test_base64io_decode_str(encoding, bytes_to_generate, bytes_per_round, number_of_rounds, total_bytes_to_expect):
plaintext_source = os.urandom(bytes_to_generate)
plaintext_b64 = io.StringIO(base64.b64encode(plaintext_source).decode('ascii'))
plaintext_b64 = io.StringIO(base64.b64encode(plaintext_source).decode(encoding))
plaintext_wrapped = Base64IO(plaintext_b64)

test = b""
Expand Down Expand Up @@ -313,9 +314,10 @@ def test_base64io_decode_with_whitespace(plaintext_source, b64_plaintext_with_wh
assert test == plaintext_source[:read_bytes]


@pytest.mark.parametrize("encoding", ("ascii", "utf-8"))
@pytest.mark.parametrize("plaintext_source, b64_plaintext_with_whitespace, read_bytes", build_whitespace_testcases())
def test_base64io_decode_with_whitespace_str(plaintext_source, b64_plaintext_with_whitespace, read_bytes):
with Base64IO(io.StringIO(b64_plaintext_with_whitespace.decode('ascii'))) as decoder:
def test_base64io_decode_with_whitespace_str(encoding, plaintext_source, b64_plaintext_with_whitespace, read_bytes):
with Base64IO(io.StringIO(b64_plaintext_with_whitespace.decode(encoding))) as decoder:
test = decoder.read(read_bytes)

assert test == plaintext_source[:read_bytes]
Expand Down
10 changes: 9 additions & 1 deletion test/unit/test_base64io.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,16 @@ def test_file():
if is_python2:
# If we are in Python 2, the "file" assignment should not
# happen because it is a builtin object.
assert not hasattr(base64io, 'file')
assert not hasattr(base64io, "file")
else:
# If we are in Python 3, the "file" assignment should happen
# to provide a concrete definition of the "file" name.
assert base64io.file is NotImplemented


@pytest.mark.parametrize(
"source, expected",
(("asdf", b"asdf"), (b"\x00\x01\x02\x03", b"\x00\x01\x02\x03"), (u"\u1111\u2222", b"\xe1\x84\x91\xe2\x88\xa2")),
)
def test_to_bytes(source, expected):
assert base64io._to_bytes(source) == expected
39 changes: 38 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ basepython = {[testenv:default-python]basepython}
deps =
flake8
flake8-docstrings
flake8-import-order
flake8-isort
# https://github.com/JBKahn/flake8-print/pull/30
flake8-print>=3.1.0
commands =
Expand Down Expand Up @@ -129,6 +129,35 @@ deps =
commands =
{[testenv:blacken-src]commands} --diff

[testenv:isort-seed]
basepython = python3
deps = seed-isort-config
commands = seed-isort-config

[testenv:isort]
basepython = python3
deps = isort
commands = isort -rc \
src \
test \
doc \
setup.py \
{posargs}

[testenv:isort-check]
basepython = python3
deps = {[testenv:isort]deps}
commands = {[testenv:isort]commands} -c

[testenv:autoformat]
basepython = python3
deps =
{[testenv:blacken]deps}
{[testenv:isort]deps}
commands =
{[testenv:blacken]commands}
{[testenv:isort]commands}

[testenv:pylint]
basepython = {[testenv:default-python]basepython}
deps =
Expand Down Expand Up @@ -204,6 +233,14 @@ deps = -rdoc/requirements.txt
commands =
sphinx-build -E -c doc/ -b html doc/ doc/build/html

[testenv:docs-autobuild]
basepython = {[testenv:default-python]basepython}
deps =
{[testenv:docs]deps}
sphinx-autobuild
commands =
sphinx-autobuild -E -c {toxinidir}/doc/ -b html {toxinidir}/doc/ {toxinidir}/doc/build/html

[testenv:serve-docs]
basepython = {[testenv:default-python]basepython}
skip_install = true
Expand Down

0 comments on commit e11d72b

Please sign in to comment.