diff --git a/Doc/library/lzma.rst b/Doc/library/lzma.rst index 69f7cb8d48d7ae..e31a8b5d59fb05 100644 --- a/Doc/library/lzma.rst +++ b/Doc/library/lzma.rst @@ -145,7 +145,7 @@ Reading and writing compressed files Compressing and decompressing data in memory -------------------------------------------- -.. class:: LZMACompressor(format=FORMAT_XZ, check=-1, preset=None, filters=None) +.. class:: LZMACompressor(format=FORMAT_XZ, check=-1, preset=None, filters=None, *, mt_options=None) Create a compressor object, which can be used to compress data incrementally. @@ -196,13 +196,23 @@ Compressing and decompressing data in memory Higher presets produce smaller output, but make the compression process slower. + Additionally when *format* is specified as :const:`FORMAT_XZ`, adding the + *mt_options* dictionary argument instructs the module to use the + multithreaded compressor implementation. These options provided in + *mt_options* currently have a meaning, anything else is silently ignored: + + * *threads*: the desired number of threads the underlying library should use + + * *block_size*: Maximum uncompressed size of a block. + .. note:: In addition to being more CPU-intensive, compression with higher presets also requires much more memory (and produces output that needs more memory to decompress). With preset ``9`` for example, the overhead for an - :class:`LZMACompressor` object can be as high as 800 MiB. For this reason, - it is generally best to stick with the default preset. + :class:`LZMACompressor` object can be as high as 800 MiB per worker + thread. For this reason, it is generally best to stick with the default + preset. The *filters* argument (if provided) should be a filter chain specifier. See :ref:`filter-chain-specs` for details. @@ -246,6 +256,19 @@ Compressing and decompressing data in memory :const:`FORMAT_RAW`, but should not be used for other formats. See :ref:`filter-chain-specs` for more information about filter chains. + Additionally when *format* is specified as :const:`FORMAT_XZ`, adding the + *mt_options* dictionary argument instructs the module to use the + multithreaded decompressor implementation which decompresses blocks in + parallel. These options provided in *mt_options* currently have a meaning, + anything else is silently ignored: + + * *threads*: the desired number of threads the underlying library should use + + * *memlimit_threading*: A soft memory limit. Lets the underlying library + scale (down) the actual number of worker threads to stay within the budget. + At least one worker will always be used even if over this limit. Use + *memlimit* argument if there is a hard memory limit to enforce. + .. note:: This class does not transparently handle inputs containing multiple compressed streams, unlike :func:`decompress` and :class:`LZMAFile`. To @@ -302,16 +325,16 @@ Compressing and decompressing data in memory .. versionadded:: 3.5 -.. function:: compress(data, format=FORMAT_XZ, check=-1, preset=None, filters=None) +.. function:: compress(data, format=FORMAT_XZ, check=-1, preset=None, filters=None, *, mt_options=None) Compress *data* (a :class:`bytes` object), returning the compressed data as a :class:`bytes` object. See :class:`LZMACompressor` above for a description of the *format*, *check*, - *preset* and *filters* arguments. + *preset*, *filters* and *mt_options* arguments. -.. function:: decompress(data, format=FORMAT_AUTO, memlimit=None, filters=None) +.. function:: decompress(data, format=FORMAT_AUTO, memlimit=None, filters=None, *, mt_options=None) Decompress *data* (a :class:`bytes` object), returning the uncompressed data as a :class:`bytes` object. @@ -320,7 +343,7 @@ Compressing and decompressing data in memory decompress all of these streams, and return the concatenation of the results. See :class:`LZMADecompressor` above for a description of the *format*, - *memlimit* and *filters* arguments. + *preset*, *filters* and *mt_options* arguments. Miscellaneous diff --git a/Lib/lzma.py b/Lib/lzma.py index 316066d024ea02..e4fe88cd2e601b 100644 --- a/Lib/lzma.py +++ b/Lib/lzma.py @@ -47,7 +47,8 @@ class LZMAFile(_streams.BaseStream): """ def __init__(self, filename=None, mode="r", *, - format=None, check=-1, preset=None, filters=None): + format=None, check=-1, preset=None, filters=None, + mt_options=None): """Open an LZMA-compressed file in binary mode. filename can be either an actual file name (given as a str, @@ -102,14 +103,18 @@ def __init__(self, filename=None, mode="r", *, raise ValueError("Cannot specify a preset compression " "level when opening a file for reading") if format is None: - format = FORMAT_AUTO + if mt_options is None: + format = FORMAT_AUTO + else: + format = FORMAT_XZ mode_code = _MODE_READ elif mode in ("w", "wb", "a", "ab", "x", "xb"): if format is None: format = FORMAT_XZ mode_code = _MODE_WRITE self._compressor = LZMACompressor(format=format, check=check, - preset=preset, filters=filters) + preset=preset, filters=filters, + mt_options=mt_options) self._pos = 0 else: raise ValueError("Invalid mode: {!r}".format(mode)) @@ -324,29 +329,33 @@ def open(filename, mode="rb", *, return binary_file -def compress(data, format=FORMAT_XZ, check=-1, preset=None, filters=None): +def compress(data, format=FORMAT_XZ, check=-1, preset=None, filters=None, *, + mt_options=None): """Compress a block of data. Refer to LZMACompressor's docstring for a description of the - optional arguments *format*, *check*, *preset* and *filters*. + optional arguments *format*, *check*, *preset*, *filters* and *mt_options*. For incremental compression, use an LZMACompressor instead. """ - comp = LZMACompressor(format, check, preset, filters) + comp = LZMACompressor(format, check, preset, filters, + mt_options=mt_options) return comp.compress(data) + comp.flush() -def decompress(data, format=FORMAT_AUTO, memlimit=None, filters=None): +def decompress(data, format=FORMAT_AUTO, memlimit=None, filters=None, *, + mt_options=None): """Decompress a block of data. Refer to LZMADecompressor's docstring for a description of the - optional arguments *format*, *check* and *filters*. + optional arguments *format*, *check*, *preset*, *filters* and *mt_options*. For incremental decompression, use an LZMADecompressor instead. """ results = [] while True: - decomp = LZMADecompressor(format, memlimit, filters) + decomp = LZMADecompressor(format, memlimit, filters, + mt_options=mt_options) try: res = decomp.decompress(data) except LZMAError: diff --git a/Lib/test/test_lzma.py b/Lib/test/test_lzma.py index e93c3c37354e27..d97911fc1df480 100644 --- a/Lib/test/test_lzma.py +++ b/Lib/test/test_lzma.py @@ -73,6 +73,35 @@ def test_bad_filter_spec(self): with self.assertRaises(ValueError): LZMACompressor(filters=[{"id": lzma.FILTER_X86, "foo": 0}]) + def test_bad_mt_options(self): + with self.assertRaises(TypeError): + LZMACompressor(format=lzma.FORMAT_XZ, mt_options=3) + with self.assertRaises(TypeError): + LZMACompressor(format=lzma.FORMAT_XZ, mt_options={"threads": 3.45}) + with self.assertRaises(TypeError): + LZMACompressor(format=lzma.FORMAT_XZ, mt_options={"flags": "asdf"}) + # Can only specify MT encoder with XZ + with self.assertRaises(ValueError): + LZMACompressor(format=lzma.FORMAT_AUTO, mt_options=MT_OPTIONS_1) + with self.assertRaises(ValueError): + LZMACompressor(format=lzma.FORMAT_RAW, mt_options=MT_OPTIONS_1) + with self.assertRaises(ValueError): + LZMACompressor(format=lzma.FORMAT_ALONE, mt_options=MT_OPTIONS_1) + + with self.assertRaises(TypeError): + LZMADecompressor(format=lzma.FORMAT_XZ, mt_options=3) + with self.assertRaises(TypeError): + LZMADecompressor(format=lzma.FORMAT_XZ, + mt_options={"threads": 3.45}) + with self.assertRaises(TypeError): + LZMADecompressor(format=lzma.FORMAT_XZ, + mt_options={"flags": "asdf"}) + # Can only specify MT encoder with XZ + with self.assertRaises(ValueError): + LZMADecompressor(format=lzma.FORMAT_RAW, mt_options=MT_OPTIONS_1) + with self.assertRaises(ValueError): + LZMADecompressor(format=lzma.FORMAT_ALONE, mt_options=MT_OPTIONS_1) + def test_decompressor_after_eof(self): lzd = LZMADecompressor() lzd.decompress(COMPRESSED_XZ) @@ -85,6 +114,10 @@ def test_decompressor_memlimit(self): lzd = LZMADecompressor(lzma.FORMAT_XZ, memlimit=1024) self.assertRaises(LZMAError, lzd.decompress, COMPRESSED_XZ) + lzd = LZMADecompressor(lzma.FORMAT_XZ, memlimit=1024, + mt_options=MT_OPTIONS_1) + self.assertRaises(LZMAError, lzd.decompress, COMPRESSED_XZ) + lzd = LZMADecompressor(lzma.FORMAT_ALONE, memlimit=1024) self.assertRaises(LZMAError, lzd.decompress, COMPRESSED_ALONE) @@ -109,6 +142,10 @@ def test_decompressor_xz(self): lzd = LZMADecompressor(lzma.FORMAT_XZ) self._test_decompressor(lzd, COMPRESSED_XZ, lzma.CHECK_CRC64) + def test_decompressor_xz_mt(self): + lzd = LZMADecompressor(lzma.FORMAT_XZ, mt_options=MT_OPTIONS_1) + self._test_decompressor(lzd, COMPRESSED_XZ, lzma.CHECK_CRC64) + def test_decompressor_alone(self): lzd = LZMADecompressor(lzma.FORMAT_ALONE) self._test_decompressor(lzd, COMPRESSED_ALONE, lzma.CHECK_NONE) @@ -281,6 +318,12 @@ def test_roundtrip_xz(self): lzd = LZMADecompressor() self._test_decompressor(lzd, cdata, lzma.CHECK_CRC64) + def test_roundtrip_xz_mt(self): + lzc = LZMACompressor(format=lzma.FORMAT_XZ, mt_options=MT_OPTIONS_1) + cdata = lzc.compress(INPUT) + lzc.flush() + lzd = LZMADecompressor() + self._test_decompressor(lzd, cdata, lzma.CHECK_CRC64) + def test_roundtrip_alone(self): lzc = LZMACompressor(lzma.FORMAT_ALONE) cdata = lzc.compress(INPUT) + lzc.flush() @@ -2092,6 +2135,8 @@ def test_filter_properties_roundtrip(self): b'\xeb#\x182\x96I\xf7l\xf3r\x00' ) +MT_OPTIONS_1 = {"threads": 4} + if __name__ == "__main__": unittest.main() diff --git a/Misc/ACKS b/Misc/ACKS index f5f15f2eb7ea24..1096034c72bea9 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1052,6 +1052,7 @@ Toshio Kuratomi Ilia Kurenkov Vladimir Kushnir Erno Kuusela +Ondřej Kuzník Kabir Kwatra Ross Lagerwall Cameron Laird diff --git a/Misc/NEWS.d/next/Library/2025-10-27-20-19-55.gh-issue-114953.3PtWGJ.rst b/Misc/NEWS.d/next/Library/2025-10-27-20-19-55.gh-issue-114953.3PtWGJ.rst new file mode 100644 index 00000000000000..bb14485e5c985e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-27-20-19-55.gh-issue-114953.3PtWGJ.rst @@ -0,0 +1 @@ +Support the MT (MultiThreaded) encoder and decoder in :mod:`lzma` module. diff --git a/Modules/_lzmamodule.c b/Modules/_lzmamodule.c index 6fc072f6d0a382..ddb746741c6f18 100644 --- a/Modules/_lzmamodule.c +++ b/Modules/_lzmamodule.c @@ -428,6 +428,60 @@ parse_filter_chain_spec(_lzma_state *state, lzma_filter filters[], PyObject *fil return 0; } +static int +parse_mt_options_spec(lzma_mt *mt_options, PyObject *spec) +{ + PyObject *option_obj = NULL; + + if (!PyMapping_Check(spec)) { + PyErr_SetString(PyExc_TypeError, + "MT Options specifier must be a dict or dict-like object"); + return -1; + } + if (PyMapping_GetOptionalItemString(spec, "flags", &option_obj) == 1) { + if (!_PyLong_UInt32_Converter(option_obj, &mt_options->flags)) { + goto error; + } + Py_CLEAR(option_obj); + } + if (PyMapping_GetOptionalItemString(spec, "threads", &option_obj) == 1) { + if (!_PyLong_UInt32_Converter(option_obj, &mt_options->threads)) { + goto error; + } + Py_CLEAR(option_obj); + } + if (PyMapping_GetOptionalItemString(spec, "block_size", &option_obj) == 1) { + if (!_PyLong_UInt64_Converter(option_obj, &mt_options->block_size)) { + goto error; + } + Py_CLEAR(option_obj); + } + if (PyMapping_GetOptionalItemString(spec, "timeout", &option_obj) == 1) { + if (!_PyLong_UInt32_Converter(option_obj, &mt_options->timeout)) { + goto error; + } + Py_CLEAR(option_obj); + } + if (PyMapping_GetOptionalItemString(spec, "memlimit_threading", &option_obj) == 1) { + if (!_PyLong_UInt64_Converter(option_obj, &mt_options->memlimit_threading)) { + goto error; + } + Py_CLEAR(option_obj); + } + if (PyMapping_GetOptionalItemString(spec, "memlimit_stop", &option_obj) == 1) { + if (!_PyLong_UInt64_Converter(option_obj, &mt_options->memlimit_stop)) { + goto error; + } + Py_CLEAR(option_obj); + } + + return 0; + +error: + Py_XDECREF(option_obj); + return -1; +} + /* Filter specifier construction. @@ -657,19 +711,40 @@ _lzma_LZMACompressor_flush_impl(Compressor *self) static int Compressor_init_xz(_lzma_state *state, lzma_stream *lzs, - int check, uint32_t preset, PyObject *filterspecs) + int check, uint32_t preset, PyObject *filterspecs, + PyObject *mtspec) { lzma_ret lzret; - if (filterspecs == Py_None) { + if (filterspecs == Py_None && mtspec == Py_None) { lzret = lzma_easy_encoder(lzs, preset, check); } else { lzma_filter filters[LZMA_FILTERS_MAX + 1]; + int have_filters = 0; - if (parse_filter_chain_spec(state, filters, filterspecs) == -1) - return -1; - lzret = lzma_stream_encoder(lzs, filters, check); - free_filter_chain(filters); + if (filterspecs != Py_None) { + if (parse_filter_chain_spec(state, filters, filterspecs) == -1) + return -1; + have_filters = 1; + } + + if (mtspec) { + lzma_mt mt_options = { + .timeout = 300, .preset = preset, .check = check, + }; + if (have_filters) + mt_options.filters = filters; + if (parse_mt_options_spec(&mt_options, mtspec) == -1) { + if (have_filters) + free_filter_chain(filters); + return -1; + } + lzret = lzma_stream_encoder_mt(lzs, &mt_options); + } else { + lzret = lzma_stream_encoder(lzs, filters, check); + } + if (have_filters) + free_filter_chain(filters); } if (catch_lzma_error(state, lzret)) { return -1; @@ -762,6 +837,10 @@ _lzma.LZMACompressor.__new__ have an entry for "id" indicating the ID of the filter, plus additional entries for options to the filter. + mt_options: object = None + If provided should be a dict, in which case the threaded implementation + will be used. + Create a compressor object for compressing data incrementally. The settings used by the compressor can be specified either as a @@ -771,25 +850,30 @@ and FORMAT_ALONE, the default is to use the PRESET_DEFAULT preset level. For FORMAT_RAW, the caller must always specify a filter chain; the raw compressor does not support preset compression levels. +The MT (multi-threaded) compressor is chosen instead of the default if the +'mt_options' dictionary argument has been passed which supports additional +settings. + For one-shot compression, use the compress() function instead. [-clinic start generated code]*/ static PyObject * Compressor_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) { - static char *arg_names[] = {"format", "check", "preset", "filters", NULL}; + static char *arg_names[] = {"format", "check", "preset", "filters", "mt_options", NULL}; int format = FORMAT_XZ; int check = -1; uint32_t preset = LZMA_PRESET_DEFAULT; PyObject *preset_obj = Py_None; PyObject *filterspecs = Py_None; + PyObject *mt_specs = Py_None; Compressor *self; _lzma_state *state = PyType_GetModuleState(type); assert(state != NULL); if (!PyArg_ParseTupleAndKeywords(args, kwargs, - "|iiOO:LZMACompressor", arg_names, + "|iiOO$O:LZMACompressor", arg_names, &format, &check, &preset_obj, - &filterspecs)) { + &filterspecs, &mt_specs)) { return NULL; } @@ -805,6 +889,12 @@ Compressor_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) return NULL; } + if (format != FORMAT_XZ && mt_specs != Py_None) { + PyErr_SetString(PyExc_ValueError, + "Multi-threaded mode only supported by FORMAT_XZ"); + return NULL; + } + if (preset_obj != Py_None && !_PyLong_UInt32_Converter(preset_obj, &preset)) { return NULL; } @@ -833,7 +923,7 @@ Compressor_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) if (check == -1) { check = LZMA_CHECK_CRC64; } - if (Compressor_init_xz(state, &self->lzs, check, preset, filterspecs) != 0) { + if (Compressor_init_xz(state, &self->lzs, check, preset, filterspecs, mt_specs) != 0) { goto error; } break; @@ -883,7 +973,8 @@ static PyMethodDef Compressor_methods[] = { }; PyDoc_STRVAR(Compressor_doc, -"LZMACompressor(format=FORMAT_XZ, check=-1, preset=None, filters=None)\n" +"LZMACompressor(format=FORMAT_XZ, check=-1, preset=None, filters=None, *,\n" +" mt_options=None)\n" "\n" "Create a compressor object for compressing data incrementally.\n" "\n" @@ -908,6 +999,10 @@ PyDoc_STRVAR(Compressor_doc, "have an entry for \"id\" indicating the ID of the filter, plus\n" "additional entries for options to the filter.\n" "\n" +"The MT (multi-threaded) compressor is chosen instead of the default if the\n" +"\'mt_options\' dictionary argument has been passed which supports additional\n" +"settings. This is only available when FORMAT_XZ is set.\n" +"\n" "For one-shot compression, use the compress() function instead.\n"); static PyType_Slot lzma_compressor_type_slots[] = { @@ -1195,15 +1290,26 @@ _lzma.LZMADecompressor.__new__ sequence of dicts, each indicating the ID and options for a single filter. + * + + mt_options as mtspec: object = None + If provided should be a dict, in which case the threaded implementation + will be used. + Create a decompressor object for decompressing data incrementally. +The MT (multi-threaded) decompressor is chosen instead of the default if the +'mt_options' dictionary argument has been passed which supports additional +settings. You have to specify FORMAT_XZ explicitly for this. + For one-shot decompression, use the decompress() function instead. [clinic start generated code]*/ static PyObject * _lzma_LZMADecompressor_impl(PyTypeObject *type, int format, - PyObject *memlimit, PyObject *filters) -/*[clinic end generated code: output=2d46d5e70f10bc7f input=ca40cd1cb1202b0d]*/ + PyObject *memlimit, PyObject *filters, + PyObject *mtspec) +/*[clinic end generated code: output=efd8dd5b40b20994 input=b867df8d782c8601]*/ { Decompressor *self; const uint32_t decoder_flags = LZMA_TELL_ANY_CHECK | LZMA_TELL_NO_CHECK; @@ -1232,6 +1338,11 @@ _lzma_LZMADecompressor_impl(PyTypeObject *type, int format, "Cannot specify filters except with FORMAT_RAW"); return NULL; } + if (format != FORMAT_XZ && mtspec != Py_None) { + PyErr_SetString(PyExc_ValueError, + "Multi-threaded mode only supported by FORMAT_XZ"); + return NULL; + } assert(type != NULL && type->tp_alloc != NULL); self = (Decompressor *)type->tp_alloc(type, 0); @@ -1266,7 +1377,17 @@ _lzma_LZMADecompressor_impl(PyTypeObject *type, int format, break; case FORMAT_XZ: - lzret = lzma_stream_decoder(&self->lzs, memlimit_, decoder_flags); + if (mtspec != Py_None) { + lzma_mt mt_options = { + .flags = decoder_flags, .memlimit_stop = memlimit_, + }; + if (parse_mt_options_spec(&mt_options, mtspec) == -1) { + goto error; + } + lzret = lzma_stream_decoder_mt(&self->lzs, &mt_options); + } else { + lzret = lzma_stream_decoder(&self->lzs, memlimit_, decoder_flags); + } if (catch_lzma_error(state, lzret)) { goto error; } diff --git a/Modules/clinic/_lzmamodule.c.h b/Modules/clinic/_lzmamodule.c.h index ebdc81a0dac2f0..559de7886af0d0 100644 --- a/Modules/clinic/_lzmamodule.c.h +++ b/Modules/clinic/_lzmamodule.c.h @@ -166,7 +166,8 @@ _lzma_LZMADecompressor_decompress(PyObject *self, PyObject *const *args, Py_ssiz } PyDoc_STRVAR(_lzma_LZMADecompressor__doc__, -"LZMADecompressor(format=FORMAT_AUTO, memlimit=None, filters=None)\n" +"LZMADecompressor(format=FORMAT_AUTO, memlimit=None, filters=None, *,\n" +" mt_options=None)\n" "--\n" "\n" "Create a decompressor object for decompressing data incrementally.\n" @@ -185,12 +186,20 @@ PyDoc_STRVAR(_lzma_LZMADecompressor__doc__, " not accepted with any other format. When provided, this should be a\n" " sequence of dicts, each indicating the ID and options for a single\n" " filter.\n" +" mt_options\n" +" If provided should be a dict, in which case the threaded implementation\n" +" will be used.\n" +"\n" +"The MT (multi-threaded) decompressor is chosen instead of the default if the\n" +"\'mt_options\' dictionary argument has been passed which supports additional\n" +"settings. You have to specify FORMAT_XZ explicitly for this.\n" "\n" "For one-shot decompression, use the decompress() function instead."); static PyObject * _lzma_LZMADecompressor_impl(PyTypeObject *type, int format, - PyObject *memlimit, PyObject *filters); + PyObject *memlimit, PyObject *filters, + PyObject *mtspec); static PyObject * _lzma_LZMADecompressor(PyTypeObject *type, PyObject *args, PyObject *kwargs) @@ -198,7 +207,7 @@ _lzma_LZMADecompressor(PyTypeObject *type, PyObject *args, PyObject *kwargs) PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - #define NUM_KEYWORDS 3 + #define NUM_KEYWORDS 4 static struct { PyGC_Head _this_is_not_used; PyObject_VAR_HEAD @@ -207,7 +216,7 @@ _lzma_LZMADecompressor(PyTypeObject *type, PyObject *args, PyObject *kwargs) } _kwtuple = { .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) .ob_hash = -1, - .ob_item = { &_Py_ID(format), &_Py_ID(memlimit), &_Py_ID(filters), }, + .ob_item = { &_Py_ID(format), &_Py_ID(memlimit), &_Py_ID(filters), &_Py_ID(mt_options), }, }; #undef NUM_KEYWORDS #define KWTUPLE (&_kwtuple.ob_base.ob_base) @@ -216,20 +225,21 @@ _lzma_LZMADecompressor(PyTypeObject *type, PyObject *args, PyObject *kwargs) # define KWTUPLE NULL #endif // !Py_BUILD_CORE - static const char * const _keywords[] = {"format", "memlimit", "filters", NULL}; + static const char * const _keywords[] = {"format", "memlimit", "filters", "mt_options", NULL}; static _PyArg_Parser _parser = { .keywords = _keywords, .fname = "LZMADecompressor", .kwtuple = KWTUPLE, }; #undef KWTUPLE - PyObject *argsbuf[3]; + PyObject *argsbuf[4]; PyObject * const *fastargs; Py_ssize_t nargs = PyTuple_GET_SIZE(args); Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 0; int format = FORMAT_AUTO; PyObject *memlimit = Py_None; PyObject *filters = Py_None; + PyObject *mtspec = Py_None; fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, /*minpos*/ 0, /*maxpos*/ 3, /*minkw*/ 0, /*varpos*/ 0, argsbuf); @@ -254,9 +264,19 @@ _lzma_LZMADecompressor(PyTypeObject *type, PyObject *args, PyObject *kwargs) goto skip_optional_pos; } } - filters = fastargs[2]; + if (fastargs[2]) { + filters = fastargs[2]; + if (!--noptargs) { + goto skip_optional_pos; + } + } skip_optional_pos: - return_value = _lzma_LZMADecompressor_impl(type, format, memlimit, filters); + if (!noptargs) { + goto skip_optional_kwonly; + } + mtspec = fastargs[3]; +skip_optional_kwonly: + return_value = _lzma_LZMADecompressor_impl(type, format, memlimit, filters, mtspec); exit: return return_value; @@ -333,4 +353,4 @@ _lzma__decode_filter_properties(PyObject *module, PyObject *const *args, Py_ssiz return return_value; } -/*[clinic end generated code: output=6386084cb43d2533 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=5f316bd2efcc6302 input=a9049054013a1b77]*/