Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-31412: wave.open takes a path-like object #3484

Closed
wants to merge 10 commits into from
7 changes: 5 additions & 2 deletions Doc/library/wave.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ The :mod:`wave` module defines the following function and exception:

.. function:: open(file, mode=None)

If *file* is a string, open the file by that name, otherwise treat it as a
file-like object. *mode* can be:
If *file* is a string or :term:`path-like object`, open the file
by that name, otherwise treat it as a file-like object. *mode* can be:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a reference to the "file-like object" term?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked in the glossary and "file-like object" redirects to "file object" so I used that term instead. Hope that that's okay.


``'rb'``
Read only mode.
Expand All @@ -47,6 +47,9 @@ The :mod:`wave` module defines the following function and exception:
.. versionchanged:: 3.4
Added support for unseekable files.

.. versionchanged:: 3.7
Added support for :term:`path-like object`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or :term:'path-like objects <path-like object>' (use backquotes instead of apostrophes).


.. function:: openfp(file, mode)

A synonym for :func:`.open`, maintained for backwards compatibility.
Expand Down
24 changes: 23 additions & 1 deletion Lib/test/test_wave.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
from test import audiotests
from test import support
from audioop import byteswap
import io
import pathlib
import os
import sys
import wave


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert this change.

class WaveTest(audiotests.AudioWriteTests,
audiotests.AudioTestsWithSourceFile):
module = wave
Expand Down Expand Up @@ -108,6 +110,26 @@ def test__all__(self):
blacklist = {'WAVE_FORMAT_PCM'}
support.check__all__(self, wave, blacklist=blacklist)

def test_open(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rename this to something like test_open_pathlike.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test checks only failed cases. It would be better to test successful opens.

Test not only reading, but writing too.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it acceptable to add a wave file to the test directory? I didn't test on a .wav file since even a small one can be 20-30k -- if that's okay then I will add one and test that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are sample wave files in the audiodata directory. See how other tests use them.

# test that a.wave file could in theory be opened.
# For file size reasons, we do not have an actual .wav file,
# so we will pass in this file, but check that a
# a wave.Error (invalid data) will be raised,
# and not a TypeError or AttributeError
# showing that the opening was actually completed.
this_file = os.path.abspath(__file__)

with self.assertRaises(wave.Error):
wave.open(this_file)

this_file_as_path = pathlib.Path(this_file)
with self.assertRaises(wave.Error):
wave.open(this_file_as_path)

this_string_io = io.StringIO()
with self.assertRaises(EOFError):
wave.open(this_string_io)


if __name__ == '__main__':
unittest.main()
27 changes: 19 additions & 8 deletions Lib/wave.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""Stuff to parse WAVE files.
"""Module for parsing WAVE audio files.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is out of scope for this issue.


Usage.

Reading WAVE files:
f = wave.open(file, 'r')
where file is either the name of a file or an open file pointer.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you create a separate PR for these changes? We've removed duplicate docstrings from several modules (e.g. the venv module) to avoid future maintenance costs. I think we can also drop this giant docstring too.

The open file pointer must have methods read(), seek(), and close().
When the setpos() and rewind() methods are not used, the seek()
method is not necessary.
where file is either a str, bytes, or Pathlike object
representing a file or an open file pointer.
The open file pointer must have methods read(), and close().
If the setpos() and rewind() methods are used, then a seek()
method is also necessary.

This returns an instance of a class with the following public methods:
getnchannels() -- returns number of audio channels (1 for
Expand Down Expand Up @@ -37,9 +38,11 @@

Writing WAVE files:
f = wave.open(file, 'w')
where file is either the name of a file or an open file pointer.
The open file pointer must have methods write(), tell(), seek(), and
close().
where file is either a str, bytes, or Pathlike object
representing a file or an open file pointer.
The open file pointer must have methods write(), tell(), and
close(). If the pointer does not have a seek() method, then
setnframes() must be given an accurate value

This returns an instance of a class with the following public methods:
setnchannels(n) -- set the number of channels
Expand Down Expand Up @@ -83,6 +86,7 @@ class Error(Exception):
_array_fmts = None, 'b', 'h', None, 'i'

import audioop
import os
import struct
import sys
from chunk import Chunk
Expand Down Expand Up @@ -495,6 +499,13 @@ def open(f, mode=None):
mode = f.mode
else:
mode = 'rb'
try:
f = os.fspath(f)
except TypeError:
if not hasattr(f, 'read') or not hasattr(f, 'close'):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also check for a write method here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is enough to check for a read in Wave_read and for a write on Wave_write. close should be optional.

Note that in Wave_read and Wave_write constructors there are checks isinstance(f, str). This means that bytes patch are not supported, and file descriptors are not supported. I would use hasattr(f, 'read') or hasattr(f, 'write') instead and just pass other objects to builtins.open().

raise TypeError('open() takes str, bytes, a PathLike object, '
+ f'or an open filehandle, not {type(f)}')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{type(f).__name__!r} would give us a more readable name.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually bytes are not supported with current implementation. See the comment above.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we also need to add from None to make the traceback clearer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"open filehandle" -> file-like object


if mode in ('r', 'rb'):
return Wave_read(f)
elif mode in ('w', 'wb'):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
`wave.open()` now accepts PathLike objects. Contributed by Michael Scott
methane marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PathLike -> :class:`os.PathLike`

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or just path-like. Or :term:'path-like objects <path-like object>' (use backquotes instead of apostrophes).

methane marked this conversation as resolved.
Show resolved Hide resolved
Cuthbert.