Skip to content

Commit c8b49bd

Browse files
committed
issue #216
1 parent a389045 commit c8b49bd

7 files changed

Lines changed: 77 additions & 13 deletions

File tree

CHANGELOG.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@ Changelog
44
2.1.2 (2019-XX-XX)
55
++++++++++++++++++
66

7+
**Improvements**:
8+
9+
- ``PerformanceWarning`` will be issued when Python-Redmine does some unnecessary work under the hood to fix the
10+
clients code problems
11+
712
**Bugfixes**:
813

9-
- ``session()`` doesn't restore previous engine if fails (`Issue #211 <https://github.com/maxtepkeev/
14+
- ``Redmine.upload()`` fails under certain circumstances when used with a file-like object and it contains unicode
15+
instead of bytes (`Issue #216 <https://github.com/maxtepkeev/python-redmine/issues/216>`__)
16+
- ``Redmine.session()`` doesn't restore previous engine if fails (`Issue #211 <https://github.com/maxtepkeev/
1017
python-redmine/issues/211>`__) (thanks to `Dmitry Logvinenko <https://github.com/dm-logv>`__)
1118

1219
2.1.1 (2018-05-02)

docs/resources/issue.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ create
5656

5757
.. code-block:: python
5858
59-
>>> from io import StringIO
59+
>>> from io import BytesIO
6060
>>> issue = redmine.issue.create(
6161
... project_id='vacation',
6262
... subject='Vacation',
@@ -72,7 +72,7 @@ create
7272
... estimated_hours=4,
7373
... done_ratio=40,
7474
... custom_fields=[{'id': 1, 'value': 'foo'}, {'id': 2, 'value': 'bar'}],
75-
... uploads=[{'path': '/absolute/path/to/file'}, {'path': StringIO('I am content of file 2')}]
75+
... uploads=[{'path': '/absolute/path/to/file'}, {'path': BytesIO(b'I am content of file 2')}]
7676
... )
7777
>>> issue
7878
<redminelib.resources.Issue #123 "Vacation">
@@ -323,7 +323,7 @@ update
323323

324324
.. code-block:: python
325325
326-
>>> from io import StringIO
326+
>>> from io import BytesIO
327327
>>> redmine.issue.update(
328328
... 1,
329329
... project_id=1,
@@ -341,7 +341,7 @@ update
341341
... estimated_hours=4,
342342
... done_ratio=40,
343343
... custom_fields=[{'id': 1, 'value': 'foo'}, {'id': 2, 'value': 'bar'}],
344-
... uploads=[{'path': '/absolute/path/to/file'}, {'path': StringIO('I am content of file 2')}]
344+
... uploads=[{'path': '/absolute/path/to/file'}, {'path': BytesIO(b'I am content of file 2')}]
345345
... )
346346
True
347347

docs/resources/wiki_page.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ create
4242

4343
.. code-block:: python
4444
45-
>>> from io import StringIO
45+
>>> from io import BytesIO
4646
>>> wiki_page = redmine.wiki_page.create(
4747
... project_id='vacation',
4848
... title='FooBar',
4949
... text='foo',
5050
... parent_title='Yada',
5151
... comments='bar',
52-
... uploads=[{'path': '/absolute/path/to/file'}, {'path': StringIO('I am content of file 2')}]
52+
... uploads=[{'path': '/absolute/path/to/file'}, {'path': BytesIO(b'I am content of file 2')}]
5353
... )
5454
>>> wiki_page
5555
<redminelib.resources.WikiPage "FooBar">
@@ -194,15 +194,15 @@ update
194194

195195
.. code-block:: python
196196
197-
>>> from io import StringIO
197+
>>> from io import BytesIO
198198
>>> redmine.wiki_page.update(
199199
... 'Foo',
200200
... project_id='vacation',
201201
... title='FooBar',
202202
... text='foo',
203203
... parent_title='Yada',
204204
... comments='bar',
205-
... uploads=[{'path': '/absolute/path/to/file'}, {'path': StringIO('I am content of file 2')}]
205+
... uploads=[{'path': '/absolute/path/to/file'}, {'path': BytesIO(b'I am content of file 2')}]
206206
... )
207207
True
208208

