Skip to content

Commit 53bc72f

Browse files
authored
Eliminate data copy when calling str() on a bytes-like object (#963)
* Eliminate data copy when calling str() on a bytes-like object * Update after review
1 parent d7b8bcd commit 53bc72f

File tree

5 files changed

+66
-4
lines changed

5 files changed

+66
-4
lines changed

Src/IronPython/Runtime/ByteArray.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using Microsoft.Scripting.Runtime;
1616
using Microsoft.Scripting.Utils;
1717

18+
using IronPython.Runtime.Exceptions;
1819
using IronPython.Runtime.Operations;
1920
using IronPython.Runtime.Types;
2021

@@ -1078,6 +1079,13 @@ private string Repr() {
10781079
}
10791080
}
10801081

1082+
public virtual string __str__(CodeContext context) {
1083+
if (context.LanguageContext.PythonOptions.BytesWarning) {
1084+
PythonOps.Warn(context, PythonExceptions.BytesWarning, "str() on a bytearray instance");
1085+
}
1086+
return Repr();
1087+
}
1088+
10811089
public virtual string __repr__(CodeContext context) => Repr();
10821090

10831091
public override string ToString() => Repr();

Src/IronPython/Runtime/Bytes.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using Microsoft.Scripting.Runtime;
1717
using Microsoft.Scripting.Utils;
1818

19+
using IronPython.Runtime.Exceptions;
1920
using IronPython.Runtime.Operations;
2021
using IronPython.Runtime.Types;
2122
using NotNullWhenAttribute = System.Diagnostics.CodeAnalysis.NotNullWhenAttribute;
@@ -854,6 +855,13 @@ public PythonTuple __reduce__(CodeContext context) {
854855
);
855856
}
856857

858+
public virtual string __str__(CodeContext context) {
859+
if (context.LanguageContext.PythonOptions.BytesWarning) {
860+
PythonOps.Warn(context, PythonExceptions.BytesWarning, "str() on a bytes instance");
861+
}
862+
return _bytes.BytesRepr();
863+
}
864+
857865
public virtual string __repr__(CodeContext context) {
858866
return _bytes.BytesRepr();
859867
}

Src/IronPython/Runtime/Operations/StringOps.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,6 @@ internal static object FastNew(CodeContext/*!*/ context, object? x) {
162162
return "None";
163163
}
164164
if (x is string) {
165-
// check ascii
166165
return x;
167166
}
168167

@@ -287,10 +286,13 @@ public static object __new__(CodeContext/*!*/ context, [NotNull]PythonType cls,
287286
}
288287

289288
[StaticExtensionMethod]
290-
public static object __new__(CodeContext/*!*/ context, [NotNull]PythonType cls, [BytesLike, NotNull]IList<byte> @object, [NotNull]string encoding, [NotNull]string errors = "strict") {
289+
public static object __new__(CodeContext/*!*/ context, [NotNull]PythonType cls, [NotNull]IBufferProtocol @object, [NotNull]string encoding, [NotNull]string errors = "strict") {
291290
if (cls == TypeCache.String) {
292-
if (@object is Bytes) return ((Bytes)@object).decode(context, encoding, errors);
293-
return new Bytes(@object).decode(context, encoding, errors);
291+
try {
292+
return RawDecode(context, @object, encoding, errors);
293+
} catch (BufferException) {
294+
throw PythonOps.TypeErrorForBadInstance("decoding to str: need a bytes-like object, {0} found", @object);
295+
}
294296
} else {
295297
return cls.CreateInstance(context, __new__(context, TypeCache.String, @object, encoding, errors));
296298
}

Tests/test_bytes.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import itertools
88
import sys
99
import unittest
10+
import warnings
1011

1112
from iptest import IronPythonTestCase, is_cli, is_cpython, long, run_test, skipUnlessIronPython
1213

@@ -299,6 +300,40 @@ def __index__(self):
299300
self.assertRaises(TypeError, int.from_bytes, IndexableInt(2), 'big')
300301
self.assertRaises(TypeError, int.from_bytes, 2, 'big')
301302

303+
@unittest.skipUnless(sys.flags.bytes_warning, "Run Python with the '-b' flag on command line for this test")
304+
def test_byteswarning(self):
305+
with warnings.catch_warnings(record=True) as ws:
306+
warnings.simplefilter("always")
307+
308+
with self.assertWarnsRegex(BytesWarning, r"^str\(\) on a bytes instance$"):
309+
self.assertEqual(str(b'abc'), "b'abc'")
310+
self.assertEqual(str(b'abc', 'ascii'), 'abc')
311+
312+
with self.assertWarnsRegex(BytesWarning, r"^str\(\) on a bytearray instance$"):
313+
self.assertEqual(str(bytearray(b'abc')), "bytearray(b'abc')")
314+
self.assertEqual(str(bytearray(b'abc'), 'ascii'), 'abc')
315+
316+
class B(bytes):
317+
def __str__(self):
318+
return "This is B"
319+
320+
self.assertEqual(str(B(b'abc')), "This is B") # no warning
321+
322+
class B2(bytes): pass
323+
with self.assertWarnsRegex(BytesWarning, r"^str\(\) on a bytes instance$"):
324+
self.assertEqual(str(B2(b'abc')), "b'abc'")
325+
326+
self.assertEqual(len(ws), 0) # no unchecked warnings
327+
328+
def test_byteswarning_user(self):
329+
with warnings.catch_warnings(record=True) as ws:
330+
warnings.simplefilter("always")
331+
332+
with self.assertWarnsRegex(BytesWarning, r"^test warning$"):
333+
warnings.warn("test warning", BytesWarning)
334+
335+
self.assertEqual(len(ws), 0) # no unchecked warnings
336+
302337
def test_capitalize(self):
303338
tests = [(b'foo', b'Foo'),
304339
(b' foo', b' foo'),

Tests/test_str.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import os
77
import sys
88
import unittest
9+
import warnings
910

1011
from iptest import IronPythonTestCase, is_cli, run_test, skipUnlessIronPython
1112

@@ -54,6 +55,14 @@ def test_constructor(self):
5455
if is_cli:
5556
self.assertEqual('ä', str('ä'.Chars[0])) # StringOps.__new__(..., char)
5657

58+
self.assertRegex(str(memoryview(b'abc')), r"^<memory at .+>$")
59+
self.assertEqual(str(memoryview(b'abc'), 'ascii'), 'abc')
60+
self.assertRaises(TypeError, str, memoryview(b'abc')[::2], 'ascii')
61+
62+
import array
63+
self.assertEqual(str(array.array('B', b'abc')), "array('B', [97, 98, 99])")
64+
self.assertEqual(str(array.array('B', b'abc'), 'ascii'), 'abc')
65+
5766
def test_add_mul(self):
5867
self.assertRaises(TypeError, lambda: "a" + 3)
5968
self.assertRaises(TypeError, lambda: 3 + "a")

0 commit comments

Comments
 (0)