diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 05f20e12f4653d..f07f5e8040acf0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -585,31 +585,31 @@ jobs: - name: Tests run: xvfb-run make ci - build-tsan: - name: >- - Thread sanitizer - ${{ fromJSON(matrix.free-threading) && '(free-threading)' || '' }} + build-san: + name: >- # ${{ '' } is a hack to nest jobs under the same sidebar category + Sanitizers${{ '' }} needs: build-context if: needs.build-context.outputs.run-tests == 'true' strategy: fail-fast: false matrix: + check-name: + - Thread free-threading: - false - true - uses: ./.github/workflows/reusable-tsan.yml + sanitizer: + - TSan + include: + - check-name: Undefined behavior + sanitizer: UBSan + free-threading: false + uses: ./.github/workflows/reusable-san.yml with: + sanitizer: ${{ matrix.sanitizer }} config_hash: ${{ needs.build-context.outputs.config-hash }} free-threading: ${{ matrix.free-threading }} - build-ubsan: - name: Undefined behavior sanitizer - needs: build-context - if: needs.build-context.outputs.run-tests == 'true' - uses: ./.github/workflows/reusable-ubsan.yml - with: - config_hash: ${{ needs.build-context.outputs.config-hash }} - cross-build-linux: name: Cross build Linux runs-on: ubuntu-latest @@ -708,7 +708,7 @@ jobs: - build-wasi - test-hypothesis - build-asan - - build-tsan + - build-san - cross-build-linux - cifuzz if: always() @@ -743,7 +743,7 @@ jobs: build-wasi, test-hypothesis, build-asan, - build-tsan, + build-san, cross-build-linux, ' || '' diff --git a/.github/workflows/reusable-san.yml b/.github/workflows/reusable-san.yml new file mode 100644 index 00000000000000..e6ff02e4838ee6 --- /dev/null +++ b/.github/workflows/reusable-san.yml @@ -0,0 +1,124 @@ +name: Reusable Sanitizer + +on: + workflow_call: + inputs: + sanitizer: + required: true + type: string + config_hash: + required: true + type: string + free-threading: + description: Whether to use free-threaded mode + required: false + type: boolean + default: false + +env: + FORCE_COLOR: 1 + +jobs: + build-san-reusable: + name: >- + ${{ inputs.sanitizer }}${{ + inputs.free-threading + && ' (free-threading)' + || '' + }} + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Runner image version + run: echo "IMAGE_OS_VERSION=${ImageOS}-${ImageVersion}" >> "$GITHUB_ENV" + - name: Restore config.cache + uses: actions/cache@v4 + with: + path: config.cache + key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ inputs.sanitizer }}-${{ inputs.config_hash }} + - name: Install dependencies + run: | + sudo ./.github/workflows/posix-deps-apt.sh + # Install clang + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + + if [ "${SANITIZER}" = "TSan" ]; then + sudo ./llvm.sh 17 # gh-121946: llvm-18 package is temporarily broken + sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-17 100 + sudo update-alternatives --set clang /usr/bin/clang-17 + sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-17 100 + sudo update-alternatives --set clang++ /usr/bin/clang++-17 + # Reduce ASLR to avoid TSan crashing + sudo sysctl -w vm.mmap_rnd_bits=28 + else + sudo ./llvm.sh 20 + fi + + - name: Sanitizer option setup + run: | + if [ "${SANITIZER}" = "TSan" ]; then + echo "TSAN_OPTIONS=${SAN_LOG_OPTION} suppressions=${GITHUB_WORKSPACE}/Tools/tsan/suppressions${{ + fromJSON(inputs.free-threading) + && '_free_threading' + || '' + }}.txt handle_segv=0" >> "$GITHUB_ENV" + else + echo "UBSAN_OPTIONS=${SAN_LOG_OPTION}" >> "$GITHUB_ENV" + fi + echo "CC=clang" >> "$GITHUB_ENV" + echo "CXX=clang++" >> "$GITHUB_ENV" + env: + SANITIZER: ${{ inputs.sanitizer }} + SAN_LOG_OPTION: log_path=${{ github.workspace }}/san_log + - name: Add ccache to PATH + run: | + echo "PATH=/usr/lib/ccache:$PATH" >> "$GITHUB_ENV" + - name: Configure ccache action + uses: hendrikmuhs/ccache-action@v1.2 + with: + save: ${{ github.event_name == 'push' }} + max-size: "200M" + - name: Configure CPython + run: >- + ./configure + --config-cache + ${{ + inputs.sanitizer == 'TSan' + && '--with-thread-sanitizer' + || '--with-undefined-behavior-sanitizer' + }} + --with-pydebug + ${{ fromJSON(inputs.free-threading) && '--disable-gil' || '' }} + - name: Build CPython + run: make -j4 + - name: Display build info + run: make pythoninfo + - name: Tests + run: >- + ./python -m test + ${{ inputs.sanitizer == 'TSan' && '--tsan' || '' }} + -j4 + - name: Parallel tests + if: >- + inputs.sanitizer == 'TSan' + && fromJSON(inputs.free-threading) + run: ./python -m test --tsan-parallel --parallel-threads=4 -j4 + - name: Display logs + if: always() + run: find "${GITHUB_WORKSPACE}" -name 'san_log.*' | xargs head -n 1000 + - name: Archive logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: >- + ${{ inputs.sanitizer }}-logs-${{ + fromJSON(inputs.free-threading) + && 'free-threading' + || 'default' + }} + path: san_log.* + if-no-files-found: ignore diff --git a/.github/workflows/reusable-tsan.yml b/.github/workflows/reusable-tsan.yml deleted file mode 100644 index 6a58e5305f8e09..00000000000000 --- a/.github/workflows/reusable-tsan.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: Reusable Thread Sanitizer - -on: - workflow_call: - inputs: - config_hash: - required: true - type: string - free-threading: - description: Whether to use free-threaded mode - required: false - type: boolean - default: false - -env: - FORCE_COLOR: 1 - -jobs: - build-tsan-reusable: - name: 'Thread sanitizer' - runs-on: ubuntu-24.04 - timeout-minutes: 60 - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: Runner image version - run: echo "IMAGE_OS_VERSION=${ImageOS}-${ImageVersion}" >> "$GITHUB_ENV" - - name: Restore config.cache - uses: actions/cache@v4 - with: - path: config.cache - key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ inputs.config_hash }} - - name: Install dependencies - run: | - sudo ./.github/workflows/posix-deps-apt.sh - # Install clang-18 - wget https://apt.llvm.org/llvm.sh - chmod +x llvm.sh - sudo ./llvm.sh 17 # gh-121946: llvm-18 package is temporarily broken - sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-17 100 - sudo update-alternatives --set clang /usr/bin/clang-17 - sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-17 100 - sudo update-alternatives --set clang++ /usr/bin/clang++-17 - # Reduce ASLR to avoid TSAN crashing - sudo sysctl -w vm.mmap_rnd_bits=28 - - name: TSAN option setup - run: | - echo "TSAN_OPTIONS=log_path=${GITHUB_WORKSPACE}/tsan_log suppressions=${GITHUB_WORKSPACE}/Tools/tsan/suppressions${{ - fromJSON(inputs.free-threading) - && '_free_threading' - || '' - }}.txt handle_segv=0" >> "$GITHUB_ENV" - echo "CC=clang" >> "$GITHUB_ENV" - echo "CXX=clang++" >> "$GITHUB_ENV" - - name: Add ccache to PATH - run: | - echo "PATH=/usr/lib/ccache:$PATH" >> "$GITHUB_ENV" - - name: Configure ccache action - uses: hendrikmuhs/ccache-action@v1.2 - with: - save: ${{ github.event_name == 'push' }} - max-size: "200M" - - name: Configure CPython - run: >- - ./configure - --config-cache - --with-thread-sanitizer - --with-pydebug - ${{ fromJSON(inputs.free-threading) && '--disable-gil' || '' }} - - name: Build CPython - run: make -j4 - - name: Display build info - run: make pythoninfo - - name: Tests - run: ./python -m test --tsan -j4 - - name: Parallel tests - if: fromJSON(inputs.free-threading) - run: ./python -m test --tsan-parallel --parallel-threads=4 -j4 - - name: Display TSAN logs - if: always() - run: find "${GITHUB_WORKSPACE}" -name 'tsan_log.*' | xargs head -n 1000 - - name: Archive TSAN logs - if: always() - uses: actions/upload-artifact@v4 - with: - name: >- - tsan-logs-${{ - fromJSON(inputs.free-threading) - && 'free-threading' - || 'default' - }} - path: tsan_log.* - if-no-files-found: ignore diff --git a/.github/workflows/reusable-ubsan.yml b/.github/workflows/reusable-ubsan.yml deleted file mode 100644 index cf93932f13b827..00000000000000 --- a/.github/workflows/reusable-ubsan.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Reusable Undefined Behavior Sanitizer - -on: - workflow_call: - inputs: - config_hash: - required: true - type: string - -env: - FORCE_COLOR: 1 - -jobs: - build-ubsan-reusable: - name: 'Undefined behavior sanitizer' - runs-on: ubuntu-24.04 - timeout-minutes: 60 - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: Runner image version - run: echo "IMAGE_OS_VERSION=${ImageOS}-${ImageVersion}" >> "$GITHUB_ENV" - - name: Restore config.cache - uses: actions/cache@v4 - with: - path: config.cache - key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ inputs.config_hash }} - - name: Install dependencies - run: | - sudo ./.github/workflows/posix-deps-apt.sh - # Install clang-20 - wget https://apt.llvm.org/llvm.sh - chmod +x llvm.sh - sudo ./llvm.sh 20 - - name: UBSAN option setup - run: | - echo "UBSAN_OPTIONS=halt_on_error=1:abort_on_error=1:print_summary=1:print_stacktrace=1" >> "$GITHUB_ENV" - echo "CC=clang" >> "$GITHUB_ENV" - echo "CXX=clang++" >> "$GITHUB_ENV" - - name: Add ccache to PATH - run: | - echo "PATH=/usr/lib/ccache:$PATH" >> "$GITHUB_ENV" - - name: Configure ccache action - uses: hendrikmuhs/ccache-action@v1.2 - with: - save: ${{ github.event_name == 'push' }} - max-size: "200M" - - name: Configure CPython - run: >- - ./configure - --config-cache - --with-undefined-behavior-sanitizer - --with-pydebug - - name: Set up UBSAN log after configuration - run: | - echo "UBSAN_OPTIONS=${UBSAN_OPTIONS}:log_path=${GITHUB_WORKSPACE}/ubsan_log" >> "$GITHUB_ENV" - - name: Build CPython - run: make -j4 - - name: Display build info - run: make pythoninfo - - name: Tests - run: ./python -m test -j4 - - name: Display UBSAN logs - if: always() - run: find "${GITHUB_WORKSPACE}" -name 'ubsan_log.*' | xargs head -n 1000 - - name: Archive UBSAN logs - if: always() - uses: actions/upload-artifact@v4 - with: - name: >- - ubsan-logs - path: ubsan_log.* - if-no-files-found: ignore diff --git a/Doc/library/gzip.rst b/Doc/library/gzip.rst index c9d96085ef739d..c59014a6f5bdce 100644 --- a/Doc/library/gzip.rst +++ b/Doc/library/gzip.rst @@ -26,7 +26,7 @@ Note that additional file formats which can be decompressed by the The module defines the following items: -.. function:: open(filename, mode='rb', compresslevel=9, encoding=None, errors=None, newline=None) +.. function:: open(filename, mode='rb', compresslevel=6, encoding=None, errors=None, newline=None) Open a gzip-compressed file in binary or text mode, returning a :term:`file object`. @@ -59,6 +59,11 @@ The module defines the following items: .. versionchanged:: 3.6 Accepts a :term:`path-like object`. + .. versionchanged:: next + The default compression level was reduced to 6 (down from 9). + It is the default level used by most compression tools and a better + tradeoff between speed and performance. + .. exception:: BadGzipFile An exception raised for invalid gzip files. It inherits from :exc:`OSError`. @@ -67,7 +72,7 @@ The module defines the following items: .. versionadded:: 3.8 -.. class:: GzipFile(filename=None, mode=None, compresslevel=9, fileobj=None, mtime=None) +.. class:: GzipFile(filename=None, mode=None, compresslevel=6, fileobj=None, mtime=None) Constructor for the :class:`GzipFile` class, which simulates most of the methods of a :term:`file object`, with the exception of the :meth:`~io.IOBase.truncate` @@ -181,8 +186,13 @@ The module defines the following items: Remove the ``filename`` attribute, use the :attr:`~GzipFile.name` attribute instead. + .. versionchanged:: next + The default compression level was reduced to 6 (down from 9). + It is the default level used by most compression tools and a better + tradeoff between speed and performance. + -.. function:: compress(data, compresslevel=9, *, mtime=0) +.. function:: compress(data, compresslevel=6, *, mtime=0) Compress the *data*, returning a :class:`bytes` object containing the compressed data. *compresslevel* and *mtime* have the same meaning as in @@ -206,6 +216,10 @@ The module defines the following items: The *mtime* parameter now defaults to 0 for reproducible output. For the previous behaviour of using the current time, pass ``None`` to *mtime*. + .. versionchanged:: next + The default compression level was reduced to 6 (down from 9). + It is the default level used by most compression tools and a better + tradeoff between speed and performance. .. function:: decompress(data) diff --git a/Doc/library/tarfile.rst b/Doc/library/tarfile.rst index c6c0033837562a..acaec5b592bf6e 100644 --- a/Doc/library/tarfile.rst +++ b/Doc/library/tarfile.rst @@ -123,7 +123,7 @@ Some facts and figures: For modes ``'w:gz'``, ``'x:gz'``, ``'w|gz'``, ``'w:bz2'``, ``'x:bz2'``, ``'w|bz2'``, :func:`tarfile.open` accepts the keyword argument - *compresslevel* (default ``9``) to specify the compression level of the file. + *compresslevel* (default ``6``) to specify the compression level of the file. For modes ``'w:xz'``, ``'x:xz'`` and ``'w|xz'``, :func:`tarfile.open` accepts the keyword argument *preset* to specify the compression level of the file. @@ -198,6 +198,10 @@ Some facts and figures: .. versionchanged:: 3.14 The *preset* keyword argument also works for streams. + .. versionchanged:: next + The default compression level was reduced to 6 (down from 9). + It is the default level used by most compression tools and a better + tradeoff between speed and performance. .. class:: TarFile :noindex: diff --git a/Doc/library/types.rst b/Doc/library/types.rst index 2bedd7fdd3c8c8..207024a7619902 100644 --- a/Doc/library/types.rst +++ b/Doc/library/types.rst @@ -333,6 +333,16 @@ Standard names are defined for the following types: :attr:`tb.tb_frame ` if ``tb`` is a traceback object. +.. data:: FrameLocalsProxyType + + The type of frame locals proxy objects, as found on the + :attr:`frame.f_locals` attribute. + + .. versionadded:: next + + .. seealso:: :pep:`667` + + .. data:: GetSetDescriptorType The type of objects defined in extension modules with ``PyGetSetDef``, such diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 7e47fa263d9a5e..4d4fb77ad4f030 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -312,6 +312,15 @@ tarfile and :cve:`2025-4435`.) +types +------ + +* Expose the write-through :func:`locals` proxy type + as :data:`types.FrameLocalsProxyType`. + This represents the type of the :attr:`frame.f_locals` attribute, + as described in :pep:`667`. + + unittest -------- diff --git a/Lib/gzip.py b/Lib/gzip.py index c00f51858de0f0..89c4738ec69325 100644 --- a/Lib/gzip.py +++ b/Lib/gzip.py @@ -30,7 +30,7 @@ _WRITE_BUFFER_SIZE = 4 * io.DEFAULT_BUFFER_SIZE -def open(filename, mode="rb", compresslevel=_COMPRESS_LEVEL_BEST, +def open(filename, mode="rb", compresslevel=_COMPRESS_LEVEL_TRADEOFF, encoding=None, errors=None, newline=None): """Open a gzip-compressed file in binary or text mode. @@ -158,7 +158,7 @@ class GzipFile(_streams.BaseStream): myfileobj = None def __init__(self, filename=None, mode=None, - compresslevel=_COMPRESS_LEVEL_BEST, fileobj=None, mtime=None): + compresslevel=_COMPRESS_LEVEL_TRADEOFF, fileobj=None, mtime=None): """Constructor for the GzipFile class. At least one of fileobj and filename must be given a @@ -621,7 +621,7 @@ def _rewind(self): self._new_member = True -def compress(data, compresslevel=_COMPRESS_LEVEL_BEST, *, mtime=0): +def compress(data, compresslevel=_COMPRESS_LEVEL_TRADEOFF, *, mtime=0): """Compress data in one shot and return the compressed string. compresslevel sets the compression level in range of 0-9. diff --git a/Lib/tarfile.py b/Lib/tarfile.py index 068aa13ed70356..80d8644af86f74 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -1927,7 +1927,7 @@ def not_compressed(comptype): if "preset" in kwargs and comptype not in ("xz",): raise ValueError("preset is only valid for w|xz mode") - compresslevel = kwargs.pop("compresslevel", 9) + compresslevel = kwargs.pop("compresslevel", 6) preset = kwargs.pop("preset", None) stream = _Stream(name, filemode, comptype, fileobj, bufsize, compresslevel, preset) @@ -1953,7 +1953,7 @@ def taropen(cls, name, mode="r", fileobj=None, **kwargs): return cls(name, mode, fileobj, **kwargs) @classmethod - def gzopen(cls, name, mode="r", fileobj=None, compresslevel=9, **kwargs): + def gzopen(cls, name, mode="r", fileobj=None, compresslevel=6, **kwargs): """Open gzip compressed tar archive name for reading or writing. Appending is not allowed. """ diff --git a/Lib/test/test_gzip.py b/Lib/test/test_gzip.py index a12ff5662a73db..9a2e1dd248fe94 100644 --- a/Lib/test/test_gzip.py +++ b/Lib/test/test_gzip.py @@ -353,7 +353,7 @@ def test_mtime(self): def test_metadata(self): mtime = 123456789 - with gzip.GzipFile(self.filename, 'w', mtime = mtime) as fWrite: + with gzip.GzipFile(self.filename, 'w', mtime = mtime, compresslevel = 9) as fWrite: fWrite.write(data1) with open(self.filename, 'rb') as fRead: diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 4f3983d83c7c06..30e01b8cd87a75 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -5786,6 +5786,7 @@ def test_types_module_has_signatures(self): 'AsyncGeneratorType': {'athrow'}, 'CoroutineType': {'throw'}, 'GeneratorType': {'throw'}, + 'FrameLocalsProxyType': {'setdefault', 'pop', 'get'}, } self._test_module_has_signatures(types, unsupported_signature=unsupported_signature, diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index fccdcc975e6c43..9b0ae709d7968d 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -53,6 +53,7 @@ def test_names(self): 'AsyncGeneratorType', 'BuiltinFunctionType', 'BuiltinMethodType', 'CapsuleType', 'CellType', 'ClassMethodDescriptorType', 'CodeType', 'CoroutineType', 'EllipsisType', 'FrameType', 'FunctionType', + 'FrameLocalsProxyType', 'GeneratorType', 'GenericAlias', 'GetSetDescriptorType', 'LambdaType', 'MappingProxyType', 'MemberDescriptorType', 'MethodDescriptorType', 'MethodType', 'MethodWrapperType', @@ -711,6 +712,16 @@ def call(part): """ assert_python_ok("-c", code) + def test_frame_locals_proxy_type(self): + self.assertIsInstance(types.FrameLocalsProxyType, type) + self.assertIsInstance(types.FrameLocalsProxyType.__doc__, str) + self.assertEqual(types.FrameLocalsProxyType.__module__, 'builtins') + self.assertEqual(types.FrameLocalsProxyType.__name__, 'FrameLocalsProxy') + + frame = inspect.currentframe() + self.assertIsNotNone(frame) + self.assertIsInstance(frame.f_locals, types.FrameLocalsProxyType) + class UnionTests(unittest.TestCase): diff --git a/Lib/types.py b/Lib/types.py index cf0549315a7814..f96c75b46daba7 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -58,7 +58,10 @@ def _m(self): pass raise TypeError except TypeError as exc: TracebackType = type(exc.__traceback__) - FrameType = type(exc.__traceback__.tb_frame) + + _f = (lambda: sys._getframe())() + FrameType = type(_f) + FrameLocalsProxyType = type(_f.f_locals) GetSetDescriptorType = type(FunctionType.__code__) MemberDescriptorType = type(FunctionType.__globals__) diff --git a/Misc/NEWS.d/next/Library/2025-03-19-12-41-42.gh-issue-91349.8eTOCP.rst b/Misc/NEWS.d/next/Library/2025-03-19-12-41-42.gh-issue-91349.8eTOCP.rst new file mode 100644 index 00000000000000..0e866fa4ef6169 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-03-19-12-41-42.gh-issue-91349.8eTOCP.rst @@ -0,0 +1,3 @@ +Adjust default ``compressionlevel=`` to 6 (down from 9) in :mod:`gzip` and :mod:`tarfile`. +It is the default level used by most compression tools and a better +tradeoff between speed and performance. diff --git a/Misc/NEWS.d/next/Library/2025-07-11-10-23-44.gh-issue-136492.BVi5h0.rst b/Misc/NEWS.d/next/Library/2025-07-11-10-23-44.gh-issue-136492.BVi5h0.rst new file mode 100644 index 00000000000000..7ab5b068a7f691 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-07-11-10-23-44.gh-issue-136492.BVi5h0.rst @@ -0,0 +1 @@ +Expose :pep:`667`'s :data:`~types.FrameLocalsProxyType` in the :mod:`types` module. diff --git a/Modules/_typesmodule.c b/Modules/_typesmodule.c index a30a88196e7192..df6b4c93cb87a6 100644 --- a/Modules/_typesmodule.c +++ b/Modules/_typesmodule.c @@ -28,6 +28,7 @@ _types_exec(PyObject *m) EXPORT_STATIC_TYPE("CoroutineType", PyCoro_Type); EXPORT_STATIC_TYPE("EllipsisType", PyEllipsis_Type); EXPORT_STATIC_TYPE("FrameType", PyFrame_Type); + EXPORT_STATIC_TYPE("FrameLocalsProxyType", PyFrameLocalsProxy_Type); EXPORT_STATIC_TYPE("FunctionType", PyFunction_Type); EXPORT_STATIC_TYPE("GeneratorType", PyGen_Type); EXPORT_STATIC_TYPE("GenericAlias", Py_GenericAliasType); diff --git a/Objects/frameobject.c b/Objects/frameobject.c index 601fc69c4b1f60..97de1e06efe1b2 100644 --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -913,6 +913,15 @@ static PyMethodDef framelocalsproxy_methods[] = { {NULL, NULL} /* sentinel */ }; +PyDoc_STRVAR(framelocalsproxy_doc, +"FrameLocalsProxy($frame)\n" +"--\n" +"\n" +"Create a write-through view of the locals dictionary for a frame.\n" +"\n" +" frame\n" +" the frame object to wrap."); + PyTypeObject PyFrameLocalsProxy_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) .tp_name = "FrameLocalsProxy", @@ -933,6 +942,7 @@ PyTypeObject PyFrameLocalsProxy_Type = { .tp_alloc = PyType_GenericAlloc, .tp_new = framelocalsproxy_new, .tp_free = PyObject_GC_Del, + .tp_doc = framelocalsproxy_doc, }; PyObject *