redminelib/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
"""
44

55
import os
6+
import io
7+
import sys
68
import inspect
9+
import warnings
710
import contextlib
811

912
from distutils.version import LooseVersion
@@ -103,6 +106,19 @@ def upload(self, f):
103106
# stream ourselves as we have no idea of what the client is going to do with it afterwards, so we
104107
# leave the closing part to the client or to the garbage collector
105108
if hasattr(f, 'close'):
109+
try:
110+
c = f.read(0)
111+
except (AttributeError, TypeError):
112+
raise exceptions.FileObjectError
113+
114+
# We need to send bytes over the socket, so in case a file-like object contains a unicode
115+
# object underneath, we need to convert it to bytes, otherwise we'll get an exception
116+
if isinstance(c, str if sys.version_info[0] >= 3 else unicode):
117+
warnings.warn("File-like object contains unicode, hence an additional step is performed to convert "
118+
"it's content to bytes, please consider switching to bytes to eliminate this warning",
119+
exceptions.PerformanceWarning)
120+
f = io.BytesIO(f.read().encode('utf-8'))
121+
106122
stream = f
107123
close = False
108124
else:

redminelib/exceptions.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
"""
22
Python-Redmine tries it's best to provide human readable errors in all situations.
3-
This is a list of all exceptions that Python-Redmine can throw.
3+
This is a list of all exceptions or warnings that Python-Redmine can throw/raise.
44
"""
55

66
from . import utilities
77

88

9+
@utilities.fix_unicode
10+
class BaseRedmineWarning(Warning):
11+
"""
12+
Base warning class for Redmine warnings.
13+
"""
14+
15+
16+
class PerformanceWarning(BaseRedmineWarning):
17+
"""
18+
Warning raised when there's a possible performance impact.
19+
"""
20+
21+
922
@utilities.fix_unicode
1023
class BaseRedmineError(Exception):
1124
"""
@@ -29,6 +42,14 @@ def __init__(self):
2942
super(NoFileError, self).__init__("Can't upload a file that doesn't exist or is empty")
3043

3144

45+
class FileObjectError(BaseRedmineError):
46+
"""
47+
File-like object isn't supported as it doesn't support the read(size) method.
48+
"""
49+
def __init__(self):
50+
super(FileObjectError, self).__init__("File-like object doesn't support the read(size) method")
51+
52+
3253
class ResourceNotFoundError(BaseRedmineError):
3354
"""
3455
Requested resource doesn't exist.

tests/test_managers.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import warnings
2+
13
from . import mock, BaseRedmineTestCase
24
from .responses import responses
35

@@ -120,7 +122,11 @@ def test_create_resource_with_stream_uploads(self):
120122
'issue': {'subject': 'Foo', 'project_id': 1, 'id': 1}
121123
}
122124
stream = StringIO(b'\xcf\x86oo'.decode('utf8'))
123-
issue = self.redmine.issue.create(project_id=1, subject='Foo', uploads=[{'path': stream}])
125+
with warnings.catch_warnings(record=True) as w:
126+
warnings.simplefilter('always')
127+
issue = self.redmine.issue.create(project_id=1, subject='Foo', uploads=[{'path': stream}])
128+
self.assertEquals(len(w), 1)
129+
self.assertIs(w[0].category, exceptions.PerformanceWarning)
124130
self.assertEqual(issue.project_id, 1)
125131
self.assertEqual(issue.subject, 'Foo')
126132

@@ -153,7 +159,11 @@ def test_update_resource_with_stream_uploads(self):
153159
mock.Mock(status_code=200, history=[], content='')
154160
])
155161
stream = StringIO(b'\xcf\x86oo'.decode('utf8'))
156-
self.assertEqual(self.redmine.issue.update(1, subject='Bar', uploads=[{'path': stream}]), True)
162+
with warnings.catch_warnings(record=True) as w:
163+
warnings.simplefilter('always')
164+
self.assertEqual(self.redmine.issue.update(1, subject='Bar', uploads=[{'path': stream}]), True)
165+
self.assertEquals(len(w), 1)
166+
self.assertIs(w[0].category, exceptions.PerformanceWarning)
157167

158168
def test_delete_resource(self):
159169
self.response.content = ''

tests/test_redmine.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import warnings
2+
13
from . import mock, BaseRedmineTestCase, Redmine
24

35
from redminelib import engines, resultsets, exceptions
@@ -66,7 +68,11 @@ def test_successful_filestream_upload(self):
6668
from io import StringIO
6769
self.response.status_code = 201
6870
self.response.json.return_value = {'upload': {'id': 1, 'token': '456789'}}
69-
self.assertEqual(self.redmine.upload(StringIO(b'\xcf\x86oo'.decode('utf8')))['token'], '456789')
71+
with warnings.catch_warnings(record=True) as w:
72+
warnings.simplefilter('always')
73+
self.assertEqual(self.redmine.upload(StringIO(b'\xcf\x86oo'.decode('utf8')))['token'], '456789')
74+
self.assertEquals(len(w), 1)
75+
self.assertIs(w[0].category, exceptions.PerformanceWarning)
7076

7177
@mock.patch('redminelib.open', mock.mock_open(), create=True)
7278
def test_successful_file_download(self):
@@ -86,6 +92,10 @@ def test_file_url_exception(self):
8692
def test_file_upload_no_file_exception(self):
8793
self.assertRaises(exceptions.NoFileError, lambda: self.redmine.upload('foo',))
8894

95+
def test_file_upload_file_object_exception(self):
96+
f = type('FileObject', (), {'close': lambda obj: None})()
97+
self.assertRaises(exceptions.FileObjectError, lambda: self.redmine.upload(f))
98+
8999
def test_file_upload_not_supported_exception(self):
90100
self.redmine.ver = '1.0.0'
91101
self.assertRaises(exceptions.VersionMismatchError, lambda: self.redmine.upload('foo',))

0 commit comments

Comments
 (0)