Skip to content

Commit f4e5508

Browse files
authored
Merge pull request numpy#25617 from cook-1229/patch-1
BUG: Pass newline to datasource.open() in numpy.lib.npio.
2 parents 467278b + 275f080 commit f4e5508

File tree

3 files changed

+125
-32
lines changed

3 files changed

+125
-32
lines changed

numpy/lib/npyio.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1388,7 +1388,7 @@ def _savetxt_dispatcher(fname, X, fmt=None, delimiter=None, newline=None,
13881388

13891389

13901390
@array_function_dispatch(_savetxt_dispatcher)
1391-
def savetxt(fname, X, fmt='%.18e', delimiter=' ', newline='\n', header='',
1391+
def savetxt(fname, X, fmt='%.18e', delimiter=' ', newline=None, header='',
13921392
footer='', comments='# ', encoding=None):
13931393
"""
13941394
Save an array to a text file.
@@ -1417,7 +1417,7 @@ def savetxt(fname, X, fmt='%.18e', delimiter=' ', newline='\n', header='',
14171417
delimiter : str, optional
14181418
String or character separating columns.
14191419
newline : str, optional
1420-
String or character separating lines.
1420+
String or character separating lines. Default is universal newline.
14211421
14221422
.. versionadded:: 1.5.0
14231423
header : str, optional
@@ -1548,13 +1548,24 @@ def first_write(self, v):
15481548
self.write_bytes(v)
15491549
self.write = self.write_bytes
15501550

1551+
# _datasource.open() needs to be passed None to enable universal
1552+
# newlines, and this function needs to write newlines.
1553+
if newline is None:
1554+
open_newline = None
1555+
newline = os.linesep
1556+
else:
1557+
open_newline = newline
1558+
15511559
own_fh = False
15521560
if isinstance(fname, os_PathLike):
15531561
fname = os_fspath(fname)
15541562
if _is_string_like(fname):
15551563
# datasource doesn't support creating a new file ...
15561564
open(fname, 'wt').close()
1557-
fh = np.lib._datasource.open(fname, 'wt', encoding=encoding)
1565+
fh = np.lib._datasource.open(fname,
1566+
'wt',
1567+
encoding=encoding,
1568+
newline=open_newline)
15581569
own_fh = True
15591570
elif hasattr(fname, 'write'):
15601571
# wrap to handle byte output streams
@@ -1607,7 +1618,7 @@ def first_write(self, v):
16071618
raise ValueError('invalid fmt: %r' % (fmt,))
16081619

16091620
if len(header) > 0:
1610-
header = header.replace('\n', '\n' + comments)
1621+
header = header.replace(newline, newline + comments)
16111622
fh.write(comments + header + newline)
16121623
if iscomplex_X:
16131624
for row in X:
@@ -1628,7 +1639,7 @@ def first_write(self, v):
16281639
fh.write(v)
16291640

16301641
if len(footer) > 0:
1631-
footer = footer.replace('\n', '\n' + comments)
1642+
footer = footer.replace(newline, newline + comments)
16321643
fh.write(comments + footer + newline)
16331644
finally:
16341645
if own_fh:

numpy/lib/npyio.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ def savetxt(
189189
X: ArrayLike,
190190
fmt: str | Sequence[str] = ...,
191191
delimiter: str = ...,
192-
newline: str = ...,
192+
newline: None | str = ...,
193193
header: str = ...,
194194
footer: str = ...,
195195
comments: str = ...,

numpy/lib/tests/test_io.py

Lines changed: 108 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -356,22 +356,25 @@ def test_array(self):
356356
np.savetxt(c, a, fmt=fmt)
357357
c.seek(0)
358358
assert_equal(c.readlines(),
359-
[asbytes((fmt + ' ' + fmt + '\n') % (1, 2)),
360-
asbytes((fmt + ' ' + fmt + '\n') % (3, 4))])
359+
[asbytes((fmt + ' ' + fmt + os.linesep) % (1, 2)),
360+
asbytes((fmt + ' ' + fmt + os.linesep) % (3, 4))])
361361

362362
a = np.array([[1, 2], [3, 4]], int)
363363
c = BytesIO()
364364
np.savetxt(c, a, fmt='%d')
365365
c.seek(0)
366-
assert_equal(c.readlines(), [b'1 2\n', b'3 4\n'])
366+
assert_equal(c.readlines(), [b'1 2'+os.linesep.encode(),
367+
b'3 4'+os.linesep.encode()])
367368

368369
def test_1D(self):
369370
a = np.array([1, 2, 3, 4], int)
370371
c = BytesIO()
371372
np.savetxt(c, a, fmt='%d')
372373
c.seek(0)
373374
lines = c.readlines()
374-
assert_equal(lines, [b'1\n', b'2\n', b'3\n', b'4\n'])
375+
newline = os.linesep.encode()
376+
assert_equal(lines, [b'1'+newline, b'2'+newline, b'3'+newline,
377+
b'4'+newline])
375378

376379
def test_0D_3D(self):
377380
c = BytesIO()
@@ -383,7 +386,8 @@ def test_structured(self):
383386
c = BytesIO()
384387
np.savetxt(c, a, fmt='%d')
385388
c.seek(0)
386-
assert_equal(c.readlines(), [b'1 2\n', b'3 4\n'])
389+
newline = os.linesep.encode()
390+
assert_equal(c.readlines(), [b'1 2'+newline, b'3 4'+newline])
387391

388392
def test_structured_padded(self):
389393
# gh-13297
@@ -393,7 +397,8 @@ def test_structured_padded(self):
393397
c = BytesIO()
394398
np.savetxt(c, a[['foo', 'baz']], fmt='%d')
395399
c.seek(0)
396-
assert_equal(c.readlines(), [b'1 3\n', b'4 6\n'])
400+
newline = os.linesep.encode()
401+
assert_equal(c.readlines(), [b'1 3'+newline, b'4 6'+newline])
397402

398403
def test_multifield_view(self):
399404
a = np.ones(1, dtype=[('x', 'i4'), ('y', 'i4'), ('z', 'f4')])
@@ -409,68 +414,135 @@ def test_delimiter(self):
409414
c = BytesIO()
410415
np.savetxt(c, a, delimiter=',', fmt='%d')
411416
c.seek(0)
412-
assert_equal(c.readlines(), [b'1,2\n', b'3,4\n'])
417+
newline = os.linesep.encode()
418+
assert_equal(c.readlines(), [b'1,2'+newline, b'3,4'+newline])
413419

414420
def test_format(self):
415421
a = np.array([(1, 2), (3, 4)])
422+
newline = os.linesep.encode()
416423
c = BytesIO()
417424
# Sequence of formats
418425
np.savetxt(c, a, fmt=['%02d', '%3.1f'])
419426
c.seek(0)
420-
assert_equal(c.readlines(), [b'01 2.0\n', b'03 4.0\n'])
427+
assert_equal(c.readlines(), [b'01 2.0'+newline, b'03 4.0'+newline])
421428

422429
# A single multiformat string
423430
c = BytesIO()
424431
np.savetxt(c, a, fmt='%02d : %3.1f')
425432
c.seek(0)
426433
lines = c.readlines()
427-
assert_equal(lines, [b'01 : 2.0\n', b'03 : 4.0\n'])
434+
assert_equal(lines, [b'01 : 2.0'+newline, b'03 : 4.0'+newline])
428435

429436
# Specify delimiter, should be overridden
430437
c = BytesIO()
431438
np.savetxt(c, a, fmt='%02d : %3.1f', delimiter=',')
432439
c.seek(0)
433440
lines = c.readlines()
434-
assert_equal(lines, [b'01 : 2.0\n', b'03 : 4.0\n'])
441+
assert_equal(lines, [b'01 : 2.0'+newline, b'03 : 4.0'+newline])
435442

436443
# Bad fmt, should raise a ValueError
437444
c = BytesIO()
438445
assert_raises(ValueError, np.savetxt, c, a, fmt=99)
439446

447+
def test_newline(self):
448+
a = np.array([(1, 2), (3, 4)])
449+
c = BytesIO()
450+
451+
# Universal newline, implicit and explicit
452+
newline = os.linesep.encode()
453+
np.savetxt(c, a, fmt='%d')
454+
c.seek(0)
455+
assert_equal(c.readlines(), [b'1 2'+newline, b'3 4'+newline],
456+
err_msg='Universal newline, implicit')
457+
c = BytesIO()
458+
np.savetxt(c, a, fmt='%d', newline=None)
459+
c.seek(0)
460+
assert_equal(c.readlines(), [b'1 2'+newline, b'3 4'+newline],
461+
err_msg='Universal newline, explicit')
462+
463+
# POSIX newline
464+
newline = '\n'
465+
c = BytesIO()
466+
np.savetxt(c, a, fmt='%d', newline=newline)
467+
c.seek(0)
468+
lines = c.readlines()
469+
newline = newline.encode()
470+
assert_equal(lines, [b'1 2'+newline, b'3 4'+newline],
471+
err_msg='POSIX newline')
472+
473+
# NT newline
474+
newline = '\r\n'
475+
c = BytesIO()
476+
np.savetxt(c, a, fmt='%d', newline=newline)
477+
c.seek(0)
478+
lines = c.readlines()
479+
newline = newline.encode()
480+
assert_equal(lines, [b'1 2'+newline, b'3 4'+newline],
481+
err_msg='NT newline')
482+
483+
# Tab "newline"
484+
newline = '\t'
485+
c = BytesIO()
486+
np.savetxt(c, a, fmt='%d', newline=newline)
487+
c.seek(0)
488+
lines = c.readlines()
489+
newline = newline.encode()
490+
assert_equal(lines, [b'1 2'+newline+b'3 4'+newline, ],
491+
err_msg='Tab newline')
492+
440493
def test_header_footer(self):
441494
# Test the functionality of the header and footer keyword argument.
442495

443496
c = BytesIO()
444497
a = np.array([(1, 2), (3, 4)], dtype=int)
498+
a_txt = '1 2' + os.linesep + '3 4' + os.linesep
445499
test_header_footer = 'Test header / footer'
446500
# Test the header keyword argument
447501
np.savetxt(c, a, fmt='%1d', header=test_header_footer)
448502
c.seek(0)
449503
assert_equal(c.read(),
450-
asbytes('# ' + test_header_footer + '\n1 2\n3 4\n'))
504+
asbytes('# ' + test_header_footer + os.linesep
505+
+ a_txt))
451506
# Test the footer keyword argument
452507
c = BytesIO()
453508
np.savetxt(c, a, fmt='%1d', footer=test_header_footer)
454509
c.seek(0)
455510
assert_equal(c.read(),
456-
asbytes('1 2\n3 4\n# ' + test_header_footer + '\n'))
511+
asbytes(a_txt + '# ' + test_header_footer + os.linesep))
457512
# Test the commentstr keyword argument used on the header
458513
c = BytesIO()
459514
commentstr = '% '
460515
np.savetxt(c, a, fmt='%1d',
461516
header=test_header_footer, comments=commentstr)
462517
c.seek(0)
463518
assert_equal(c.read(),
464-
asbytes(commentstr + test_header_footer + '\n' + '1 2\n3 4\n'))
519+
asbytes(commentstr + test_header_footer + os.linesep
520+
+ a_txt))
465521
# Test the commentstr keyword argument used on the footer
466522
c = BytesIO()
467523
commentstr = '% '
468524
np.savetxt(c, a, fmt='%1d',
469525
footer=test_header_footer, comments=commentstr)
470526
c.seek(0)
471527
assert_equal(c.read(),
472-
asbytes('1 2\n3 4\n' + commentstr + test_header_footer + '\n'))
528+
asbytes(a_txt + commentstr + test_header_footer
529+
+ os.linesep))
473530

531+
@pytest.mark.parametrize("newline", ['\n', '\r\n'])
532+
def test_newline_header_footer(self, newline):
533+
c = BytesIO()
534+
a = np.array([(1, 2), (3, 4)], dtype=int)
535+
a_txt = '1 2' + newline + '3 4' + newline
536+
test_header_footer = 'Test header / footer'
537+
# Test the header and footer keyword argument
538+
np.savetxt(c, a, fmt='%1d', newline=newline, header=test_header_footer,
539+
footer=test_header_footer)
540+
c.seek(0)
541+
assert_equal(c.read(),
542+
asbytes('# ' + test_header_footer + newline
543+
+ a_txt
544+
+ '# ' + test_header_footer + newline))
545+
474546
def test_file_roundtrip(self):
475547
with temppath() as name:
476548
a = np.array([(1, 2), (3, 4)])
@@ -485,6 +557,7 @@ def test_complex_arrays(self):
485557
re = np.pi
486558
im = np.e
487559
a[:] = re + 1.0j * im
560+
newline = os.linesep.encode()
488561

489562
# One format only
490563
c = BytesIO()
@@ -493,8 +566,10 @@ def test_complex_arrays(self):
493566
lines = c.readlines()
494567
assert_equal(
495568
lines,
496-
[b' ( +3.142e+00+ +2.718e+00j) ( +3.142e+00+ +2.718e+00j)\n',
497-
b' ( +3.142e+00+ +2.718e+00j) ( +3.142e+00+ +2.718e+00j)\n'])
569+
[b' ( +3.142e+00+ +2.718e+00j) ( +3.142e+00+ +2.718e+00j)'
570+
+ newline,
571+
b' ( +3.142e+00+ +2.718e+00j) ( +3.142e+00+ +2.718e+00j)'
572+
+ newline])
498573

499574
# One format for each real and imaginary part
500575
c = BytesIO()
@@ -503,8 +578,10 @@ def test_complex_arrays(self):
503578
lines = c.readlines()
504579
assert_equal(
505580
lines,
506-
[b' +3.142e+00 +2.718e+00 +3.142e+00 +2.718e+00\n',
507-
b' +3.142e+00 +2.718e+00 +3.142e+00 +2.718e+00\n'])
581+
[b' +3.142e+00 +2.718e+00 +3.142e+00 +2.718e+00'
582+
+ newline,
583+
b' +3.142e+00 +2.718e+00 +3.142e+00 +2.718e+00'
584+
+ newline])
508585

509586
# One format for each complex number
510587
c = BytesIO()
@@ -513,8 +590,10 @@ def test_complex_arrays(self):
513590
lines = c.readlines()
514591
assert_equal(
515592
lines,
516-
[b'(3.142e+00+2.718e+00j) (3.142e+00+2.718e+00j)\n',
517-
b'(3.142e+00+2.718e+00j) (3.142e+00+2.718e+00j)\n'])
593+
[b'(3.142e+00+2.718e+00j) (3.142e+00+2.718e+00j)'
594+
+ newline,
595+
b'(3.142e+00+2.718e+00j) (3.142e+00+2.718e+00j)'
596+
+ newline])
518597

519598
def test_complex_negative_exponent(self):
520599
# Previous to 1.15, some formats generated x+-yj, gh 7895
@@ -524,14 +603,17 @@ def test_complex_negative_exponent(self):
524603
re = np.pi
525604
im = np.e
526605
a[:] = re - 1.0j * im
606+
newline = os.linesep.encode()
527607
c = BytesIO()
528608
np.savetxt(c, a, fmt='%.3e')
529609
c.seek(0)
530610
lines = c.readlines()
531611
assert_equal(
532612
lines,
533-
[b' (3.142e+00-2.718e+00j) (3.142e+00-2.718e+00j)\n',
534-
b' (3.142e+00-2.718e+00j) (3.142e+00-2.718e+00j)\n'])
613+
[b' (3.142e+00-2.718e+00j) (3.142e+00-2.718e+00j)'
614+
+ newline,
615+
b' (3.142e+00-2.718e+00j) (3.142e+00-2.718e+00j)'
616+
+ newline])
535617

536618

537619
def test_custom_writer(self):
@@ -577,15 +659,15 @@ def test_unicode_bytestream(self):
577659
s = BytesIO()
578660
np.savetxt(s, a, fmt=['%s'], encoding='UTF-8')
579661
s.seek(0)
580-
assert_equal(s.read().decode('UTF-8'), utf8 + '\n')
662+
assert_equal(s.read().decode('UTF-8'), utf8 + os.linesep)
581663

582664
def test_unicode_stringstream(self):
583665
utf8 = b'\xcf\x96'.decode('UTF-8')
584666
a = np.array([utf8], dtype=np.str_)
585667
s = StringIO()
586668
np.savetxt(s, a, fmt=['%s'], encoding='UTF-8')
587669
s.seek(0)
588-
assert_equal(s.read(), utf8 + '\n')
670+
assert_equal(s.read(), utf8 + os.linesep)
589671

590672
@pytest.mark.parametrize("fmt", ["%f", b"%f"])
591673
@pytest.mark.parametrize("iotype", [StringIO, BytesIO])
@@ -596,9 +678,9 @@ def test_unicode_and_bytes_fmt(self, fmt, iotype):
596678
np.savetxt(s, a, fmt=fmt)
597679
s.seek(0)
598680
if iotype is StringIO:
599-
assert_equal(s.read(), "%f\n" % 1.)
681+
assert_equal(s.read(), "%f%s" % (1., os.linesep))
600682
else:
601-
assert_equal(s.read(), b"%f\n" % 1.)
683+
assert_equal(s.read(), b"%f%s" % (1., os.linesep.encode()))
602684

603685
@pytest.mark.skipif(sys.platform=='win32', reason="files>4GB may not work")
604686
@pytest.mark.slow

0 commit comments

Comments
 (0)