diff --git a/.editorconfig b/.editorconfig index e92f3ffb..d72c6d22 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,3 +9,6 @@ insert_final_newline = true trim_trailing_whitespace = true end_of_line = lf charset = utf-8 + +[*.yml] +indent_size = 2 diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml new file mode 100644 index 00000000..15ca1cc2 --- /dev/null +++ b/.github/workflows/build-and-deploy.yml @@ -0,0 +1,114 @@ +name: Build and upload to PyPI + +# Build on every branch push, tag push, and pull request change: +on: [push, pull_request] +# Alternatively, to publish when a (published) GitHub Release is created, use the following: +# on: +# push: +# pull_request: +# release: +# types: +# - published + +jobs: + build_wheels: + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - 'ubuntu-latest' + - 'windows-latest' + - 'macos-latest' + + steps: + - uses: actions/checkout@v2 + + - name: Set up QEMU + if: runner.os == 'Linux' + uses: docker/setup-qemu-action@v1 + with: + platforms: all + + - name: Build wheels + uses: pypa/cibuildwheel@v2.2.2 + env: + CIBW_TEST_COMMAND: >- + python -m simplejson.tests._cibw_runner "{project}" + CIBW_SKIP: "pp*" + CIBW_ARCHS_WINDOWS: "auto" + CIBW_ARCHS_LINUX: "auto aarch64" + CIBW_ARCHS_MACOS: "x86_64 universal2 arm64" + + - name: Build Python 2.7 wheels + if: runner.os != 'Windows' + uses: pypa/cibuildwheel@v1.12.0 + env: + CIBW_TEST_COMMAND: >- + python -m simplejson.tests._cibw_runner "{project}" + CIBW_BUILD: "cp27-*" + CIBW_SKIP: "pp*" + CIBW_ARCHS_LINUX: "auto aarch64" + + - uses: actions/upload-artifact@v2 + if: "github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/')" + with: + path: ./wheelhouse/*.whl + + build_sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-python@v2 + name: Install Python + with: + python-version: '3.11' + + - name: Build sdist + run: python setup.py sdist + + - uses: actions/upload-artifact@v2 + if: "github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/')" + with: + path: dist/*.tar.gz + + upload_pypi: + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + # upload to PyPI on every tag starting with 'v' + if: "github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')" + # alternatively, to publish when a GitHub Release is created, use the following rule: + # if: github.event_name == 'release' && github.event.action == 'published' + steps: + - uses: actions/download-artifact@v2 + with: + name: artifact + path: dist + + - uses: pypa/gh-action-pypi-publish@v1.4.2 + with: + user: __token__ + password: ${{ secrets.PYPI_PASSWORD }} + # To test: repository_url: https://test.pypi.org/legacy/ + + upload_pypi_test: + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + # upload to PyPI on every tag starting with 'v' + if: "github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/test-v')" + # alternatively, to publish when a GitHub Release is created, use the following rule: + # if: github.event_name == 'release' && github.event.action == 'published' + steps: + - uses: actions/download-artifact@v2 + with: + name: artifact + path: dist + + - uses: pypa/gh-action-pypi-publish@v1.4.2 + with: + user: __token__ + password: ${{ secrets.PYPI_PASSWORD_TEST }} + repository_url: https://test.pypi.org/legacy/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 17b813fe..00000000 --- a/.travis.yml +++ /dev/null @@ -1,70 +0,0 @@ -language: python -cache: - directories: - - "$HOME/.cache/pip" - - "$HOME/.pyenv" -matrix: - include: - - python: 2.6 - dist: trusty - - python: 2.7 - - python: 3.3 - dist: trusty - - python: 3.4 - - python: 3.5 - - python: 3.6 - - python: 3.6 - arch: arm64 - - python: 3.7 - - python: 3.7 - arch: arm64 - - python: 3.8 - - python: 3.8 - arch: arm64 - - python: pypy - - python: pypy3 - - python: 3.8 - services: - - docker - env: - - BUILD_SDIST=true - - BUILD_WHEEL=true - - python: 3.8 - arch: arm64 - services: - - docker - env: BUILD_WHEEL=true - - name: Python 2.7.17 on macOS - os: osx - language: objective-c - env: PYENV_VERSION=2.7.17 - - name: Python 3.6.9 on macOS - os: osx - language: objective-c - env: PYENV_VERSION=3.6.9 - - name: Python 3.7.5 on macOS - os: osx - osx_image: xcode11.3 - language: objective-c - env: PYENV_VERSION=3.7.5 - - name: Python 3.8.0 on macOS - os: osx - osx_image: xcode11.3 - language: objective-c - env: PYENV_VERSION=3.8.0 -install: -- "./.travis/install.sh" -script: -- "./.travis/run.sh" -deploy: - provider: releases - file: - - dist/*.whl - - dist/*.tar.gz - file_glob: true - on: - repo: simplejson/simplejson - tags: true - skip_cleanup: true - api_key: - secure: FhcBCuL/33fsotaDLv157pQ0HpxYAXI9h4TNhT0CDgrh1i2J1m/hmJMqlrrn0j/E2TpYyXf1citLra6QrBP//FnFMGQ43el369tC3W9RPXYpfThzU6JcJecoWGQMMMiJgPLpOC7+tyDpphsNFKmySG/ITvX+OEZ4lzL8+1CWyRk= diff --git a/.travis/install.sh b/.travis/install.sh deleted file mode 100755 index 26e31e9a..00000000 --- a/.travis/install.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -set -e -set -x - -if [[ $TRAVIS_OS_NAME == 'osx' ]]; then - if [ ! -e "$HOME/.pyenv-simplejson/.git" ]; then - if [ -e "$HOME/.pyenv-simplejson" ]; then - rm -rf ~/.pyenv-simplejson - fi - git clone https://github.com/pyenv/pyenv.git ~/.pyenv-simplejson - else - (cd ~/.pyenv-simplejson; git pull) - fi - PYENV_ROOT="$HOME/.pyenv-simplejson" - PATH="$PYENV_ROOT/bin:$PATH" - hash -r - eval "$(pyenv init -)" - hash -r - pyenv install --list - pyenv install -s $PYENV_VERSION - pip install wheel -fi - -if [[ $BUILD_WHEEL == 'true' ]]; then - pip install wheel cibuildwheel==1.5.2 -fi diff --git a/.travis/run.sh b/.travis/run.sh deleted file mode 100755 index c3fa89c7..00000000 --- a/.travis/run.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -set -e -set -x - -if [[ $TRAVIS_OS_NAME == 'osx' ]]; then - PYENV_ROOT="$HOME/.pyenv-simplejson" - PATH="$PYENV_ROOT/bin:$PATH" - hash -r - eval "$(pyenv init -)" -fi -REQUIRE_SPEEDUPS=1 python setup.py build_ext -i -python -m compileall -f . -python setup.py test - -if [[ $TRAVIS_OS_NAME == 'osx' ]]; then - python setup.py bdist_wheel -fi - -if [[ $BUILD_WHEEL == 'true' ]]; then - cibuildwheel --output-dir dist -fi - -if [[ $BUILD_SDIST == 'true' ]]; then - python setup.py sdist -fi diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..ee3ad589 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.formatting.provider": "black", + "esbonio.sphinx.confDir": "" +} diff --git a/CHANGES.txt b/CHANGES.txt index bc21b7ec..7d574e2f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,37 @@ +Version 3.18.0 released 2022-11-14 + +* Allow serialization of classes that implement for_json or _asdict by + ignoring TypeError when those methods are called + https://github.com/simplejson/simplejson/pull/302 +* Raise JSONDecodeError instead of ValueError in invalid unicode escape + sequence edge case + https://github.com/simplejson/simplejson/pull/298 + +Version 3.17.6 released 2021-11-15 + +* Declare support for Python 3.10 and add wheels + https://github.com/simplejson/simplejson/pull/291 + https://github.com/simplejson/simplejson/pull/292 + +Version 3.17.5 released 2021-08-23 + +* Fix the C extension module to harden is_namedtuple against looks-a-likes such + as Mocks. Also prevent dict encoding from causing an unraised SystemError when + encountering a non-Dict. Noticed by running user tests against a CPython + interpreter with C asserts enabled (COPTS += -UNDEBUG). + https://github.com/simplejson/simplejson/pull/284 + +Version 3.17.4 released 2021-08-19 + +* Upgrade cibuildwheel + https://github.com/simplejson/simplejson/pull/287 + +Version 3.17.3 released 2021-07-09 + +* Replaced Travis-CI and AppVeyor with Github Actions, + adding wheels for Python 3.9. + https://github.com/simplejson/simplejson/pull/283 + Version 3.17.2 released 2020-07-16 * Added arm64 to build matrix and reintroduced diff --git a/README.rst b/README.rst index 79f84022..6580ddac 100644 --- a/README.rst +++ b/README.rst @@ -1,12 +1,6 @@ simplejson ---------- -.. image:: https://travis-ci.org/simplejson/simplejson.svg?branch=master - :target: https://travis-ci.org/simplejson/simplejson - -.. image:: https://ci.appveyor.com/api/projects/status/3riqhss6vca680gi/branch/master?svg=true - :target: https://ci.appveyor.com/project/etrepum/simplejson/branch/master - simplejson is a simple, fast, complete, correct and extensible JSON encoder and decoder for Python 3.3+ with legacy support for Python 2.5+. It is pure Python code diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 3eec1614..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,82 +0,0 @@ -environment: - - global: - # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the - # /E:ON and /V:ON options are not enabled in the batch script interpreter - # See: http://stackoverflow.com/a/13751649/163740 - WITH_COMPILER: "cmd /E:ON /V:ON /C .\\appveyor\\run_with_env.cmd" - - matrix: - - PYTHON: "C:\\Python27" - PYTHON_VERSION: "2.7.14" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python33" - PYTHON_VERSION: "3.3.6" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python34" - PYTHON_VERSION: "3.4.3" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python35" - PYTHON_VERSION: "3.5.6" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python36" - PYTHON_VERSION: "3.6.6" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python37" - PYTHON_VERSION: "3.7.0" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python27-x64" - PYTHON_VERSION: "2.7.14" - PYTHON_ARCH: "64" - - - PYTHON: "C:\\Python33-x64" - PYTHON_VERSION: "3.3.6" - PYTHON_ARCH: "64" - - - PYTHON: "C:\\Python34-x64" - PYTHON_VERSION: "3.4.3" - PYTHON_ARCH: "64" - - - PYTHON: "C:\\Python35-x64" - PYTHON_VERSION: "3.5.6" - PYTHON_ARCH: "64" - - - PYTHON: "C:\\Python36-x64" - PYTHON_VERSION: "3.6.6" - PYTHON_ARCH: "64" - - - PYTHON: "C:\\Python37-x64" - PYTHON_VERSION: "3.7.5" - PYTHON_ARCH: "64" - - - PYTHON: "C:\\Python37-x64" - PYTHON_VERSION: "3.8.0" - PYTHON_ARCH: "64" - -clone_depth: 25 - -init: - - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" - -install: - - "powershell appveyor\\install.ps1" - -build: off - -test_script: - - "%WITH_COMPILER% %PYTHON%/python setup.py build_ext -i" - - "%WITH_COMPILER% %PYTHON%/python setup.py test" - - "%WITH_COMPILER% %PYTHON%/python setup.py bdist_wheel" - -artifacts: - - path: dist\*.exe - - path: dist\*.whl - -#on_success: -# - TODO: upload the content of dist/*.whl to a public wheelhouse diff --git a/appveyor/install.ps1 b/appveyor/install.ps1 deleted file mode 100644 index 3f056282..00000000 --- a/appveyor/install.ps1 +++ /dev/null @@ -1,85 +0,0 @@ -# Sample script to install Python and pip under Windows -# Authors: Olivier Grisel and Kyle Kastner -# License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ - -$BASE_URL = "https://www.python.org/ftp/python/" -$GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" -$GET_PIP_PATH = "C:\get-pip.py" - - -function DownloadPython ($python_version, $platform_suffix) { - $webclient = New-Object System.Net.WebClient - $filename = "python-" + $python_version + $platform_suffix + ".msi" - $url = $BASE_URL + $python_version + "/" + $filename - - $basedir = $pwd.Path + "\" - $filepath = $basedir + $filename - if (Test-Path $filename) { - Write-Host "Reusing" $filepath - return $filepath - } - - # Download and retry up to 5 times in case of network transient errors. - Write-Host "Downloading" $filename "from" $url - $retry_attempts = 3 - for($i=0; $i -lt $retry_attempts; $i++){ - try { - $webclient.DownloadFile($url, $filepath) - break - } - Catch [Exception]{ - Start-Sleep 1 - } - } - Write-Host "File saved at" $filepath - return $filepath -} - - -function InstallPython ($python_version, $architecture, $python_home) { - Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home - if (Test-Path $python_home) { - Write-Host $python_home "already exists, skipping." - return $false - } - if ($architecture -eq "32") { - $platform_suffix = "" - } else { - $platform_suffix = ".amd64" - } - $filepath = DownloadPython $python_version $platform_suffix - Write-Host "Installing" $filepath "to" $python_home - $args = "/qn /i $filepath TARGETDIR=$python_home" - Write-Host "msiexec.exe" $args - Start-Process -FilePath "msiexec.exe" -ArgumentList $args -Wait -Passthru - Write-Host "Python $python_version ($architecture) installation complete" - return $true -} - - -function InstallPip ($python_home) { - $pip_path = $python_home + "/Scripts/pip.exe" - $python_path = $python_home + "/python.exe" - if (-not(Test-Path $pip_path)) { - Write-Host "Installing pip..." - $webclient = New-Object System.Net.WebClient - $webclient.DownloadFile($GET_PIP_URL, $GET_PIP_PATH) - Write-Host "Executing:" $python_path $GET_PIP_PATH - Start-Process -FilePath "$python_path" -ArgumentList "$GET_PIP_PATH" -Wait -Passthru - } else { - Write-Host "pip already installed." - } -} - -function InstallPackage ($python_home, $pkg) { - $pip_path = $python_home + "/Scripts/pip.exe" - & $pip_path install $pkg -} - -function main () { - InstallPython $env:PYTHON_VERSION $env:PYTHON_ARCH $env:PYTHON - InstallPip $env:PYTHON - InstallPackage $env:PYTHON wheel -} - -main diff --git a/appveyor/run_with_env.cmd b/appveyor/run_with_env.cmd deleted file mode 100644 index 0a722f3b..00000000 --- a/appveyor/run_with_env.cmd +++ /dev/null @@ -1,89 +0,0 @@ -:: To build extensions for 64 bit Python 3, we need to configure environment -:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: -:: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1) -:: -:: To build extensions for 64 bit Python 2, we need to configure environment -:: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of: -:: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0) -:: -:: 32 bit builds, and 64-bit builds for 3.5 and beyond, do not require specific -:: environment configurations. -:: -:: Note: this script needs to be run with the /E:ON and /V:ON flags for the -:: cmd interpreter, at least for (SDK v7.0) -:: -:: More details at: -:: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows -:: http://stackoverflow.com/a/13751649/163740 -:: -:: Author: Olivier Grisel -:: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ -:: -:: Notes about batch files for Python people: -:: -:: Quotes in values are literally part of the values: -:: SET FOO="bar" -:: FOO is now five characters long: " b a r " -:: If you don't want quotes, don't include them on the right-hand side. -:: -:: The CALL lines at the end of this file look redundant, but if you move them -:: outside of the IF clauses, they do not run properly in the SET_SDK_64==Y -:: case, I don't know why. -@ECHO OFF - -SET REQUIRE_SPEEDUPS=Y -SET COMMAND_TO_RUN=%* -SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows -SET WIN_WDK=c:\Program Files (x86)\Windows Kits\10\Include\wdf - -:: Extract the major and minor versions, and allow for the minor version to be -:: more than 9. This requires the version number to have two dots in it. -SET MAJOR_PYTHON_VERSION=%PYTHON_VERSION:~0,1% -IF "%PYTHON_VERSION:~3,1%" == "." ( - SET MINOR_PYTHON_VERSION=%PYTHON_VERSION:~2,1% -) ELSE ( - SET MINOR_PYTHON_VERSION=%PYTHON_VERSION:~2,2% -) - -:: Based on the Python version, determine what SDK version to use, and whether -:: to set the SDK for 64-bit. -IF %MAJOR_PYTHON_VERSION% == 2 ( - SET WINDOWS_SDK_VERSION="v7.0" - SET SET_SDK_64=Y -) ELSE ( - IF %MAJOR_PYTHON_VERSION% == 3 ( - SET WINDOWS_SDK_VERSION="v7.1" - IF %MINOR_PYTHON_VERSION% LEQ 4 ( - SET SET_SDK_64=Y - ) ELSE ( - SET SET_SDK_64=N - IF EXIST "%WIN_WDK%" ( - :: See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/ - REN "%WIN_WDK%" 0wdf - ) - ) - ) ELSE ( - ECHO Unsupported Python version: "%MAJOR_PYTHON_VERSION%" - EXIT 1 - ) -) - -IF %PYTHON_ARCH% == 64 ( - IF %SET_SDK_64% == Y ( - ECHO Configuring Windows SDK %WINDOWS_SDK_VERSION% for Python %MAJOR_PYTHON_VERSION% on a 64 bit architecture - SET DISTUTILS_USE_SDK=1 - SET MSSdk=1 - "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% - "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release - ECHO Executing: %COMMAND_TO_RUN% - call %COMMAND_TO_RUN% || EXIT 1 - ) ELSE ( - ECHO Using default MSVC build environment for 64 bit architecture - ECHO Executing: %COMMAND_TO_RUN% - call %COMMAND_TO_RUN% || EXIT 1 - ) -) ELSE ( - ECHO Using default MSVC build environment for 32 bit architecture - ECHO Executing: %COMMAND_TO_RUN% - call %COMMAND_TO_RUN% || EXIT 1 -) diff --git a/conf.py b/conf.py index 9d912056..694446cf 100644 --- a/conf.py +++ b/conf.py @@ -36,15 +36,15 @@ # General substitutions. project = 'simplejson' -copyright = '2020, Bob Ippolito' +copyright = '2021, Bob Ippolito' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # # The short X.Y version. -version = '3.17' +version = '3.18' # The full version, including alpha/beta/rc tags. -release = '3.17.2' +release = '3.18.0' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: diff --git a/scripts/release.py b/scripts/release.py deleted file mode 100644 index 34e23ffe..00000000 --- a/scripts/release.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -from urllib.request import urlopen - -import json -import os -import subprocess -import sys -import getpass - - -def get_json(url): - return json.loads(urlopen(url).read().decode('utf-8')) - - -def download_file(src_url, dest_path): - print(dest_path) - subprocess.call( - ['curl', '-L', '-#', '-o', dest_path, src_url]) - - -def download_appveyor_artifacts(): - api_url = 'https://ci.appveyor.com/api' - builds = get_json( - '{}/projects/etrepum/simplejson'.format(api_url)) - - for job in builds['build']['jobs']: - url = '{api_url}/buildjobs/{jobId}/artifacts'.format( - api_url=api_url, **job) - for artifact in get_json(url): - download_file( - '{url}/{fileName}'.format(url=url, **artifact), - artifact['fileName']) - - -def download_github_artifacts(): - release = get_json( - 'https://api.github.com/repos/simplejson/simplejson/releases/latest') - for asset in release['assets']: - download_file(asset['browser_download_url'], 'dist/{name}'.format(**asset)) - - -def get_version(): - return subprocess.check_output( - [sys.executable, 'setup.py', '--version'], - encoding='utf8' - ).strip() - - -def artifact_matcher(version): - prefix = 'simplejson-{}'.format(version) - def matches(fn): - return ( - fn.startswith(prefix) and - fn.endswith('.whl') and - not fn.endswith('-none-any.whl') - ) or fn == '{}.tar.gz'.format(prefix) - return matches - - -def upload_artifacts(version): - artifacts = set(os.listdir('dist')) - matches = artifact_matcher(version) - args = ['twine', 'upload'] - for fn in artifacts: - if matches(fn): - args.append(os.path.join('dist', fn)) - subprocess.check_call(args) - - -def main(): - try: - os.makedirs('dist') - except OSError: - pass - download_appveyor_artifacts() - download_github_artifacts() - upload_artifacts(get_version()) - - -if __name__ == '__main__': - main() diff --git a/setup.py b/setup.py index 3252f2f1..336f4433 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ DistutilsPlatformError IS_PYPY = hasattr(sys, 'pypy_translation_info') -VERSION = '3.17.2' +VERSION = '3.18.0' DESCRIPTION = "Simple, fast, extensible JSON encoder/decoder for Python" with open('README.rst', 'r') as f: @@ -36,6 +36,9 @@ 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries :: Python Modules', @@ -116,7 +119,7 @@ def run_setup(with_binary): try: run_setup(not IS_PYPY) except BuildFailed: - if os.environ.get('REQUIRE_SPEEDUPS'): + if os.environ.get('REQUIRE_SPEEDUPS') or os.environ.get('CIBUILDWHEEL', '0') == '1': raise BUILD_EXT_WARNING = ("WARNING: The C extension could not be compiled, " "speedups are not enabled.") diff --git a/simplejson/__init__.py b/simplejson/__init__.py index 2e3b838e..c79ad1bc 100644 --- a/simplejson/__init__.py +++ b/simplejson/__init__.py @@ -118,7 +118,7 @@ """ from __future__ import absolute_import -__version__ = '3.17.2' +__version__ = '3.18.0' __all__ = [ 'dump', 'dumps', 'load', 'loads', 'JSONDecoder', 'JSONDecodeError', 'JSONEncoder', @@ -300,7 +300,7 @@ def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, iterable_as_array=False, **kw): """Serialize ``obj`` to a JSON formatted ``str``. - If ``skipkeys`` is false then ``dict`` keys that are not basic types + If ``skipkeys`` is true then ``dict`` keys that are not basic types (``str``, ``int``, ``long``, ``float``, ``bool``, ``None``) will be skipped instead of raising a ``TypeError``. @@ -360,7 +360,7 @@ def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, If specified, *item_sort_key* is a callable used to sort the items in each dictionary. This is useful if you want to sort items other than - in alphabetical order by key. This option takes precendence over + in alphabetical order by key. This option takes precedence over *sort_keys*. If *sort_keys* is true (default: ``False``), the output of dictionaries diff --git a/simplejson/_speedups.c b/simplejson/_speedups.c index e7101288..ec054c72 100644 --- a/simplejson/_speedups.c +++ b/simplejson/_speedups.c @@ -25,6 +25,15 @@ #define JSON_InternFromString PyString_InternFromString #endif /* PY_MAJOR_VERSION < 3 */ +#if PY_VERSION_HEX < 0x03090000 +#if !defined(PyObject_CallNoArgs) +#define PyObject_CallNoArgs(callable) PyObject_CallFunctionObjArgs(callable, NULL); +#endif +#if !defined(PyObject_CallOneArg) +#define PyObject_CallOneArg(callable, arg) PyObject_CallFunctionObjArgs(callable, arg, NULL); +#endif +#endif /* PY_VERSION_HEX < 0x03090000 */ + #if PY_VERSION_HEX < 0x02070000 #if !defined(PyOS_string_to_double) #define PyOS_string_to_double json_PyOS_string_to_double @@ -108,6 +117,9 @@ JSON_Accu_Destroy(JSON_Accu *acc); #define ERR_STRING_CONTROL "Invalid control character %r at" #define ERR_STRING_ESC1 "Invalid \\X escape sequence %r" #define ERR_STRING_ESC4 "Invalid \\uXXXX escape sequence" +#define FOR_JSON_METHOD_NAME "for_json" +#define ASDICT_METHOD_NAME "_asdict" + typedef struct _PyScannerObject { PyObject_HEAD @@ -243,12 +255,10 @@ static int _convertPyInt_AsSsize_t(PyObject *o, Py_ssize_t *size_ptr); static PyObject * _convertPyInt_FromSsize_t(Py_ssize_t *size_ptr); +static int +_call_json_method(PyObject *obj, const char *method_name, PyObject **result); static PyObject * encoder_encode_float(PyEncoderObject *s, PyObject *obj); -static int -_is_namedtuple(PyObject *obj); -static int -_has_for_json_hook(PyObject *obj); static PyObject * moduleinit(void); @@ -383,30 +393,26 @@ maybe_quote_bigint(PyEncoderObject* s, PyObject *encoded, PyObject *obj) } static int -_is_namedtuple(PyObject *obj) +_call_json_method(PyObject *obj, const char *method_name, PyObject **result) { int rval = 0; - PyObject *_asdict = PyObject_GetAttrString(obj, "_asdict"); - if (_asdict == NULL) { + PyObject *method = PyObject_GetAttrString(obj, method_name); + if (method == NULL) { PyErr_Clear(); return 0; } - rval = PyCallable_Check(_asdict); - Py_DECREF(_asdict); - return rval; -} - -static int -_has_for_json_hook(PyObject *obj) -{ - int rval = 0; - PyObject *for_json = PyObject_GetAttrString(obj, "for_json"); - if (for_json == NULL) { - PyErr_Clear(); - return 0; + if (PyCallable_Check(method)) { + PyObject *tmp = PyObject_CallNoArgs(method); + if (tmp == NULL && PyErr_ExceptionMatches(PyExc_TypeError)) { + PyErr_Clear(); + } else { + // This will set result to NULL if a TypeError occurred, + // which must be checked by the caller + *result = tmp; + rval = 1; + } } - rval = PyCallable_Check(for_json); - Py_DECREF(for_json); + Py_DECREF(method); return rval; } @@ -638,7 +644,7 @@ encoder_stringify_key(PyEncoderObject *s, PyObject *key) if (!(PyInt_CheckExact(key) || PyLong_CheckExact(key))) { /* See #118, do not trust custom str/repr */ PyObject *res; - PyObject *tmp = PyObject_CallFunctionObjArgs((PyObject *)&PyLong_Type, key, NULL); + PyObject *tmp = PyObject_CallOneArg((PyObject *)&PyLong_Type, key); if (tmp == NULL) { return NULL; } @@ -792,7 +798,7 @@ join_list_string(PyObject *lst) if (joinfn == NULL) return NULL; } - return PyObject_CallFunctionObjArgs(joinfn, lst, NULL); + return PyObject_CallOneArg(joinfn, lst); } #endif /* PY_MAJOR_VERSION < 3 */ @@ -1484,7 +1490,7 @@ _parse_object_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_ /* if pairs_hook is not None: rval = object_pairs_hook(pairs) */ if (s->pairs_hook != Py_None) { - val = PyObject_CallFunctionObjArgs(s->pairs_hook, pairs, NULL); + val = PyObject_CallOneArg(s->pairs_hook, pairs); if (val == NULL) goto bail; Py_DECREF(pairs); @@ -1494,7 +1500,7 @@ _parse_object_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_ /* if object_hook is not None: rval = object_hook(rval) */ if (s->object_hook != Py_None) { - val = PyObject_CallFunctionObjArgs(s->object_hook, rval, NULL); + val = PyObject_CallOneArg(s->object_hook, rval); if (val == NULL) goto bail; Py_DECREF(rval); @@ -1648,7 +1654,7 @@ _parse_object_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ss /* if pairs_hook is not None: rval = object_pairs_hook(pairs) */ if (s->pairs_hook != Py_None) { - val = PyObject_CallFunctionObjArgs(s->pairs_hook, pairs, NULL); + val = PyObject_CallOneArg(s->pairs_hook, pairs); if (val == NULL) goto bail; Py_DECREF(pairs); @@ -1658,7 +1664,7 @@ _parse_object_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ss /* if object_hook is not None: rval = object_hook(rval) */ if (s->object_hook != Py_None) { - val = PyObject_CallFunctionObjArgs(s->object_hook, rval, NULL); + val = PyObject_CallOneArg(s->object_hook, rval); if (val == NULL) goto bail; Py_DECREF(rval); @@ -1851,7 +1857,7 @@ _parse_constant(PyScannerObject *s, PyObject *constant, Py_ssize_t idx, Py_ssize PyObject *rval; /* rval = parse_constant(constant) */ - rval = PyObject_CallFunctionObjArgs(s->parse_constant, constant, NULL); + rval = PyObject_CallOneArg(s->parse_constant, constant); idx += PyString_GET_SIZE(constant); *next_idx_ptr = idx; return rval; @@ -1937,7 +1943,7 @@ _match_number_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t start, Py_ssiz if (is_float) { /* parse as a float using a fast path if available, otherwise call user defined method */ if (s->parse_float != (PyObject *)&PyFloat_Type) { - rval = PyObject_CallFunctionObjArgs(s->parse_float, numstr, NULL); + rval = PyObject_CallOneArg(s->parse_float, numstr); } else { /* rval = PyFloat_FromDouble(PyOS_ascii_atof(PyString_AS_STRING(numstr))); */ @@ -1951,7 +1957,7 @@ _match_number_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t start, Py_ssiz else { /* parse as an int using a fast path if available, otherwise call user defined method */ if (s->parse_int != (PyObject *)&PyInt_Type) { - rval = PyObject_CallFunctionObjArgs(s->parse_int, numstr, NULL); + rval = PyObject_CallOneArg(s->parse_int, numstr); } else { rval = PyInt_FromString(PyString_AS_STRING(numstr), NULL, 10); @@ -2055,7 +2061,7 @@ _match_number_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t start, Py_ if (is_float) { /* parse as a float using a fast path if available, otherwise call user defined method */ if (s->parse_float != (PyObject *)&PyFloat_Type) { - rval = PyObject_CallFunctionObjArgs(s->parse_float, numstr, NULL); + rval = PyObject_CallOneArg(s->parse_float, numstr); } else { #if PY_MAJOR_VERSION >= 3 @@ -2067,7 +2073,7 @@ _match_number_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t start, Py_ } else { /* no fast path for unicode -> int, just call */ - rval = PyObject_CallFunctionObjArgs(s->parse_int, numstr, NULL); + rval = PyObject_CallOneArg(s->parse_int, numstr); } Py_DECREF(numstr); *next_idx_ptr = idx; @@ -2746,7 +2752,7 @@ encoder_encode_float(PyEncoderObject *s, PyObject *obj) else { /* See #118, do not trust custom str/repr */ PyObject *res; - PyObject *tmp = PyObject_CallFunctionObjArgs((PyObject *)&PyFloat_Type, obj, NULL); + PyObject *tmp = PyObject_CallOneArg((PyObject *)&PyFloat_Type, obj); if (tmp == NULL) { return NULL; } @@ -2765,7 +2771,7 @@ encoder_encode_string(PyEncoderObject *s, PyObject *obj) if (s->fast_encode) { return py_encode_basestring_ascii(NULL, obj); } - encoded = PyObject_CallFunctionObjArgs(s->encoder, obj, NULL); + encoded = PyObject_CallOneArg(s->encoder, obj); if (encoded != NULL && #if PY_MAJOR_VERSION < 3 !PyString_Check(encoded) && @@ -2796,6 +2802,7 @@ encoder_listencode_obj(PyEncoderObject *s, JSON_Accu *rval, PyObject *obj, Py_ss /* Encode Python object obj to a JSON term, rval is a PyList */ int rv = -1; do { + PyObject *newobj; if (obj == Py_None || obj == Py_True || obj == Py_False) { PyObject *cstr = _encoded_const(obj); if (cstr != NULL) @@ -2815,7 +2822,7 @@ encoder_listencode_obj(PyEncoderObject *s, JSON_Accu *rval, PyObject *obj, Py_ss } else { /* See #118, do not trust custom str/repr */ - PyObject *tmp = PyObject_CallFunctionObjArgs((PyObject *)&PyLong_Type, obj, NULL); + PyObject *tmp = PyObject_CallOneArg((PyObject *)&PyLong_Type, obj); if (tmp == NULL) { encoded = NULL; } @@ -2836,26 +2843,37 @@ encoder_listencode_obj(PyEncoderObject *s, JSON_Accu *rval, PyObject *obj, Py_ss if (encoded != NULL) rv = _steal_accumulate(rval, encoded); } - else if (s->for_json && _has_for_json_hook(obj)) { - PyObject *newobj; - if (Py_EnterRecursiveCall(" while encoding a JSON object")) - return rv; - newobj = PyObject_CallMethod(obj, "for_json", NULL); - if (newobj != NULL) { - rv = encoder_listencode_obj(s, rval, newobj, indent_level); + else if (s->for_json && _call_json_method(obj, FOR_JSON_METHOD_NAME, &newobj)) { + if (newobj == NULL) { + return -1; + } + if (Py_EnterRecursiveCall(" while encoding a JSON object")) { Py_DECREF(newobj); + return rv; } + rv = encoder_listencode_obj(s, rval, newobj, indent_level); + Py_DECREF(newobj); Py_LeaveRecursiveCall(); } - else if (s->namedtuple_as_object && _is_namedtuple(obj)) { - PyObject *newobj; - if (Py_EnterRecursiveCall(" while encoding a JSON object")) + else if (s->namedtuple_as_object && _call_json_method(obj, ASDICT_METHOD_NAME, &newobj)) { + if (newobj == NULL) { + return -1; + } + if (Py_EnterRecursiveCall(" while encoding a JSON object")) { + Py_DECREF(newobj); return rv; - newobj = PyObject_CallMethod(obj, "_asdict", NULL); - if (newobj != NULL) { + } + if (PyDict_Check(newobj)) { rv = encoder_listencode_dict(s, rval, newobj, indent_level); - Py_DECREF(newobj); + } else { + PyErr_Format( + PyExc_TypeError, + "_asdict() must return a dict, not %.80s", + Py_TYPE(newobj)->tp_name + ); + rv = -1; } + Py_DECREF(newobj); Py_LeaveRecursiveCall(); } else if (PyList_Check(obj) || (s->tuple_as_array && PyTuple_Check(obj))) { @@ -2913,7 +2931,7 @@ encoder_listencode_obj(PyEncoderObject *s, JSON_Accu *rval, PyObject *obj, Py_ss } if (Py_EnterRecursiveCall(" while encoding a JSON object")) return rv; - newobj = PyObject_CallFunctionObjArgs(s->defaultfn, obj, NULL); + newobj = PyObject_CallOneArg(s->defaultfn, obj); if (newobj == NULL) { Py_XDECREF(ident); Py_LeaveRecursiveCall(); diff --git a/simplejson/decoder.py b/simplejson/decoder.py index 7f0b0568..1a8f772f 100644 --- a/simplejson/decoder.py +++ b/simplejson/decoder.py @@ -109,6 +109,8 @@ def py_scanstring(s, end, encoding=None, strict=True, uni = int(esc, 16) except ValueError: raise JSONDecodeError(msg, s, end - 1) + if uni < 0 or uni > _maxunicode: + raise JSONDecodeError(msg, s, end - 1) end += 5 # Check for surrogate pair on UCS-4 systems # Note that this will join high/low surrogate pairs diff --git a/simplejson/encoder.py b/simplejson/encoder.py index 7ea172e7..2bc8bbad 100644 --- a/simplejson/encoder.py +++ b/simplejson/encoder.py @@ -450,6 +450,15 @@ def _make_iterencode(markers, _default, _encoder, _indent, _floatstr, not isinstance(_int_as_string_bitcount, integer_types))): raise TypeError("int_as_string_bitcount must be a positive integer") + def call_method(obj, method_name): + method = getattr(obj, method_name, None) + if callable(method): + try: + return (method(),) + except TypeError: + pass + return None + def _encode_int(value): skip_quoting = ( _int_as_string_bitcount is None @@ -512,15 +521,18 @@ def _iterencode_list(lst, _current_indent_level): yield buf + str(value) else: yield buf - for_json = _for_json and getattr(value, 'for_json', None) - if for_json and callable(for_json): - chunks = _iterencode(for_json(), _current_indent_level) + for_json = _for_json and call_method(value, 'for_json') + if for_json: + chunks = _iterencode(for_json[0], _current_indent_level) elif isinstance(value, list): chunks = _iterencode_list(value, _current_indent_level) else: - _asdict = _namedtuple_as_object and getattr(value, '_asdict', None) - if _asdict and callable(_asdict): - chunks = _iterencode_dict(_asdict(), + _asdict = _namedtuple_as_object and call_method(value, '_asdict') + if _asdict: + dct = _asdict[0] + if not isinstance(dct, dict): + raise TypeError("_asdict() must return a dict, not %s" % (type(dct).__name__,)) + chunks = _iterencode_dict(dct, _current_indent_level) elif _tuple_as_array and isinstance(value, tuple): chunks = _iterencode_list(value, _current_indent_level) @@ -633,15 +645,18 @@ def _iterencode_dict(dct, _current_indent_level): elif _use_decimal and isinstance(value, Decimal): yield str(value) else: - for_json = _for_json and getattr(value, 'for_json', None) - if for_json and callable(for_json): - chunks = _iterencode(for_json(), _current_indent_level) + for_json = _for_json and call_method(value, 'for_json') + if for_json: + chunks = _iterencode(for_json[0], _current_indent_level) elif isinstance(value, list): chunks = _iterencode_list(value, _current_indent_level) else: - _asdict = _namedtuple_as_object and getattr(value, '_asdict', None) - if _asdict and callable(_asdict): - chunks = _iterencode_dict(_asdict(), + _asdict = _namedtuple_as_object and call_method(value, '_asdict') + if _asdict: + dct = _asdict[0] + if not isinstance(dct, dict): + raise TypeError("_asdict() must return a dict, not %s" % (type(dct).__name__,)) + chunks = _iterencode_dict(dct, _current_indent_level) elif _tuple_as_array and isinstance(value, tuple): chunks = _iterencode_list(value, _current_indent_level) @@ -676,18 +691,20 @@ def _iterencode(o, _current_indent_level): elif isinstance(o, float): yield _floatstr(o) else: - for_json = _for_json and getattr(o, 'for_json', None) - if for_json and callable(for_json): - for chunk in _iterencode(for_json(), _current_indent_level): + for_json = _for_json and call_method(o, 'for_json') + if for_json: + for chunk in _iterencode(for_json[0], _current_indent_level): yield chunk elif isinstance(o, list): for chunk in _iterencode_list(o, _current_indent_level): yield chunk else: - _asdict = _namedtuple_as_object and getattr(o, '_asdict', None) - if _asdict and callable(_asdict): - for chunk in _iterencode_dict(_asdict(), - _current_indent_level): + _asdict = _namedtuple_as_object and call_method(o, '_asdict') + if _asdict: + dct = _asdict[0] + if not isinstance(dct, dict): + raise TypeError("_asdict() must return a dict, not %s" % (type(dct).__name__,)) + for chunk in _iterencode_dict(dct, _current_indent_level): yield chunk elif (_tuple_as_array and isinstance(o, tuple)): for chunk in _iterencode_list(o, _current_indent_level): diff --git a/simplejson/tests/__init__.py b/simplejson/tests/__init__.py index 25d3305b..79d1d170 100644 --- a/simplejson/tests/__init__.py +++ b/simplejson/tests/__init__.py @@ -7,6 +7,7 @@ class NoExtensionTestSuite(unittest.TestSuite): def run(self, result): import simplejson + simplejson._toggle_speedups(False) result = unittest.TestSuite.run(self, result) simplejson._toggle_speedups(True) @@ -15,16 +16,17 @@ def run(self, result): class TestMissingSpeedups(unittest.TestCase): def runTest(self): - if hasattr(sys, 'pypy_translation_info'): + if hasattr(sys, "pypy_translation_info"): "PyPy doesn't need speedups! :)" - elif hasattr(self, 'skipTest'): - self.skipTest('_speedups.so is missing!') + elif hasattr(self, "skipTest"): + self.skipTest("_speedups.so is missing!") -def additional_tests(suite=None): +def additional_tests(suite=None, project_dir=None): import simplejson import simplejson.encoder import simplejson.decoder + if suite is None: suite = unittest.TestSuite() try: @@ -36,39 +38,54 @@ def additional_tests(suite=None): raise for mod in (simplejson, simplejson.encoder, simplejson.decoder): suite.addTest(doctest.DocTestSuite(mod)) - suite.addTest(doctest.DocFileSuite('../../index.rst')) + if project_dir is not None: + suite.addTest( + doctest.DocFileSuite( + os.path.join(project_dir, "index.rst"), module_relative=False + ) + ) return suite -def all_tests_suite(): +def all_tests_suite(project_dir=None): def get_suite(): suite_names = [ - 'simplejson.tests.%s' % (os.path.splitext(f)[0],) + "simplejson.tests.%s" % (os.path.splitext(f)[0],) for f in os.listdir(os.path.dirname(__file__)) - if f.startswith('test_') and f.endswith('.py') + if f.startswith("test_") and f.endswith(".py") ] return additional_tests( - unittest.TestLoader().loadTestsFromNames(suite_names)) + suite=unittest.TestLoader().loadTestsFromNames(suite_names), + project_dir=project_dir, + ) + suite = get_suite() import simplejson + if simplejson._import_c_make_encoder() is None: suite.addTest(TestMissingSpeedups()) else: - suite = unittest.TestSuite([ - suite, - NoExtensionTestSuite([get_suite()]), - ]) + suite = unittest.TestSuite( + [ + suite, + NoExtensionTestSuite([get_suite()]), + ] + ) return suite -def main(): - runner = unittest.TextTestRunner(verbosity=1 + sys.argv.count('-v')) - suite = all_tests_suite() +def main(project_dir=None): + runner = unittest.TextTestRunner(verbosity=1 + sys.argv.count("-v")) + suite = all_tests_suite(project_dir=project_dir) raise SystemExit(not runner.run(suite).wasSuccessful()) -if __name__ == '__main__': +if __name__ == "__main__": import os import sys - sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) - main() + + project_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + sys.path.insert(0, project_dir) + main(project_dir=project_dir) diff --git a/simplejson/tests/_cibw_runner.py b/simplejson/tests/_cibw_runner.py new file mode 100644 index 00000000..bffb70b8 --- /dev/null +++ b/simplejson/tests/_cibw_runner.py @@ -0,0 +1,7 @@ +"""Internal module for running tests from cibuildwheel""" + +import sys +import simplejson.tests + +if __name__ == '__main__': + simplejson.tests.main(project_dir=sys.argv[1]) diff --git a/simplejson/tests/test_namedtuple.py b/simplejson/tests/test_namedtuple.py index 43878940..cc0f8aa1 100644 --- a/simplejson/tests/test_namedtuple.py +++ b/simplejson/tests/test_namedtuple.py @@ -3,6 +3,11 @@ import simplejson as json from simplejson.compat import StringIO +try: + from unittest import mock +except ImportError: + mock = None + try: from collections import namedtuple except ImportError: @@ -105,18 +110,65 @@ def test_namedtuple_dump_false(self): def test_asdict_not_callable_dump(self): for f in CONSTRUCTORS: - self.assertRaises(TypeError, - json.dump, f(DeadDuck()), StringIO(), namedtuple_as_object=True) + self.assertRaises( + TypeError, + json.dump, + f(DeadDuck()), + StringIO(), + namedtuple_as_object=True + ) sio = StringIO() json.dump(f(DeadDict()), sio, namedtuple_as_object=True) self.assertEqual( json.dumps(f({})), sio.getvalue()) + self.assertRaises( + TypeError, + json.dump, + f(Value), + StringIO(), + namedtuple_as_object=True + ) def test_asdict_not_callable_dumps(self): for f in CONSTRUCTORS: self.assertRaises(TypeError, json.dumps, f(DeadDuck()), namedtuple_as_object=True) + self.assertRaises( + TypeError, + json.dumps, + f(Value), + namedtuple_as_object=True + ) self.assertEqual( json.dumps(f({})), json.dumps(f(DeadDict()), namedtuple_as_object=True)) + + def test_asdict_unbound_method_dumps(self): + for f in CONSTRUCTORS: + self.assertEqual( + json.dumps(f(Value), default=lambda v: v.__name__), + json.dumps(f(Value.__name__)) + ) + + def test_asdict_does_not_return_dict(self): + if not mock: + if hasattr(unittest, "SkipTest"): + raise unittest.SkipTest("unittest.mock required") + else: + print("unittest.mock not available") + return + fake = mock.Mock() + self.assertTrue(hasattr(fake, '_asdict')) + self.assertTrue(callable(fake._asdict)) + self.assertFalse(isinstance(fake._asdict(), dict)) + # https://github.com/simplejson/simplejson/pull/284 + # when running under a debug build of CPython (COPTS=-UNDEBUG) + # a C assertion could fire due to an unchecked error of an PyDict + # API call on a non-dict internally in _speedups.c. Without a debug + # build of CPython this test likely passes either way despite the + # potential for internal data corruption. Getting it to crash in + # a debug build is not always easy either as it requires an + # assert(!PyErr_Occurred()) that could fire later on. + with self.assertRaises(TypeError): + json.dumps({23: fake}, namedtuple_as_object=True, for_json=False) diff --git a/simplejson/tests/test_scanstring.py b/simplejson/tests/test_scanstring.py index d5de1801..c6c53b81 100644 --- a/simplejson/tests/test_scanstring.py +++ b/simplejson/tests/test_scanstring.py @@ -132,6 +132,8 @@ def _test_scanstring(self, scanstring): self.assertRaises(ValueError, scanstring, '\\ud834\\x0123"', 0, None, True) + self.assertRaises(json.JSONDecodeError, scanstring, "\\u-123", 0, None, True) + def test_issue3623(self): self.assertRaises(ValueError, json.decoder.scanstring, "xxx", 1, "xxx")