diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 73217136f14472..8993049a720b1c 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -2611,8 +2611,10 @@ requires, and these work on all supported platforms. | ``%M`` | Minute as a zero-padded | 00, 01, ..., 59 | \(9) | | | decimal number. | | | +-----------+--------------------------------+------------------------+-------+ -| ``%n`` | The newline character | ``\n`` | \(0) | -| | (``'\n'``). | | | +| ``%n`` | The newline character | ``\n`` | | +| | (``'\n'``). For | | | +| | :meth:`!strptime`, zero or | | | +| | more whitespace. | | | +-----------+--------------------------------+------------------------+-------+ | ``%p`` | Locale's equivalent of either || AM, PM (en_US); | \(1), | | | AM or PM. || am, pm (de_DE) | \(3) | @@ -2625,8 +2627,9 @@ requires, and these work on all supported platforms. | ``%S`` | Second as a zero-padded | 00, 01, ..., 59 | \(4), | | | decimal number. | | \(9) | +-----------+--------------------------------+------------------------+-------+ -| ``%t`` | The tab character | ``\t`` | \(0) | -| | (``'\t'``). | | | +| ``%t`` | The tab character (``'\t'``). | ``\t`` | | +| | For :meth:`!strptime`, | | | +| | zero or more whitespace. | | | +-----------+--------------------------------+------------------------+-------+ | ``%T`` | ISO 8601 time format, | 10:01:59 | | | | equivalent to ``%H:%M:%S``. | | | @@ -2717,7 +2720,8 @@ differences between platforms in handling of unsupported format specifiers. ``%:z`` was added for :meth:`~.datetime.strftime`. .. versionadded:: 3.15 - ``%:z``, ``%F``, and ``%D`` were added for :meth:`~.datetime.strptime`. + ``%D``, ``%F``, ``%n``, ``%t``, and ``%:z`` were added for + :meth:`~.datetime.strptime`. Technical detail diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index 6b55daa9b6eae0..7ae399eb95ba6e 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -5251,6 +5251,11 @@ Note, the *elem* argument to the :meth:`~object.__contains__`, :meth:`~set.discard` methods may be a set. To support searching for an equivalent frozenset, a temporary one is created from *elem*. +.. seealso:: + + For detailed information on thread-safety guarantees for :class:`set` + objects, see :ref:`thread-safety-set`. + .. _typesmapping: diff --git a/Doc/library/threadsafety.rst b/Doc/library/threadsafety.rst index 7ab5921c7ec298..4f2eda19b85e07 100644 --- a/Doc/library/threadsafety.rst +++ b/Doc/library/threadsafety.rst @@ -342,3 +342,108 @@ thread, iterate over a copy: Consider external synchronization when sharing :class:`dict` instances across threads. + + +.. _thread-safety-set: + +Thread safety for set objects +============================== + +The :func:`len` function is lock-free and :term:`atomic `. + +The following read operation is lock-free. It does not block concurrent +modifications and may observe intermediate states from operations that +hold the per-object lock: + +.. code-block:: + :class: good + + elem in s # set.__contains__ + +This operation may compare elements using :meth:`~object.__eq__`, which can +execute arbitrary Python code. During such comparisons, the set may be +modified by another thread. For built-in types like :class:`str`, +:class:`int`, and :class:`float`, :meth:`!__eq__` does not release the +underlying lock during comparisons and this is not a concern. + +All other operations from here on hold the per-object lock. + +Adding or removing a single element is safe to call from multiple threads +and will not corrupt the set: + +.. code-block:: + :class: good + + s.add(elem) # add element + s.remove(elem) # remove element, raise if missing + s.discard(elem) # remove element if present + s.pop() # remove and return arbitrary element + +These operations also compare elements, so the same :meth:`~object.__eq__` +considerations as above apply. + +The :meth:`~set.copy` method returns a new object and holds the per-object lock +for the duration so that it is always atomic. + +The :meth:`~set.clear` method holds the lock for its duration. Other +threads cannot observe elements being removed. + +The following operations only accept :class:`set` or :class:`frozenset` +as operands and always lock both objects: + +.. code-block:: + :class: good + + s |= other # other must be set/frozenset + s &= other # other must be set/frozenset + s -= other # other must be set/frozenset + s ^= other # other must be set/frozenset + s & other # other must be set/frozenset + s | other # other must be set/frozenset + s - other # other must be set/frozenset + s ^ other # other must be set/frozenset + +:meth:`set.update`, :meth:`set.union`, :meth:`set.intersection` and +:meth:`set.difference` can take multiple iterables as arguments. They all +iterate through all the passed iterables and do the following: + + * :meth:`set.update` and :meth:`set.union` lock both objects only when + the other operand is a :class:`set`, :class:`frozenset`, or :class:`dict`. + * :meth:`set.intersection` and :meth:`set.difference` always try to lock + all objects. + +:meth:`set.symmetric_difference` tries to lock both objects. + +The update variants of the above methods also have some differences between +them: + + * :meth:`set.difference_update` and :meth:`set.intersection_update` try + to lock all objects one-by-one. + * :meth:`set.symmetric_difference_update` only locks the arguments if it is + of type :class:`set`, :class:`frozenset`, or :class:`dict`. + +The following methods always try to lock both objects: + +.. code-block:: + :class: good + + s.isdisjoint(other) # both locked + s.issubset(other) # both locked + s.issuperset(other) # both locked + +Operations that involve multiple accesses, as well as iteration, are never +atomic: + +.. code-block:: + :class: bad + + # NOT atomic: check-then-act + if elem in s: + s.remove(elem) + + # NOT thread-safe: iteration while modifying + for elem in s: + process(elem) # another thread may modify s + +Consider external synchronization when sharing :class:`set` instances +across threads. See :ref:`freethreading-python-howto` for more information. diff --git a/Doc/library/wave.rst b/Doc/library/wave.rst index ff020b52da3f23..9d30a14f112937 100644 --- a/Doc/library/wave.rst +++ b/Doc/library/wave.rst @@ -9,14 +9,19 @@ -------------- The :mod:`!wave` module provides a convenient interface to the Waveform Audio -"WAVE" (or "WAV") file format. Only uncompressed PCM encoded wave files are -supported. +"WAVE" (or "WAV") file format. + +The module supports uncompressed PCM and IEEE floating-point WAV formats. .. versionchanged:: 3.12 Support for ``WAVE_FORMAT_EXTENSIBLE`` headers was added, provided that the extended format is ``KSDATAFORMAT_SUBTYPE_PCM``. +.. versionchanged:: next + + Support for reading and writing ``WAVE_FORMAT_IEEE_FLOAT`` files was added. + The :mod:`!wave` module defines the following function and exception: @@ -60,6 +65,21 @@ The :mod:`!wave` module defines the following function and exception: specification or hits an implementation deficiency. +.. data:: WAVE_FORMAT_PCM + + Format code for uncompressed PCM audio. + + +.. data:: WAVE_FORMAT_IEEE_FLOAT + + Format code for IEEE floating-point audio. + + +.. data:: WAVE_FORMAT_EXTENSIBLE + + Format code for WAVE extensible headers. + + .. _wave-read-objects: Wave_read Objects @@ -98,6 +118,14 @@ Wave_read Objects Returns number of audio frames. + .. method:: getformat() + + Returns the frame format code. + + This is one of :data:`WAVE_FORMAT_PCM`, + :data:`WAVE_FORMAT_IEEE_FLOAT`, or :data:`WAVE_FORMAT_EXTENSIBLE`. + + .. method:: getcomptype() Returns compression type (``'NONE'`` is the only supported type). @@ -112,8 +140,8 @@ Wave_read Objects .. method:: getparams() Returns a :func:`~collections.namedtuple` ``(nchannels, sampwidth, - framerate, nframes, comptype, compname)``, equivalent to output of the - ``get*()`` methods. + framerate, nframes, comptype, compname)``, equivalent to output + of the ``get*()`` methods. .. method:: readframes(n) @@ -190,6 +218,9 @@ Wave_write Objects Set the sample width to *n* bytes. + For :data:`WAVE_FORMAT_IEEE_FLOAT`, only 4-byte (32-bit) and + 8-byte (64-bit) sample widths are supported. + .. method:: getsampwidth() @@ -238,11 +269,32 @@ Wave_write Objects Return the human-readable compression type name. + .. method:: setformat(format) + + Set the frame format code. + + Supported values are :data:`WAVE_FORMAT_PCM` and + :data:`WAVE_FORMAT_IEEE_FLOAT`. + + When setting :data:`WAVE_FORMAT_IEEE_FLOAT`, the sample width must be + 4 or 8 bytes. + + + .. method:: getformat() + + Return the current frame format code. + + .. method:: setparams(tuple) - The *tuple* should be ``(nchannels, sampwidth, framerate, nframes, comptype, - compname)``, with values valid for the ``set*()`` methods. Sets all - parameters. + The *tuple* should be + ``(nchannels, sampwidth, framerate, nframes, comptype, compname, format)``, + with values valid for the ``set*()`` methods. Sets all parameters. + + For backwards compatibility, a 6-item tuple without *format* is also + accepted and defaults to :data:`WAVE_FORMAT_PCM`. + + For ``format=WAVE_FORMAT_IEEE_FLOAT``, *sampwidth* must be 4 or 8. .. method:: getparams() @@ -279,3 +331,6 @@ Wave_write Objects Note that it is invalid to set any parameters after calling :meth:`writeframes` or :meth:`writeframesraw`, and any attempt to do so will raise :exc:`wave.Error`. + + For :data:`WAVE_FORMAT_IEEE_FLOAT` output, a ``fact`` chunk is written as + required by the WAVE specification for non-PCM formats. diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 7daea13d31c83d..d5b14216770906 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -912,6 +912,9 @@ shelve * Added new :meth:`!reorganize` method to :mod:`shelve` used to recover unused free space previously occupied by deleted entries. (Contributed by Andrea Oliveri in :gh:`134004`.) +* Add support for custom serialization and deserialization functions + in the :mod:`shelve` module. + (Contributed by Furkan Onder in :gh:`99631`.) socket @@ -1515,6 +1518,21 @@ typing wave ---- +* Added support for IEEE floating-point WAVE audio + (``WAVE_FORMAT_IEEE_FLOAT``) in :mod:`wave`. + +* Added :meth:`wave.Wave_read.getformat`, :meth:`wave.Wave_write.getformat`, + and :meth:`wave.Wave_write.setformat` for explicit frame format handling. + +* :meth:`wave.Wave_write.setparams` accepts both 7-item tuples including + ``format`` and 6-item tuples for backwards compatibility (defaulting to + ``WAVE_FORMAT_PCM``). + +* ``WAVE_FORMAT_IEEE_FLOAT`` output now includes a ``fact`` chunk, + as required for non-PCM WAVE formats. + +(Contributed by Lionel Koenig and Michiel W. Beijen in :gh:`60729`.) + * Removed the ``getmark()``, ``setmark()`` and ``getmarkers()`` methods of the :class:`~wave.Wave_read` and :class:`~wave.Wave_write` classes, which were deprecated since Python 3.13. diff --git a/Lib/_strptime.py b/Lib/_strptime.py index 0d81ff6765e1ed..3367ac485a590c 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -382,7 +382,10 @@ def __init__(self, locale_time=None): 'Z': self.__seqToRE((tz for tz_names in self.locale_time.timezone for tz in tz_names), 'Z'), - '%': '%'} + 'n': r'\s*', + 't': r'\s*', + '%': '%', + } if self.locale_time.LC_alt_digits is None: for d in 'dmyCHIMS': mapping['O' + d] = r'(?P<%s>\d\d|\d| \d)' % d diff --git a/Lib/pprint.py b/Lib/pprint.py index e111bd59d4152c..a0e484b1c097c3 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -238,7 +238,7 @@ def _pprint_dict(self, object, stream, indent, allowance, context, level): def _pprint_frozendict(self, object, stream, indent, allowance, context, level): write = stream.write cls = object.__class__ - stream.write(cls.__name__ + '(') + write(cls.__name__ + '(') length = len(object) if length: self._pprint_dict(object, stream, diff --git a/Lib/test/audiodata/pluck-float32.wav b/Lib/test/audiodata/pluck-float32.wav new file mode 100644 index 00000000000000..2030fb16d6e3bd Binary files /dev/null and b/Lib/test/audiodata/pluck-float32.wav differ diff --git a/Lib/test/audiotests.py b/Lib/test/audiotests.py index 9d6c4cc2b4b02c..394097df17dca9 100644 --- a/Lib/test/audiotests.py +++ b/Lib/test/audiotests.py @@ -27,17 +27,18 @@ def tearDown(self): unlink(TESTFN) def check_params(self, f, nchannels, sampwidth, framerate, nframes, - comptype, compname): + comptype, compname, format): self.assertEqual(f.getnchannels(), nchannels) self.assertEqual(f.getsampwidth(), sampwidth) self.assertEqual(f.getframerate(), framerate) self.assertEqual(f.getnframes(), nframes) self.assertEqual(f.getcomptype(), comptype) self.assertEqual(f.getcompname(), compname) + self.assertEqual(f.getformat(), format) params = f.getparams() self.assertEqual(params, - (nchannels, sampwidth, framerate, nframes, comptype, compname)) + (nchannels, sampwidth, framerate, nframes, comptype, compname)) self.assertEqual(params.nchannels, nchannels) self.assertEqual(params.sampwidth, sampwidth) self.assertEqual(params.framerate, framerate) @@ -51,13 +52,17 @@ def check_params(self, f, nchannels, sampwidth, framerate, nframes, class AudioWriteTests(AudioTests): + readonly = False def create_file(self, testfile): + if self.readonly: + self.skipTest('Read only file format') f = self.fout = self.module.open(testfile, 'wb') f.setnchannels(self.nchannels) f.setsampwidth(self.sampwidth) f.setframerate(self.framerate) f.setcomptype(self.comptype, self.compname) + f.setformat(self.format) return f def check_file(self, testfile, nframes, frames): @@ -67,13 +72,14 @@ def check_file(self, testfile, nframes, frames): self.assertEqual(f.getframerate(), self.framerate) self.assertEqual(f.getnframes(), nframes) self.assertEqual(f.readframes(nframes), frames) + self.assertEqual(f.getformat(), self.format) def test_write_params(self): f = self.create_file(TESTFN) f.setnframes(self.nframes) f.writeframes(self.frames) self.check_params(f, self.nchannels, self.sampwidth, self.framerate, - self.nframes, self.comptype, self.compname) + self.nframes, self.comptype, self.compname, self.format) f.close() def test_write_context_manager_calls_close(self): @@ -257,7 +263,7 @@ def test_read_params(self): f = self.f = self.module.open(self.sndfilepath) #self.assertEqual(f.getfp().name, self.sndfilepath) self.check_params(f, self.nchannels, self.sampwidth, self.framerate, - self.sndfilenframes, self.comptype, self.compname) + self.sndfilenframes, self.comptype, self.compname, self.format) def test_close(self): with open(self.sndfilepath, 'rb') as testfile: @@ -298,6 +304,8 @@ def test_read(self): f.setpos(f.getnframes() + 1) def test_copy(self): + if self.readonly: + self.skipTest('Read only file format') f = self.f = self.module.open(self.sndfilepath) fout = self.fout = self.module.open(TESTFN, 'wb') fout.setparams(f.getparams()) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 97eec618932aa5..e264433ca590bf 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2207,6 +2207,20 @@ def test_strptime_D_format(self): self.theclass.strptime(test_date, "%m/%d/%y") ) + def test_strptime_n_and_t_format(self): + format_directives = ('%n', '%t', '%n%t', '%t%n') + whitespaces = ('', ' ', '\t', '\r', '\v', '\n', '\f') + for fd in format_directives: + for ws in (*whitespaces, ''.join(whitespaces)): + with self.subTest(format_directive=fd, whitespace=ws): + self.assertEqual( + self.theclass.strptime( + f"2026{ws}02{ws}03", + f"%Y{fd}%m{fd}%d", + ), + self.theclass(2026, 2, 3), + ) + ############################################################################# # datetime tests diff --git a/Lib/test/test_strptime.py b/Lib/test/test_strptime.py index fd8525feb88d53..dfc8ef6d2c5b7e 100644 --- a/Lib/test/test_strptime.py +++ b/Lib/test/test_strptime.py @@ -670,6 +670,23 @@ def test_strptime_D_format(self): time.strptime(test_date, "%m/%d/%y") ) + def test_strptime_n_and_t_format(self): + format_directives = ('%n', '%t', '%n%t', '%t%n') + whitespaces = ('', ' ', '\t', '\r', '\v', '\n', '\f') + for fd in format_directives: + for ws in (*whitespaces, ''.join(whitespaces)): + with self.subTest(format_directive=fd, whitespace=ws): + self.assertEqual( + time.strptime( + f"2026{ws}02{ws}03", + f"%Y{fd}%m{fd}%d", + ), + time.strptime( + f'2026-02-03', + "%Y-%m-%d", + ), + ) + class Strptime12AMPMTests(unittest.TestCase): """Test a _strptime regression in '%I %p' at 12 noon (12 PM)""" diff --git a/Lib/test/test_structseq.py b/Lib/test/test_structseq.py index 9622151143cd78..74506fc54de50e 100644 --- a/Lib/test/test_structseq.py +++ b/Lib/test/test_structseq.py @@ -1,4 +1,5 @@ import copy +import gc import os import pickle import re @@ -355,6 +356,14 @@ def test_reference_cycle(self): type(t).refcyle = t """)) + def test_replace_gc_tracked(self): + # Verify that __replace__ results are properly GC-tracked + time_struct = time.gmtime(0) + lst = [] + replaced_struct = time_struct.__replace__(tm_year=lst) + lst.append(replaced_struct) + + self.assertTrue(gc.is_tracked(replaced_struct)) if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index da5fd16b8b6291..be8f6b057654c2 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -359,7 +359,7 @@ def test_strptime(self): # raising an exception. tt = time.gmtime(self.t) for directive in ('a', 'A', 'b', 'B', 'c', 'd', 'D', 'F', 'H', 'I', - 'j', 'm', 'M', 'p', 'S', 'T', + 'j', 'm', 'M', 'n', 'p', 'S', 't', 'T', 'U', 'w', 'W', 'x', 'X', 'y', 'Y', 'Z', '%'): format = '%' + directive if directive == 'd': diff --git a/Lib/test/test_wave.py b/Lib/test/test_wave.py index 4c21f16553775c..a1afe91e3774b9 100644 --- a/Lib/test/test_wave.py +++ b/Lib/test/test_wave.py @@ -1,7 +1,7 @@ import unittest from test import audiotests from test import support -from test.support.os_helper import FakePath +from test.support.os_helper import FakePath, unlink import io import os import struct @@ -22,6 +22,7 @@ class WavePCM8Test(WaveTest, unittest.TestCase): sampwidth = 1 framerate = 11025 nframes = 48 + format = wave.WAVE_FORMAT_PCM comptype = 'NONE' compname = 'not compressed' frames = bytes.fromhex("""\ @@ -39,6 +40,7 @@ class WavePCM16Test(WaveTest, unittest.TestCase): sampwidth = 2 framerate = 11025 nframes = 48 + format = wave.WAVE_FORMAT_PCM comptype = 'NONE' compname = 'not compressed' frames = bytes.fromhex("""\ @@ -60,6 +62,7 @@ class WavePCM24Test(WaveTest, unittest.TestCase): sampwidth = 3 framerate = 11025 nframes = 48 + format = wave.WAVE_FORMAT_PCM comptype = 'NONE' compname = 'not compressed' frames = bytes.fromhex("""\ @@ -87,6 +90,8 @@ class WavePCM24ExtTest(WaveTest, unittest.TestCase): sampwidth = 3 framerate = 11025 nframes = 48 + format = wave.WAVE_FORMAT_EXTENSIBLE + readonly = True # Writing EXTENSIBLE wave format is not supported. comptype = 'NONE' compname = 'not compressed' frames = bytes.fromhex("""\ @@ -114,6 +119,7 @@ class WavePCM32Test(WaveTest, unittest.TestCase): sampwidth = 4 framerate = 11025 nframes = 48 + format = wave.WAVE_FORMAT_PCM comptype = 'NONE' compname = 'not compressed' frames = bytes.fromhex("""\ @@ -134,14 +140,140 @@ class WavePCM32Test(WaveTest, unittest.TestCase): frames = wave._byteswap(frames, 4) +class WaveIeeeFloatingPointTest(WaveTest, unittest.TestCase): + sndfilename = 'pluck-float32.wav' + sndfilenframes = 3307 + nchannels = 2 + sampwidth = 4 + framerate = 11025 + nframes = 48 + format = wave.WAVE_FORMAT_IEEE_FLOAT + comptype = 'NONE' + compname = 'not compressed' + frames = bytes.fromhex("""\ + 60598B3C001423BA 1FB4163F8054FA3B 0E4FC43E80C51D3D 53467EBF4030843D \ + FC84D0BE304C563D 3053113F40BEFC3C B72F00BFC03E583C E0FEDA3C805142BC \ + 54510FBFE02638BD 569F16BF40FDCABD C060A63EECA421BE 3CE5523E2C3349BE \ + 0C2E10BE14725BBE 5268E7BEDC3B6CBE 985AE03D80497ABE B4B606BEECB67EBE \ + B0B12E3FC87C6CBE 005519BD4C0F3EBE F8BD1B3EECDF03BE 924E9FBE588D8DBD \ + D4E150BF501711BD B079A0BD20FBFBBC 5863863D40760CBD 0E3C83BE40E217BD \ + 04FF0B3EF07839BD E29AFB3E80A714BD B91007BFE042D3BC B5AD4D3F80CDA0BB \ + 1AB1C3BEB04E023D D33A063FC0A8973D 8012F9BEE074EC3D 7341223FD415153E \ + D80409BE04A63A3E 00F27BBFBC25333E 0000803FFC29223E 000080BF38A7143E \ + 3638133F283BEB3D 7C6E253F00CADB3D 686A02BE88FDF53D 920CC7BE28E1FB3D \ + 185B5ABED8A2CE3D 5189463FC8A7A53D E88F8C3DF0FFA13D 1CE6AE3EE0A0B03D \ + DF90223F184EE43D 376768BF2CD8093E 281612BF60B3EE3D 2F26083F88B4A53D \ + """) + class MiscTestCase(unittest.TestCase): def test__all__(self): - not_exported = {'WAVE_FORMAT_PCM', 'WAVE_FORMAT_EXTENSIBLE', 'KSDATAFORMAT_SUBTYPE_PCM'} + not_exported = {'KSDATAFORMAT_SUBTYPE_PCM'} support.check__all__(self, wave, not_exported=not_exported) class WaveLowLevelTest(unittest.TestCase): + def test_setparams_6_tuple_defaults_to_pcm(self): + with tempfile.NamedTemporaryFile(delete_on_close=False) as fp: + filename = fp.name + self.addCleanup(unlink, filename) + + with wave.open(filename, 'wb') as w: + w.setformat(wave.WAVE_FORMAT_IEEE_FLOAT) + w.setparams((1, 2, 22050, 0, 'NONE', 'not compressed')) + self.assertEqual(w.getformat(), wave.WAVE_FORMAT_PCM) + + def test_setparams_7_tuple_uses_format(self): + with tempfile.NamedTemporaryFile(delete_on_close=False) as fp: + filename = fp.name + self.addCleanup(unlink, filename) + + with wave.open(filename, 'wb') as w: + w.setparams((1, 4, 22050, 0, 'NONE', 'not compressed', + wave.WAVE_FORMAT_IEEE_FLOAT)) + self.assertEqual(w.getformat(), wave.WAVE_FORMAT_IEEE_FLOAT) + + def test_setparams_7_tuple_ieee_64bit_sampwidth(self): + with tempfile.NamedTemporaryFile(delete_on_close=False) as fp: + filename = fp.name + self.addCleanup(unlink, filename) + + with wave.open(filename, 'wb') as w: + w.setparams((1, 8, 22050, 0, 'NONE', 'not compressed', + wave.WAVE_FORMAT_IEEE_FLOAT)) + self.assertEqual(w.getformat(), wave.WAVE_FORMAT_IEEE_FLOAT) + self.assertEqual(w.getsampwidth(), 8) + + def test_getparams_backward_compatible_shape(self): + with tempfile.NamedTemporaryFile(delete_on_close=False) as fp: + filename = fp.name + self.addCleanup(unlink, filename) + + with wave.open(filename, 'wb') as w: + w.setparams((1, 4, 22050, 0, 'NONE', 'not compressed', + wave.WAVE_FORMAT_IEEE_FLOAT)) + params = w.getparams() + self.assertEqual(params, (1, 4, 22050, 0, 'NONE', 'not compressed')) + + def test_getformat_setformat(self): + with tempfile.NamedTemporaryFile(delete_on_close=False) as fp: + filename = fp.name + self.addCleanup(unlink, filename) + + with wave.open(filename, 'wb') as w: + w.setnchannels(1) + w.setsampwidth(4) + w.setframerate(22050) + self.assertEqual(w.getformat(), wave.WAVE_FORMAT_PCM) + w.setformat(wave.WAVE_FORMAT_IEEE_FLOAT) + self.assertEqual(w.getformat(), wave.WAVE_FORMAT_IEEE_FLOAT) + + def test_setformat_ieee_requires_32_or_64_bit_sampwidth(self): + with tempfile.NamedTemporaryFile(delete_on_close=False) as fp: + filename = fp.name + self.addCleanup(unlink, filename) + + with wave.open(filename, 'wb') as w: + w.setnchannels(1) + w.setsampwidth(2) + w.setframerate(22050) + with self.assertRaisesRegex(wave.Error, + 'unsupported sample width for IEEE float format'): + w.setformat(wave.WAVE_FORMAT_IEEE_FLOAT) + + def test_setsampwidth_ieee_requires_32_or_64_bit(self): + with tempfile.NamedTemporaryFile(delete_on_close=False) as fp: + filename = fp.name + self.addCleanup(unlink, filename) + + with wave.open(filename, 'wb') as w: + w.setnchannels(1) + w.setframerate(22050) + w.setformat(wave.WAVE_FORMAT_IEEE_FLOAT) + with self.assertRaisesRegex(wave.Error, + 'unsupported sample width for IEEE float format'): + w.setsampwidth(2) + w.setsampwidth(4) + + def test_setsampwidth_ieee_accepts_64_bit(self): + with tempfile.NamedTemporaryFile(delete_on_close=False) as fp: + filename = fp.name + self.addCleanup(unlink, filename) + + with wave.open(filename, 'wb') as w: + w.setnchannels(1) + w.setframerate(22050) + w.setformat(wave.WAVE_FORMAT_IEEE_FLOAT) + w.setsampwidth(8) + self.assertEqual(w.getsampwidth(), 8) + + def test_read_getformat(self): + b = b'RIFF' + struct.pack(' 4: + if self._format == WAVE_FORMAT_IEEE_FLOAT: + if sampwidth not in (4, 8): + raise Error('unsupported sample width for IEEE float format') + elif sampwidth < 1 or sampwidth > 4: raise Error('bad sample width') self._sampwidth = sampwidth @@ -518,6 +546,18 @@ def setcomptype(self, comptype, compname): self._comptype = comptype self._compname = compname + def setformat(self, format): + if self._datawritten: + raise Error('cannot change parameters after starting to write') + if format not in (WAVE_FORMAT_IEEE_FLOAT, WAVE_FORMAT_PCM): + raise Error('unsupported wave format') + if format == WAVE_FORMAT_IEEE_FLOAT and self._sampwidth and self._sampwidth not in (4, 8): + raise Error('unsupported sample width for IEEE float format') + self._format = format + + def getformat(self): + return self._format + def getcomptype(self): return self._comptype @@ -525,10 +565,15 @@ def getcompname(self): return self._compname def setparams(self, params): - nchannels, sampwidth, framerate, nframes, comptype, compname = params if self._datawritten: raise Error('cannot change parameters after starting to write') + if len(params) == 6: + nchannels, sampwidth, framerate, nframes, comptype, compname = params + format = WAVE_FORMAT_PCM + else: + nchannels, sampwidth, framerate, nframes, comptype, compname, format = params self.setnchannels(nchannels) + self.setformat(format) self.setsampwidth(sampwidth) self.setframerate(framerate) self.setnframes(nframes) @@ -589,6 +634,9 @@ def _ensure_header_written(self, datasize): raise Error('sampling rate not specified') self._write_header(datasize) + def _needs_fact_chunk(self): + return self._format == WAVE_FORMAT_IEEE_FLOAT + def _write_header(self, initlength): assert not self._headerwritten self._file.write(b'RIFF') @@ -599,12 +647,23 @@ def _write_header(self, initlength): self._form_length_pos = self._file.tell() except (AttributeError, OSError): self._form_length_pos = None - self._file.write(struct.pack('