diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..71c30feb0 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,26 @@ +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + target-branch: "master" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" + rebase-strategy: auto + groups: + github-actions: + patterns: + - "*" + + - package-ecosystem: "github-actions" + target-branch: "lxml-6.1" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" + rebase-strategy: auto + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml new file mode 100644 index 000000000..bb47551f1 --- /dev/null +++ b/.github/workflows/benchmarks.yml @@ -0,0 +1,115 @@ +name: Benchmarks + +on: + push: + paths: + - 'src/**' + - 'benchmark/**' + - '.github/workflows/benchmarks.yml' + - '.github/workflows/cache_libs.yml' + - setup*.py + - build*.py + pull_request: + paths: + - 'src/**' + - 'benchmark/**' + - '.github/workflows/benchmarks.yml' + - '.github/workflows/cache_libs.yml' + - setup*.py + - build*.py + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + + +jobs: + cache_libs: + uses: ./.github/workflows/cache_libs.yml + secrets: inherit + + benchmarks: + needs: [ cache_libs ] + runs-on: ubuntu-latest + + # The benchmarks are currently inconclusive and take a very long time to run. + #if: false + + env: + CFLAGS: -march=core2 -O3 -flto -fPIC -g -Wall -Wextra + CCACHE_SLOPPINESS: "pch_defines,time_macros" + CCACHE_COMPRESS: 1 + CCACHE_COMPRESSLEVEL: 5 + STATIC_DEPS: true + LXML_CSTD: c11 + ZLIB_VERSION: ${{ needs.cache_libs.outputs.ZLIB_VERSION }} + LIBICONV_VERSION: ${{ needs.cache_libs.outputs.LIBICONV_VERSION }} + LIBXML2_VERSION: ${{ needs.cache_libs.outputs.LIBXML2_VERSION }} + LIBXSLT_VERSION: ${{ needs.cache_libs.outputs.LIBXSLT_VERSION }} + + steps: + - name: Checkout repo + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + fetch-tags: true + + - name: ccache + uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 # v1.2.23 + if: runner.os == 'Linux' || runner.os == 'macOS' + with: + max-size: 150M + create-symlink: true + key: ${{ runner.os }}-benchmarks-${{ env.LIBXML2_VERSION }}-${{ env.LIBXSLT_VERSION }} + + - name: Cache [libs] + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + if: env.STATIC_DEPS + with: + path: | + libs/*.xz + libs/*.gz + libs/*.zip + key: libs-${{ runner.os }}-${{ runner.arch }}-${{ env.LIBXML2_VERSION }}-${{ env.LIBXSLT_VERSION }} + + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: | + 3.15t-dev + 3.15-dev + 3.12 + + - name: Run Benchmarks + run: | + # Run benchmarks in all Python versions. + for PYTHON in python3.15 python3.15t python3.12 ; do + ${PYTHON} -m pip install setuptools "Cython>=3.2.2" + # Compare against arbitrary 6.0-pre baseline revision (compatible with Cython 3.1) and current master. + ${PYTHON} benchmark/run_benchmarks.py --report benchmark_results_${PYTHON}.csv \ + "0eb4f0029497957e58a9f15280b3529bdb18d117" "origin/master" "HEAD" + done | tee benchmarks.log + + - name: Create summary + run: | + { echo "## Benchmark results:"; + echo; python benchmark/report.py -t timings benchmark_results_*.csv; + } >> $GITHUB_STEP_SUMMARY + + - name: Upload Log + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: benchmark_log.txt + path: benchmarks.log + if-no-files-found: ignore + + - name: Upload timings + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: benchmark_results_csv + path: benchmark_results_*.csv + if-no-files-found: ignore diff --git a/.github/workflows/cache_libs.yml b/.github/workflows/cache_libs.yml new file mode 100644 index 000000000..80c30d4a8 --- /dev/null +++ b/.github/workflows/cache_libs.yml @@ -0,0 +1,103 @@ +name: Cache library dependencies + +on: + workflow_call: + inputs: + ZLIB_VERSION: + default: "1.3.2" + required: false + type: string + LIBICONV_VERSION: + default: "1.18" + required: false + type: string + LIBXML2_VERSION: + default: "2.14.6" + required: false + type: string + LIBXSLT_VERSION: + default: "1.1.43" + required: false + type: string + WIN_ZLIB_VERSION: + default: "1.3.2" + required: false + type: string + WIN_LIBICONV_VERSION: + default: "1.17.1" + required: false + type: string + WIN_LIBXML2_VERSION: + default: "2.11.9" + required: false + type: string + WIN_LIBXSLT_VERSION: + default: "1.1.45" + required: false + type: string + + outputs: + ZLIB_VERSION: + value: ${{ inputs.ZLIB_VERSION }} + LIBICONV_VERSION: + value: ${{ inputs.LIBICONV_VERSION }} + LIBXML2_VERSION: + value: ${{ inputs.LIBXML2_VERSION }} + LIBXSLT_VERSION: + value: ${{ inputs.LIBXSLT_VERSION }} + WIN_ZLIB_VERSION: + value: ${{ inputs.WIN_ZLIB_VERSION }} + WIN_LIBICONV_VERSION: + value: ${{ inputs.WIN_LIBICONV_VERSION }} + WIN_LIBXML2_VERSION: + value: ${{ inputs.WIN_LIBXML2_VERSION }} + WIN_LIBXSLT_VERSION: + value: ${{ inputs.WIN_LIBXSLT_VERSION }} + + +jobs: + cache_libs: + strategy: + fail-fast: false + matrix: + os: + - "ubuntu-22.04" + - "ubuntu-22.04-arm" + - "macos-latest" + - "windows-2022" # win32 + - "windows-latest" # win_amd64 + - "windows-11-arm" + + runs-on: ${{ matrix.os }} + + env: + ZLIB_VERSION: ${{ contains(matrix.os, 'windows-') && inputs.WIN_ZLIB_VERSION || inputs.ZLIB_VERSION }} + LIBICONV_VERSION: ${{ contains(matrix.os, 'windows-') && inputs.WIN_LIBICONV_VERSION || inputs.LIBICONV_VERSION }} + LIBXML2_VERSION: ${{ contains(matrix.os, 'windows-') && inputs.WIN_LIBXML2_VERSION || inputs.LIBXML2_VERSION }} + LIBXSLT_VERSION: ${{ contains(matrix.os, 'windows-') && inputs.WIN_LIBXSLT_VERSION || inputs.LIBXSLT_VERSION }} + + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Cache [libs] + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + libs/*.xz + libs/*.gz + libs/*.zip + key: libs-${{ runner.os }}-${{ matrix.os == 'windows-2022' && 'i686' || runner.arch }}-${{ env.LIBXML2_VERSION }}-${{ env.LIBXSLT_VERSION }}-${{ env.LIBICONV_VERSION }}-${{ env.ZLIB_VERSION }} + + - name: Download latest libraries + env: + GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: python3 buildlibxml.py --download-only ${{ matrix.os == 'windows-2022' && 'win32' || '' }} + + - name: Check Windows library versions + if: ${{ contains(matrix.os, 'windows-') }} + run: | + bash -c ' + for file in libs/zlib-${{ inputs.WIN_ZLIB_VERSION }}.*.zip libs/iconv-${{ inputs.WIN_LIBICONV_VERSION }}.*.zip libs/libxml2-${{ inputs.WIN_LIBXML2_VERSION }}.*.zip libs/libxslt-${{ inputs.WIN_LIBXSLT_VERSION }}.*.zip; do + [[ -f "$file" ]] || { echo "MISSING: $file" ; exit 1; } + done + ' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51d77a4e4..803b9f54e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,56 @@ name: CI -on: [push, pull_request] +on: + push: + paths: + - '**' + - '!.github/**' + - '!benchmark/**' + - '.github/workflows/ci.yml' + - '.github/workflows/cache_libs.yml' + - '.github/workflows/compiled_python.yml' + pull_request: + paths: + - '**' + - '!.github/**' + - '!benchmark/**' + - '.github/workflows/ci.yml' + - '.github/workflows/cache_libs.yml' + - '.github/workflows/compiled_python.yml' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + jobs: + cache_libs: + uses: ./.github/workflows/cache_libs.yml + secrets: inherit + ci: + needs: [ cache_libs ] + + env: + OS_NAME: ${{ matrix.os }} + PYTHON_VERSION: ${{ matrix.python-version }} + MACOSX_DEPLOYMENT_TARGET: "11.0" + COVERAGE: false + GCC_VERSION: 9 + USE_CCACHE: 1 + CCACHE_SLOPPINESS: "pch_defines,time_macros" + CCACHE_COMPRESS: 1 + CCACHE_MAXSIZE: "100M" + LXML_CSTD: c11 + ZLIB_VERSION: ${{ startsWith(matrix.os, 'windows-') && needs.cache_libs.outputs.WIN_ZLIB_VERSION || needs.cache_libs.outputs.ZLIB_VERSION }} + LIBICONV_VERSION: ${{ startsWith(matrix.os, 'windows-') && needs.cache_libs.outputs.WIN_LIBICONV_VERSION || needs.cache_libs.outputs.LIBICONV_VERSION }} + LIBXML2_VERSION: ${{ startsWith(matrix.os, 'windows-') && needs.cache_libs.outputs.WIN_LIBXML2_VERSION || needs.cache_libs.outputs.LIBXML2_VERSION }} + LIBXSLT_VERSION: ${{ startsWith(matrix.os, 'windows-') && needs.cache_libs.outputs.WIN_LIBXSLT_VERSION || needs.cache_libs.outputs.LIBXSLT_VERSION }} + strategy: # Allows for matrix sub-jobs to fail without canceling the rest fail-fast: false @@ -21,132 +68,253 @@ jobs: matrix: # Tests [amd64] # - os: [ubuntu-18.04, macos-10.15] + os: + - "ubuntu-22.04" + - "ubuntu-22.04-arm" + - "macos-latest" + - "windows-2022" + - "windows-11-arm" python-version: - - 2.7 - - 3.5 - - 3.6 - - 3.7 - - 3.8 - - 3.9 - - "3.10" # quotes to avoid being interpreted as the number 3.1 - - "3.11-dev" - # - "3.12-dev" - env: [{ STATIC_DEPS: true }, { STATIC_DEPS: false }] + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.13t" + - "3.14" + - "3.14t" + - "3.15-dev" + - "3.15t-dev" + env: + - { STATIC_DEPS: true } + - { STATIC_DEPS: false } include: - # Temporary - Allow failure on all 3.11-dev jobs until beta comes out. - - os: ubuntu-18.04 - python-version: 3.11-dev - allowed_failure: true - - os: ubuntu-18.04 - python-version: 3.11-dev + #- os: ubuntu-22.04 + # python-version: "3.15-dev" + # allowed_failure: true + + - os: ubuntu-latest + python-version: "3.9" env: {STATIC_DEPS: true, WITH_REFNANNY: true} extra_hash: "-refnanny" - allowed_failure: true + - os: ubuntu-latest + python-version: "3.13" + env: {STATIC_DEPS: true, WITH_REFNANNY: true} + extra_hash: "-refnanny" + # Coverage setup - - os: ubuntu-18.04 - python-version: 3.9 - env: { COVERAGE: true } + - os: ubuntu-latest + python-version: "3.10" + env: { COVERAGE: true, STATIC_DEPS: true } extra_hash: "-coverage" - allowed_failure: true # shouldn't fail but currently does... - - os: ubuntu-18.04 - python-version: 3.9 - env: { STATIC_DEPS: false, EXTRA_DEPS: "docutils pygments sphinx sphinx-rtd-theme" } + - os: ubuntu-latest + python-version: "3.10" + env: { STATIC_DEPS: false, EXTRA_DEPS: "docutils pygments sphinx sphinx_book_theme" } extra_hash: "-docs" - allowed_failure: true # shouldn't fail but currently does... + # Old library setup with minimum version requirements - - os: ubuntu-18.04 - python-version: 3.9 + - os: ubuntu-latest + python-version: "3.12" env: { STATIC_DEPS: true, LIBXML2_VERSION: 2.9.2, LIBXSLT_VERSION: 1.1.27, } - extra_hash: "-oldlibs" - allowed_failure: true # shouldn't fail but currently does... + extra_hash: "-oldlibs29" + - os: ubuntu-latest + python-version: "3.12" + env: { + STATIC_DEPS: true, + LIBXML2_VERSION: 2.10.3, + LIBXSLT_VERSION: 1.1.37, + } + extra_hash: "-oldlibs210" + - os: ubuntu-latest + python-version: "3.12" + env: { + STATIC_DEPS: true, + LIBXML2_VERSION: 2.11.8, + LIBXSLT_VERSION: 1.1.37, + } + extra_hash: "-oldlibs211" + - os: ubuntu-latest + python-version: "3.12" + env: { + STATIC_DEPS: true, + LIBXML2_VERSION: 2.12.10, + LIBXSLT_VERSION: 1.1.37, + } + extra_hash: "-oldlibs212" + - os: ubuntu-latest + python-version: "3.12" + env: { + STATIC_DEPS: true, + LIBXML2_VERSION: 2.13.7, + LIBXSLT_VERSION: 1.1.40, + } + extra_hash: "-oldlibs213" + + - os: ubuntu-latest + python-version: "3.12" + #allowed_failure: true + env: { + STATIC_DEPS: true, + LIBXML2_VERSION: "", + LIBXSLT_VERSION: "", + } + extra_hash: "-latestlibs" + - os: ubuntu-latest + python-version: pypy-3.11 + #python-version: pypy-3.11 + allowed_failure: true + env: { + STATIC_DEPS: true, + LIBXML2_VERSION: "", + LIBXSLT_VERSION: "", + } + extra_hash: "-latestlibs" + + - os: ubuntu-latest + python-version: "3.12" + #allowed_failure: true + env: { + STATIC_DEPS: "true", + LIBXML2_VERSION: "", + LIBXSLT_VERSION: "", + WITHOUT_ZLIB: "true", + } + extra_hash: "-nozlib" + # Ubuntu sub-jobs: # ================ # Pypy - - os: ubuntu-18.04 - python-version: pypy-2.7 + - os: ubuntu-latest + python-version: pypy-3.9 + env: { STATIC_DEPS: false } + allowed_failure: true + - os: ubuntu-latest + python-version: pypy-3.10 env: { STATIC_DEPS: false } allowed_failure: true - - os: ubuntu-18.04 - python-version: pypy-3.7 + - os: ubuntu-latest + python-version: pypy-3.11 + #python-version: pypy-3.11 env: { STATIC_DEPS: false } allowed_failure: true # MacOS sub-jobs # ============== - - os: macos-10.15 - allowed_failure: true # Unicode parsing fails in Py3 + #- os: macos-latest + # allowed_failure: true # Unicode parsing fails in Py3 + + # Legacy jobs + # =========== + #- os: ubuntu-22.04 + # python-version: "3.7" + # env: { STATIC_DEPS: true } + #- os: ubuntu-22.04 + # python-version: "3.7" + # env: { STATIC_DEPS: false } + + exclude: + # Windows sub-jobs + # ============== + - os: windows-2022 + env: { STATIC_DEPS: false } # always static + - os: windows-11-arm + env: { STATIC_DEPS: false } # always static + - os: windows-11-arm + python-version: "3.9" # setup-python only supports 3.11+ on windows arm + - os: windows-11-arm + python-version: "3.10" # setup-python only supports 3.11+ on windows arm # This defaults to 360 minutes (6h) which is way too long and if a test gets stuck, it can block other pipelines. - # From testing, the runs tend to take ~3 minutes, so a limit of 20 minutes should be enough. This can always be - # changed in the future if needed. - timeout-minutes: 20 - runs-on: ${{ matrix.os }} + # From testing, the runs tend to take 8-20 minutes, so a limit of 45 minutes should be enough. + timeout-minutes: 45 - env: - OS_NAME: ${{ matrix.os }} - PYTHON_VERSION: ${{ matrix.python-version }} - MACOSX_DEPLOYMENT_TARGET: 10.15 - LIBXML2_VERSION: 2.9.14 - LIBXSLT_VERSION: 1.1.35 - COVERAGE: false - GCC_VERSION: 8 - USE_CCACHE: 1 - CCACHE_SLOPPINESS: "pch_defines,time_macros" - CCACHE_COMPRESS: 1 - CCACHE_MAXSIZE: "100M" + runs-on: ${{ matrix.os }} steps: - name: Checkout repo - uses: actions/checkout@v2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 1 - - name: Setup python - uses: actions/setup-python@v2 + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} - - name: Cache [ccache] - uses: pat-s/always-upload-cache@v2.1.3 - if: startsWith(runner.os, 'Linux') + - name: Install MacOS dependencies + if: runner.os == 'macOS' + run: | + brew install automake libtool ccache + ln -s /usr/local/bin/glibtoolize /usr/local/bin/libtoolize + + - name: ccache + uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 # v1.2.23 + if: runner.os == 'Linux' || runner.os == 'macOS' + with: + max-size: 100M + create-symlink: true + key: ${{ runner.os }}-${{ runner.arch }}-ccache${{ matrix.extra_hash }}-${{ matrix.python-version }}-${{ matrix.env.STATIC_DEPS }}-${{ matrix.env.LIBXML2_VERSION || env.LIBXML2_VERSION }}-${{ matrix.env.LIBXSLT_VERSION || env.LIBXSLT_VERSION }} + + - name: Cache [libs] + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + if: matrix.env.STATIC_DEPS with: - path: ~/.ccache - key: ${{ runner.os }}-ccache${{ matrix.extra_hash }}-${{ matrix.python-version }}-${{ hashFiles('.github/workflows/ci.yml', 'tools/ci-run.sh') }} + path: | + libs/*.xz + libs/*.gz + libs/*.zip + key: libs-${{ runner.os }}-${{ runner.arch }}-${{ env.LIBXML2_VERSION }}-${{ env.LIBXSLT_VERSION }}-${{ env.LIBICONV_VERSION }}-${{ env.ZLIB_VERSION }} - name: Run CI continue-on-error: ${{ matrix.allowed_failure || false }} env: ${{ matrix.env }} - run: bash ./tools/ci-run.sh + run: bash -c 'GITHUB_API_TOKEN="${{ secrets.GITHUB_TOKEN }}" bash ./tools/ci-run.sh' - name: Build docs - if: contains( env.EXTRA_DEPS, 'sphinx') + if: contains( matrix.env.EXTRA_DEPS, 'sphinx') run: make html - name: Upload docs - uses: actions/upload-artifact@v2 - if: ${{ matrix.extra_hash == '-docs' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: matrix.extra_hash == '-docs' with: name: website_html path: doc/html if-no-files-found: ignore - name: Upload Coverage Report - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: matrix.env.COVERAGE with: name: pycoverage_html path: coverage* if-no-files-found: ignore - - name: Upload Wheel - uses: actions/upload-artifact@v2 - if: ${{ matrix.env.STATIC_DEPS == 'true' && env.COVERAGE == 'false' }} - with: - name: wheels-${{ runner.os }} - path: dist/*.whl - if-no-files-found: ignore + pydebug: + needs: [ cache_libs ] + uses: ./.github/workflows/compiled_python.yml + with: + name: pydebug + freethreading: false + ZLIB_VERSION: ${{ needs.cache_libs.outputs.ZLIB_VERSION }} + LIBICONV_VERSION: ${{ needs.cache_libs.outputs.LIBICONV_VERSION }} + LIBXML2_VERSION: ${{ needs.cache_libs.outputs.LIBXML2_VERSION }} + LIBXSLT_VERSION: ${{ needs.cache_libs.outputs.LIBXSLT_VERSION }} + secrets: inherit + + pydebug-ft: + needs: [ cache_libs ] + uses: ./.github/workflows/compiled_python.yml + with: + name: pydebug + freethreading: true + ZLIB_VERSION: ${{ needs.cache_libs.outputs.ZLIB_VERSION }} + LIBICONV_VERSION: ${{ needs.cache_libs.outputs.LIBICONV_VERSION }} + LIBXML2_VERSION: ${{ needs.cache_libs.outputs.LIBXML2_VERSION }} + LIBXSLT_VERSION: ${{ needs.cache_libs.outputs.LIBXSLT_VERSION }} + secrets: inherit diff --git a/.github/workflows/compiled_python.yml b/.github/workflows/compiled_python.yml new file mode 100644 index 000000000..099dfd019 --- /dev/null +++ b/.github/workflows/compiled_python.yml @@ -0,0 +1,163 @@ +name: Run with compiled Python + +on: + workflow_call: + inputs: + sanitize: + required: false + default: + type: string + freethreading: + required: false + default: false + type: string + compiler: + required: false + default: + type: string + cpp_compiler: + required: false + default: + type: string + name: + required: true + type: string + ZLIB_VERSION: + required: false + default: + type: string + LIBICONV_VERSION: + required: false + default: + type: string + LIBXML2_VERSION: + required: false + default: + type: string + LIBXSLT_VERSION: + required: false + default: + type: string + +jobs: + do_run: + name: ${{inputs.name}} + runs-on: ubuntu-latest + + env: + BACKEND: c,cpp + PYTHON_VERSION: 3.x-dev + CONFIGURE_ARGS: --with-pydebug + SANITIZER_CFLAGS: "" + + steps: + - name: Checkout repo + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set compiler + if: ${{inputs.compiler}} + run: | + CC=${{inputs.compiler}} + CXX=${{inputs.cpp_compiler}} + echo EXTERNAL_OVERRIDE_CC=1 >> $GITHUB_ENV + clangv=$($CC -v 2> >(grep "clang version")) + echo $clangv + if [[ $clangv == *"version 18"* && "${{inputs.sanitize}}" == *"thread"* ]]; then + # Python uses clang-17 instead of 18 because of bugs so do the same + CC=clang-17 + CXX=clang-17 + fi + echo "CC=$CC" >> $GITHUB_ENV + echo "CXX=$CXX" >> $GITHUB_ENV + + - name: Set up sanitizer args + if: ${{inputs.sanitize}} + run: | + SANITIZER_CFLAGS="" + CONFIGURE_ARGS="" + EXTRA_CONFIGURE_CFLAGS="" + if [[ "${{inputs.sanitize}}" == *"address"* ]]; then + CONFIGURE_ARGS="$CONFIGURE_ARGS --with-address-sanitizer --without-pymalloc" + SANITIZER_CFLAGS="$SANITIZER_CFLAGS -fsanitize=address" + echo "ASAN_OPTIONS=detect_leaks=false log_path=${{ github.workspace }}/san_log" >> $GITHUB_ENV + fi + # TODO - memory sanitizer requires rebuilding almost all of CPython's dependencies + # with memory sanitizer too, so isn't really usable for us. + if [[ "${{inputs.sanitize}}" == *"memory"* ]]; then + CONFIGURE_ARGS="$CONFIGURE_ARGS --with-memory-sanitizer" + SANITIZER_CFLAGS="$SANITIZER_CFLAGS -fsanitize=memory" + fi + if [[ "${{inputs.sanitize}}" == *"undefined"* ]]; then + CONFIGURE_ARGS="$CONFIGURE_ARGS --with-undefined-behavior-sanitizer" + # We call functions through slightly incorrect pointer types a lot so disable this check for now + EXTRA_CONFIGURE_CFLAGS="$EXTRA_CONFIGURE_CFLAGS -fno-sanitize=function" + # omit vptr because it's largely C++-only and requires linking with clang++ (which breaks other things) + SANITIZER_CFLAGS="$SANITIZER_CFLAGS -fsanitize=undefined -fno-sanitize=function -fno-sanitize=vptr -fno-omit-frame-pointer" + echo "print_stacktrace=1" >> $GITHUB_ENV + echo "UBSAN_OPTIONS=log_path=${{ github.workspace }}/san_log" >> $GITHUB_ENV + fi + if [[ "${{inputs.sanitize}}" == *"thread"* ]]; then + CONFIGURE_ARGS="$CONFIGURE_ARGS --with-thread-sanitizer" + SANITIZER_CFLAGS="$SANITIZER_CFLAGS -fsanitize=thread" + if [[ "${{inputs.freethreading}}" == "true" ]]; then + TSAN_SUPPRESSIONS="${GITHUB_WORKSPACE}/cpython_main/Tools/tsan/suppressions_free_threading.txt" + else + TSAN_SUPPRESSIONS="${GITHUB_WORKSPACE}/cpython_main/Tools/tsan/suppressions.txt" + fi + echo "TSAN_OPTIONS=suppressions=$TSAN_SUPPRESSIONS log_path=${{ github.workspace }}/san_log" >> $GITHUB_ENV + # Having too many workers seems to lead to an exit without a diagnostic message - possibly memory? + echo "TEST_PARALLELISM=-j3" >> $GITHUB_ENV + fi + # https://github.com/google/sanitizers/issues/934 + echo "LD_PRELOAD=$(realpath "$(clang -print-file-name=libstdc++.so)")" >> $GITHUB_ENV + echo "CONFIGURE_ARGS=$CONFIGURE_ARGS" >> $GITHUB_ENV + echo "SANITIZER_CFLAGS=$SANITIZER_CFLAGS" >> $GITHUB_ENV + echo "EXTRA_CONFIGURE_CFLAGS=$EXTRA_CONFIGURE_CFLAGS" >> $GITHUB_ENV + + - name: Install build dependencies + run: | + sudo apt-get update -y -q + sudo apt-get install -y -q libbz2-dev liblzma-dev libreadline-dev libgmp-dev + + - name: Build Python + run: | + git clone --branch main --depth 1 https://github.com/python/cpython/ cpython_main + cd cpython_main + if [[ "${{inputs.freethreading}}" == "true" ]]; then + echo "PYTHON_VERSION=3.xt-dev" >> $GITHUB_ENV + CONFIGURE_ARGS="$CONFIGURE_ARGS --disable-gil" + fi + ./configure ${CONFIGURE_ARGS} --prefix=${GITHUB_WORKSPACE}/cpython_install CFLAGS="-O2 $EXTRA_CONFIGURE_CFLAGS" + make -j8 + make install + ${GITHUB_WORKSPACE}/cpython_install/bin/python3 -m venv ${GITHUB_WORKSPACE}/venv_pydebug + + - name: Cache [libs] + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + libs/*.xz + libs/*.gz + libs/*.zip + key: libs-${{ runner.os }}-${{ runner.arch }}-${{ inputs.LIBXML2_VERSION }}-${{ inputs.LIBXSLT_VERSION }} + + - name: Run CI + env: + COVERAGE: false + LXML_CSTD: c11 + STATIC_DEPS: true + ZLIB_VERSION: ${{ inputs.ZLIB_VERSION }} + LIBICONV_VERSION: ${{ inputs.LIBICONV_VERSION }} + LIBXML2_VERSION: ${{ inputs.LIBXML2_VERSION }} + LIBXSLT_VERSION: ${{ inputs.LIBXSLT_VERSION }} + run: | + cd "${GITHUB_WORKSPACE}/" + bash -c 'source venv_pydebug/bin/activate; GITHUB_API_TOKEN="${{ secrets.GITHUB_TOKEN }}" bash ./tools/ci-run.sh' + + - name: Archive logs + if: ${{ inputs.sanitize && always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{inputs.sanitize}}-logs + path: san_log.* + if-no-files-found: ignore diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 09dc7c9d7..f5f6bbd8e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -3,170 +3,338 @@ name: Wheel build on: release: types: [created] + schedule: + # ┌───────────── minute (0 - 59) + # │ ┌───────────── hour (0 - 23) + # │ │ ┌───────────── day of the month (1 - 31) + # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) + # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) + # │ │ │ │ │ + - cron: "42 3 * * 4" + push: + paths: + - .github/workflows/wheels.yml + - .github/workflows/cache_libs.yml + - requirements.txt + - pyproject.toml + - MANIFEST.in + - Makefile + - setup* + - build* + pull_request: + types: [opened, synchronize, reopened] + paths: + - .github/workflows/wheels.yml + - .github/workflows/cache_libs.yml + - requirements.txt + - pyproject.toml + - MANIFEST.in + - Makefile + - setup* + - build* + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +permissions: {} + jobs: + cache_libs: + uses: ./.github/workflows/cache_libs.yml + secrets: inherit + sdist: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 + + permissions: + contents: write steps: - - uses: actions/checkout@v2 + - name: Check out the repo + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: 3.9 + python-version: "3.x" - name: Install lib dependencies - run: sudo apt-get update -y -q && sudo apt-get install -y -q "libxml2=2.9.10*" "libxml2-dev=2.9.10*" libxslt1.1 libxslt1-dev + run: sudo apt-get update -y -q && sudo apt-get install -y -q "libxml2=2.9.14*" "libxml2-dev=2.9.14*" libxslt1.1 libxslt1-dev - name: Install Python dependencies - run: python -m pip install -U pip setuptools && python -m pip install -U docutils pygments sphinx sphinx-rtd-theme -r requirements.txt + run: python -m pip install -U pip setuptools && python -m pip install -U docutils pygments sphinx sphinx-book-theme -r requirements.txt - name: Build docs and sdist run: make html sdist - env: { STATIC_DEPS: false } - - - name: Release - uses: softprops/action-gh-release@v1 - if: startsWith(github.ref, 'refs/tags/') - with: - files: dist/*.tar.gz + env: { STATIC_DEPS: false; CFLAGS="-Og" } # it's run-once, so build more quickly - name: Upload sdist - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: sdist path: dist/*.tar.gz + compression-level: 0 - name: Upload website - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: website path: doc/html - Linux: + generate-wheels-matrix: + # Create a matrix of all architectures & versions to build. + # This enables the next step to run cibuildwheel in parallel. + # From https://iscinumpy.dev/post/cibuildwheel-2-10-0/#only-210 + name: Generate wheels matrix runs-on: ubuntu-latest + outputs: + include: ${{ steps.set-matrix.outputs.include }} + steps: + - name: Check out the repo + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Install cibuildwheel + # Nb. keep cibuildwheel version pin consistent with job below + run: pipx install cibuildwheel==4.1.0 + + - id: set-matrix + run: | + MATRIX=$( + { + cibuildwheel --print-build-identifiers --platform linux \ + | jq -nRc '{"only": inputs, "os": "ubuntu-latest"}' \ + | sed -e '/aarch64\|armv7l/s|ubuntu-latest|ubuntu-24.04-arm|' \ + && cibuildwheel --print-build-identifiers --platform macos \ + | jq -nRc '{"only": inputs, "os": "macos-latest"}' \ + && cibuildwheel --print-build-identifiers --platform windows --archs AMD64,x86 \ + | jq -nRc '{"only": inputs, "os": "windows-2022"}' \ + && cibuildwheel --print-build-identifiers --platform windows --archs ARM64 \ + | jq -nRc '{"only": inputs, "os": "windows-11-arm"}' + } | jq -sc 'map(. + {"platform": (.only | sub("^[^-]+-";"")), "python_version": (.only | sub("-.+";""))})' + ) + echo "include=$MATRIX" + echo "include=$MATRIX" >> $GITHUB_OUTPUT + + build_wheels: + name: Build for ${{ matrix.only }} + needs: [ cache_libs, generate-wheels-matrix ] + runs-on: ${{ matrix.os }} + + env: + LXML_CSTD: "c11" + STATIC_DEPS: "true" + ZLIB_VERSION: ${{ startsWith(matrix.os, 'windows-') && needs.cache_libs.outputs.WIN_ZLIB_VERSION || needs.cache_libs.outputs.ZLIB_VERSION }} + LIBICONV_VERSION: ${{ startsWith(matrix.os, 'windows-') && needs.cache_libs.outputs.WIN_LIBICONV_VERSION || needs.cache_libs.outputs.LIBICONV_VERSION }} + LIBXML2_VERSION: ${{ startsWith(matrix.os, 'windows-') && needs.cache_libs.outputs.WIN_LIBXML2_VERSION || needs.cache_libs.outputs.LIBXML2_VERSION }} + LIBXSLT_VERSION: ${{ startsWith(matrix.os, 'windows-') && needs.cache_libs.outputs.WIN_LIBXSLT_VERSION || needs.cache_libs.outputs.LIBXSLT_VERSION }} strategy: - # Allows for matrix sub-jobs to fail without canceling the rest fail-fast: false - matrix: - image: - - manylinux1_x86_64 - - manylinux1_i686 - #- manylinux2010_x86_64 - #- manylinux2010_i686 - - manylinux_2_24_x86_64 - - manylinux_2_24_i686 - - manylinux_2_24_aarch64 - - musllinux_1_1_x86_64 - - musllinux_1_1_aarch64 - #- manylinux_2_24_ppc64le - #- manylinux_2_24_ppc64le - #- manylinux_2_24_s390x - pyversion: ["*"] - - exclude: - - image: manylinux_2_24_aarch64 - pyversion: "*" - - image: musllinux_1_1_aarch64 - pyversion: "*" - include: - - image: manylinux2014_aarch64 - pyversion: "cp36*" - - image: manylinux_2_24_aarch64 - pyversion: "cp37*" - - image: manylinux_2_24_aarch64 - pyversion: "cp38*" - - image: manylinux_2_24_aarch64 - pyversion: "cp39*" - - image: manylinux_2_24_aarch64 - pyversion: "cp310*" - - - image: musllinux_1_1_aarch64 - pyversion: "cp36*" - - image: musllinux_1_1_aarch64 - pyversion: "cp37*" - - image: musllinux_1_1_aarch64 - pyversion: "cp38*" - - image: musllinux_1_1_aarch64 - pyversion: "cp39*" - - image: musllinux_1_1_aarch64 - pyversion: "cp310*" + include: ${{ fromJson(needs.generate-wheels-matrix.outputs.include) }} steps: - - uses: actions/checkout@v2 + - name: Check out the repo + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.8 + - name: Cache [libs] + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + libs/*.xz + libs/*.gz + libs/*.zip + key: libs-${{ runner.os }}-${{ contains(matrix.only, 'win32') && 'i686' || runner.arch }}-${{ env.LIBXML2_VERSION }}-${{ env.LIBXSLT_VERSION }}-${{ env.LIBICONV_VERSION }}-${{ env.ZLIB_VERSION }} - - name: Install dependencies - run: python -m pip install -r requirements.txt + - name: Set up QEMU + if: runner.os == 'Linux' && !(endsWith(matrix.platform, 'x86_64') || endsWith(matrix.platform, 'i686') || endsWith(matrix.platform, 'aarch64')) + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + with: + platforms: all - - name: Build Linux wheels - run: make sdist wheel_${{ matrix.image }} - env: { STATIC_DEPS: true, PYTHON_BUILD_VERSION: "${{ matrix.pyversion }}" } + - name: ccache + uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 # v1.2.23 + if: runner.os == 'Linux' || runner.os == 'macOS' + with: + max-size: 100M + create-symlink: true + key: ccache-${{ matrix.platform }}-${{ env.LIBXML2_VERSION }}-${{ env.LIBXSLT_VERSION }}-${{ env.LIBICONV_VERSION }}-${{ env.ZLIB_VERSION }} - - name: Release - uses: softprops/action-gh-release@v1 - if: startsWith(github.ref, 'refs/tags/') - with: - files: wheelhouse/*/*-m*linux*.whl # manylinux / musllinux + - name: Set up ccache host directory + if: runner.os == 'Linux' + run: | + HOST_CCACHE_DIR="$(ccache --get-config cache_dir)" + DIST_INSTALL="${{ startsWith(matrix.platform, 'musllinux') && 'apk add' || endsWith(matrix.platform, 'armv7l') && 'apt-get update && apt-get install -y' || 'dnf check-update || true && dnf -y install' }}" + echo "CIBW_BEFORE_BUILD_LINUX=${DIST_INSTALL} ccache && ccache -o cache_dir=/host${HOST_CCACHE_DIR} && ccache -p && for dir in /host/usr/lib/ccache /host/usr/local/opt/ccache/libexec; do ls -l \$dir || true; done || echo '::notice::ccache installation failed on ${{ matrix.only }}'" | tee -a "${GITHUB_ENV}" - - name: Upload wheels - uses: actions/upload-artifact@v2 - with: - name: wheels-${{ matrix.image }} - path: wheelhouse/*/*-m*linux*.whl # manylinux / musllinux - if-no-files-found: ignore + - name: Build wheels + uses: pypa/cibuildwheel@294735312765b09d24a2fbec22660ce817587d55 # v4.1.0 + with: + only: ${{ matrix.only }} - non-Linux: - strategy: - # Allows for matrix sub-jobs to fail without canceling the rest - fail-fast: false + # - name: Build faster Linux wheels + # # also build wheels with the most recent manylinux images and gcc + # # DISABLED: the images below are now the default. + # if: runner.os == 'Linux' && (contains(matrix.only, 'x86_64') || contains(matrix.only, 'aarch64')) + # uses: pypa/cibuildwheel@294735312765b09d24a2fbec22660ce817587d55 # v4.1.0 + # env: + # CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 + # CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28 + # CIBW_MANYLINUX_PPC64LE_IMAGE: manylinux_2_28 + # CIBW_MANYLINUX_S390X_IMAGE: manylinux_2_28 + # CIBW_MANYLINUX_PYPY_X86_64_IMAGE: manylinux_2_28 + # CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: manylinux_2_28 + # CIBW_MUSLLINUX_X86_64_IMAGE: musllinux_1_2 + # CIBW_MUSLLINUX_AARCH64_IMAGE: musllinux_1_2 + # CIBW_MUSLLINUX_PPC64LE_IMAGE: musllinux_1_2 + # CIBW_MUSLLINUX_S390X_IMAGE: musllinux_1_2 + # with: + # only: ${{ matrix.only }} - matrix: - #os: [macos-10.15, windows-latest] - #os: [macos-10.15, macOS-M1] - os: [macos-10.15] - python_version: ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", "pypy-3.7-v7.3.3", "pypy-3.8-v7.3.7"] + - name: Set up ccache host directory for old wheels + if: >- + contains(matrix.only, '-manylinux_') && + (contains(matrix.only, 'i686') || contains(matrix.only, 'x86_64') || contains(matrix.only, 'aarch64')) + run: echo "CIBW_BEFORE_BUILD_LINUX=${CIBW_BEFORE_BUILD_LINUX//dnf /yum }" | tee -a "${GITHUB_ENV}" + - name: Build old Linux wheels + if: >- + contains(matrix.only, '-manylinux_') && + (contains(matrix.only, 'i686') || contains(matrix.only, 'x86_64') || contains(matrix.only, 'aarch64')) + uses: pypa/cibuildwheel@294735312765b09d24a2fbec22660ce817587d55 # v4.1.0 + env: + CIBW_MANYLINUX_i686_IMAGE: manylinux2014 + CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 + CIBW_MANYLINUX_AARCH64_IMAGE: manylinux2014 + with: + only: ${{ matrix.only }} + + - name: Upload wheels + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + path: ./wheelhouse/*.whl + name: lxml-wheel-${{ matrix.only }} + compression-level: 0 + + build_abi3_wheels: + name: (disabled) Build abi3 wheels on ${{ matrix.os }} + needs: [ cache_libs ] runs-on: ${{ matrix.os }} - env: { LIBXML2_VERSION: 2.9.14, LIBXSLT_VERSION: 1.1.35, MACOSX_DEPLOYMENT_TARGET: 10.15 } + + # The Limited API is not currently capable enough to support lxml's proxy back-references. + if: false + + env: + ZLIB_VERSION: ${{ needs.cache_libs.outputs.ZLIB_VERSION }} + LIBICONV_VERSION: ${{ needs.cache_libs.outputs.LIBICONV_VERSION }} + LIBXML2_VERSION: ${{ needs.cache_libs.outputs.LIBXML2_VERSION }} + LIBXSLT_VERSION: ${{ needs.cache_libs.outputs.LIBXSLT_VERSION }} + + strategy: + fail-fast: false + matrix: + os: + - 'ubuntu-latest' + - 'ubuntu-24.04' # for emulated builds + - 'ubuntu-24.04-arm' + - 'windows-latest' + - 'windows-11-arm' + - 'macos-latest' steps: - - uses: actions/checkout@v2 + - name: Check out the repo + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python_version }} + - name: Cache [libs] + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + libs/*.xz + libs/*.gz + libs/*.zip + key: libs-${{ runner.os }}-${{ runner.arch }}-${{ env.LIBXML2_VERSION }}-${{ env.LIBXSLT_VERSION }} - - name: Install MacOS dependencies - if: startsWith(matrix.os, 'mac') - run: | - brew install automake libtool - ln -s /usr/local/bin/glibtoolize /usr/local/bin/libtoolize + - name: Set up QEMU + if: runner.os == 'Linux' && !(endsWith(matrix.platform, 'x86_64') || endsWith(matrix.platform, 'i686') || endsWith(matrix.platform, 'aarch64')) + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + with: + platforms: all - - name: Install dependencies - run: python -m pip install setuptools wheel -r requirements.txt + - name: Build ABI3 wheels + uses: pypa/cibuildwheel@294735312765b09d24a2fbec22660ce817587d55 # v4.1.0 + env: + # Smaller set of platforms that we only provide Stable ABI wheels for + CIBW_BUILD: | + ${{ + matrix.os == 'ubuntu-latest' && 'cp312-*linux_i686 cp312-musllinux_x86_64' || + matrix.os == 'ubuntu-24.04-arm' && 'cp312-*armv7l cp312-musllinux_aarch64' || + matrix.os == 'ubuntu-24.04' && 'cp312-manylinux_ppc64le cp312-manylinux_riscv64' || + matrix.os == 'windows-latest' && 'cp312-*win32' || + matrix.os == 'windows-11-arm' && 'cp312-*win_arm64' || + matrix.os == 'macos-latest' && 'cp312-*macosx_x86_64' || + '' }} - - name: Build wheels - run: make sdist wheel - env: { STATIC_DEPS: true, RUN_TESTS: true } + - name: Upload wheels + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + path: ./wheelhouse/*.whl + name: lxml-wheel-abi3-${{ matrix.os }} + compression-level: 0 - - name: Release - uses: softprops/action-gh-release@v1 - if: startsWith(github.ref, 'refs/tags/') - with: - files: dist/lxml-*.whl + upload_release_assets: + name: Upload Release Assets + #needs: [ sdist, build_wheels, build_abi3_wheels ] + needs: [ sdist, build_wheels ] + runs-on: ubuntu-latest - - name: Upload wheels - uses: actions/upload-artifact@v2 - with: - name: wheels-${{ matrix.os }} - path: dist/lxml-*.whl - if-no-files-found: ignore + permissions: + contents: write + + defaults: + run: + shell: bash + + steps: + - name: Download wheels + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: ./release_upload + pattern: lxml-wheel-* + merge-multiple: true + + - name: Download sdist + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: ./release_upload + name: sdist + + - name: List downloaded artifacts + run: | + cd ./release_upload + ls -la + echo; echo "Wheels built: "; ls *.whl | wc -l + echo; echo "Validating wheel success…" + RESULT_CODE=0 + for PYVERSION in 39 310 311 312 313 314 ; do \ + for WHEEL_ARCH in "win32" "win_amd64" "manylinux*i686" "manylinux*x86_64" "musllinux*x86_64" "macosx"; do \ + ls *-cp${PYVERSION}-*${WHEEL_ARCH}*.whl >/dev/null 2>&1 || { echo "MISSING WHEEL: cp${PYVERSION}-${WHEEL_ARCH}" ; RESULT_CODE=1; } ; \ + done; \ + done; \ + exit ${RESULT_CODE} + + - name: Upload wheels + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + path: ./release_upload/*.whl + name: all_wheels + + - name: Release + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 + if: github.ref_type == 'tag' + with: + files: | + ./release_upload/*.whl + ./release_upload/*.tar.gz diff --git a/.gitignore b/.gitignore index 66a48a6e4..30164c48a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,37 +1,62 @@ -*.pyc .tox .idea .vscode +.hg +.cache +.coverage +.ipynb_checkpoints/ build +doc/_build +doc/pdf +doc/html +doc/sphinx dist wheelhouse wheels venvs -venv -doc/html +*venv +*dump +cython_debug/ +py[0-9][0-9] +lxml-*/ libs *.egg-info +*.pickle +*.pyc *.pdb *.so *.o *.pyd +*.whl +*.log +*.patch +*.orig +*.rej +*.gz +*.xz +*.bz2 +*.zip +*.tgz +*~ +callgrind.out.* +coverty +coverage +coverage.xml +coverage.html MANIFEST +TEST doc/api/lxml*.rst doc/api/_build/ doc/s5/lxml-ep2008.html src/lxml/includes/*/ src/lxml/includes/lxml-version.h -src/lxml/*.html +src/lxml/html/*.html src/lxml/html/*.c -src/lxml/_elementpath.c -src/lxml/builder.c -src/lxml/etree.c +src/lxml/*.html +src/lxml/*.c src/lxml/etree.h src/lxml/etree_api.h src/lxml/lxml.etree.c src/lxml/lxml.etree.h src/lxml/lxml.etree_api.h -src/lxml/objectify.c -src/lxml/lxml.objectify.c -src/lxml/sax.c diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9d8a9f424..000000000 --- a/.travis.yml +++ /dev/null @@ -1,86 +0,0 @@ -os: linux -language: python - -cache: - pip: true - directories: - - $HOME/.ccache - - libs - -python: - - nightly - - 3.10 - - 2.7 - - 3.9 - - 3.8 - - 3.7 - - 3.6 - - 3.5 - -env: - global: - - USE_CCACHE=1 - - CCACHE_SLOPPINESS=pch_defines,time_macros - - CCACHE_COMPRESS=1 - - CCACHE_MAXSIZE=70M - - PATH="/usr/lib/ccache:$PATH" - - LIBXML2_VERSION=2.9.10 - - LIBXSLT_VERSION=1.1.34 - matrix: - - STATIC_DEPS=false - - STATIC_DEPS=true - -matrix: - include: - - python: 3.8 - env: - - STATIC_DEPS=false - - EXTRA_DEPS="docutils pygments sphinx sphinx-rtd-theme" - script: make html - - python: 3.8 - env: - - STATIC_DEPS=false - - EXTRA_DEPS="coverage<5" - - python: 3.8 - env: - - STATIC_DEPS=true - - LIBXML2_VERSION=2.9.2 # minimum version requirements - - LIBXSLT_VERSION=1.1.27 - - python: pypy - env: STATIC_DEPS=false - - python: pypy3 - env: STATIC_DEPS=false - - python: 3.8 - env: STATIC_DEPS=false - arch: arm64 - - python: 3.8 - env: STATIC_DEPS=true - arch: arm64 - - python: 3.8 - env: STATIC_DEPS=false - arch: ppc64le - - python: 3.8 - env: STATIC_DEPS=true - arch: ppc64le - allow_failures: - - python: nightly - - python: pypy - - python: pypy3 - -install: - - pip install -U pip wheel - - if [ -z "${TRAVIS_PYTHON_VERSION##*-dev}" ]; - then pip install --install-option=--no-cython-compile https://github.com/cython/cython/archive/master.zip; - else pip install -r requirements.txt; - fi - - pip install -U beautifulsoup4 cssselect html5lib rnc2rng==2.6.5 ${EXTRA_DEPS} - -script: - - CFLAGS="-O0 -g -fPIC" python -u setup.py build_ext --inplace - $(if [ -n "${TRAVIS_PYTHON_VERSION##2.*}" -a -n "${TRAVIS_PYTHON_VERSION##3.[34]*}" ]; then echo -n " -j7 "; fi ) - $(if [ -n "$EXTRA_DEPS" -a -z "${EXTRA_DEPS##*coverage*}" ]; then echo -n "--with-coverage"; fi ) - - ccache -s || true - - CFLAGS="-O0 -g -fPIC" PYTHONUNBUFFERED=x make test - - ccache -s || true - - python setup.py install - - python -c "from lxml import etree" diff --git a/CHANGES.txt b/CHANGES.txt index b2e0c8f03..f5ed145d6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,6 +2,694 @@ lxml changelog ============== +7.0.0a3 (2026-06-16) +==================== + +Features added +-------------- + +* ``.find()`` and ``.findall()`` now take advantage of their read-locked tree + to skip tree modification checks, making them visibly faster. + +* Python Element proxy instantiation was tuned another bit to shave off more of the + performance disadvantages added by tree locking. + +* The path compilation cache used by ElementPath uses an LRU eviction scheme. + It previously just cleared the whole cache when overflowing. + +Bugs fixed +---------- + +* Attribute iteration now remembers a safe integer index instead of a tree attribute pointer + that is potentially subject of user side modifications. This may make attribute iteration + slightly slower for long sequences of attributes but allows for attribute manipulation + between iteration steps, giving similar iteration+manipulation semantics as Python lists. + +* Several issues (race conditions, memory leaks on error handling, crashes) + were found via an automated review and subsequently resolved. + Review contributed by devdanzin. + + +7.0.0a2 (2026-06-09) +==================== + +Features added +-------------- + +* GH#511: The ElementPath implementation (``.find()`` etc) was rewritten based on + native traversal of the libxml2 tree instead of passing through Element proxy + objects. This makes some path operations like child steps or predicate evaluation + several times faster. Indexing can be more than 100x faster. + + Note that some operations are also slower due to the new lock usages in lxml 7.0. + Usually, the more selective the path, the faster it runs. + + Original idea and implementation by mahomaho. + + +7.0.0a1 (2026-05-29) +==================== + +Features added +-------------- + +* GH#477, LP#2111289: Experimental support for freethreading Python is enabled + in Python 3.14t and later. + +* GH#466: The shared parser name dict is now local to a parser (as opposed to global), + which allows to control its lifetime and cross-document usage more easily. + It is now also unbounded in size if the ``huge_tree=True`` option is provided. + +* The default chunk size for reading from file-likes in ``iterparse()`` was increased + from 32 KiB to 64 KiB and is now configurable with a new ``chunk_size`` argument. + +* Writing to Python file objects minimises data copying by passing buffers around. + +* GH#502: Predicate evaluation in ``ElementPath`` (``.find*()``) is faster. + +* HTML diffing is faster, following optimisations in CPython 3.15. + +* If multiple Python exceptions get raised from within an lxml operation, they no longer + overwrite old ones but get stacked in the exception context of the latest exception. + +Bugs fixed +---------- + +* Reading from file-like objects that start with a UTF-32 BOM failed to detect the BOM. + +Other changes +------------- + +* Support for Python 3.8 was removed. Python 3.9 will continue to be supported for + several years due to its usage in LTS Linux distributions. + +* Usage on PyPy can currently crash due to the Freethreading Python changes. + This is expected to get fixed in the further releases towards 7.0 final. + +* Some internal adaptations were made for libxml2 2.14.x and 2.15.x. + +* The ``xmlDict...`` C functions of libxml2 are now declared in ``tree.pxd``. + They remain in ``xmlparser.pxd`` for legacy reasons but Cython code that uses them + should migrate the import to ``tree.pxd``. + +* The list of software licenses in LICENSES.txt was clarified and updated + to match the actual set of software being shipped. + +* Built using Cython 3.2.5. + + +6.1.1 (2026-05-18) +================== + +Bugs fixed +---------- + +* The known link attributes in ``lxml.html.defs.link_attrs`` were missing ``xlink:href``, + which can be used for URL bypass attacks in embedded SVG/MathML/etc. content. + https://github.com/fedora-python/lxml_html_clean/security/advisories/GHSA-4jhm-jv67-739f + +* The Linux wheels use a patched libxslt 1.1.43, fixing CVE-2025-7424 and CVE-2025-11731. + +* The Windows wheels use libxslt 1.1.45, fixing CVE-2025-7424 and CVE-2025-11731. + + +6.1.0 (2026-04-17) +================== + +This release fixes a possible external entity injection (XXE) vulnerability in +``iterparse()`` and the ``ETCompatXMLParser``. + +Features added +-------------- + +* GH#486: The HTML ARIA accessibility attributes were added to the set of safe attributes + in ``lxml.html.defs``. This allows ``lxml_html_clean`` to pass them through. + Patch by oomsveta. + +* The default chunk size for reading from file-likes in ``iterparse()`` is now configurable + with a new ``chunk_size`` argument. + +Bugs fixed +---------- + +* LP#2146291: The ``resolve_entities`` option was still set to ``True`` for + ``iterparse`` and ``ETCompatXMLParser``, allowing for external entity injection (XXE) + when using these parsers without setting this option explicitly. + The default was now changed to ``'internal'`` only (as for the normal XML and HTML parsers + since lxml 5.0). + Issue found by Sihao Qiu as CVE-2026-41066. + + +6.0.4 (2026-04-12) +================== + +Bugs fixed +---------- + +* LP#2148019: Spurious MemoryError during namespace cleanup. + + +6.0.3 (2026-04-09) +================== + +Bugs fixed +---------- + +* Several out of memory error cases now raise ``MemoryError`` that were not handled before. + +* Slicing with large step values (outside of ``+/- sys.maxsize``) could trigger undefined C behaviour. + +* LP#2125399: Some failing tests were fixed or disabled in PyPy. + +* LP#2138421: Memory leak in error cases when setting the ``public_id`` or ``system_url`` of a document. + +* Memory leak in case of a memory allocation failure when copying document subtrees. + +* When mapping an XPath result to Python failed, the result memory could leak. + +* When preparing an XSLT transform failed, the XSLT parameter memory could leak. + +Other changes +------------- + +* Built using Cython 3.2.4. + +* Binary wheels use zlib 1.3.2. + + +6.0.2 (2025-09-21) +================== + +Bugs fixed +---------- + +* LP#2125278: Compilation with libxml2 2.15.0 failed. + Original patch by Xi Ruoyao. + +* Setting ``decompress=True`` in the parser had no effect in libxml2 2.15. + +* Binary wheels on Linux and macOS use the library version libxml2 2.14.6. + See https://gitlab.gnome.org/GNOME/libxml2/-/releases/v2.14.6 + +* Test failures in libxml2 2.15.0 were fixed. + +Other changes +------------- + +* Binary wheels for Py3.9-3.11 on the ``riscv64`` architecture were added. + +* Error constants were updated to match libxml2 2.15.0. + +* Built using Cython 3.1.4. + + +6.0.1 (2025-08-22) +================== + +Bugs fixed +---------- + +* LP#2116333: ``lxml.sax._getNsTag()`` could fail with an exception on malformed input. + +* GH#467: Some test adaptations were made for libxml2 2.15. + Patch by Nick Wellnhofer. + +* LP2119510, GH#473: A Python compatibility test was fixed for Python 3.14+. + Patch by Lumír Balhar. + +* GH#471: Wheels for "riscv64" on recent Python versions were added. + Patch by ffgan. + +* GH#469: The wheel build no longer requires the ``wheel`` package unconditionally. + Patch by Miro Hrončok. + +* Binary wheels use the library version libxml2 2.14.5. + See https://gitlab.gnome.org/GNOME/libxml2/-/releases/v2.14.5 + +* Windows binary wheels continue to use a security patched library version libxml2 2.11.9. + + +6.0.0 (2025-06-26) +================== + +Features added +-------------- + +* GH#463: ``lxml.html.diff`` is faster and provides structurally better diffs. + Original patch by Steven Fernandez. + +* GH#405: The factories ``Element`` and ``ElementTree`` can now be used in type hints. + +* GH#448: Parsing from ``memoryview`` and other buffers is supported to allow zero-copy parsing. + +* GH#437: ``lxml.html.builder`` was missing several HTML5 tag names. + Patch by Nick Tarleton. + +* GH#458: ``CDATA`` can now be written into the incremental ``xmlfile()`` writer. + Original patch by Lane Shaw. + +* A new parser option ``decompress=False`` was added that controls the automatic + input decompression when using libxml2 2.15.0 or later. Disabling this option + by default will effectively prevent decompression bombs when handling untrusted + input. Code that depends on automatic decompression must enable this option. + Note that libxml2 2.15.0 was not released yet, so this option currently has no + effect but can already be used. + +* The set of compile time / runtime supported libxml2 feature names is available as + ``etree.LIBXML_COMPILED_FEATURES`` and ``etree.LIBXML_FEATURES``. + This currently includes + ``catalog``, ``ftp``, ``html``, ``http``, ``iconv``, ``icu``, + ``lzma``, ``regexp``, ``schematron``, ``xmlschema``, ``xpath``, ``zlib``. + +Bugs fixed +---------- + +* GH#353: Predicates in ``.find*()`` could mishandle tag indices if a default namespace is provided. + Original patch by Luise K. + +* GH#272: The ``head`` and ``body`` properties of ``lxml.html`` elements failed if no such element + was found. They now return ``None`` instead. + Original patch by FVolral. + +* Tag names provided by code (API, not data) that are longer than ``INT_MAX`` + could be truncated or mishandled in other ways. + +* ``.text_content()`` on ``lxml.html`` elements accidentally returned a "smart string" + without additional information. It now returns a plain string. + +* LP#2109931: When building lxml with coverage reporting, it now disables the ``sys.monitoring`` + support due to the lack of support in https://github.com/nedbat/coveragepy/issues/1790 + +Other changes +------------- + +* Support for Python < 3.8 was removed. + +* Parsing directly from zlib (or lzma) compressed data is now considered an optional + feature in lxml. It may get removed from libxml2 at some point for security reasons + (compression bombs) and is therefore no longer guaranteed to be available in lxml. + + As of this release, zlib support is still normally available in the binary wheels + but may get disabled or removed in later (x.y.0) releases. To test the availability, + use ``"zlib" in etree.LIBXML_FEATURES``. + +* The ``Schematron`` class is deprecated and will become non-functional in a future lxml version. + The feature will soon be removed from libxml2 and stop being available. + +* GH#438: Wheels include the ``arm7l`` target. + +* GH#465: Windows wheels include the ``arm64`` target. + Patch by Finn Womack. + +* Binary wheels use the library versions libxml2 2.14.4 and libxslt 1.1.43. + Note that this disables direct HTTP and FTP support for parsing from URLs. + Use Python URL request tools instead (which usually also support HTTPS). + To test the availability, use ``"http" in etree.LIBXML_FEATURES``. + +* Windows binary wheels use the library versions libxml2 2.11.9, libxslt 1.1.39 and libiconv 1.17. + They are now based on VS-2022. + +* Built using Cython 3.1.2. + +* The debug methods ``MemDebug.dump()`` and ``MemDebug.show()`` were removed completely. + libxml2 2.13.0 discarded this feature. + + +5.4.0 (2025-04-22) +================== + +Bugs fixed +---------- + +* LP#2107279: Binary wheels use libxml2 2.13.8 and libxslt 1.1.43 to resolve several CVEs. + (Binary wheels for Windows continue to use a patched libxml2 2.11.9 and libxslt 1.1.39.) + Issue found by Anatoly Katyushin. + + +5.3.2 (2025-04-05) +================== + +This release resolves CVE-2025-24928 as described in +https://gitlab.gnome.org/GNOME/libxml2/-/issues/847 + +Bugs fixed +---------- + +* Binary wheels use libxml2 2.12.10 and libxslt 1.1.42. + +* Binary wheels for Windows use a patched libxml2 2.11.9 and libxslt 1.1.39. + + +5.3.1 (2025-02-09) +================== + +Bugs fixed +---------- + +* GH#440: Some tests were adapted for libxml2 2.14.0. + Patch by Nick Wellnhofer. + +* LP#2097175: ``DTD(external_id="…")`` erroneously required a byte string as ID value. + +* GH#450: ``iterparse()`` internally triggered the `DeprecationWarning`` added in lxml 5.3.0 when parsing HTML. + +Other changes +------------- + +* GH#442: Binary wheels for macOS no longer use the linker flag ``-flat_namespace``. + + +5.3.0 (2024-08-10) +================== + +Features added +-------------- + +* GH#421: Nested ``CDATA`` sections are no longer rejected but split on output + to represent ``]]>`` correctly. + Patch by Gertjan Klein. + +Bugs fixed +---------- + +* LP#2060160: Attribute values serialised differently in ``xmlfile.element()`` and ``xmlfile.write()``. + +* LP#2058177: The ISO-Schematron implementation could fail on unknown prefixes. + Patch by David Lakin. + +Other changes +------------- + +* LP#2067707: The ``strip_cdata`` option in ``HTMLParser()`` turned out to be useless and is now deprecated. + +* Binary wheels use the library versions libxml2 2.12.9 and libxslt 1.1.42. + +* Windows binary wheels use the library versions libxml2 2.11.8 and libxslt 1.1.39. + +* Built with Cython 3.0.11. + + +5.2.2 (2024-05-12) +================== + +Bugs fixed +---------- + +* GH#417: The ``test_feed_parser`` test could fail if ``lxml_html_clean`` was not installed. + It is now skipped in that case. + +* LP#2059910: The minimum CPU architecture for the Linux x86 binary wheels was set back to + "core2", without SSE 4.2. + +* If libxml2 uses iconv, the compile time version is available as `etree.ICONV_COMPILED_VERSION`. + + +5.2.1 (2024-04-02) +================== + +Bugs fixed +---------- + +* LP#2059910: The minimum CPU architecture for the Linux x86 binary wheels was set back to + "core2", but with SSE 4.2 enabled. + +* LP#2059977: ``Element.iterfind("//absolute_path")`` failed with a ``SyntaxError`` + where it should have issued a warning. + +* GH#416: The documentation build was using the non-standard ``which`` command. + Patch by Michał Górny. + + +5.2.0 (2024-03-30) +================== + +Other changes +------------- + +* LP#1958539: The ``lxml.html.clean`` implementation suffered from several (only if used) + security issues in the past and was now extracted into a separate library: + + https://github.com/fedora-python/lxml_html_clean + + Projects that use lxml without "lxml.html.clean" will not notice any difference, + except that they won't have potentially vulnerable code installed. + The module is available as an "extra" setuptools dependency "lxml[html_clean]", + so that Projects that need "lxml.html.clean" will need to switch their requirements + from "lxml" to "lxml[html_clean]", or install the new library themselves. + +* The minimum CPU architecture for the Linux x86 binary wheels was upgraded to + "sandybridge" (launched 2011), and glibc 2.28 / gcc 12 (manylinux_2_28) wheels were added. + +* Built with Cython 3.0.10. + + +5.1.1 (2024-03-28) +================== + +Bugs fixed +---------- + +* LP#2048920: ``iterlinks()`` in ``lxml.html`` rejected ``bytes`` input in 5.1.0. + +* High source line numbers from the parser are no longer truncated + (up to a C ``long``) when using libxml2 2.11 or later. + +Other changes +------------- + +* GH#407: A compatibility test was adapted to recent expat versions. + Patch by Miro Hrončok. + +* Binary wheels use the library versions libxml2 2.12.6 and libxslt 1.1.39. + +* Windows binary wheels use the library versions libxml2 2.11.7 and libxslt 1.1.39. + +* Built with Cython 3.0.9. + + +5.1.0 (2024-01-05) +================== + +Features added +-------------- + +* Parsing ASCII strings is slightly faster. + +Bugs fixed +---------- + +* GH#349: The HTML ``Cleaner()`` interpreted an accidentally provided string parameter + for the ``host_whitelist`` as list of characters and silently failed to reject any hosts. + Passing a non-collection is now rejected. + +Other changes +------------- + +* Support for Python 2.7 and Python versions < 3.6 was removed. + +* The wheel build was migrated to use ``cibuildwheel``. + Patch by Primož Godec. + + +5.0.2 (2024-03-28) +================== + +Other changes +------------- + +* GH#407: A compatibility test was adapted to recent expat versions. + Patch by Miro Hrončok. + +* Binary wheels use the library versions libxml2 2.12.6 and libxslt 1.1.39. + +* Built with Cython 3.0.9. + + +5.0.1 (2024-01-05) +================== + +Bugs fixed +---------- + +* LP#2046208: Parsing non-BMP Python Unicode strings could fail on macOS. + +* LP#2044225: When incrementally parsing broken HTML, reporting start events on + missing structural tags failed and could lead to subsequent exceptions. + +* LP#2045435: Some (not all) issues with stricter C compilers were resolved. + +* The binary wheels in the 5.0.0 release did not validate cleanly (but installed ok). + + +.. _latest_release: + +5.0.0 (2023-12-29) +================== + +Features added +-------------- + +* Character escaping in ``C14N2`` serialisation now uses a single pass over the text + instead of searching for each unescaped character separately. + +* Early support for Python 3.13a2 was added. + +Bugs fixed +---------- + +* LP#1976304: The ``Element.addnext()`` method previously inserted the new element + before existing tail text. The tail text of both sibling elements now stays on + the respective elements. + +* LP#1980767, GH#379: ``TreeBuilder.close()`` could fail with a ``TypeError`` after + parsing incorrect input. Original patch by Enrico Minack. + +* ``Element.itertext(with_tail=False)`` returned the tail text of comments and + processing instructions, despite the explicit option. + +* GH#370: A crash with recent libxml2 2.11.x versions was resolved. + Patch by Michael Schlenker. + +* A compile problem with recent libxml2 2.12.x versions was resolved. + +* The internal exception handling in C callbacks was improved for Cython 3.0. + +* The exception declarations of ``xmlInputReadCallback``, ``xmlInputCloseCallback``, + ``xmlOutputWriteCallback`` and ``xmlOutputCloseCallback`` in ``tree.pxd`` were + corrected to prevent running Python code or calling into the C-API with a live + exception set. + +* GH#385: The long deprecated ``unittest.m̀akeSuite()`` function is no longer used. + Patch by Miro Hrončok. + +* LP#1522052: A file-system specific test is now optional and should no longer fail + on systems that don't support it. + +* GH#392: Some tests were adapted for libxml2 2.13. + Patch by Nick Wellnhofer. + +* Contains all fixes from lxml 4.9.4. + +Other changes +------------- + +* LP#1742885: lxml no longer expands external entities (XXE) by default to prevent + the security risk of loading arbitrary files and URLs. If this feature is needed, + it can be enabled in a backwards compatible way by using a parser with the option + ``resolve_entities=True``. The new default is ``resolve_entities='internal'``. + +* With libxml2 2.10.4 and later (as provided by the lxml 5.0 binary wheels), + parsing HTML tags with "prefixes" no longer builds a namespace dictionary + in ``nsmap`` but considers the ``prefix:name`` string the actual tag name. + With older libxml2 versions, since 2.9.11, the prefix was removed. Before + that, the prefix was parsed as XML prefix. + + lxml 5.0 does not try to hide this difference but now changes the ElementPath + implementation to let ``element.find("part1:part2")`` search for the tag + ``part1:part2`` in documents parsed as HTML, instead of looking only for ``part2``. + +* LP#2024343: The validation of the schema file itself is now optional in the + ISO-Schematron implementation. This was done because some lxml distributions + discard the RNG validation schema file due to licensing issues. The validation + can now always be disabled with ``Schematron(..., validate_schema=False)``. + It is enabled by default if available and disabled otherwise. The module + constant ``lxml.isoschematron.schematron_schema_valid_supported`` can be used + to detect whether schema file validation is available. + +* Some redundant and long deprecated methods were removed: + ``parser.setElementClassLookup()``, + ``xslt_transform.apply()``, + ``xpath.evaluate()``. + +* Some incorrect declarations were removed from ``python.pxd``. In general, this file + should not be used by external Cython code. Use the C-API declarations provided by + Cython itself instead. + +* Binary wheels use the library versions libxml2 2.12.3 and libxslt 1.1.39. + +* Built with Cython 3.0.7, updated to follow recent changes in Cython 3.1-dev. + + +4.9.4 (2023-12-19) +================== + +Bugs fixed +---------- + +* LP#2046398: Inserting/replacing an ancestor into a node's children could loop indefinitely. + +* LP#1980767, GH#379: ``TreeBuilder.close()`` could fail with a ``TypeError`` after + parsing incorrect input. Original patch by Enrico Minack. + +* LP#1522052: A file-system specific test is now optional and should no longer fail + on systems that don't support it. + +Other changes +------------- + +* Wheels include zlib 1.3, libxml2 2.10.3 and libxslt 1.1.39 + (zlib 1.2.12, libxml2 2.10.3 and libxslt 1.1.37 on Windows). + +* Built with Cython 0.29.37. + + +4.9.3 (2023-07-05) +================== + +Bugs fixed +---------- + +* LP#2008911: ``lxml.objectify`` accepted non-decimal numbers like ``²²²`` as integers. + +* A memory leak in ``lxml.html.clean`` was resolved by switching to Cython 0.29.34+. + +* GH#348: URL checking in the HTML cleaner was improved. + Patch by Tim McCormack. + +* GH#371, GH#373: Some regex strings were changed to raw strings to fix Python warnings. + Patches by Jakub Wilk and Anthony Sottile. + +Other changes +------------- + +* Wheels include zlib 1.2.13, libxml2 2.10.3 and libxslt 1.1.38 + (zlib 1.2.12, libxml2 2.10.3 and libxslt 1.1.37 on Windows). + +* Built with Cython 0.29.36 to adapt to changes in Python 3.12. + + +4.9.2 (2022-12-13) +================== + +Bugs fixed +---------- + +* CVE-2022-2309: A Bug in libxml2 2.9.1[0-4] could let namespace declarations + from a failed parser run leak into later parser runs. This bug was worked around + in lxml and resolved in libxml2 2.10.0. + https://gitlab.gnome.org/GNOME/libxml2/-/issues/378 + +Other changes +------------- + +* LP#1981760: ``Element.attrib`` now registers as ``collections.abc.MutableMapping``. + +* lxml now has a static build setup for macOS on ARM64 machines (not used for building wheels). + Patch by Quentin Leffray. + + +4.9.1 (2022-07-01) +================== + +Bugs fixed +---------- + +* A crash was resolved when using ``iterwalk()`` (or ``canonicalize()``) + after parsing certain incorrect input. Note that ``iterwalk()`` can crash + on *valid* input parsed with the same parser *after* failing to parse the + incorrect input. + + 4.9.0 (2022-06-01) ================== @@ -1938,7 +2626,7 @@ Bugs fixed * When an open file-like object is passed into ``parse()`` or ``iterparse()``, the parser will no longer close it after use. This reverts a change in lxml 2.3 where all files would be closed. It is - the users responsibility to properly close the file(-like) object, + the user's responsibility to properly close the file(-like) object, also in error cases. * Assertion error in lxml.html.cleaner when discarding top-level elements. @@ -2593,7 +3281,7 @@ Features added * lxml.etree now tries to find the absolute path name of files when parsing from a file-like object. This helps custom resolvers when - resolving relative URLs, as lixbml2 can prepend them with the path + resolving relative URLs, as libxml2 can prepend them with the path of the source document. Bugs fixed @@ -3555,7 +4243,7 @@ Bugs fixed Features added -------------- -* Module ``lxml.pyclasslookup`` module implements an Element class lookup +* The ``lxml.pyclasslookup`` module implements an Element class lookup scheme that can access the entire tree in read-only mode to help determining a suitable Element class diff --git a/DD.py b/DD.py deleted file mode 100644 index 47dfec767..000000000 --- a/DD.py +++ /dev/null @@ -1,916 +0,0 @@ -#! /usr/bin/env python -# $Id: DD.py,v 1.2 2001/11/05 19:53:33 zeller Exp $ -# Enhanced Delta Debugging class -# Copyright (c) 1999, 2000, 2001 Andreas Zeller. - -# This module (written in Python) implements the base delta debugging -# algorithms and is at the core of all our experiments. This should -# easily run on any platform and any Python version since 1.6. -# -# To plug this into your system, all you have to do is to create a -# subclass with a dedicated `test()' method. Basically, you would -# invoke the DD test case minimization algorithm (= the `ddmin()' -# method) with a list of characters; the `test()' method would combine -# them to a document and run the test. This should be easy to realize -# and give you some good starting results; the file includes a simple -# sample application. -# -# This file is in the public domain; feel free to copy, modify, use -# and distribute this software as you wish - with one exception. -# Passau University has filed a patent for the use of delta debugging -# on program states (A. Zeller: `Isolating cause-effect chains', -# Saarland University, 2001). The fact that this file is publicly -# available does not imply that I or anyone else grants you any rights -# related to this patent. -# -# The use of Delta Debugging to isolate failure-inducing code changes -# (A. Zeller: `Yesterday, my program worked', ESEC/FSE 1999) or to -# simplify failure-inducing input (R. Hildebrandt, A. Zeller: -# `Simplifying failure-inducing input', ISSTA 2000) is, as far as I -# know, not covered by any patent, nor will it ever be. If you use -# this software in any way, I'd appreciate if you include a citation -# such as `This software uses the delta debugging algorithm as -# described in (insert one of the papers above)'. -# -# All about Delta Debugging is found at the delta debugging web site, -# -# http://www.st.cs.uni-sb.de/dd/ -# -# Happy debugging, -# -# Andreas Zeller - - -# Start with some helpers. -class OutcomeCache(object): - # This class holds test outcomes for configurations. This avoids - # running the same test twice. - - # The outcome cache is implemented as a tree. Each node points - # to the outcome of the remaining list. - # - # Example: ([1, 2, 3], PASS), ([1, 2], FAIL), ([1, 4, 5], FAIL): - # - # (2, FAIL)--(3, PASS) - # / - # (1, None) - # \ - # (4, None)--(5, FAIL) - - def __init__(self): - self.tail = {} # Points to outcome of tail - self.result = None # Result so far - - def add(self, c, result): - """Add (C, RESULT) to the cache. C must be a list of scalars.""" - cs = c[:] - cs.sort() - - p = self - for start in c: - if start not in p.tail: - p.tail[start] = OutcomeCache() - p = p.tail[start] - - p.result = result - - def lookup(self, c): - """Return RESULT if (C, RESULT) is in the cache; None, otherwise.""" - p = self - for start in c: - if start not in p.tail: - return None - p = p.tail[start] - - return p.result - - def lookup_superset(self, c, start = 0): - """Return RESULT if there is some (C', RESULT) in the cache with - C' being a superset of C or equal to C. Otherwise, return None.""" - - # FIXME: Make this non-recursive! - if start >= len(c): - if self.result: - return self.result - elif self.tail != {}: - # Select some superset - superset = self.tail[list(self.tail.keys())[0]] - return superset.lookup_superset(c, start + 1) - else: - return None - - if c[start] in self.tail: - return self.tail[c[start]].lookup_superset(c, start + 1) - - # Let K0 be the largest element in TAIL such that K0 <= C[START] - k0 = None - for k in self.tail.keys(): - if (k0 is None or k > k0) and k <= c[start]: - k0 = k - - if k0 is not None: - return self.tail[k0].lookup_superset(c, start) - - return None - - def lookup_subset(self, c): - """Return RESULT if there is some (C', RESULT) in the cache with - C' being a subset of C or equal to C. Otherwise, return None.""" - p = self - for start in range(len(c)): - if c[start] in p.tail: - p = p.tail[c[start]] - - return p.result - - - - -# Test the outcome cache -def oc_test(): - oc = OutcomeCache() - - assert oc.lookup([1, 2, 3]) is None - oc.add([1, 2, 3], 4) - assert oc.lookup([1, 2, 3]) == 4 - assert oc.lookup([1, 2, 3, 4]) is None - - assert oc.lookup([5, 6, 7]) is None - oc.add([5, 6, 7], 8) - assert oc.lookup([5, 6, 7]) == 8 - - assert oc.lookup([]) is None - oc.add([], 0) - assert oc.lookup([]) == 0 - - assert oc.lookup([1, 2]) is None - oc.add([1, 2], 3) - assert oc.lookup([1, 2]) == 3 - assert oc.lookup([1, 2, 3]) == 4 - - assert oc.lookup_superset([1]) == 3 or oc.lookup_superset([1]) == 4 - assert oc.lookup_superset([1, 2]) == 3 or oc.lookup_superset([1, 2]) == 4 - assert oc.lookup_superset([5]) == 8 - assert oc.lookup_superset([5, 6]) == 8 - assert oc.lookup_superset([6, 7]) == 8 - assert oc.lookup_superset([7]) == 8 - assert oc.lookup_superset([]) is not None - - assert oc.lookup_superset([9]) is None - assert oc.lookup_superset([7, 9]) is None - assert oc.lookup_superset([-5, 1]) is None - assert oc.lookup_superset([1, 2, 3, 9]) is None - assert oc.lookup_superset([4, 5, 6, 7]) is None - - assert oc.lookup_subset([]) == 0 - assert oc.lookup_subset([1, 2, 3]) == 4 - assert oc.lookup_subset([1, 2, 3, 4]) == 4 - assert oc.lookup_subset([1, 3]) is None - assert oc.lookup_subset([1, 2]) == 3 - - assert oc.lookup_subset([-5, 1]) is None - assert oc.lookup_subset([-5, 1, 2]) == 3 - assert oc.lookup_subset([-5]) == 0 - - -# Main Delta Debugging algorithm. -class DD(object): - # Delta debugging base class. To use this class for a particular - # setting, create a subclass with an overloaded `test()' method. - # - # Main entry points are: - # - `ddmin()' which computes a minimal failure-inducing configuration, and - # - `dd()' which computes a minimal failure-inducing difference. - # - # See also the usage sample at the end of this file. - # - # For further fine-tuning, you can implement an own `resolve()' - # method (tries to add or remove configuration elements in case of - # inconsistencies), or implement an own `split()' method, which - # allows you to split configurations according to your own - # criteria. - # - # The class includes other previous delta debugging algorithms, - # which are obsolete now; they are only included for comparison - # purposes. - - # Test outcomes. - PASS = "PASS" - FAIL = "FAIL" - UNRESOLVED = "UNRESOLVED" - - # Resolving directions. - ADD = "ADD" # Add deltas to resolve - REMOVE = "REMOVE" # Remove deltas to resolve - - # Debugging output (set to 1 to enable) - debug_test = 0 - debug_dd = 0 - debug_split = 0 - debug_resolve = 0 - - def __init__(self): - self.__resolving = 0 - self.__last_reported_length = 0 - self.monotony = 0 - self.outcome_cache = OutcomeCache() - self.cache_outcomes = 1 - self.minimize = 1 - self.maximize = 1 - self.assume_axioms_hold = 1 - - # Helpers - def __listminus(self, c1, c2): - """Return a list of all elements of C1 that are not in C2.""" - s2 = {} - for delta in c2: - s2[delta] = 1 - - c = [] - for delta in c1: - if delta not in s2: - c.append(delta) - - return c - - def __listintersect(self, c1, c2): - """Return the common elements of C1 and C2.""" - s2 = {} - for delta in c2: - s2[delta] = 1 - - c = [] - for delta in c1: - if delta in s2: - c.append(delta) - - return c - - def __listunion(self, c1, c2): - """Return the union of C1 and C2.""" - s1 = {} - for delta in c1: - s1[delta] = 1 - - c = c1[:] - for delta in c2: - if delta not in s1: - c.append(delta) - - return c - - def __listsubseteq(self, c1, c2): - """Return 1 if C1 is a subset or equal to C2.""" - s2 = {} - for delta in c2: - s2[delta] = 1 - - for delta in c1: - if delta not in s2: - return 0 - - return 1 - - # Output - def coerce(self, c): - """Return the configuration C as a compact string""" - # Default: use printable representation - return repr(c) - - def pretty(self, c): - """Like coerce(), but sort beforehand""" - sorted_c = c[:] - sorted_c.sort() - return self.coerce(sorted_c) - - # Testing - def test(self, c): - """Test the configuration C. Return PASS, FAIL, or UNRESOLVED""" - c.sort() - - # If we had this test before, return its result - if self.cache_outcomes: - cached_result = self.outcome_cache.lookup(c) - if cached_result is not None: - return cached_result - - if self.monotony: - # Check whether we had a passing superset of this test before - cached_result = self.outcome_cache.lookup_superset(c) - if cached_result == self.PASS: - return self.PASS - - cached_result = self.outcome_cache.lookup_subset(c) - if cached_result == self.FAIL: - return self.FAIL - - if self.debug_test: - print('') - print("test(%s)..." % (self.coerce(c),)) - - outcome = self._test(c) - - if self.debug_test: - print("test(%s) = %r" % (self.coerce(c), outcome)) - - if self.cache_outcomes: - self.outcome_cache.add(c, outcome) - - return outcome - - def _test(self, c): - """Stub to overload in subclasses""" - return self.UNRESOLVED # Placeholder - - - # Splitting - def split(self, c, n): - """Split C into [C_1, C_2, ..., C_n].""" - if self.debug_split: - print("split(%s, %r)..." % (self.coerce(c), n)) - - outcome = self._split(c, n) - - if self.debug_split: - print("split(%s, %r) = %r" % (self.coerce(c), n, outcome)) - - return outcome - - def _split(self, c, n): - """Stub to overload in subclasses""" - subsets = [] - start = 0 - for i in range(n): - subset = c[start:start + (len(c) - start) // (n - i)] - subsets.append(subset) - start = start + len(subset) - return subsets - - - # Resolving - def resolve(self, csub, c, direction): - """If direction == ADD, resolve inconsistency by adding deltas - to CSUB. Otherwise, resolve by removing deltas from CSUB.""" - - if self.debug_resolve: - print("resolve(%r, %s, %r)..." % (csub, self.coerce(c), direction)) - - outcome = self._resolve(csub, c, direction) - - if self.debug_resolve: - print("resolve(%r, %s, %r) = %r" % (csub, self.coerce(c), direction, outcome)) - - return outcome - - - def _resolve(self, csub, c, direction): - """Stub to overload in subclasses.""" - # By default, no way to resolve - return None - - - # Test with fixes - def test_and_resolve(self, csub, r, c, direction): - """Repeat testing CSUB + R while unresolved.""" - - initial_csub = csub[:] - c2 = self.__listunion(r, c) - - csubr = self.__listunion(csub, r) - t = self.test(csubr) - - # necessary to use more resolving mechanisms which can reverse each - # other, can (but needn't) be used in subclasses - self._resolve_type = 0 - - while t == self.UNRESOLVED: - self.__resolving = 1 - csubr = self.resolve(csubr, c, direction) - - if csubr is None: - # Nothing left to resolve - break - - if len(csubr) >= len(c2): - # Added everything: csub == c2. ("Upper" Baseline) - # This has already been tested. - csubr = None - break - - if len(csubr) <= len(r): - # Removed everything: csub == r. (Baseline) - # This has already been tested. - csubr = None - break - - t = self.test(csubr) - - self.__resolving = 0 - if csubr is None: - return self.UNRESOLVED, initial_csub - - # assert t == self.PASS or t == self.FAIL - csub = self.__listminus(csubr, r) - return t, csub - - # Inquiries - def resolving(self): - """Return 1 while resolving.""" - return self.__resolving - - - # Logging - def report_progress(self, c, title): - if len(c) != self.__last_reported_length: - print('') - print("%s: %d deltas left: %s" % (title, len(c), self.coerce(c))) - self.__last_reported_length = len(c) - - - # Delta Debugging (old ESEC/FSE version) - def old_dd(self, c, r = [], n = 2): - """Return the failure-inducing subset of C""" - - assert self.test([]) == dd.PASS - assert self.test(c) == dd.FAIL - - if self.debug_dd: - print("dd(%s, %r, %r)..." % (self.pretty(c), r, n)) - - outcome = self._old_dd(c, r, n) - - if self.debug_dd: - print("dd(%s, %r, %r) = %r" % (self.pretty(c), r, n, outcome)) - - return outcome - - def _old_dd(self, c, r, n): - """Stub to overload in subclasses""" - - if not r: - assert self.test([]) == self.PASS - assert self.test(c) == self.FAIL - else: - assert self.test(r) != self.FAIL - assert self.test(c + r) != self.PASS - - assert self.__listintersect(c, r) == [] - - if len(c) == 1: - # Nothing to split - return c - - run = 1 - next_c = c[:] - next_r = r[:] - - # We replace the tail recursion from the paper by a loop - while 1: - self.report_progress(c, "dd") - - cs = self.split(c, n) - - print('') - print("dd (run #%r): trying %s" % (run, ' + '.join(map(str, cs)))) - print('') - - # Check subsets - ts = [] - for i in range(n): - if self.debug_dd: - print("dd: trying cs[%d] = %s" % (i, self.pretty(cs[i]))) - - t, cs[i] = self.test_and_resolve(cs[i], r, c, self.REMOVE) - ts.append(t) - if t == self.FAIL: - # Found - if self.debug_dd: - print("dd: found %d deltas: %s" % (len(cs[i]), self.pretty(cs[i]))) - return self.dd(cs[i], r) - - # Check complements - cbars = [] - tbars = [] - - for i in range(n): - cbar = self.__listminus(c, cs[i] + r) - tbar, cbar = self.test_and_resolve(cbar, r, c, self.ADD) - - - doubled = self.__listintersect(cbar, cs[i]) - if doubled: - cs[i] = self.__listminus(cs[i], doubled) - - - cbars.append(cbar) - tbars.append(tbar) - - if ts[i] == self.PASS and tbars[i] == self.PASS: - # Interference - if self.debug_dd: - print("dd: interference of %s and %s" % (self.pretty(cs[i]), self.pretty(cbars[i]))) - - d = self.dd(cs[i][:], cbars[i] + r) - dbar = self.dd(cbars[i][:], cs[i] + r) - return d + dbar - - if ts[i] == self.UNRESOLVED and tbars[i] == self.PASS: - # Preference - if self.debug_dd: - print("dd: preferring %d deltas: %s" % (len(cs[i]), self.pretty(cs[i]))) - - return self.dd(cs[i][:], cbars[i] + r) - - if ts[i] == self.PASS or tbars[i] == self.FAIL: - if self.debug_dd: - excluded = self.__listminus(next_c, cbars[i]) - print("dd: excluding %d deltas: %s" % (len(excluded), self.pretty(excluded))) - - if ts[i] == self.PASS: - next_r = self.__listunion(next_r, cs[i]) - next_c = self.__listintersect(next_c, cbars[i]) - self.report_progress(next_c, "dd") - - next_n = min(len(next_c), n * 2) - - if next_n == n and next_c[:] == c[:] and next_r[:] == r[:]: - # Nothing left - if self.debug_dd: - print("dd: nothing left") - return next_c - - # Try again - if self.debug_dd: - print("dd: try again") - - c = next_c - r = next_r - n = next_n - run = run + 1 - - - def test_mix(self, csub, c, direction): - if self.minimize: - (t, csub) = self.test_and_resolve(csub, [], c, direction) - if t == self.FAIL: - return t, csub - - if self.maximize: - csubbar = self.__listminus(self.CC, csub) - cbar = self.__listminus(self.CC, c) - if direction == self.ADD: - directionbar = self.REMOVE - else: - directionbar = self.ADD - - (tbar, csubbar) = self.test_and_resolve(csubbar, [], cbar, - directionbar) - - csub = self.__listminus(self.CC, csubbar) - - if tbar == self.PASS: - t = self.FAIL - elif tbar == self.FAIL: - t = self.PASS - else: - t = self.UNRESOLVED - - return t, csub - - - # Delta Debugging (new ISSTA version) - def ddgen(self, c, minimize, maximize): - """Return a 1-minimal failing subset of C""" - - self.minimize = minimize - self.maximize = maximize - - n = 2 - self.CC = c - - if self.debug_dd: - print("dd(%s, %r)..." % (self.pretty(c), n)) - - outcome = self._dd(c, n) - - if self.debug_dd: - print("dd(%s, %r) = %r" % (self.pretty(c), n, outcome)) - - return outcome - - def _dd(self, c, n): - """Stub to overload in subclasses""" - - assert self.test([]) == self.PASS - - run = 1 - cbar_offset = 0 - - # We replace the tail recursion from the paper by a loop - while 1: - tc = self.test(c) - assert tc == self.FAIL or tc == self.UNRESOLVED - - if n > len(c): - # No further minimizing - print("dd: done") - return c - - self.report_progress(c, "dd") - - cs = self.split(c, n) - - print('') - print("dd (run #%d): trying %s" % (run, ' + '.join(map(str, cs)))) - print('') - - c_failed = 0 - cbar_failed = 0 - - next_c = c[:] - next_n = n - - # Check subsets - for i in range(n): - if self.debug_dd: - print("dd: trying %s" % (self.pretty(cs[i]),)) - - (t, cs[i]) = self.test_mix(cs[i], c, self.REMOVE) - - if t == self.FAIL: - # Found - if self.debug_dd: - print("dd: found %d deltas: %s" % (len(cs[i]), self.pretty(cs[i]))) - - c_failed = 1 - next_c = cs[i] - next_n = 2 - cbar_offset = 0 - self.report_progress(next_c, "dd") - break - - if not c_failed: - # Check complements - cbars = n * [self.UNRESOLVED] - - # print "cbar_offset =", cbar_offset - - for j in range(n): - i = int((j + cbar_offset) % n) - cbars[i] = self.__listminus(c, cs[i]) - t, cbars[i] = self.test_mix(cbars[i], c, self.ADD) - - doubled = self.__listintersect(cbars[i], cs[i]) - if doubled: - cs[i] = self.__listminus(cs[i], doubled) - - if t == self.FAIL: - if self.debug_dd: - print("dd: reduced to %d deltas: %s" % (len(cbars[i]), self.pretty(cbars[i]))) - - cbar_failed = 1 - next_c = self.__listintersect(next_c, cbars[i]) - next_n = next_n - 1 - self.report_progress(next_c, "dd") - - # In next run, start removing the following subset - cbar_offset = i - break - - if not c_failed and not cbar_failed: - if n >= len(c): - # No further minimizing - print("dd: done") - return c - - next_n = min(len(c), n * 2) - print("dd: increase granularity to %d" % next_n) - cbar_offset = (cbar_offset * next_n) / n - - c = next_c - n = next_n - run = run + 1 - - def ddmin(self, c): - return self.ddgen(c, 1, 0) - - def ddmax(self, c): - return self.ddgen(c, 0, 1) - - def ddmix(self, c): - return self.ddgen(c, 1, 1) - - - # General delta debugging (new TSE version) - def dddiff(self, c): - n = 2 - - if self.debug_dd: - print("dddiff(%s, %d)..." % (self.pretty(c), n)) - - outcome = self._dddiff([], c, n) - - if self.debug_dd: - print("dddiff(%s, %d) = %r" % (self.pretty(c), n, outcome)) - - return outcome - - def _dddiff(self, c1, c2, n): - run = 1 - cbar_offset = 0 - - # We replace the tail recursion from the paper by a loop - while 1: - if self.debug_dd: - print("dd: c1 = %s" % (self.pretty(c1),)) - print("dd: c2 = %s" % (self.pretty(c2),)) - - if self.assume_axioms_hold: - t1 = self.PASS - t2 = self.FAIL - else: - t1 = self.test(c1) - t2 = self.test(c2) - - assert t1 == self.PASS - assert t2 == self.FAIL - assert self.__listsubseteq(c1, c2) - - c = self.__listminus(c2, c1) - - if self.debug_dd: - print("dd: c2 - c1 = %s" % (self.pretty(c),)) - - if n > len(c): - # No further minimizing - print("dd: done") - return c, c1, c2 - - self.report_progress(c, "dd") - - cs = self.split(c, n) - - print('') - print("dd (run #%d): trying %s" % (run, ' + '.join(map(str, cs)))) - print('') - - progress = 0 - - next_c1 = c1[:] - next_c2 = c2[:] - next_n = n - - # Check subsets - for j in range(n): - i = int((j + cbar_offset) % n) - - if self.debug_dd: - print("dd: trying %s" % (self.pretty(cs[i]),)) - - (t, csub) = self.test_and_resolve(cs[i], c1, c, self.REMOVE) - csub = self.__listunion(c1, csub) - - if t == self.FAIL and t1 == self.PASS: - # Found - progress = 1 - next_c2 = csub - next_n = 2 - cbar_offset = 0 - - if self.debug_dd: - print("dd: reduce c2 to %d deltas: %s" % (len(next_c2), self.pretty(next_c2))) - break - - if t == self.PASS and t2 == self.FAIL: - # Reduce to complement - progress = 1 - next_c1 = csub - next_n = max(next_n - 1, 2) - cbar_offset = i - - if self.debug_dd: - print("dd: increase c1 to %d deltas: %s", (len(next_c1), self.pretty(next_c1))) - break - - - csub = self.__listminus(c, cs[i]) - (t, csub) = self.test_and_resolve(csub, c1, c, self.ADD) - csub = self.__listunion(c1, csub) - - if t == self.PASS and t2 == self.FAIL: - # Found - progress = 1 - next_c1 = csub - next_n = 2 - cbar_offset = 0 - - if self.debug_dd: - print("dd: increase c1 to %d deltas: %s" % (len(next_c1), self.pretty(next_c1))) - break - - if t == self.FAIL and t1 == self.PASS: - # Increase - progress = 1 - next_c2 = csub - next_n = max(next_n - 1, 2) - cbar_offset = i - - if self.debug_dd: - print("dd: reduce c2 to %d deltas: %s" % (len(next_c2), self.pretty(next_c2))) - break - - if progress: - self.report_progress(self.__listminus(next_c2, next_c1), "dd") - else: - if n >= len(c): - # No further minimizing - print("dd: done") - return c, c1, c2 - - next_n = min(len(c), n * 2) - print("dd: increase granularity to %d" % next_n) - cbar_offset = (cbar_offset * next_n) / n - - c1 = next_c1 - c2 = next_c2 - n = next_n - run = run + 1 - - def dd(self, c): - return self.dddiff(c) # Backwards compatibility - - - - - -if __name__ == '__main__': - # Test the outcome cache - oc_test() - - # Define our own DD class, with its own test method - class MyDD(DD): - def _test_a(self, c): - "Test the configuration C. Return PASS, FAIL, or UNRESOLVED." - - # Just a sample - # if 2 in c and not 3 in c: - # return self.UNRESOLVED - # if 3 in c and not 7 in c: - # return self.UNRESOLVED - if 7 in c and not 2 in c: - return self.UNRESOLVED - if 5 in c and 8 in c: - return self.FAIL - return self.PASS - - def _test_b(self, c): - if not c: - return self.PASS - if 1 in c and 2 in c and 3 in c and 4 in c and \ - 5 in c and 6 in c and 7 in c and 8 in c: - return self.FAIL - return self.UNRESOLVED - - def _test_c(self, c): - if 1 in c and 2 in c and 3 in c and 4 in c and \ - 6 in c and 8 in c: - if 5 in c and 7 in c: - return self.UNRESOLVED - else: - return self.FAIL - if 1 in c or 2 in c or 3 in c or 4 in c or \ - 6 in c or 8 in c: - return self.UNRESOLVED - return self.PASS - - def __init__(self): - self._test = self._test_c - DD.__init__(self) - - - print("WYNOT - a tool for delta debugging.") - mydd = MyDD() - # mydd.debug_test = 1 # Enable debugging output - # mydd.debug_dd = 1 # Enable debugging output - # mydd.debug_split = 1 # Enable debugging output - # mydd.debug_resolve = 1 # Enable debugging output - - # mydd.cache_outcomes = 0 - # mydd.monotony = 0 - - print("Minimizing failure-inducing input...") - c = mydd.ddmin([1, 2, 3, 4, 5, 6, 7, 8]) # Invoke DDMIN - print("The 1-minimal failure-inducing input is %s" % (c,)) - print("Removing any element will make the failure go away.") - print('') - - print("Computing the failure-inducing difference...") - (c, c1, c2) = mydd.dd([1, 2, 3, 4, 5, 6, 7, 8]) # Invoke DD - print("The 1-minimal failure-inducing difference is %s" % (c,)) - print("%s passes, %s fails" % (c1, c2)) - - - -# Local Variables: -# mode: python -# End: diff --git a/INSTALL.txt b/INSTALL.txt index 94d6a3ecb..b2770aefc 100644 --- a/INSTALL.txt +++ b/INSTALL.txt @@ -32,7 +32,7 @@ Try something like :: - sudo port install py27-lxml + sudo port install py39-lxml To install a newer version or to install lxml on other systems, see below. @@ -41,7 +41,10 @@ see below. Requirements ------------ -You need Python 2.7 or 3.4+. +You need Python 3.9+ for lxml 7.0 and later. +You need Python 3.8+ for lxml 6.0 and later. +You need Python 3.6+ for lxml 5.0 and later. +lxml versions before 5.0 support Python 2.7 and 3.6+. Unless you are using a static binary distribution (e.g. from a Windows binary installer), lxml requires libxml2 and libxslt to @@ -90,7 +93,7 @@ To install a specific version, either download the distribution manually and let pip install that, or pass the desired version to pip:: - pip install lxml==3.4.2 + pip install lxml==5.0.0 .. _pip: http://pypi.python.org/pypi/pip @@ -105,14 +108,15 @@ the ``CFLAGS`` environment variable:: MS Windows .......... -For MS Windows, recent lxml releases feature community donated -binary distributions, although you might still want to take a look -at the related `FAQ entry `_. -If you fail to build lxml on your MS Windows system from the signed -and tested sources that we release, consider using the binary builds -from PyPI or the `unofficial Windows binaries -`_ -that Christoph Gohlke generously provides. +For MS Windows, we try to provide binary wheels with reasonably up-to-date +libraries, although you might still want to take a look at the related +`FAQ entry `_. +Since it is generally difficult to build software on Windows, the library +versions (libxml2, libxslt, libiconv, zlib) might not always be at the +same version level as the builds on Linux or macOS. This usually means +that the `WinLibs project `_ +has not updated their repositories yet. If you need a more recent version, +please file a ticket on their side to update it. Linux ..... @@ -131,8 +135,13 @@ both libraries automatically in their latest version, e.g. MacOS-X ....... -On MacOS-X, use the following to build the source distribution, -and make sure you have a working Internet connection, as this will +On MacOS-X, we provide binary wheels ("universal2" for Python 3.9+), +so just use:: + + sudo pip3 install lxml + +To build the source distribution, use the following and +make sure you have a working Internet connection, as this will download libxml2 and libxslt in order to build them:: STATIC_DEPS=true sudo pip install lxml @@ -185,7 +194,7 @@ packages, too. Source builds on MS Windows --------------------------- -Most MS Windows systems lack the necessarily tools to build software, +Most MS Windows systems lack the necessary tools to build software, starting with a C compiler already. Microsoft leaves it to users to install and configure them, which is usually not trivial and means that distributors cannot rely on these dependencies being available diff --git a/LICENSE.txt b/LICENSE.txt index a76d0ed5a..0bdf03913 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,3 +1,5 @@ +BSD 3-Clause License + Copyright (c) 2004 Infrae. All rights reserved. Redistribution and use in source and binary forms, with or without @@ -6,7 +8,7 @@ met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the diff --git a/LICENSES.txt b/LICENSES.txt index 9f97c18aa..e8946e1df 100644 --- a/LICENSES.txt +++ b/LICENSES.txt @@ -2,8 +2,15 @@ lxml is copyright Infrae and distributed under the BSD license (see doc/licenses/BSD.txt), with the following exceptions: Some code, such a selftest.py, selftest2.py and -src/lxml/_elementpath.py are derived from ElementTree and +src/lxml/elementpath.pxi are derived from ElementTree and cElementTree. See doc/licenses/elementtree.txt for the license text. +Newer versions reuse code from CPython's 'xml.etree' implementation, +licensed under the PSF-License v2. +See https://docs.python.org/3/license.html + +A copy of difflib is used as part of lxml.html, forked from the CPython +standard library and licensed under the PSF-License v2. +See https://docs.python.org/3/license.html lxml.cssselect and lxml.html are copyright Ian Bicking and distributed under the BSD license (see doc/licenses/BSD.txt). @@ -16,14 +23,105 @@ of the rest of the package. The isoschematron implementation uses several XSL and RelaxNG resources: * The (XML syntax) RelaxNG schema for schematron, copyright International - Organization for Standardization (see + Organization for Standardization (see src/lxml/isoschematron/resources/rng/iso-schematron.rng for the license text) * The skeleton iso-schematron-xlt1 pure-xslt schematron implementation xsl stylesheets, copyright Rick Jelliffe and Academia Sinica Computing - Center, Taiwan (see the xsl files here for the license text: + Center, Taiwan (see the xsl files here for the license text: src/lxml/isoschematron/resources/xsl/iso-schematron-xslt1/) * The xsd/rng schema schematron extraction xsl transformations are unlicensed - and copyright the respective authors as noted (see + and copyright the respective authors as noted (see src/lxml/isoschematron/resources/xsl/RNG2Schtrn.xsl and src/lxml/isoschematron/resources/xsl/XSD2Schtrn.xsl) + + +Binary wheels +------------- + +The officiall distributed binary wheels include bundled versions of the libraries +zlib, iconv, libxml2, libxslt and libexslt. +These libraries are linked, used and distributed under their following +respective licenses: + +**zlib**: zlib-license, https://www.zlib.net/zlib_license.html + +**iconv**: LGPL 2.1, https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html + +**libxml2**: MIT + +Except where otherwise noted in the source code (e.g. the files dict.c and +list.c, which are covered by a similar licence but with different Copyright +notices) all the files are: + + Copyright (C) 1998-2012 Daniel Veillard. All Rights Reserved. + Copyright (C) The Libxml2 Contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is fur- +nished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FIT- +NESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +**libxslt**: MIT + + Copyright (C) 2001-2002 Daniel Veillard. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is fur- +nished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FIT- +NESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +DANIEL VEILLARD BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CON- +NECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of Daniel Veillard shall not +be used in advertising or otherwise to promote the sale, use or other deal- +ings in this Software without prior written authorization from him. + +**libexslt**: MIT + + Copyright (C) 2001-2002 Thomas Broyer, Charlie Bozeman and Daniel Veillard. + All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is fur- +nished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FIT- +NESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CON- +NECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of the authors shall not +be used in advertising or otherwise to promote the sale, use or other deal- +ings in this Software without prior written authorization from him. diff --git a/MANIFEST.in b/MANIFEST.in index f05c25735..1592ed3e7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -15,5 +15,6 @@ recursive-include src/lxml/html/tests *.data *.txt recursive-include samples *.xml recursive-include benchmark *.py recursive-include doc *.py *.txt *.html *.css *.xml *.mgp pubkey.asc Makefile +recursive-include doc/html/apidoc *.js *.png *.inv recursive-include doc/s5/ui *.gif *.htc *.png *.js recursive-include doc/s5/ep2008 *.py *.png *.rng diff --git a/Makefile b/Makefile index 1e0a9119a..4fcaedd76 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,18 @@ -PYTHON?=python -PYTHON3?=python3 -TESTFLAGS=-p -v +PYTHON?=python3 +TESTFLAGS=-p -vv TESTOPTS= SETUPFLAGS= -LXMLVERSION:=$(shell $(PYTHON3) -c 'import re; print(re.findall(r"__version__\s*=\s*\"([^\"]+)\"", open("src/lxml/__init__.py").read())[0])' ) +TSAN_FLAGS=-DLXML_DEBUG_ATOMICS=1 -fsanitize=thread +LXMLVERSION:=$(shell $(PYTHON) -c 'import re; print(re.findall(r"__version__\s*=\s*\"([^\"]+)\"", open("src/lxml/__init__.py").read())[0])' ) -PARALLEL?=$(shell $(PYTHON) -c 'import sys; print("-j7" if sys.version_info >= (3, 5) else "")' ) -PARALLEL3?=$(shell $(PYTHON3) -c 'import sys; print("-j7" if sys.version_info >= (3, 5) else "")' ) PYTHON_WITH_CYTHON?=$(shell $(PYTHON) -c 'import Cython.Build.Dependencies' >/dev/null 2>/dev/null && echo " --with-cython" || true) -PY3_WITH_CYTHON?=$(shell $(PYTHON3) -c 'import Cython.Build.Dependencies' >/dev/null 2>/dev/null && echo " --with-cython" || true) CYTHON_WITH_COVERAGE?=$(shell $(PYTHON) -c 'import Cython.Coverage; import sys; assert not hasattr(sys, "pypy_version_info")' >/dev/null 2>/dev/null && echo " --coverage" || true) -CYTHON3_WITH_COVERAGE?=$(shell $(PYTHON3) -c 'import Cython.Coverage; import sys; assert not hasattr(sys, "pypy_version_info")' >/dev/null 2>/dev/null && echo " --coverage" || true) + +BUILD_COMMAND=setup.py $(SETUPFLAGS) build_ext $(PYTHON_WITH_CYTHON) --warnings $(subst --,--with-,$(CYTHON_WITH_COVERAGE)) -j7 PYTHON_BUILD_VERSION ?= * -MANYLINUX_LIBXML2_VERSION=2.9.14 -MANYLINUX_LIBXSLT_VERSION=1.1.35 +MANYLINUX_LIBXML2_VERSION=2.14.6 +MANYLINUX_LIBXSLT_VERSION=1.1.43 MANYLINUX_CFLAGS=-O3 -g1 -pipe -fPIC -flto MANYLINUX_LDFLAGS=-flto @@ -26,20 +24,25 @@ MANYLINUX_IMAGES= \ manylinux2014_aarch64 \ manylinux_2_24_aarch64 \ manylinux_2_24_ppc64le \ + manylinux_2_28_x86_64 \ + manylinux_2_28_aarch64 \ + manylinux_2_28_ppc64le \ manylinux_2_24_s390x \ musllinux_1_1_x86_64 \ musllinux_1_1_aarch64 -.PHONY: all inplace inplace3 rebuild-sdist sdist build require-cython wheel_manylinux wheel +.PHONY: all inplace rebuild-sdist sdist build require-cython wheel_manylinux wheel all: inplace # Build in-place inplace: - $(PYTHON) setup.py $(SETUPFLAGS) build_ext -i $(PYTHON_WITH_CYTHON) --warnings $(subst --,--with-,$(CYTHON_WITH_COVERAGE)) $(PARALLEL) + $(PYTHON) $(BUILD_COMMAND) -i -inplace3: - $(PYTHON3) setup.py $(SETUPFLAGS) build_ext -i $(PY3_WITH_CYTHON) --warnings $(subst --,--with-,$(CYTHON3_WITH_COVERAGE)) $(PARALLEL3) +tsan: + CFLAGS="$$CFLAGS $(TSAN_FLAGS)" \ + TSAN_OPTIONS="suppressions=tools/tsan.supp" \ + $(PYTHON) $(BUILD_COMMAND) -i rebuild-sdist: require-cython rm -f dist/lxml-$(LXMLVERSION).tar.gz @@ -52,7 +55,7 @@ dist/lxml-$(LXMLVERSION).tar.gz: sdist: dist/lxml-$(LXMLVERSION).tar.gz build: - $(PYTHON) setup.py $(SETUPFLAGS) build $(PYTHON_WITH_CYTHON) + $(PYTHON) setup.py $(SETUPFLAGS) build $(PYTHON_WITH_CYTHON) --warnings require-cython: @[ -n "$(PYTHON_WITH_CYTHON)" ] || { \ @@ -72,6 +75,7 @@ wheel_%: dist/lxml-$(LXMLVERSION).tar.gz -e RANLIB=gcc-ranlib \ -e CFLAGS="$(MANYLINUX_CFLAGS) $(if $(patsubst %aarch64,,$@),-march=core2,-march=armv8-a -mtune=cortex-a72)" \ -e LDFLAGS="$(MANYLINUX_LDFLAGS)" \ + -e STATIC_DEPS="${STATIC_DEPS}" \ -e LIBXML2_VERSION="$(MANYLINUX_LIBXML2_VERSION)" \ -e LIBXSLT_VERSION="$(MANYLINUX_LIBXSLT_VERSION)" \ -e PYTHON_BUILD_VERSION="$(PYTHON_BUILD_VERSION)" \ @@ -80,10 +84,10 @@ wheel_%: dist/lxml-$(LXMLVERSION).tar.gz bash /io/tools/manylinux/build-wheels.sh /io/$< wheel: - $(PYTHON) setup.py $(SETUPFLAGS) bdist_wheel $(PYTHON_WITH_CYTHON) + $(PYTHON) setup.py $(SETUPFLAGS) bdist_wheel $(PYTHON_WITH_CYTHON) --warnings wheel_static: - $(PYTHON) setup.py $(SETUPFLAGS) bdist_wheel $(PYTHON_WITH_CYTHON) --static-deps + $(PYTHON) setup.py $(SETUPFLAGS) bdist_wheel $(PYTHON_WITH_CYTHON) --warnings --static-deps test_build: build $(PYTHON) test.py $(TESTFLAGS) $(TESTOPTS) @@ -91,9 +95,6 @@ test_build: build test_inplace: inplace $(PYTHON) test.py $(TESTFLAGS) $(TESTOPTS) $(CYTHON_WITH_COVERAGE) -test_inplace3: inplace3 - $(PYTHON3) test.py $(TESTFLAGS) $(TESTOPTS) $(CYTHON3_WITH_COVERAGE) - valgrind_test_inplace: inplace valgrind --tool=memcheck --leak-check=full --num-callers=30 --suppressions=valgrind-python.supp \ $(PYTHON) test.py @@ -104,8 +105,8 @@ fuzz: clean CFLAGS="$$CFLAGS -fsanitize=fuzzer-no-link -g2" \ CXX="/usr/bin/clang++" \ CXXFLAGS="-fsanitize=fuzzer-no-link" \ - inplace3 - $(PYTHON3) src/lxml/tests/fuzz_xml_parse.py + inplace + $(PYTHON) src/lxml/tests/fuzz_xml_parse.py gdb_test_inplace: inplace @echo "file $(PYTHON)\nrun test.py" > .gdb.command @@ -123,31 +124,32 @@ ftest_build: build ftest_inplace: inplace $(PYTHON) test.py -f $(TESTFLAGS) $(TESTOPTS) -apidoc: apidocclean inplace3 - @[ -x "`which sphinx-apidoc`" ] \ +apidoc: apidocclean inplace + @[ -x "`command -v sphinx-apidoc`" ] \ && (echo "Generating API docs ..." && \ - PYTHONPATH=src:$(PYTHONPATH) sphinx-apidoc -e -P -T -o doc/api src/lxml \ + PYTHONPATH=src:$(PYTHONPATH) sphinx-apidoc -e -P -T -d1 -o doc/api src/lxml \ "*includes" "*tests" "*pyclasslookup.py" "*usedoctest.py" "*html/_html5builder.py" \ + "*html/_diff*" "*html/_setmixin*" \ "*.so" "*.pyd") \ || (echo "not generating Sphinx autodoc API rst files") -apihtml: apidoc inplace3 - @[ -x "`which sphinx-build`" ] \ +apihtml: apidoc inplace + @[ -x "`command -v sphinx-build`" ] \ && (echo "Generating API docs ..." && \ make -C doc/api html) \ || (echo "not generating Sphinx autodoc API documentation") -website: inplace3 docclean - PYTHONPATH=src:$(PYTHONPATH) $(PYTHON3) doc/mkhtml.py doc/html . ${LXMLVERSION} +website: inplace docclean + PYTHONPATH=src:$(PYTHONPATH) $(PYTHON) doc/mkhtml.py doc/html . ${LXMLVERSION} html: apihtml website s5 s5: $(MAKE) -C doc/s5 slides -apipdf: apidoc inplace3 +apipdf: apidoc inplace rm -fr doc/api/_build - @[ -x "`which sphinx-build`" ] \ + @[ -x "`command -v sphinx-build`" ] \ && (echo "Generating API PDF docs ..." && \ make -C doc/api latexpdf) \ || (echo "not generating Sphinx autodoc API PDF documentation") @@ -164,8 +166,6 @@ pdf: apipdf pdfclean test: test_inplace -test3: test_inplace3 - valtest: valgrind_test_inplace gdbtest: gdb_test_inplace @@ -175,7 +175,7 @@ bench: bench_inplace ftest: ftest_inplace clean: - find . \( -name '*.o' -o -name '*.so' -o -name '*.py[cod]' -o -name '*.dll' \) -exec rm -f {} \; + find src \( -name '*.o' -o -name '*.so' -o -name '*.py[cod]' -o -name '*.dll' \) -exec rm -f {} \; rm -rf build docclean: diff --git a/README.rst b/README.rst index e8705ab92..96cd4462a 100644 --- a/README.rst +++ b/README.rst @@ -8,7 +8,7 @@ For an introduction and further documentation, see `doc/main.txt`_. For installation information, see `INSTALL.txt`_. -For issue tracker, see https://bugs.launchpad.net/lxml +For the issue tracker, see https://bugs.launchpad.net/lxml Support the project ------------------- @@ -63,22 +63,44 @@ Crypto currencies do not fit into that ambition. .. _`doc/main.txt`: https://github.com/lxml/lxml/blob/master/doc/main.txt .. _`INSTALL.txt`: http://lxml.de/installation.html -`Travis-CI `_ and `AppVeyor `_ -support the lxml project with their build and CI servers. -Jetbrains supports the lxml project by donating free licenses of their -`PyCharm IDE `_. -Another supporter of the lxml project is -`COLOGNE Webdesign `_. +`GitHub Actions `_ +supports the lxml project with their build and CI servers. Project income report --------------------- -* Total project income in 2021: EUR 4890.37 (407.53 € / month) +lxml has `well over 250 million downloads `_ +per month on PyPI. + +* Total project income in 2025: EUR 3868.43 (322.37 € / month, 1.46 € / 1,000,000 downloads) + + - Tidelift: EUR 2642.35 + - thanks.dev: EUR 449.78 + - Paypal: EUR 388.15 + +* Total project income in 2024: EUR 3032.06 (252.57 € / month, 2.09 € / 1,000,000 downloads) + + - Tidelift: EUR 2777.34 + - thanks.dev: EUR 205.77 + - Paypal: EUR 48.95 + +* Total project income in 2023: EUR 2821,16 (235.09 € / month, 2.93 € / 1,000,000 downloads) + + - Tidelift: EUR 2738.46 + - thanks.dev: EUR 44.60 + - Paypal: EUR 38.10 + +* Total project income in 2022: EUR 2566.38 (213.87 € / month, 3.56 € / 1,000,000 downloads) + + - Tidelift: EUR 2539.38 + - Paypal: EUR 24.32 + +* Total project income in 2021: EUR 4640.37 (386.70 € / month) - Tidelift: EUR 4066.66 - Paypal: EUR 223.71 - - other: EUR 600.00 + - other: EUR 350.00 * Total project income in 2020: EUR 6065,86 (506.49 € / month) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..08d5d6124 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,17 @@ +# Security Policy + +If you have discovered a security vulnerability in this project, please report it +privately. **Do not disclose it as a public issue.** This gives us time to work with you +to fix the issue before public exposure, reducing the chance that the exploit will be +used before a patch is released. + +Please submit the report as +[Github draft security advisory](https://github.com/lxml/lxml/security/advisories). + +Please provide the following information in your report: + +- A description of the vulnerability and its impact. +- How to reproduce the issue. + +This project is maintained by a few maintainers on a reasonable-effort basis. +As such, we ask that you give us 90 days to work on a fix before public exposure. diff --git a/TODO.txt b/TODO.txt index d51ef6964..d9ca17f15 100644 --- a/TODO.txt +++ b/TODO.txt @@ -8,6 +8,13 @@ lxml In general ---------- +* libxml2 2.14: + + disableSAX -> xmlCtxtIsStopped + replaceEntities -> XML_PARSE_NOENT + progressive (unbenutzt) + node -> nicht mehr deprecated aber Benutzung unerwünscht + * more testing on multi-threading * better exception messages for XPath and schemas based on error log, diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 344019035..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,52 +0,0 @@ -version: 1.0.{build} -image: Visual Studio 2019 - -environment: - matrix: - - python: 310 - - python: 310-x64 - - python: 39 - - python: 39-x64 - - python: 27 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2013 - - python: 27-x64 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2013 - - python: 38 - - python: 38-x64 - - python: 37 - - python: 37-x64 - - python: 36 - - python: 36-x64 - - python: 35 - - python: 35-x64 - - python: 310 - arch: arm64 - env: STATIC_DEPS=true - - python: 39 - arch: arm64 - env: STATIC_DEPS=true - - python: 38 - arch: arm64 - env: STATIC_DEPS=true - -install: - - SET PATH=C:\\Python%PYTHON%;c:\\Python%PYTHON%\\scripts;%PATH% - - ps: | - $env:PYTHON = "C:\\Python$($env:PYTHON)" - if (-not (Test-Path $env:PYTHON)) { - curl -o install_python.ps1 https://raw.githubusercontent.com/matthew-brett/multibuild/11a389d78892cf90addac8f69433d5e22bfa422a/install_python.ps1 - .\\install_python.ps1 - } - # remove the above when appveyor has proper Python 3.8 support - - python -m pip.__main__ install -U pip wheel setuptools - - pip install -r requirements.txt - -build: off -build_script: - - python -u setup.py bdist_wheel --static-deps - - python -u setup.py build_ext --inplace --static-deps - - python -u test.py -vv -p - -test: off -test_script: - - ps: Get-ChildItem dist\*.whl | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } diff --git a/benchmark/bench_etree.py b/benchmark/bench_etree.py index 69ac5208e..0769a6436 100644 --- a/benchmark/bench_etree.py +++ b/benchmark/bench_etree.py @@ -4,36 +4,42 @@ import benchbase from benchbase import (with_attributes, with_text, onlylib, - serialized, children, nochange) + serialized, children, nochange, + anytree, widetree, widesubtree) TEXT = "some ASCII text" -UTEXT = u"some klingon: \F8D2" +UTEXT = u"some klingon: \uF8D2" ############################################################ # Benchmarks ############################################################ class BenchMark(benchbase.TreeBenchMark): + @anytree @nochange def bench_iter_children(self, root): for child in root: pass + @anytree @nochange def bench_iter_children_reversed(self, root): for child in reversed(root): pass + @anytree @nochange def bench_first_child(self, root): for i in self.repeat1000: child = root[0] + @anytree @nochange def bench_last_child(self, root): for i in self.repeat1000: child = root[-1] + @widetree @nochange def bench_middle_child(self, root): pos = len(root) // 2 @@ -125,11 +131,13 @@ def bench_iterparse_bytesIO_clear(self, root_xml): for event, element in self.etree.iterparse(f): element.clear() + @anytree def bench_append_from_document(self, root1, root2): # == "1,2 2,3 1,3 3,1 3,2 2,1" # trees 1 and 2, or 2 and 3, or ... for el in root2: root1.append(el) + @anytree def bench_insert_from_document(self, root1, root2): pos = len(root1)//2 for el in root2: @@ -138,19 +146,21 @@ def bench_insert_from_document(self, root1, root2): def bench_rotate_children(self, root): # == "1 2 3" # runs on any single tree independently - for i in range(100): + for i in range(50): el = root[0] del root[0] root.append(el) + @widetree def bench_reorder(self, root): - for i in range(1,len(root)//2): + for i in range(1,len(root)//2, 3): el = root[0] del root[0] root[-i:-i] = [ el ] + @widetree def bench_reorder_slice(self, root): - for i in range(1,len(root)//2): + for i in range(1,len(root)//2, 3): els = root[0:1] del root[0] root[-i:-i] = els @@ -158,31 +168,29 @@ def bench_reorder_slice(self, root): def bench_clear(self, root): root.clear() - @nochange - @children - def bench_has_children(self, children): - for child in children: - if child and child and child and child and child: - pass - + @widetree @nochange @children def bench_len(self, children): for child in children: map(len, repeat(child, 20)) + @widetree @children def bench_create_subelements(self, children): SubElement = self.etree.SubElement for child in children: SubElement(child, '{test}test') - def bench_append_elements(self, root): + @widetree + @children + def bench_append_elements(self, children): Element = self.etree.Element - for child in root: + for child in children: el = Element('{test}test') child.append(el) + @widetree @nochange @children def bench_makeelement(self, children): @@ -190,6 +198,7 @@ def bench_makeelement(self, children): for child in children: child.makeelement('{test}test', empty_attrib) + @widetree @nochange @children def bench_create_elements(self, children): @@ -197,6 +206,7 @@ def bench_create_elements(self, children): for child in children: Element('{test}test') + @widetree @children def bench_replace_children_element(self, children): Element = self.etree.Element @@ -204,25 +214,30 @@ def bench_replace_children_element(self, children): el = Element('{test}test') child[:] = [el] + @widetree @children def bench_replace_children(self, children): els = [ self.etree.Element("newchild") ] for child in children: child[:] = els + @widetree def bench_remove_children(self, root): - for child in root: + for child in islice(root, 0, 2**30, 7): root.remove(child) + @widetree def bench_remove_children_reversed(self, root): - for child in reversed(root): + for child in islice(reversed(root), 0, 2**30, 7): root.remove(child) + @widetree @children def bench_set_attributes(self, children): for child in children: child.set('a', 'bla') + @widetree @with_attributes(True) @children @nochange @@ -231,6 +246,7 @@ def bench_get_attributes(self, children): child.get('bla1') child.get('{attr}test1') + @widetree @children def bench_setget_attributes(self, children): for child in children: @@ -238,26 +254,31 @@ def bench_setget_attributes(self, children): for child in children: child.get('a') + @widetree @nochange def bench_root_getchildren(self, root): root.getchildren() + @widetree @nochange def bench_root_list_children(self, root): list(root) + @widesubtree @nochange @children def bench_getchildren(self, children): for child in children: child.getchildren() + @widesubtree @nochange @children def bench_get_children_slice(self, children): for child in children: child[:] + @widesubtree @nochange @children def bench_get_children_slice_2x(self, children): @@ -270,7 +291,7 @@ def bench_get_children_slice_2x(self, children): @with_attributes(True, False) @with_text(utext=True, text=True, no_text=True) def bench_deepcopy(self, children): - for child in children: + for child in islice(children, 0, 2**30, 7): copy.deepcopy(child) @nochange @@ -279,12 +300,14 @@ def bench_deepcopy(self, children): def bench_deepcopy_all(self, root): copy.deepcopy(root) + @widetree @nochange @children def bench_tag(self, children): for child in children: child.tag + @widetree @nochange @children def bench_tag_repeat(self, children): @@ -292,6 +315,7 @@ def bench_tag_repeat(self, children): for i in self.repeat100: child.tag + @widetree @nochange @with_text(utext=True, text=True, no_text=True) @children @@ -299,38 +323,44 @@ def bench_text(self, children): for child in children: child.text + @widetree @nochange @with_text(utext=True, text=True, no_text=True) @children def bench_text_repeat(self, children): for child in children: - for i in self.repeat500: + for i in self.repeat100: child.text + @widetree @children def bench_set_text(self, children): text = TEXT for child in children: child.text = text + @widetree @children def bench_set_utext(self, children): text = UTEXT for child in children: child.text = text + @widetree @nochange @onlylib('lxe') def bench_index(self, root): for child in root: root.index(child) + @widetree @nochange @onlylib('lxe') def bench_index_slice(self, root): for child in root[5:100]: root.index(child, 5, 100) + @widetree @nochange @onlylib('lxe') def bench_index_slice_neg(self, root): @@ -339,31 +369,31 @@ def bench_index_slice_neg(self, root): @nochange def bench_iter_all(self, root): - list(root.iter()) + all(el is not None for el in root.iter()) @nochange def bench_iter_one_at_a_time(self, root): - list(islice(root.iter(), 2**30, None)) + all(el is not None for el in islice(root.iter(), 2**30, None, 3)) @nochange def bench_iter_islice(self, root): - list(islice(root.iter(), 10, 110)) + all(el is not None for el in islice(root.iter(), 10, 110)) @nochange def bench_iter_tag(self, root): - list(islice(root.iter(self.SEARCH_TAG), 3, 10)) + all(el is not None for el in islice(root.iter(self.SEARCH_TAG), 3, 10)) @nochange def bench_iter_tag_all(self, root): - list(root.iter(self.SEARCH_TAG)) + all(el is not None for el in root.iter(self.SEARCH_TAG)) @nochange def bench_iter_tag_one_at_a_time(self, root): - list(islice(root.iter(self.SEARCH_TAG), 2**30, None)) + all(el is not None for el in islice(root.iter(self.SEARCH_TAG), 2**30, None, 3)) @nochange def bench_iter_tag_none(self, root): - list(root.iter("{ThisShould}NeverExist")) + all(el is not None for el in root.iter("{ThisShould}NeverExist")) @nochange def bench_iter_tag_text(self, root): @@ -377,6 +407,10 @@ def bench_findall(self, root): def bench_findall_child(self, root): root.findall(".//*/" + self.SEARCH_TAG) + @nochange + def bench_findall_child_predicate(self, root): + root.findall(f".//*[{self.SEARCH_TAG}]") + @nochange def bench_findall_tag(self, root): root.findall(".//" + self.SEARCH_TAG) @@ -394,15 +428,23 @@ def bench_xpath_path(self, root): @nochange def bench_iterfind(self, root): - list(root.iterfind(".//*")) + all(el is not None for el in root.iterfind(".//*")) @nochange def bench_iterfind_tag(self, root): - list(root.iterfind(".//" + self.SEARCH_TAG)) + all(el is not None for el in root.iterfind(".//" + self.SEARCH_TAG)) + + @nochange + def bench_iterfind_child_predicate(self, root): + all(el is not None for el in root.iterfind(f".//*[{self.SEARCH_TAG}]")) + + @nochange + def bench_iterfind_child_text_predicate(self, root): + all(el is not None for el in root.iterfind(f".//*[{self.SEARCH_TAG} = '']")) @nochange def bench_iterfind_islice(self, root): - list(islice(root.iterfind(".//*"), 10, 110)) + all(el is not None for el in islice(root.iterfind(".//*"), 10, 110)) _bench_xpath_single_xpath = None diff --git a/benchmark/bench_objectify.py b/benchmark/bench_objectify.py index 9b7126743..ac134001c 100644 --- a/benchmark/bench_objectify.py +++ b/benchmark/bench_objectify.py @@ -17,7 +17,7 @@ def __init__(self, lib): self.objectify = objectify parser = etree.XMLParser(remove_blank_text=True) lookup = objectify.ObjectifyElementClassLookup() - parser.setElementClassLookup(lookup) + parser.set_element_class_lookup(lookup) super(BenchMark, self).__init__(etree, parser) @nochange diff --git a/benchmark/bench_xpath.py b/benchmark/bench_xpath.py index 59cdc78cd..9c04ca8ff 100644 --- a/benchmark/bench_xpath.py +++ b/benchmark/bench_xpath.py @@ -29,7 +29,7 @@ def bench_xpath_class_repeat(self, children): def bench_xpath_element(self, root): xpath = self.etree.XPathElementEvaluator(root) for child in root: - xpath.evaluate("./*[1]") + xpath("./*[1]") @nochange @onlylib('lxe') diff --git a/benchmark/bench_xslt.py b/benchmark/bench_xslt.py index abfdb7c58..3b7cd021a 100644 --- a/benchmark/bench_xslt.py +++ b/benchmark/bench_xslt.py @@ -1,39 +1,12 @@ -from itertools import * - import benchbase from benchbase import onlylib + ############################################################ # Benchmarks ############################################################ class XSLTBenchMark(benchbase.TreeBenchMark): - @onlylib('lxe') - def bench_xslt_extensions_old(self, root): - tree = self.etree.XML("""\ - - TEST - - - - - - - - -""") - def return_child(_, elements): - return elements[0][0] - - extensions = {('testns', 'child') : return_child} - - transform = self.etree.XSLT(tree, extensions) - for i in range(10): - transform(root) - @onlylib('lxe') def bench_xslt_document(self, root): transform = self.etree.XSLT(self.etree.XML("""\ @@ -52,5 +25,6 @@ def bench_xslt_document(self, root): """)) transform(root) + if __name__ == '__main__': benchbase.main(XSLTBenchMark) diff --git a/benchmark/benchbase.py b/benchmark/benchbase.py index a9f9ad857..4fe729512 100644 --- a/benchmark/benchbase.py +++ b/benchmark/benchbase.py @@ -1,23 +1,15 @@ import sys, re, string, copy, gc -from itertools import * +import itertools import time - -try: - izip -except NameError: - izip = zip # Py3 - -def exec_(code, glob): - if sys.version_info[0] >= 3: - exec(code, glob) - else: - exec("exec code in glob") +from contextlib import contextmanager +from functools import partial TREE_FACTOR = 1 # increase tree size with '-l / '-L' cmd option +DEFAULT_REPEAT = 7 _TEXT = "some ASCII text" * TREE_FACTOR -_UTEXT = u"some klingon: \F8D2" * TREE_FACTOR +_UTEXT = u"some klingon: \uF8D2" * TREE_FACTOR _ATTRIBUTES = { '{attr}test1' : _TEXT, '{attr}test2' : _TEXT, @@ -99,6 +91,22 @@ def nochange(function): function.NO_CHANGE = True return function +def anytree(function): + "Decorator for benchmarks that do not depend on the concrete tree" + function.ANY_TREE = True + return function + +def widetree(function): + "Decorator for benchmarks that use only tree 2" + function.TREES = "2" + return function + +def widesubtree(function): + "Decorator for benchmarks that use only tree 1" + function.TREES = "1" + return function + + ############################################################ # benchmark baseclass ############################################################ @@ -106,7 +114,7 @@ def nochange(function): class SkippedTest(Exception): pass -class TreeBenchMark(object): +class TreeBenchMark: atoz = string.ascii_lowercase repeat100 = range(100) repeat500 = range(500) @@ -198,7 +206,7 @@ def generate_elem(append, elem, level): } # create function object - exec_("\n".join(output), namespace) + exec("\n".join(output), namespace) return namespace["element_factory"] def _all_trees(self): @@ -250,7 +258,7 @@ def _setup_tree3(self, text, attributes): children = [root] for i in range(6 + TREE_FACTOR): children = [ SubElement(c, "{cdefg}a%05d" % (i%8), attributes) - for i,c in enumerate(chain(children, children, children)) ] + for i,c in enumerate(itertools.chain(children, children, children)) ] for child in children: child.text = text child.tail = text @@ -282,15 +290,27 @@ def benchmarks(self): for name in dir(self): if not name.startswith('bench_'): continue + method = getattr(self, name) + + serialized = getattr(method, 'STRING', False) + children = getattr(method, 'CHILDREN', False) + no_change = getattr(method, 'NO_CHANGE', False) + any_tree = getattr(method, 'ANY_TREE', False) + tree_sets = getattr(method, 'TREES', None) + if hasattr(method, 'LIBS') and self.lib_name not in method.LIBS: method_call = None else: method_call = method - if method.__doc__: + + if tree_sets: + tree_sets = tree_sets.split() + elif method.__doc__: tree_sets = method.__doc__.split() else: tree_sets = () + if tree_sets: tree_tuples = [list(map(int, tree_set.split(','))) for tree_set in tree_sets] @@ -302,11 +322,11 @@ def benchmarks(self): arg_count = method.__code__.co_argcount - 1 except AttributeError: arg_count = 1 - tree_tuples = self._permutations(all_trees, arg_count) - serialized = getattr(method, 'STRING', False) - children = getattr(method, 'CHILDREN', False) - no_change = getattr(method, 'NO_CHANGE', False) + if any_tree: + tree_tuples = [all_trees[-arg_count:]] + else: + tree_tuples = self._permutations(all_trees, arg_count) for tree_tuple in tree_tuples: for tn in sorted(getattr(method, 'TEXT', (0,))): @@ -372,49 +392,101 @@ def printSetupTimes(benchmark_suites): print(" T%d: %s" % (i+1, ' '.join("%6.4f" % t for t in tree_times))) print('') + +def autorange(bench_func, min_runtime=0.2, max_number=None): + i = 1 + # Quickly scale up by factors of 10. + # Note that this will be increasingly off for fast non-linear benchmarks, so we stop an order away. + time_taken = bench_func(i) + while time_taken * 130 < min_runtime: + i *= 10 + time_taken *= 10 + + last_min = 0. + while True: + for j in 1, 2, 5: + number = i * j + if max_number is not None and number >= max_number: + return max_number + time_taken = bench_func(number) + + if time_taken >= min_runtime: + if (time_taken - min_runtime) / (time_taken - last_min) > .4: + # Avoid large overshoots due to large j steps. + number -= i // (3 if j == 1 else 2 if j == 2 else 1) + return number + + last_min = time_taken + + i *= 10 + + +@contextmanager +def nogc(): + gc.collect() + gc.disable() + try: + yield + finally: + gc.enable() + + def runBench(suite, method_name, method_call, tree_set, tn, an, - serial, children, no_change): + serial, children, no_change, timer=time.perf_counter, repeat=DEFAULT_REPEAT): if method_call is None: raise SkippedTest - current_time = time.time - call_repeat = range(10) - + rebuild_trees = not no_change and not serial tree_builders = [ suite.tree_builder(tree, tn, an, serial, children) for tree in tree_set ] - rebuild_trees = not no_change and not serial - - args = tuple([ build() for build in tree_builders ]) - method_call(*args) # run once to skip setup overhead + def new_trees(count=range(len(tree_builders)), trees=[None] * len(tree_builders)): + for i in count: + trees[i] = tree_builders[i]() + return tuple(trees) + + if rebuild_trees: + def time_benchmark(loops): + t_all_calls = 0.0 + for _ in range(loops): + run_benchmark = partial(method_call, *new_trees()) + t_one_call = timer() + run_benchmark() + t_one_call = timer() - t_one_call + t_all_calls += t_one_call + return t_all_calls + else: + def time_benchmark(loops, run_benchmark=partial(method_call, *new_trees())): + _loops = range(loops) + t_one_call = timer() + for _ in _loops: + run_benchmark() + t_all_calls = timer() - t_one_call + return t_all_calls + + with nogc(): + time_benchmark(1) # run once for tree warm-up + + with nogc(): + # Adjust "min_runtime" to avoid long tree rebuild times for short benchmarks. + inner_loops = autorange( + time_benchmark, + min_runtime=0.1 if rebuild_trees else 0.2, + max_number=200 if rebuild_trees else None, + ) times = [] - for i in range(3): - gc.collect() - gc.disable() - t = -1 - for i in call_repeat: - if rebuild_trees: - args = [ build() for build in tree_builders ] - t_one_call = current_time() - method_call(*args) - t_one_call = current_time() - t_one_call - if t < 0: - t = t_one_call - else: - t = min(t, t_one_call) - times.append(1000.0 * t) - gc.enable() - if rebuild_trees: - args = () - args = () + for _ in range(repeat): + with nogc(): + t_one_call = time_benchmark(inner_loops) / inner_loops + times.append(1000.0 * t_one_call) # msec gc.collect() return times -def runBenchmarks(benchmark_suites, benchmarks): - for bench_calls in izip(*benchmarks): - for lib, (bench, benchmark_setup) in enumerate(izip(benchmark_suites, bench_calls)): +def runBenchmarks(benchmark_suites, benchmarks, repeat=DEFAULT_REPEAT): + for bench_calls in zip(*benchmarks): + for lib, (bench, benchmark_setup) in enumerate(zip(benchmark_suites, bench_calls)): bench_name = benchmark_setup[0] tree_set_name = build_treeset_name(*benchmark_setup[-6:-1]) sys.stdout.write("%-3s: %-28s (%-10s) " % ( @@ -422,7 +494,7 @@ def runBenchmarks(benchmark_suites, benchmarks): sys.stdout.flush() try: - result = runBench(bench, *benchmark_setup) + result = runBench(bench, *benchmark_setup, repeat=repeat) except SkippedTest: print("skipped") except KeyboardInterrupt: @@ -433,12 +505,14 @@ def runBenchmarks(benchmark_suites, benchmarks): print("failed: %s: %s" % (exc_type.__name__, exc_value)) exc_type = exc_value = None else: - print("%9.4f msec/pass, best of (%s)" % ( - min(result), ' '.join("%9.4f" % t for t in result))) + result.sort() + t_min, t_median, t_max = result[0], result[len(result) // 2], result[-1] + print(f"{t_min:9.4f} msec/pass, best of ({t_min:9.4f}, {t_median:9.4f}, {t_max:9.4f})") if len(benchmark_suites) > 1: print('') # empty line between different benchmarks + ############################################################ # Main program ############################################################ @@ -487,22 +561,6 @@ def main(benchmark_class): etree.ElementDefaultClassLookup()) if len(sys.argv) > 1: - if '-a' in sys.argv or '-c' in sys.argv: - # 'all' or 'C-implementations' ? - try: - sys.argv.remove('-c') - except ValueError: - pass - try: - import cElementTree as cET - _etrees.append(cET) - except ImportError: - try: - import xml.etree.cElementTree as cET - _etrees.append(cET) - except ImportError: - pass - try: # 'all' ? sys.argv.remove('-a') @@ -510,14 +568,10 @@ def main(benchmark_class): pass else: try: - from elementtree import ElementTree as ET + from xml.etree import ElementTree as ET _etrees.append(ET) except ImportError: - try: - from xml.etree import ElementTree as ET - _etrees.append(ET) - except ImportError: - pass + pass if not _etrees: print("No library to test. Exiting.") @@ -527,8 +581,7 @@ def main(benchmark_class): print("Preparing test suites and trees ...") selected = set( sys.argv[1:] ) - benchmark_suites, benchmarks = \ - buildSuites(benchmark_class, _etrees, selected) + benchmark_suites, benchmarks = buildSuites(benchmark_class, _etrees, selected) print("Running benchmark on", ', '.join(b.lib_name for b in benchmark_suites)) @@ -537,9 +590,8 @@ def main(benchmark_class): printSetupTimes(benchmark_suites) if callgrind_zero: - cmd = open("callgrind.cmd", 'w') - cmd.write('+Instrumentation\n') - cmd.write('Zero\n') - cmd.close() + with open("callgrind.cmd", 'w') as cmd: + cmd.write('+Instrumentation\n') + cmd.write('Zero\n') - runBenchmarks(benchmark_suites, benchmarks) + runBenchmarks(benchmark_suites, benchmarks, repeat=DEFAULT_REPEAT) diff --git a/benchmark/report.py b/benchmark/report.py new file mode 100644 index 000000000..55343a392 --- /dev/null +++ b/benchmark/report.py @@ -0,0 +1,172 @@ +""" +Report benchmark results from CSV files in Markdown format. +""" + +import csv +import itertools +import operator +import string + + +def unbreak(s): + return s.replace(' ', '\N{NO-BREAK SPACE}') + + +def concat_files(csv_files): + for csv_file in csv_files: + with open(csv_file) as f: + yield from f + + +def read_rows(csv_rows): + # CSV Format: + # benchmark_module, benchmark_name, params, revision_name, pyversion, flags, bm_time, diff + reader = csv.reader(csv_rows) + + # Sort by benchmark name. + rows = sorted(reader, key=operator.itemgetter(0, 1, 2)) + return rows + + +def time_in_seconds(time_string): + units = {"nsec": 1e-9, "usec": 1e-6, "msec": 1e-3, "sec": 1.0} + number, unit = time_string.split() + return float(number) * units[unit] + + +def warn_difference(reference, value, max_margin): + difference = abs(value - reference) / reference + is_better = value < reference + + if difference < max_margin: + return '' + if difference < max_margin * 2: + return ' \N{BLACK MEDIUM DOWN-POINTING TRIANGLE}' if is_better else ' \N{UP-POINTING RED TRIANGLE}' + if difference < max_margin * 3: + return ' \N{LARGE GREEN CIRCLE}' if is_better else ' \N{LARGE RED CIRCLE}' + else: + return ' \N{LARGE GREEN SQUARE}' if is_better else ' \N{LARGE RED SQUARE}' + + +def format_timings(tmin, diff, *, master_data=None, warn_margin=.1/3.): + warn = warn_difference(time_in_seconds(master_data[0]), time_in_seconds(tmin), warn_margin) if master_data else '' + diff_str = f" ({unbreak(diff.strip(' ()'))})" if diff else '' + return f"{unbreak(tmin)}{diff_str}{warn}" + + +def format_sizes(size, diff, *, master_data=None, warn_margin=.01): + warn = warn_difference(int(master_data[0]), int(size), warn_margin) if master_data else '' + diff_str = f" ({unbreak(diff.strip(' ()'))})" if diff else '' + return f"{size}{diff_str}{warn}" + + +def build_table(rows, title, data_formatter): + # Collect all revision names and Python versions, keeping their original order. + # (The set may not be the same for all benchmarks.) + revisions = list({row[3]: 1 for row in rows}) + python_versions = list({row[4]: 1 for row in rows}) + + # Prepare table column mapping and header. + pos = itertools.count(1) + column_map = { + (pyversion, revision, flags): next(pos) + for pyversion in python_versions + for flags in ('', 'L') + for revision in revisions + } + header = [title] + [ + f"Py{pyversion}{'-' if flags else ''}{flags}: {revision.replace('origin/', '')[:16]}" + for (pyversion, revision, flags) in column_map + ] + row_template = [''] * len(header) + + # For each benchmark, report all timings in separate columns. + table = [] + empty_column_indices = set(column_map.values()) + for (bm_module, bm_name, bm_params), bm_rows in itertools.groupby(rows, key=operator.itemgetter(0, 1, 2)): + row = row_template[:] + table.append(row) + + bm_rows = [ + (pyversion, revision_name, flags, pyversion + flags, data) + for _, _, _, revision_name, pyversion, flags, *data in bm_rows + ] + master_data_seen = { + version_key: data + for _, revision_name, _, version_key, data in bm_rows + if 'master' in revision_name + } + + bm_module = bm_module[6:] if bm_module.startswith('bench_') else bm_module + row[0] = f"{bm_module}:{bm_name}({bm_params.strip()})" + for pyversion, revision_name, flags, version_key, data in bm_rows: + column_index = column_map[(pyversion, revision_name, flags)] + empty_column_indices.discard(column_index) + master_data = master_data_seen.get(version_key) if 'HEAD' in revision_name else None + row[column_index] = data_formatter(*data, master_data=master_data) + + # Strip empty columns, highest to lowest. + for column_index in sorted(empty_column_indices, reverse=True): + del header[column_index] + for row in table: + del row[column_index] + + return header, table + + +def generate_markdown(header, table): + # Size the table columns. + column_lengths = [ + max(map(len, map(operator.itemgetter(i), itertools.chain([header], table)))) + for i in range(len(header)) + ] + + # Generate Markdown formatted table lines. + row_format = ("| {:<%ds}" + " | {:>%ds}" * (len(column_lengths) - 1) + " |\n") % tuple(column_lengths) + format_row = row_format.format + + yield format_row(*header) + yield format_row(*['-' * length for length in column_lengths]) + yield from itertools.starmap(format_row, table) + + +def parse_options(args): + from argparse import ArgumentParser, RawDescriptionHelpFormatter + parser = ArgumentParser( + description="Report benchmark numbers as markdown tables.", + formatter_class=RawDescriptionHelpFormatter, + ) + parser.add_argument( + "-t", "--type", + dest="type", default='timings', choices=['timings', 'sizes'], + help="The type of report.", + ) + parser.add_argument( + "csv_files", + nargs="*", default=[], + help="The CSV files to collect data from.", + ) + + return parser.parse_args(args) + + +def main(args): + options = parse_options(args) + + rows = read_rows(concat_files(options.csv_files)) + + if options.type == 'timings': + title = "Benchmark timings" + data_formatter = format_timings + else: + title = 'Module sizes' + data_formatter = format_sizes + + header, table = build_table(rows, title, data_formatter) + for line in generate_markdown(header, table): + print(line, end='') + + +if __name__ == "__main__": + import sys + main(sys.argv[1:]) diff --git a/benchmark/run_benchmarks.py b/benchmark/run_benchmarks.py new file mode 100644 index 000000000..38ee0388a --- /dev/null +++ b/benchmark/run_benchmarks.py @@ -0,0 +1,390 @@ +import collections +import io +import itertools +import logging +import os +import pathlib +import re +import shutil +import subprocess +import sys +import tempfile +import time +import zipfile + + +BENCHMARKS_DIR = pathlib.Path(__file__).parent + +BENCHMARK_FILES = sorted(BENCHMARKS_DIR.glob("bench_*.py")) + +ALL_BENCHMARKS = [bm.stem for bm in BENCHMARK_FILES] + +LIMITED_API_VERSION = max((3, 11), sys.version_info[:2]) + +PYTHON_VERSION = "%d.%d.%d" % sys.version_info[:3] +if hasattr(sys, '_is_gil_enabled') and not sys._is_gil_enabled(): + PYTHON_VERSION += 't' + + +try: + from distutils import sysconfig + DISTUTILS_CFLAGS = sysconfig.get_config_var('CFLAGS') +except ImportError: + DISTUTILS_CFLAGS = '' + + +parse_timings = re.compile( + r"(?P\w+):\s*" + r"(?P\w+)\s+" + r"\((?P[^)]+)\)\s*" + r"(?P[0-9.]+)\s+" + r"(?P.*)" +).match + + +def run(command, cwd=None, pythonpath=None, c_macros=None, extra_env=None, capture_output=True): + env = None + if pythonpath: + env = env or os.environ.copy() + env['PYTHONPATH'] = pythonpath + if extra_env: + env = env or os.environ.copy() + env.update(extra_env) + if c_macros: + env = env or os.environ.copy() + env['CFLAGS'] = env.get('CFLAGS', '') + " " + ' '.join(f" -D{macro}" for macro in c_macros) + + try: + return subprocess.run(command, cwd=cwd, check=True, capture_output=capture_output, env=env) + except subprocess.CalledProcessError as exc: + logging.error(f"Command failed: {' '.join(map(str, command))}\nOutput:\n{exc.stderr.decode()}") + raise + + +def copy_benchmarks(bm_dir: pathlib.Path, benchmarks=None): + bm_files = [] + shutil.copy(BENCHMARKS_DIR / 'benchbase.py', bm_dir / 'benchbase.py') + for bm_src_file in BENCHMARK_FILES: + if benchmarks and bm_src_file.stem not in benchmarks: + continue + bm_file = bm_dir / bm_src_file.name + for benchmark_file in BENCHMARKS_DIR.glob(bm_src_file.stem + ".*"): + shutil.copy(benchmark_file, bm_dir / benchmark_file.name) + bm_files.append(bm_file) + + return bm_files + + +def compile_lxml(lxml_dir: pathlib.Path, c_macros=None, env_vars=None): + rev_hash = get_git_rev(rev_dir=lxml_dir) + logging.info(f"Compiling lxml gitrev {rev_hash}") + run( + [sys.executable, "setup.py", "build_ext", "-i", "-j6"], + cwd=lxml_dir, + c_macros=c_macros, + extra_env=env_vars, + #capture_output=False, + ) + + +def get_git_rev(revision=None, rev_dir=None): + command = ["git", "describe", "--long"] + if revision: + command.append(revision) + output = run(command, cwd=rev_dir) + _, rev_hash = output.stdout.decode().strip().rsplit('-', 1) + return rev_hash[1:] + + +def git_clone(rev_dir, revision): + rev_hash = get_git_rev(revision) + run(["git", "clone", "-n", "--no-single-branch", ".", str(rev_dir)]) + run(["git", "checkout", rev_hash], cwd=rev_dir) + + +def copy_profile(bm_dir, module_name, profiler): + timestamp = int(time.time() * 1000) + profile_input = bm_dir / "profile.out" + data_file_name = f"{profiler}_{module_name}_{timestamp:X}.data" + + if profiler == 'callgrind': + bm_dir_str = str(bm_dir) + os.sep + with open(profile_input) as data_file_in: + with open(data_file_name, mode='w') as data_file_out: + for line in data_file_in: + if bm_dir_str in line: + # Remove absolute file paths to link to local file copy below. + line = line.replace(bm_dir_str, "") + data_file_out.write(line) + else: + shutil.move(profile_input, data_file_name) + + for result_file_name in (f"{module_name}.c", f"{module_name}.html"): + result_file = bm_dir / result_file_name + if result_file.exists(): + shutil.move(result_file, result_file_name) + + for ext in bm_dir.glob(f"{module_name}.*so"): + shutil.move(str(ext), ext.name) + + +def run_benchmark(bm_dir, module_name, pythonpath=None, profiler=None): + logging.info(f"Running benchmark '{module_name}'.") + + command = [] + + if profiler: + if profiler == 'perf': + command = ["perf", "record", "--quiet", "-g", "--output=profile.out"] + elif profiler == 'callgrind': + command = [ + "valgrind", "--tool=callgrind", + "--dump-instr=yes", "--collect-jumps=yes", + "--callgrind-out-file=profile.out", + ] + + command += [sys.executable, f"{module_name}.py"] + + output = run(command, cwd=bm_dir, pythonpath=pythonpath) + + if profiler: + copy_profile(bm_dir, module_name, profiler) + + lines = filter(None, output.stdout.decode().splitlines()) + for line in lines: + if line == "Setup times for trees in seconds:": + break + + other_lines = [] + timings = [] + for line in lines: + match = parse_timings(line) + if match: + timings.append((match['benchmark'], match['params'].strip(), match['lib'], float(match['besttime']), match['timings'])) + else: + other_lines.append(line) + + return other_lines, timings + + +def run_benchmarks(bm_dir, benchmarks, pythonpath=None, profiler=None): + timings = {} + for benchmark in benchmarks: + timings[benchmark] = run_benchmark(bm_dir, benchmark, pythonpath=pythonpath, profiler=profiler) + return timings + + +def benchmark_revisions(benchmarks, revisions, profiler=None, limited_revisions=(), deps_zipfile=None): + python_version = f"Python {PYTHON_VERSION}" + logging.info(f"### Comparing revisions in {python_version}: {' '.join(revisions)}.") + logging.info(f"CFLAGS={os.environ.get('CFLAGS', DISTUTILS_CFLAGS)}") + + hashes = {} + timings = {} + for revision in revisions: + rev_hash = get_git_rev(revision) + if rev_hash in hashes: + logging.info(f"### Ignoring revision '{revision}': same as '{hashes[rev_hash]}'") + continue + hashes[rev_hash] = revision + + logging.info(f"### Preparing benchmark run for lxml '{revision}'.") + timings[revision] = benchmark_revision( + revision, benchmarks, profiler, + deps_zipfile=deps_zipfile, + ) + + if revision in limited_revisions: + logging.info( + f"### Preparing benchmark run for lxml '{revision}' (Limited API {LIMITED_API_VERSION[0]}.{LIMITED_API_VERSION[1]}).") + timings['L-' + revision] = benchmark_revision( + revision, benchmarks, profiler, + deps_zipfile=deps_zipfile, + extra_env={ + 'LXML_LIMITED_API': f'{LIMITED_API_VERSION[0]}.{LIMITED_API_VERSION[1]}', + } + ) + + return timings + + +def cache_libs(lxml_dir, deps_zipfile, file_suffixes=None): + zip_content = set(deps_zipfile.namelist()) + for dir_path, _, filenames in itertools.chain((lxml_dir / "build" / "tmp").walk(), (lxml_dir / "libs").walk()): + for filename in filenames: + path = dir_path / filename + if file_suffixes is None or path.suffix in file_suffixes: + rel_path = path.relative_to(lxml_dir) + if str(rel_path) not in zip_content: + deps_zipfile.write(path, rel_path) + + +def benchmark_revision(revision, benchmarks, profiler=None, c_macros=None, extra_env=None, deps_zipfile=None): + with tempfile.TemporaryDirectory() as base_dir_str: + base_dir = pathlib.Path(base_dir_str) + lxml_dir = base_dir / "lxml" / revision + bm_dir = base_dir / "benchmarks" / revision + + git_clone(lxml_dir, revision=revision) + + bm_dir.mkdir(parents=True) + bm_files = copy_benchmarks(bm_dir, benchmarks) + + if deps_zipfile: + deps_zipfile.extractall(lxml_dir) + + compile_lxml(lxml_dir, c_macros=c_macros, env_vars=extra_env) + + if deps_zipfile: + cache_libs(lxml_dir, deps_zipfile) + + logging.info(f"### Running benchmarks for {revision}: {' '.join(bm.stem for bm in bm_files)}") + return run_benchmarks(bm_dir, benchmarks, pythonpath=f"{bm_dir}:{lxml_dir / 'src'}", profiler=profiler) + + +def report_revision_timings(rev_timings, csv_out=None): + units = {"nsec": 1e-9, "usec": 1e-6, "msec": 1e-3, "sec": 1.0} + scales = [(scale, unit) for unit, scale in reversed(units.items())] # biggest first + + def format_time(t): + pos_t = abs(t) + for scale, unit in scales: + if pos_t >= scale: + break + else: + raise RuntimeError(f"Timing is below nanoseconds: {t:f}") + return f"{t / scale :.3f} {unit}" + + timings_by_benchmark = collections.defaultdict(list) + setup_times = [] + for revision_name, bm_timings in rev_timings.items(): + for benchmark_module, (output, timings) in bm_timings.items(): + setup_times.append((benchmark_module, revision_name, output)) + for benchmark_name, params, lib, best_time, result_text in timings: + timings_by_benchmark[(benchmark_module, benchmark_name, params)].append((lib, revision_name, best_time, result_text)) + + setup_times.sort() + + for benchmark_module, revision_name, output in setup_times: + result = '\n'.join(output) + logging.info(f"Setup times for trees in seconds - {benchmark_module} / {revision_name}:\n{result}") + + differences = collections.defaultdict(list) + for (benchmark_module, benchmark_name, params), timings in timings_by_benchmark.items(): + logging.info(f"### Benchmark {benchmark_module} / {benchmark_name} ({params}):") + base_line = timings[0][2] + for lib, revision_name, bm_time, result_text in timings: + diff_str = "" + if base_line != bm_time: + pdiff = bm_time * 100 / base_line - 100 + differences[(lib, revision_name)].append((abs(pdiff), pdiff, bm_time - base_line, benchmark_module, benchmark_name, params)) + diff_str = f" {pdiff:+8.2f} %" + logging.info( + f" {lib:3} / {revision_name[:25]:25} = {bm_time:8.4f} {result_text}{diff_str}" + ) + if csv_out is not None: + is_limited = revision_name.startswith('L-') + csv_out.writerow([ + benchmark_module, benchmark_name, params, + revision_name[2:] if is_limited else revision_name, + PYTHON_VERSION, + 'L' if is_limited else '', + format_time(bm_time / 1000), diff_str, + ]) + + for (lib, revision_name), diffs in differences.items(): + diffs.sort(reverse=True) + diffs_by_sign = {True: [], False: []} + for diff in diffs: + diffs_by_sign[diff[1] < 0].append(diff) + + for is_win, diffs in diffs_by_sign.items(): + if not diffs or diffs[0][0] < 1.0: + continue + + logging.info(f"Largest {'gains' if is_win else 'losses'} for {revision_name}:") + cutoff = max(1.0, diffs[0][0] // 4) + for absdiff, pdiff, tdiff, benchmark_module, benchmark_name, params in diffs: + if absdiff < cutoff: + break + logging.info(f" {benchmark_module} / {benchmark_name:<25} ({params:>10}) {pdiff:+8.2f} % / {format_time(tdiff / 1000.0):>8}") + + +def parse_args(args): + from argparse import ArgumentParser, RawDescriptionHelpFormatter + parser = ArgumentParser( + description="Run benchmarks against different lxml tags/revisions.", + formatter_class=RawDescriptionHelpFormatter, + ) + parser.add_argument( + "-b", "--benchmarks", + dest="benchmarks", default=','.join(ALL_BENCHMARKS), + help="The list of benchmark selectors to run, simple substrings, separated by comma.", + ) + parser.add_argument( + "--with-limited", + dest="with_limited_api", action="append", default=[], + help="Also run the benchmarks for REVISION against the Limited C-API.", + ) + #parser.add_argument( + # "--with-elementtree", + # dest="with_elementtree", + # help="Include results for Python's xml.etree.ElementTree.", + #) + parser.add_argument( + "--perf", + dest="profiler", action="store_const", const="perf", default=None, + help="Run Linux 'perf record' on the benchmark process.", + ) + parser.add_argument( + "--callgrind", + dest="profiler", action="store_const", const="callgrind", default=None, + help="Run Valgrind's callgrind profiler on the benchmark process.", + ) + parser.add_argument( + "--report", + dest="report_csv", default=None, metavar="FILE", + help="Write a CSV report of the timings to FILE." + ) + parser.add_argument( + "revisions", + nargs="*", default=[], + help="The git revisions to check out and benchmark.", + ) + + return parser.parse_known_args(args) + + +if __name__ == '__main__': + options, cythonize_args = parse_args(sys.argv[1:]) + + logging.basicConfig( + stream=sys.stdout, + level=logging.INFO, + format="%(asctime)s %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + benchmark_selectors = set(bm.strip() for bm in options.benchmarks.split(",")) + benchmarks = [bm for bm in ALL_BENCHMARKS if any(selector in bm for selector in benchmark_selectors)] + if benchmark_selectors and not benchmarks: + logging.error("No benchmarks selected!") + sys.exit(1) + + deps_zipfile = zipfile.ZipFile(io.BytesIO(), mode='w') + cache_libs(BENCHMARKS_DIR.parent, deps_zipfile, file_suffixes=('.xz', '.gz', '.zip')) + + revisions = list({rev: rev for rev in (options.revisions + options.with_limited_api)}) # deduplicate in order + timings = benchmark_revisions( + benchmarks, revisions, + profiler=options.profiler, + limited_revisions=options.with_limited_api, + deps_zipfile=deps_zipfile, + ) + + if options.report_csv: + with open(options.report_csv, "w") as f: + import csv + report_revision_timings(timings, csv_out=csv.writer(f)) + else: + report_revision_timings(timings) diff --git a/bisect_crashes.py b/bisect_crashes.py deleted file mode 100644 index 7a3fe6cf6..000000000 --- a/bisect_crashes.py +++ /dev/null @@ -1,66 +0,0 @@ - -import os -import sys -import unittest - -# make sure we import test.py from the right place -script_path = os.path.abspath(os.path.dirname(sys.argv[0])) -sys.path.insert(0, script_path) - -test_base_path = os.path.join(script_path, 'src') -sys.path.insert(1, test_base_path) - -import test -from DD import DD - -cfg = test.Options() -cfg.verbosity = 0 -cfg.basedir = test_base_path -cfg.unit_tests = True - -def write(line, *args): - if args: - line = line % args - sys.stderr.write(line + '\n') - - -def find_tests(): - test_files = test.get_test_files(cfg) - return test.get_test_cases(test_files, cfg) - -class DDTester(DD): - def _test(self, test_cases): - if not test_cases: - return self.PASS - write('Running subset of %d tests %s', - len(test_cases), self.coerce(test_cases)) - test_cases = [ item[-1] for item in test_cases ] - pid = os.fork() - if not pid: - # child executes tests - runner = test.CustomTestRunner(cfg, None) - suite = unittest.TestSuite() - suite.addTests(test_cases) - os._exit( not runner.run(suite).wasSuccessful() ) - cid, retval = os.waitpid(pid, 0) - if retval: - write('exit status: %d, signal: %d', retval >> 8, retval % 0xFF) - if (retval % 0xFF) > 2: # signal received? - return self.FAIL - return self.PASS - - def coerce(self, test_cases): - if not test_cases: - return '[]' - test_cases = [ item[-1] for item in test_cases ] - return '[%s .. %s]' % (test_cases[0].id(), test_cases[-1].id()) - -def dd_tests(): - tests = find_tests() - write('Found %d tests', len(tests)) - dd = DDTester() - min_tests = dd.ddmin( list(enumerate(tests)) ) - return [ item[-1] for item in min_tests ] - -if __name__ == '__main__': - write('Failing tests:\n%s', '\n'.join([test.id() for test in dd_tests()])) diff --git a/buildlibxml.py b/buildlibxml.py index e0c558fad..fa3de69ac 100644 --- a/buildlibxml.py +++ b/buildlibxml.py @@ -1,15 +1,18 @@ -import os, re, sys, subprocess, platform +import hashlib +import json +import os +import platform +import re +import sys import tarfile -from distutils import log, version -from contextlib import closing, contextmanager +import time +from contextlib import closing from ftplib import FTP +from pathlib import Path -try: - from urlparse import urljoin, unquote, urlparse - from urllib import urlretrieve, urlopen, urlcleanup -except ImportError: - from urllib.parse import urljoin, unquote, urlparse - from urllib.request import urlretrieve, urlopen, urlcleanup +import urllib.error +from urllib.parse import urljoin, quote as urlquote, unquote, urlparse +from urllib.request import urlretrieve, urlopen, Request multi_make_options = [] try: @@ -19,61 +22,117 @@ if cpus > 5: cpus = 5 multi_make_options = ['-j%d' % (cpus+1)] -except: +except Exception: pass +# overridable to control script usage +sys_platform = sys.platform + + # use pre-built libraries on Windows -def download_and_extract_windows_binaries(destdir): - url = "https://github.com/lxml/libxml2-win-binaries/releases" - filenames = list(_list_dir_urllib(url)) - - release_path = "/download/%s/" % find_max_version( - "library release", filenames, re.compile(r"/releases/tag/([0-9.]+[0-9])$")) - url += release_path - filenames = [ - filename.rsplit('/', 1)[1] - for filename in filenames - if release_path in filename - ] +def read_file_digest(file): + buffer = bytearray(2**18) + view = memoryview(buffer) + from hashlib import sha256 + filehash = sha256() + with open(file, 'rb') as f: + while True: + size = f.readinto(buffer) + if not size: + break + filehash.update(view[:size]) + + return 'sha256:' + filehash.hexdigest() + + +def download_and_extract_windows_binaries(destdir, arch=None): # Check for native ARM64 build or the environment variable that is set by # Visual Studio for cross-compilation (same variable as setuptools uses) if platform.machine() == 'ARM64' or os.getenv('VSCMD_ARG_TGT_ARCH') == 'arm64': arch = "win-arm64" - elif sys.maxsize > 2**32: - arch = "win64" - else: + elif arch == 'win32' or (arch != 'win64' and sys.maxsize <= 2**32): arch = "win32" + else: + arch = "win64" - if sys.version_info < (3, 5): - arch = 'vs2008.' + arch + def build_libzip_name(libname, version): + return f"{libname}-{version}.{arch}.zip" - libs = {} - for libname in ['libxml2', 'libxslt', 'zlib', 'iconv']: - libs[libname] = "%s-%s.%s.zip" % ( - libname, - find_max_version(libname, filenames), - arch, + def read_latest_release(): + url = "https://api.github.com/repos/lxml/libxml2-win-binaries/releases?per_page=5" + releases, _ = read_url( + url, + accept="application/vnd.github+json", + as_json=True, + github_api_token=os.environ.get("GITHUB_API_TOKEN"), ) + max_release = {'tag_name': ''} + for release in releases: + if max_release['tag_name'] < release.get('tag_name', ''): + max_release = release + + return max_release + + def find_local_lib(libname, version): + if not version: + return None + libfn = build_libzip_name(libname, version) + destfile = os.path.join(destdir, libfn) + return libfn if os.path.exists(destfile) else None + if not os.path.exists(destdir): os.makedirs(destdir) - for libname, libfn in libs.items(): - srcfile = urljoin(url, libfn) - destfile = os.path.join(destdir, libfn) - if os.path.exists(destfile + ".keep"): - print('Using local copy of "{}"'.format(srcfile)) - else: - print('Retrieving "%s" to "%s"' % (srcfile, destfile)) - urlcleanup() # work around FTP bug 27973 in Py2.7.12+ + libs = {} + for libname in ['libxml2', 'libxslt', 'zlib', 'iconv']: + version = os.environ.get('LIBICONV_VERSION' if libname == 'iconv' else f"{libname.upper()}_VERSION") + libfn = find_local_lib(libname, version) + if libfn: + print(f'Using local copy of "{libfn}"') + libs[libname] = libfn + + if None in libs.values(): + # Need to gather version and download URL from winlibs release. + latest_release = read_latest_release() + arch_part = f'.{arch}.' + asset_files = { + asset['name']: (asset['size'], asset['digest']) + for asset in latest_release.get('assets', ()) + if arch_part in asset['name'] + } + release_tag = latest_release['tag_name'] + download_url = f"https://github.com/lxml/libxml2-win-binaries/releases/download/{urlquote(release_tag)}/" + + lib_file_names = list(asset_files) + for libname, libfn in libs.items(): + if libfn: + continue + version = find_max_version(libname, lib_file_names) + libfn = find_local_lib(libname, version) + if libfn: + libs[libname] = libfn + srcfile = urljoin(download_url, libfn) + print(f'Using local copy of "{srcfile}"') + continue + + # Need to download lib. + libfn = build_libzip_name(libname, version) + srcfile = urljoin(download_url, libfn) + destfile = os.path.join(destdir, libfn) + + print(f'Retrieving "{srcfile}" to "{destfile}"') urlretrieve(srcfile, destfile) - d = unpack_zipfile(destfile, destdir) - libs[libname] = d + libs[libname] = libfn - return libs + lib_dirs = { + libname: unpack_zipfile(os.path.join(destdir, libfn), destdir) + for libname, libfn in libs.items() + } + return lib_dirs def find_top_dir_of_zipfile(zipfile): @@ -97,20 +156,19 @@ def find_top_dir_of_zipfile(zipfile): def unpack_zipfile(zipfn, destdir): assert zipfn.endswith('.zip') import zipfile - print('Unpacking %s into %s' % (os.path.basename(zipfn), destdir)) - f = zipfile.ZipFile(zipfn) - try: + + print(f'Unpacking {os.path.basename(zipfn)} into {destdir}') + with zipfile.ZipFile(zipfn) as f: extracted_dir = os.path.join(destdir, find_top_dir_of_zipfile(f)) f.extractall(path=destdir) - finally: - f.close() + assert os.path.exists(extracted_dir), 'missing: %s' % extracted_dir return extracted_dir -def get_prebuilt_libxml2xslt(download_dir, static_include_dirs, static_library_dirs): - assert sys.platform.startswith('win') - libs = download_and_extract_windows_binaries(download_dir) +def get_prebuilt_libxml2xslt(download_dir, static_include_dirs, static_library_dirs, arch=None): + assert sys_platform.startswith('win') + libs = download_and_extract_windows_binaries(download_dir, arch=arch) for libname, path in libs.items(): i = os.path.join(path, 'include') l = os.path.join(path, 'lib') @@ -164,13 +222,28 @@ def _list_dir_ftplib(url): return parse_text_ftplist("\n".join(data)) -def _list_dir_urllib(url): - with closing(urlopen(url)) as res: +def read_url(url, decode=True, accept=None, as_json=False, github_api_token=None): + headers = {'User-Agent': 'https://github.com/lxml/lxml'} + if accept: + headers['Accept'] = accept + if github_api_token: + headers['authorization'] = "Bearer " + github_api_token + request = Request(url, headers=headers) + + with closing(urlopen(request)) as res: charset = _find_content_encoding(res) content_type = res.headers.get('Content-Type') data = res.read() - data = data.decode(charset) + if decode: + data = data.decode(charset) + if as_json: + data = json.loads(data) + return data, content_type + + +def _list_dir_urllib(url): + data, content_type = read_url(url) if content_type and content_type.startswith('text/html'): files = parse_html_filelist(data) else: @@ -178,26 +251,28 @@ def _list_dir_urllib(url): return files -def http_find_latest_version_directory(url): - with closing(urlopen(url)) as res: - charset = _find_content_encoding(res) - data = res.read() +def http_find_latest_version_directory(url, version=None): + data, _ = read_url(url) # e.g. directories = [ (int(v[0]), int(v[1])) - for v in re.findall(r' href=["\']([0-9]+)\.([0-9]+)/?["\']', data.decode(charset)) + for v in re.findall(r' href=["\']([0-9]+)\.([0-9]+)/?["\']', data) ] if not directories: return url - latest_dir = "%s.%s" % max(directories) + best_version = max(directories) + if version: + major, minor, _ = version.split(".", 2) + major, minor = int(major), int(minor) + if (major, minor) in directories: + best_version = (major, minor) + latest_dir = "%s.%s" % best_version return urljoin(url, latest_dir) + "/" def http_listfiles(url, re_pattern): - with closing(urlopen(url)) as res: - charset = _find_content_encoding(res) - data = res.read() - files = re.findall(re_pattern, data.decode(charset)) + data, _ = read_url(url) + files = re.findall(re_pattern, data) return files @@ -227,20 +302,31 @@ def tryint(s): return s -@contextmanager -def py2_tarxz(filename): - import tempfile - with tempfile.TemporaryFile() as tmp: - subprocess.check_call(["xz", "-dc", filename], stdout=tmp.fileno()) - tmp.seek(0) - with closing(tarfile.TarFile(fileobj=tmp)) as tf: - yield tf +ARCHIVE_HASHES = { + # Default hash algorithm is SHA-256. + # Prefix hash with e.g. "sha512:" for alternative algorithms. + filename: digest + for line in """ + c8b9bc81f8b590c33af8cc6c336dbff2f53409973588a351c95f1c621b13d09d libxml2-2.15.2.tar.xz + 7ce458a0affeb83f0b55f1f4f9e0e55735dbfc1a9de124ee86fb4a66b597203a libxml2-2.14.6.tar.xz + + 9acfe68419c4d06a45c550321b3212762d92f41465062ca4ea19e632ee5d216e libxslt-1.1.45.tar.xz + 5a3d6b383ca5afc235b171118e90f5ff6aa27e9fea3303065231a6d403f0183a libxslt-1.1.43.tar.xz + + 88dd96a8c0464eca144fc791ae60cd31cd8ee78321e67397e25fc095c4a19aa6 libiconv-1.19.tar.gz + 3b08f5f4f9b4eb82f151a7040bfd6fe6c6fb922efe4b1659c66ea933276965e8 libiconv-1.18.tar.gz + + bb329a0a2cd0274d05519d61c667c062e06990d72e125ee2dfa8de64f0119d16 zlib-1.3.2.tar.gz + """.strip().splitlines() + if len(line) > 64 + for digest, filename in [line.split()] +} def download_libxml2(dest_dir, version=None): """Downloads libxml2, returning the filename where the library was downloaded""" #version_re = re.compile(r'LATEST_LIBXML2_IS_([0-9.]+[0-9](?:-[abrc0-9]+)?)') - version_re = re.compile(r'libxml2-([0-9.]+[0-9]).tar.xz') + version_re = re.compile(r'libxml2-([0-9.]+[0-9])[.]tar[.]xz') filename = 'libxml2-%s.tar.xz' if version == "2.9.12": @@ -248,7 +334,7 @@ def download_libxml2(dest_dir, version=None): from_location = "https://gitlab.gnome.org/GNOME/libxml2/-/archive/dea91c97debeac7c1aaf9c19f79029809e23a353/" version = "dea91c97debeac7c1aaf9c19f79029809e23a353" else: - from_location = http_find_latest_version_directory(LIBXML2_LOCATION) + from_location = http_find_latest_version_directory(LIBXML2_LOCATION, version=version) return download_library(dest_dir, from_location, 'libxml2', version_re, filename, version=version) @@ -257,16 +343,16 @@ def download_libxml2(dest_dir, version=None): def download_libxslt(dest_dir, version=None): """Downloads libxslt, returning the filename where the library was downloaded""" #version_re = re.compile(r'LATEST_LIBXSLT_IS_([0-9.]+[0-9](?:-[abrc0-9]+)?)') - version_re = re.compile(r'libxslt-([0-9.]+[0-9]).tar.xz') + version_re = re.compile(r'libxslt-([0-9.]+[0-9])[.]tar[.]xz') filename = 'libxslt-%s.tar.xz' - from_location = http_find_latest_version_directory(LIBXSLT_LOCATION) + from_location = http_find_latest_version_directory(LIBXSLT_LOCATION, version=version) return download_library(dest_dir, from_location, 'libxslt', version_re, filename, version=version) def download_libiconv(dest_dir, version=None): """Downloads libiconv, returning the filename where the library was downloaded""" - version_re = re.compile(r'libiconv-([0-9.]+[0-9]).tar.gz') + version_re = re.compile(r'libiconv-([0-9.]+[0-9])[.]tar[.]gz') filename = 'libiconv-%s.tar.gz' return download_library(dest_dir, LIBICONV_LOCATION, 'libiconv', version_re, filename, version=version) @@ -274,7 +360,7 @@ def download_libiconv(dest_dir, version=None): def download_zlib(dest_dir, version): """Downloads zlib, returning the filename where the library was downloaded""" - version_re = re.compile(r'zlib-([0-9.]+[0-9]).tar.gz') + version_re = re.compile(r'zlib-([0-9.]+[0-9])[.]tar[.]gz') filename = 'zlib-%s.tar.gz' return download_library(dest_dir, ZLIB_LOCATION, 'zlib', version_re, filename, version=version) @@ -288,26 +374,45 @@ def find_max_version(libname, filenames, version_re=None): match = version_re.search(fn) if match: version_string = match.group(1) - versions.append((tuple(map(tryint, version_string.split('.'))), - version_string)) + versions.append(( + tuple(map(tryint, version_string.replace("-", ".-").split('.'))), + version_string, + )) if not versions: raise Exception( "Could not find the most current version of %s from the files: %s" % ( - libname, filenames)) + libname, list(filenames))) versions.sort() version_string = versions[-1][-1] print('Latest version of %s is %s' % (libname, version_string)) return version_string +def file_exists(file_path: Path, size=None, digest=None): + if not file_path.exists(): + return False + if size is not None: + if file_path.stat().st_size != size: + return False + if digest is not None and hasattr(hashlib, 'file_digest'): + hash_alg = 'sha256' + if ':' in digest: + hash_alg, _, digest = digest.partition(':') + with file_path.open(mode='rb') as f: + file_digest = hashlib.file_digest(f, hash_alg) + if digest != file_digest.hexdigest(): + return False + return True + + def download_library(dest_dir, location, name, version_re, filename, version=None): if version is None: try: if location.startswith('ftp://'): - fns = remote_listdir(location) + fns = list(remote_listdir(location)) else: - print(location) fns = http_listfiles(location, '(%s)' % filename.replace('%s', '(?:[0-9.]+[0-9])')) + print(f"Found {len(fns)} links at {location}") version = find_max_version(name, fns, version_re) except IOError: # network failure - maybe we have the files already? @@ -326,44 +431,94 @@ def download_library(dest_dir, location, name, version_re, filename, version=Non raise if version: filename = filename % version + full_url = urljoin(location, filename) - dest_filename = os.path.join(dest_dir, filename) - if os.path.exists(dest_filename): - print(('Using existing %s downloaded into %s ' - '(delete this file if you want to re-download the package)') % ( - name, dest_filename)) - else: - print('Downloading %s into %s from %s' % (name, dest_filename, full_url)) - urlcleanup() # work around FTP bug 27973 in Py2.7.12 - urlretrieve(full_url, dest_filename) - return dest_filename + dest_filepath = Path(dest_dir) / filename + if file_exists(dest_filepath, digest=ARCHIVE_HASHES.get(filename)): + print(f'Using existing {name} downloaded into {dest_filepath} ' + '(delete this file if you want to re-download the package)') + return dest_filepath + + print('Downloading %s into %s from %s' % (name, dest_filepath, full_url)) + for retry_after_seconds in (2, 5, 10, None): + try: + urlretrieve(full_url, dest_filepath) + except urllib.error.URLError as exc: + if retry_after_seconds is None: + print(f"Download failed: {exc}") + break + else: + print(f"Download failed: {exc}, retrying in {int(retry_after_seconds)} seconds…") + time.sleep(retry_after_seconds) + else: + if file_exists(dest_filepath, digest=ARCHIVE_HASHES.get(filename)): + return dest_filepath + + if not file_exists(dest_filepath, digest=ARCHIVE_HASHES.get(filename)): + raise RuntimeError(f"File download of {filename} failed to write the correct file.") + + return dest_filepath -def unpack_tarball(tar_filename, dest): +def unpack_tarball(tar_filename, dest) -> str: print('Unpacking %s into %s' % (os.path.basename(tar_filename), dest)) - if sys.version_info[0] < 3 and tar_filename.endswith('.xz'): - # Py 2.7 lacks lzma support - tar_cm = py2_tarxz(tar_filename) - else: - tar_cm = closing(tarfile.open(tar_filename)) + os_path = os.path + abs_dest = os_path.abspath(dest) + + tar_cm = tarfile.open(tar_filename) + + if hasattr(tarfile, 'data_filter'): + tar_cm.extraction_filter = tarfile.data_filter base_dir = None - with tar_cm as tar: + with closing(tar_cm) as tar: + directories = [] for member in tar: - base_name = member.name.split('/')[0] + # Guard against malicious tar file content. + path = os_path.join(dest, member.name) + abs_path = os_path.abspath(path) + if not os_path.commonpath([abs_dest, abs_path]).startswith(abs_dest): + raise RuntimeError('Unexpected path in %s: %s' % (tar_filename, member.name)) + + if member.isdir(): + directories.append(member) + continue + elif member.issym() or member.islnk(): + link_path = os_path.abspath(os_path.join( + os_path.dirname(abs_path) if member.issym() else abs_dest, + member.linkname)) + if not os_path.commonpath([abs_dest, link_path]).startswith(abs_dest): + raise RuntimeError('Unexpected path in %s: %s' % (tar_filename, member.name)) + elif member.islnk(): + link_path = os_path.abspath(os_path.join(abs_dest, member.linkname)) + elif not member.isfile(): + raise RuntimeError('Unexpected path in %s: %s' % (tar_filename, member.name)) + + # Find common base directory. + first_dir = member.name.split('/')[0] if base_dir is None: - base_dir = base_name - elif base_dir != base_name: - print('Unexpected path in %s: %s' % (tar_filename, base_name)) - tar.extractall(dest) - return os.path.join(dest, base_dir) + base_dir = first_dir + elif base_dir != first_dir: + print('Unexpected path in %s: %s' % (tar_filename, first_dir)) + continue + + # Extract only new files. + if os_path.exists(abs_path) and os_path.getsize(abs_path) == member.size: + continue + tar.extract(member, abs_dest) + + # Update directory properties/times/etc. + for member in directories: + tar.extract(member, abs_dest) + + return os_path.join(dest, base_dir) def call_subprocess(cmd, **kw): import subprocess cwd = kw.get('cwd', '.') cmd_desc = ' '.join(cmd) - log.info('Running "%s" in %s' % (cmd_desc, cwd)) + print(f'Running "{cmd_desc}" in {cwd}') returncode = subprocess.call(cmd, **kw) if returncode: raise Exception('Command "%s" returned code %s' % (cmd_desc, returncode)) @@ -393,37 +548,98 @@ def cmmi(configure_cmd, build_dir, multicore=None, **call_setup): def configure_darwin_env(env_setup): import platform - # configure target architectures on MacOS-X (x86_64 only, by default) + # configure target architectures on MacOS-X (x86_64 + Arm64, by default) major_version, minor_version = tuple(map(int, platform.mac_ver()[0].split('.')[:2])) - if major_version > 7: + if major_version >= 11: env_default = { - 'CFLAGS': "-arch x86_64 -O2", - 'LDFLAGS': "-arch x86_64", - 'MACOSX_DEPLOYMENT_TARGET': "10.6" + 'CFLAGS': "-arch x86_64 -arch arm64 -O3", + 'LDFLAGS': "-arch x86_64 -arch arm64", + 'MACOSX_DEPLOYMENT_TARGET': "11.0" } env_default.update(os.environ) env_setup['env'] = env_default -def build_libxml2xslt(download_dir, build_dir, - static_include_dirs, static_library_dirs, - static_cflags, static_binaries, - libxml2_version=None, - libxslt_version=None, - libiconv_version=None, - zlib_version=None, - multicore=None): +def build_libxml2xslt( + download_dir, build_dir, + static_include_dirs, static_library_dirs, + static_cflags, static_binaries, + libxml2_version=None, + libxslt_version=None, + libiconv_version=None, + zlib_version=None, + multicore=None, + with_zlib=True): + lib_dirs = download_libs(download_dir, build_dir, + libxml2_version, libxslt_version, libiconv_version, zlib_version, with_zlib=with_zlib) + return build_libs( + build_dir, lib_dirs, + static_include_dirs, static_library_dirs, static_cflags, static_binaries, + libxml2_version=libxml2_version, + multicore=multicore, + with_zlib=with_zlib, + ) + + +def download_libs( + download_dir, build_dir, + libxml2_version=None, + libxslt_version=None, + libiconv_version=None, + zlib_version=None, + with_zlib=True): safe_mkdir(download_dir) safe_mkdir(build_dir) - zlib_dir = unpack_tarball(download_zlib(download_dir, zlib_version), build_dir) + + zlib_dir = None + if with_zlib: + zlib_dir = unpack_tarball(download_zlib(download_dir, zlib_version), build_dir) + libiconv_dir = unpack_tarball(download_libiconv(download_dir, libiconv_version), build_dir) libxml2_dir = unpack_tarball(download_libxml2(download_dir, libxml2_version), build_dir) libxslt_dir = unpack_tarball(download_libxslt(download_dir, libxslt_version), build_dir) + + # Patch after unpacking to assure a clean target directory. + _patch_library(zlib_dir) + _patch_library(libiconv_dir) + _patch_library(libxml2_dir) + _patch_library(libxslt_dir) + + return zlib_dir, libiconv_dir, libxml2_dir, libxslt_dir + + +LIBRARY_PATCHES = { + "libxslt-1.1.43": "libxslt-1.1.43-backport1.patch", +} + + +def _patch_library(libdir): + if not libdir: + return + dirname = os.path.basename(libdir) + if dirname not in LIBRARY_PATCHES: + return + patch_file = LIBRARY_PATCHES[dirname] + + from patch_lxml_deplibs import apply_patch_file + print(f"Applying patch {patch_file} to {libdir}") + apply_patch_file(patch_file, libdir) + + +def build_libs( + build_dir, lib_dirs, + static_include_dirs, static_library_dirs, + static_cflags, static_binaries, + libxml2_version=None, + multicore=None, + with_zlib=True): + zlib_dir, libiconv_dir, libxml2_dir, libxslt_dir = lib_dirs + prefix = os.path.join(os.path.abspath(build_dir), 'libxml2') lib_dir = os.path.join(prefix, 'lib') safe_mkdir(prefix) - lib_names = ['libxml2', 'libexslt', 'libxslt', 'iconv', 'libz'] + lib_names = ['libxml2', 'libexslt', 'libxslt', 'iconv'] + (['libz'] if with_zlib else []) existing_libs = { lib: os.path.join(lib_dir, filename) for lib in lib_names @@ -444,7 +660,7 @@ def has_current_lib(name, build_dir, _build_all_following=[False]): return found call_setup = {} - if sys.platform == 'darwin': + if sys_platform == 'darwin': configure_darwin_env(call_setup) configure_cmd = ['./configure', @@ -454,12 +670,13 @@ def has_current_lib(name, build_dir, _build_all_following=[False]): ] # build zlib - zlib_configure_cmd = [ - './configure', - '--prefix=%s' % prefix, - ] - if not has_current_lib("libz", zlib_dir): - cmmi(zlib_configure_cmd, zlib_dir, multicore, **call_setup) + if with_zlib: + zlib_configure_cmd = [ + './configure', + '--prefix=%s' % prefix, + ] + if not has_current_lib("libz", zlib_dir): + cmmi(zlib_configure_cmd, zlib_dir, multicore, **call_setup) # build libiconv if not has_current_lib("iconv", libiconv_dir): @@ -469,7 +686,7 @@ def has_current_lib(name, build_dir, _build_all_following=[False]): libxml2_configure_cmd = configure_cmd + [ '--without-python', '--with-iconv=%s' % prefix, - '--with-zlib=%s' % prefix, + ('--with-zlib=%s' % prefix) if with_zlib else '--without-zlib', ] if not libxml2_version: @@ -525,3 +742,58 @@ def has_current_lib(name, build_dir, _build_all_following=[False]): if lib in filename and filename.endswith('.a')] return xml2_config, xslt_config + + +def main(with_zlib=True, download_only=False, platform=None): + static_include_dirs = [] + static_library_dirs = [] + download_dir = "libs" + + arch = None + if platform is None: + platform = sys_platform + elif platform in ('win32', 'win64', 'win_arm64'): + arch = platform + + if platform.startswith('win'): + return get_prebuilt_libxml2xslt( + download_dir, static_include_dirs, static_library_dirs, arch=arch) + + get_env = os.environ.get + zlib_version = get_env('ZLIB_VERSION') + libiconv_version = get_env('LIBICONV_VERSION') + libxml2_version = get_env('LIBXML2_VERSION') + libxslt_version = get_env('LIBXSLT_VERSION') + + build_dir = 'build/tmp' + lib_dirs = download_libs( + download_dir, build_dir, + libxml2_version=libxml2_version, + libxslt_version=libxslt_version, + libiconv_version=libiconv_version, + zlib_version=zlib_version, + with_zlib=with_zlib, + ) + if download_only: + return None, None + + return build_libs( + build_dir, lib_dirs, + static_include_dirs, static_library_dirs, + static_cflags=[], + static_binaries=[], + libxml2_version=libxml2_version, + with_zlib=with_zlib, + ) + + +if __name__ == '__main__': + args = sys.argv[1:] + download_only = '--download-only' in args + if download_only: + args.remove('--download-only') + platform_arg = args[0] if args else None + if platform_arg: + # change global sys_platform setting + sys_platform = platform_arg + main(download_only=download_only, platform=platform_arg) diff --git a/doc/FAQ.txt b/doc/FAQ.txt index caf6edf81..ac2b3dd61 100644 --- a/doc/FAQ.txt +++ b/doc/FAQ.txt @@ -42,6 +42,7 @@ ElementTree_. 4.2 My application crashes on MacOS-X! 4.3 I think I have found a bug in lxml. What should I do? 4.4 How do I know a bug is really in lxml and not in libxml2? + 4.5 My application crashes with xmlsec! 5 Threading 5.1 Can I use threads to concurrently access the lxml API? 5.2 Does my program run faster if I use threads? @@ -66,7 +67,7 @@ ElementTree_. 7.5 How can I modify the tree during iteration? -The code examples below use the `'lxml.etree`` module: +The code examples below use the ``lxml.etree`` module: .. sourcecode:: pycon @@ -162,7 +163,7 @@ lxml currently supports libxml2 2.6.20 or later, which has even better support for various XML standards. The important ones are: * XML 1.0 -* HTML 4 +* HTML 4 (recent libxml2 versions implement parts of HTML 5) * XML namespaces * XML Schema 1.0 * XPath 1.0 @@ -379,7 +380,7 @@ I'm glad you asked. Note that this beautiful quick-and-dirty converter expects children to have unique tag names and will silently overwrite any data that was contained in preceding siblings with the same name. For any -real-world application of xml-to-dict conversion, you would better +real-world application of xml-to-dict conversion, you had better write your own, longer version of this. @@ -459,21 +460,21 @@ see when (or if) a specific bug has been fixed. Where are the binary builds? ---------------------------- -Thanks to the help by Joar Wandborg, we try to make "manylinux_" binary -builds for Linux available shortly after each source release, as they -are very frequently used by continuous integration and/or build servers. +We provide binaries for Linux (`manylinux`_), macOS and MS Windows +shortly after each source release. Thanks to the help by Maximilian Hils and the Appveyor build service, -we also try to serve the frequent requests for binary builds available +we try to serve the frequent requests for binary builds available for Microsoft Windows in a timely fashion, since users of that platform usually fail to build lxml themselves. Two of the major design issues of this operating system make this non-trivial for its users: the lack -of a pre-installed standard compiler and the missing package management. +of a pre-installed standard C-compiler and the missing package management. -Besides that, Christoph Gohlke generously provides `unofficial lxml binary -builds for Windows `_ -that are usually very up to date. Consider using them if you prefer a -binary build over a signed official source release. +We currently rely on the `WinLibs project `_ +to provide library versions that are buildable on MS Windows. If the library +that we use in lxml's Windows binary wheels is outdated, it is probably because +they have not updated their repositories yet. Consider filing a ticket on their +side and notifying us when a new version is available, so that we can integrate it. .. _manylinux: https://www.python.org/dev/peps/pep-0513 @@ -483,7 +484,7 @@ Why do I get errors about missing UCS4 symbols when installing lxml? You are using a Python installation that was configured for a different internal Unicode representation than the lxml package you are trying to -install. CPython versions before 3.3 allowed to switch between two types +install. CPython versions before 3.3 allowed switching between two types at build time: the 32 bit encoding UCS4 and the 16 bit encoding UCS2. Sadly, both are not compatible, so eggs and other binary distributions can only support the one they were compiled with. @@ -549,7 +550,7 @@ subdirectories in the lxml source tree (below the ``src`` directory) and the ReST_ `text files`_ in the ``doc`` directory. We also have a `list of missing features`_ that we would like to -implement but didn't due to lack if time. If *you* find the time, +implement but didn't due to lack of time. If *you* find the time, patches are very welcome. .. _ReST: http://docutils.sourceforge.net/rst.html @@ -619,6 +620,18 @@ your problem. Remember: even if you see lxml appear in a crash stack trace, it is not necessarily lxml that *caused* the crash. +If you are using the *``xmlsec``* library together with lxml, you have to +make sure that both use the same version of libxml2. The binary wheels of +lxml statically include a (usually recent) version of libxml2, whereas +xmlsec often depends on the systemwide installed libraries. If you get +crashes or unexpected behaviour when using both, please make sure that both +get to use the same libxml2 version. Anaconda/condaforge/etc. based installations +will usually come with matching C libraries. If you use xmlsec with the system +libraries, please build lxml from sources against those as well, e.g. by installing +the development packages of libxml2 and libxslt and then installing lxml with + +``python -m pip install --no-binary lxml lxml`` + My application crashes on MacOS-X! ---------------------------------- @@ -638,7 +651,7 @@ release you are using. .. _`current developer changelog`: https://github.com/lxml/lxml/blob/master/CHANGES.txt -Also, the 'crash' section above has a few good advices what to try to see if +Also, the 'crash' section above has some good advice on what to try to see if the problem is really in lxml - and not in your setup. Believe it or not, that happens more often than you might think, especially when old libraries or even multiple library versions are installed. @@ -707,7 +720,7 @@ the respective project, namely: On the other hand, everything that seems to be related to Python code, including custom resolvers, custom XPath functions, etc. is likely outside of the scope of libxml2/libxslt. If you encounter problems -here or you are not sure where there the problem may come from, please +here or you are not sure where the problem may come from, please ask on the lxml mailing list first. In any case, a good explanation of the problem including some simple @@ -856,7 +869,7 @@ for possible approaches to solve your specific problem: * compile lxml without threading support by running ``setup.py`` with the ``--without-threading`` option. While this might be slower in certain scenarios on multi-processor systems, it *might* also keep your application - from crashing, which should be worth more to you than peek performance. + from crashing, which should be worth more to you than peak performance. Remember that lxml is fast anyway, so concurrency may not even be worth it. * look out for fancy XSLT stuff like foreign document access or @@ -1105,9 +1118,9 @@ useless for the data commonly sent through web services and can simply be disabled, which rules out several types of denial of service attacks at once. This also involves an attack that reads local files from the server, as XML entities can be -defined to expand into their content. Consequently, version -1.2 of the SOAP standard explicitly disallows entity references -in the XML stream. +defined to expand into the content of external resources. +Consequently, version 1.2 of the SOAP standard explicitly +disallows entity references in the XML stream. To disable entity expansion, use an XML parser that is configured with the option ``resolve_entities=False``. Then, after (or @@ -1115,7 +1128,11 @@ while) parsing the document, use ``root.iter(etree.Entity)`` to recursively search for entity references. If it contains any, reject the entire input document with a suitable error response. In lxml 3.x, you can also use the new DTD introspection API to -apply your own restrictions on input documents. +apply your own restrictions on input documents. Since version 5.x, +lxml disables the expansion of external entities (XXE) by default. +If you really want to allow loading external files into XML documents +using this functionality, you have to explicitly set +``resolve_entities=True``. Another attack to consider is compression bombs. If you allow compressed input into your web service, attackers can try to send @@ -1145,7 +1162,7 @@ safely expose their values to the evaluation engine. The defusedxml_ package comes with an example setup and a wrapper API for lxml that applies certain counter measures internally. -.. _defusedxml: https://bitbucket.org/tiran/defusedxml +.. _defusedxml: https://github.com/tiran/defusedxml How can I sort the attributes? diff --git a/doc/api.txt b/doc/api.txt index 2a085d2f3..8767d8fb3 100644 --- a/doc/api.txt +++ b/doc/api.txt @@ -178,7 +178,7 @@ on an ElementTree use its explicit root node as reference. Iteration --------- -The ElementTree API makes Elements iterable to supports iteration over their +The ElementTree API makes Elements iterable to support iteration over their children. Using the tree defined above, we get: .. sourcecode:: pycon @@ -289,7 +289,7 @@ transformer object. See their documentation for details. However, lxml also keeps a global error log of all errors that occurred at the application level. Whenever an exception is raised, you can retrieve the -errors that occurred and "might have" lead to the problem from the error log +errors that occurred and "might have" led to the problem from the error log copy attached to the exception: .. sourcecode:: pycon @@ -302,7 +302,7 @@ copy attached to the exception: ... ''' >>> try: ... etree.parse(StringIO(broken_xml)) - ... except etree.XMLSyntaxError, e: + ... except etree.XMLSyntaxError as e: ... pass # just put the exception into e .. @@ -325,7 +325,7 @@ error level: This might look a little cryptic at first, but it is the information that libxml2 gives you. At least the message at the end should give you a hint what went wrong and you can see that the fatal errors (FATAL) happened during -parsing (PARSER) lines 4, column 8 and line 5, column 1 of a string (, +parsing (PARSER) at line 4, column 8 and line 5, column 1 of a string (, or the filename if available). Here, PARSER is the so-called error domain, see ``lxml.etree.ErrorDomains`` for that. You can get it from a log entry like this: @@ -407,8 +407,8 @@ is required by the standard: .. sourcecode:: pycon - >>> unicode_root = etree.Element( u"t\u3120st" ) - >>> unicode_root.text = u"t\u0A0Ast" + >>> unicode_root = etree.Element( "t\u3120st" ) + >>> unicode_root.text = "t\u0A0Ast" >>> etree.tostring(unicode_root, encoding="utf-8") b't\xe0\xa8\x8ast' @@ -473,7 +473,7 @@ Incremental XML generation -------------------------- Since version 3.1, lxml provides an ``xmlfile`` API for incrementally -generating XML using the ``with`` statement. It's main purpose is to +generating XML using the ``with`` statement. Its main purpose is to freely and safely mix surrounding elements with pre-built in-memory trees, e.g. to write out large documents that consist mostly of repetitive subtrees (like database dumps). But it can be useful in diff --git a/doc/api/conf.py b/doc/api/conf.py index 7c5f134d2..96e098ed5 100644 --- a/doc/api/conf.py +++ b/doc/api/conf.py @@ -1,5 +1,6 @@ import os import sys +from datetime import date sys.path.insert(0, os.path.abspath('../../src')) from lxml import __version__ as lxml_version @@ -7,7 +8,7 @@ # -- Project information ----------------------------------------------------- project = 'lxml' -copyright = '2020, lxml dev team' +copyright = f'{date.today().year}, lxml dev team' author = 'lxml dev team' version = lxml_version @@ -17,7 +18,8 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.viewcode', - 'sphinx_rtd_theme', + #'sphinx_rtd_theme', + 'sphinx_book_theme', ] language = 'en' @@ -27,7 +29,8 @@ # -- Options for HTML output ------------------------------------------------- -html_theme = 'sphinx_rtd_theme' +#html_theme = 'sphinx_rtd_theme' +html_theme = 'sphinx_book_theme' html_logo = '../html/python-xml.png' @@ -51,6 +54,10 @@ autodoc_member_order = 'groupwise' +intersphinx_mapping = { + "lxml": ("https://lxml.de/apidoc/", None), +} + # -- Options for todo extension ---------------------------------------------- # If true, `todo` and `todoList` produce output, else they produce nothing. diff --git a/doc/api/index.rst b/doc/api/index.rst index ccf1badda..eeea83af3 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -2,7 +2,7 @@ lxml API Reference ================== .. toctree:: - :maxdepth: 4 + :maxdepth: 3 lxml diff --git a/doc/build.txt b/doc/build.txt index 33ab0455f..488e1f0be 100644 --- a/doc/build.txt +++ b/doc/build.txt @@ -34,12 +34,12 @@ Cython versions, which may or may not generate correct code for lxml. The pre-generated release sources were tested and therefore are known to work. -So, if you want a reliable build of lxml, we suggest to a) use a -source release of lxml and b) disable or uninstall Cython for the +So, if you want a reliable build of lxml, we suggest a) using a +source release of lxml and b) disabling or uninstalling Cython for the build. *Only* if you are interested in building lxml from a checkout of the -developer sources (e.g. to test a bug fix that has not been release +developer sources (e.g. to test a bug fix that has not been released yet) or if you want to be an lxml developer, then you do need a working Cython installation. You can use pip_ to install it:: @@ -47,8 +47,8 @@ working Cython installation. You can use pip_ to install it:: https://github.com/lxml/lxml/blob/master/requirements.txt -lxml currently requires at least Cython 0.29. Later release versions -are generally preferred. +lxml 5.x currently uses at least Cython 3.0. Later release versions +are generally preferred. lxml 4.x used Cython 0.29.x instead. Github, git and hg @@ -135,17 +135,29 @@ files to the include path like:: where the file is in ``/usr/include/libxml2/libxml/xmlversion.h`` +For static builds, if you get an error saying "recompile with -fPIC", +do so by adding it to your `CFLAGS` environment variable: +``env CFLAGS="$CFLAGS -fPIC"``, such as:: + + env CFLAGS="$CFLAGS -fPIC" python3 setup.py build_ext -i --with-cython --static-deps + To use lxml.etree in-place, you can place lxml's ``src`` directory on your Python module search path (PYTHONPATH) and then import ``lxml.etree`` to play with it:: # cd lxml - # PYTHONPATH=src python - Python 2.7.2 + # PYTHONPATH=src python3 + Python 3.10.2 Type "help", "copyright", "credits" or "license" for more information. >>> from lxml import etree >>> +For non-static builds, you may have to set ``LD_LIBRARY_PATH`` to where the +shared object files for libxml2 and libxslt are, such as ``/usr/local/lib``. For +example:: + + PYTHONPATH=src LD_LIBRARY_PATH=/usr/local/lib python3 + To make sure everything gets recompiled cleanly after changes, you can run ``make clean`` or delete the file ``src/lxml/etree.c``. @@ -243,7 +255,7 @@ install Unix/Linux style package management tools and new software, it actually seems to be hard to get libraries set up for exclusive usage that MacOS-X ships in an older version. Alternative distributions (like macports) install their libraries in addition to the system -libraries, but the compiler and the runtime loader on MacOS still sees +libraries, but the compiler and the runtime loader on MacOS still see the system libraries before the new libraries. This can lead to undebuggable crashes where the newer library seems to be loaded but the older system library is used. diff --git a/doc/capi.txt b/doc/capi.txt index 0471d811e..6102b021d 100644 --- a/doc/capi.txt +++ b/doc/capi.txt @@ -22,7 +22,7 @@ c-importable by extension modules implemented in Cython_. Passing generated trees through Python -------------------------------------- -This is the most simple way to integrate with lxml. It does not require +This is the simplest way to integrate with lxml. It does not require any C-level integration but uses a Python function to wrap an externally generated libxml2 document in lxml. diff --git a/doc/compatibility.txt b/doc/compatibility.txt index 654cb7c4e..f02fd9cd8 100644 --- a/doc/compatibility.txt +++ b/doc/compatibility.txt @@ -7,7 +7,7 @@ ElementTree. Nonetheless, some differences and incompatibilities exist: * Importing etree is obviously different; etree uses a lower-case package name, while ElementTree uses a combination of upper-case and - lower case in imports: + lower-case in imports: .. sourcecode:: python @@ -160,7 +160,7 @@ ElementTree. Nonetheless, some differences and incompatibilities exist: * ElementTree merges the target of a processing instruction into ``PI.text``, while lxml.etree puts it into the ``.target`` property and leaves it out of the ``.text`` property. The ``pi.text`` in ElementTree therefore - correspondents to ``pi.target + " " + pi.text`` in lxml.etree. + corresponds to ``pi.target + " " + pi.text`` in lxml.etree. * Because etree is built on top of libxml2, which is namespace prefix aware, etree preserves namespaces declarations and prefixes while ElementTree tends diff --git a/doc/element_classes.txt b/doc/element_classes.txt index 759ad7d51..53dc0f9b0 100644 --- a/doc/element_classes.txt +++ b/doc/element_classes.txt @@ -60,7 +60,7 @@ Element initialization ====================== There is one thing to know up front. Element classes *must not* have -an ``__init___`` or ``__new__`` method. There should not be any +an ``__init__`` or ``__new__`` method. There should not be any internal state either, except for the data stored in the underlying XML tree. Element instances are created and garbage collected at need, so there is normally no way to predict when and how often a @@ -98,7 +98,7 @@ cache manually up to date if the tree changes, which can get tricky in cases. For proxy initialisation, ElementBase classes have an ``_init()`` -method that can be overridden, as oppose to the normal ``__init__()`` +method that can be overridden, as opposed to the normal ``__init__()`` method. It can be used to modify the XML tree, e.g. to construct special children or verify and update attributes. @@ -121,7 +121,7 @@ The semantics of ``_init()`` are as follows: before running through the init process. * Any exceptions raised in ``_init()`` will be propagated through the API - call that lead to the creation of the Element. So be careful with the code + call that led to the creation of the Element. So be careful with the code you write here as its exceptions may turn up in various unexpected places. @@ -382,7 +382,7 @@ the elements in the tree have been instantiated as Python Element proxies. Luckily, there is a way to do this. The ``PythonElementClassLookup`` -works similar to the custom lookup scheme: +works similarly to the custom lookup scheme: .. sourcecode:: pycon @@ -394,7 +394,7 @@ works similar to the custom lookup scheme: >>> parser.set_element_class_lookup(MyLookup()) As before, the first argument to the ``lookup()`` method is the opaque -document instance that contains the Element. The second arguments is a +document instance that contains the Element. The second argument is a lightweight Element proxy implementation that is only valid during the lookup. Do not try to keep a reference to it. Once the lookup is finished, the proxy will become invalid. You will get an ``AssertionError`` if you access any of @@ -576,10 +576,10 @@ subclasses for elements of this namespace: >>> print(honk_element[0].honk()) HONK - >>> print(honk_element[0].honking) + >>> print(honk_element[0].honking) # doctest: +ELLIPSIS Traceback (most recent call last): - ... - AttributeError: 'HonkNSElement' object has no attribute 'honking' + ... + AttributeError: 'HonkNSElement' object has no attribute 'honking'... >>> print(honk_element[1].text) # uses fallback for non-elements comment diff --git a/doc/elementsoup.txt b/doc/elementsoup.txt index 9317f6545..a65ab94b2 100644 --- a/doc/elementsoup.txt +++ b/doc/elementsoup.txt @@ -115,7 +115,7 @@ finds by their character equivalent. >>> tag_soup = '©€-õƽ

' >>> body = fromstring(tag_soup).find('.//body') >>> body.text - u'\xa9\u20ac-\xf5\u01bd' + '\xa9\u20ac-\xf5\u01bd' If you want them back on the way out, you can just serialise with the default encoding, which is 'US-ASCII'. @@ -139,10 +139,10 @@ Any other encoding will output the respective byte sequences. '\xc2\xa9\xe2\x82\xac-\xc3\xb5\xc6\xbd

' >>> tostring(body, encoding='unicode') - u'\xa9\u20ac-\xf5\u01bd

' + '\xa9\u20ac-\xf5\u01bd

' >>> tostring(body, method="html", encoding='unicode') - u'\xa9\u20ac-\xf5\u01bd

' + '\xa9\u20ac-\xf5\u01bd

' Using soupparser as a fallback diff --git a/doc/extensions.txt b/doc/extensions.txt index 45bcf9795..0c5f978ab 100644 --- a/doc/extensions.txt +++ b/doc/extensions.txt @@ -46,7 +46,7 @@ and extension elements in XSLT as in the following example: XPath Extension functions ========================= -Here is how an extension function looks like. As the first argument, +Here is what an extension function looks like. As the first argument, it always receives a context object (see below). The other arguments are provided by the respective call in the XPath expression, one in the following examples. Any number of arguments is allowed: @@ -249,7 +249,7 @@ the global mapping of the FunctionNamespace objects: >>> e2('/foo:a') Traceback (most recent call last): ... - lxml.etree.XPathEvalError: Undefined namespace prefix + lxml.etree.XPathEvalError: Undefined namespace prefix... Evaluator-local extensions @@ -471,7 +471,7 @@ And then you can implement the element in Python like this: ... # just copy own content input to output ... output_parent.extend( list(self_node) ) -The arguments passed to the ``.execute()`` method are +The arguments passed to the ``.execute()`` method are context The opaque evaluation context. You need this when calling back diff --git a/doc/html5parser.txt b/doc/html5parser.txt index b03bb3ab5..afe3d09f5 100644 --- a/doc/html5parser.txt +++ b/doc/html5parser.txt @@ -23,7 +23,7 @@ Differences to regular HTML parsing There are a few differences in the returned tree to the regular HTML parsing functions from ``lxml.html``. html5lib normalizes some elements -and element structures to a common format. For example even if a tables +and element structures to a common format. For example, even if a table does not have a `tbody` html5lib will inject one automatically: .. sourcecode:: pycon @@ -74,7 +74,7 @@ Function Reference on whether the string looks like a full document, or just a fragment. -Additionally all parsing functions accept an ``parser`` keyword argument +Additionally all parsing functions accept a ``parser`` keyword argument that can be set to a custom parser instance. To create custom parsers you can subclass the ``HTMLParser`` and ``XHTMLParser`` from the same module. Note that these are the parser classes provided by html5lib. diff --git a/doc/lxml-source-howto.txt b/doc/lxml-source-howto.txt index 9cef1f7ba..022d9b73e 100644 --- a/doc/lxml-source-howto.txt +++ b/doc/lxml-source-howto.txt @@ -16,7 +16,7 @@ document that describes `how to build lxml from sources`_. .. _lxml: https://lxml.de/ .. _`how to build lxml from sources`: build.html .. _`ReStructured Text`: http://docutils.sourceforge.net/rst.html -.. _epydoc: http://epydoc.sourceforge.net/ +.. _Sphinx: https://www.sphinx-doc.org/ .. _docutils: http://docutils.sourceforge.net/ .. _`C-level API`: capi.html @@ -75,7 +75,7 @@ checkout. The main extension modules in lxml are ``lxml.etree`` and ``lxml.objectify``. All main modules have the file extension -``.pyx``, which shows the descendence from Pyrex. As usual in Python, +``.pyx``, which shows the descent from Pyrex. As usual in Python, the main files start with a short description and a couple of imports. Cython distinguishes between the run-time ``import`` statement (as known from Python) and the compile-time ``cimport`` statement, which @@ -139,15 +139,16 @@ The easiest way to generate the HTML pages is by calling:: make html This will call the script ``doc/mkhtml.py`` to run the ReST processor -on the files. After generating an HTML page the script parses it back +on the files. After generating an HTML page, the script parses it back in to build the side menu, and injects the complete menu into each page at the very end. -Running the ``make`` command will also generate the API documentation -if you have epydoc_ installed. The epydoc package will import and -introspect the extension modules and also introspect and parse the -Python modules of lxml. The aggregated information will then be -written out into an HTML documentation site. +Running ``make html`` will also generate the API documentation if you +have Sphinx_ installed. The build first runs ``sphinx-apidoc`` to +generate API reference pages in `ReStructured Text`_ format. It then +runs ``sphinx-build``, which imports and introspects the extension +and Python modules of lxml and writes the final API documentation +HTML files. lxml.etree @@ -213,7 +214,7 @@ parsertarget.pxi proxy.pxi Very low-level functions for memory allocation/deallocation and Element proxy handling. Ignoring this for the beginning - will safe your head from exploding. + will save your head from exploding. public-api.pxi The set of C functions that are exported to other extension @@ -285,9 +286,6 @@ doctestcompare.py ElementInclude.py XInclude-like document inclusion, compatible with ElementTree. -_elementpath.py - XPath-like path language, compatible with ElementTree. - sax.py SAX2 compatible interfaces to copy lxml trees from/to SAX compatible tools. diff --git a/doc/lxmlhtml.txt b/doc/lxmlhtml.txt index 3c7393be6..ab833e4c4 100644 --- a/doc/lxmlhtml.txt +++ b/doc/lxmlhtml.txt @@ -111,7 +111,7 @@ also include some extra methods: ``.find_class(class_name)``: Returns a list of all the elements with the given CSS class name. Note that class names are space separated in HTML, so - ``doc.find_class_name('highlight')`` will find an element like + ``doc.find_class('highlight')`` will find an element like ``
''')) -
A link in
- >>> print(autolink_html(''' - ...
A link in http://bar.com
''')) -
A link in http://bar.com
- >>> print(autolink_html(''' - ...
A link in http://foo.com or - ... http://bar.com
''')) -
A link in http://foo.com or - http://bar.com
- -There's also a word wrapping function, that should probably be run -after autolink:: - - >>> from lxml.html.clean import word_break_html - >>> def pascii(s): - ... print(s.encode('ascii', 'xmlcharrefreplace').decode('ascii')) - >>> pascii(word_break_html( u''' - ...
Hey you - ... 12345678901234567890123456789012345678901234567890
''')) -
Hey you - 1234567890123456789012345678901234567890​1234567890
- -Not everything is broken: - - >>> pascii(word_break_html(''' - ...
Hey you - ... 12345678901234567890123456789012345678901234567890
''')) -
Hey you - 12345678901234567890123456789012345678901234567890
- >>> pascii(word_break_html(''' - ... text''')) - text - - diff --git a/src/lxml/html/tests/test_basic.py b/src/lxml/html/tests/test_basic.py index 464d47471..29005f470 100644 --- a/src/lxml/html/tests/test_basic.py +++ b/src/lxml/html/tests/test_basic.py @@ -1,6 +1,6 @@ +import doctest import sys import unittest -from lxml.tests.common_imports import make_doctest, doctest from lxml import html class TestBasicFeatures(unittest.TestCase): @@ -9,16 +9,14 @@ def test_various_mixins(self): doc = html.fromstring(""" - &entity; """, base_url=base_url) self.assertEqual(doc.getroottree().docinfo.URL, base_url) - self.assertEqual(len(doc), 3) + self.assertEqual(len(doc), 2) self.assertIsInstance(doc[0], html.HtmlComment) - self.assertIsInstance(doc[1], html.HtmlProcessingInstruction) - self.assertIsInstance(doc[2], html.HtmlElement) + self.assertIsInstance(doc[1], html.HtmlElement) for child in doc: # base_url makes sense on all nodes (kinda) whereas `classes` or # `get_rel_links` not really @@ -41,9 +39,41 @@ def test_set_empty_attribute(self): 'c': '', }) + def test_element_head_body(self): + doc = html.fromstring(""" + + + + +

+ + + """) + + head = doc.head + body = doc.body + + self.assertIs(doc.head, head) + self.assertIs(doc.body, body) + self.assertIs(doc[0].head, head) + self.assertIs(doc[0].body, body) + self.assertIs(doc[1].head, head) + self.assertIs(doc[1].body, body) + self.assertIs(doc[1][0].head, head) + self.assertIs(doc[1][0].body, body) + + def test_element_head_body_empty(self): + doc = html.fromstring(""" + + + """) + self.assertIsNone(doc.head) + self.assertIsNone(doc.body) + + def test_suite(): suite = unittest.TestSuite() - suite.addTests([make_doctest('test_basic.txt')]) + suite.addTests([doctest.DocFileSuite('test_basic.txt')]) suite.addTests([doctest.DocTestSuite(html)]) suite.addTest(unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])) return suite diff --git a/src/lxml/html/tests/test_basic.txt b/src/lxml/html/tests/test_basic.txt index 1e85c1ac1..e9f308d1c 100644 --- a/src/lxml/html/tests/test_basic.txt +++ b/src/lxml/html/tests/test_basic.txt @@ -3,7 +3,6 @@ lxml.html adds a find_class method to elements:: >>> from lxml.etree import Comment >>> from lxml.html import document_fromstring, fragment_fromstring, tostring >>> from lxml.html import fragments_fromstring, fromstring - >>> from lxml.html.clean import clean, clean_html >>> from lxml.html import usedoctest >>> try: unicode = unicode ... except NameError: unicode = str @@ -113,6 +112,8 @@ Or to get the content of an element without the tags, use text_content():: ...

This is a bold link
''') >>> el.text_content() 'This is a bold link' + >>> type(el.text_content()) is str or type(el.text_content()) + True Or drop an element (leaving its content) or the entire tree, like:: diff --git a/src/lxml/html/tests/test_clean.py b/src/lxml/html/tests/test_clean.py deleted file mode 100644 index 2c785f563..000000000 --- a/src/lxml/html/tests/test_clean.py +++ /dev/null @@ -1,280 +0,0 @@ -import base64 -import gzip -import io -import unittest -from lxml.tests.common_imports import make_doctest - -import lxml.html -from lxml.html.clean import Cleaner, clean_html - - -class CleanerTest(unittest.TestCase): - def test_allow_tags(self): - html = """ - - - - -

some text

- - - - - - - -
helloworld
helloworld
- - - - """ - - html_root = lxml.html.document_fromstring(html) - cleaner = Cleaner( - remove_unknown_tags = False, - allow_tags = ['table', 'tr', 'td']) - result = cleaner.clean_html(html_root) - - self.assertEqual(12-5+1, len(list(result.iter()))) - - def test_allow_and_remove(self): - with self.assertRaises(ValueError): - Cleaner(allow_tags=['a'], remove_unknown_tags=True) - - def test_remove_unknown_tags(self): - html = """
lettuce, tomato, veggie patty
""" - clean_html = """
lettuce, tomato, veggie patty
""" - cleaner = Cleaner(remove_unknown_tags=True) - result = cleaner.clean_html(html) - self.assertEqual( - result, - clean_html, - msg="Unknown tags not removed. Got: %s" % result, - ) - - def test_safe_attrs_included(self): - html = """

Cyan

""" - - safe_attrs=set(lxml.html.defs.safe_attrs) - safe_attrs.add('style') - - cleaner = Cleaner( - safe_attrs_only=True, - safe_attrs=safe_attrs) - result = cleaner.clean_html(html) - - self.assertEqual(html, result) - - def test_safe_attrs_excluded(self): - html = """

Cyan

""" - expected = """

Cyan

""" - - safe_attrs=set() - - cleaner = Cleaner( - safe_attrs_only=True, - safe_attrs=safe_attrs) - result = cleaner.clean_html(html) - - self.assertEqual(expected, result) - - def test_clean_invalid_root_tag(self): - # only testing that cleaning with invalid root tags works at all - s = lxml.html.fromstring('parent child') - self.assertEqual('parent child', clean_html(s).text_content()) - - s = lxml.html.fromstring('child') - self.assertEqual('child', clean_html(s).text_content()) - - def test_clean_with_comments(self): - html = """

Cyan

""" - s = lxml.html.fragment_fromstring(html) - - self.assertEqual( - b'

Cyan

', - lxml.html.tostring(clean_html(s))) - self.assertEqual( - '

Cyan

', - clean_html(html)) - - cleaner = Cleaner(comments=False) - result = cleaner.clean_html(s) - self.assertEqual( - b'

Cyan

', - lxml.html.tostring(result)) - self.assertEqual( - '

Cyan

', - cleaner.clean_html(html)) - - def test_sneaky_noscript_in_style(self): - # This gets parsed as through into the output. - html = '', - lxml.html.tostring(clean_html(s))) - - def test_sneaky_js_in_math_style(self): - # This gets parsed as -> - # thus passing any tag/script/whatever content through into the output. - html = '' - s = lxml.html.fragment_fromstring(html) - - self.assertEqual( - b'', - lxml.html.tostring(clean_html(s))) - - def test_sneaky_import_in_style(self): - # Prevent "@@importimport" -> "@import" replacement etc. - style_codes = [ - "@@importimport(extstyle.css)", - "@ @ import import(extstyle.css)", - "@ @ importimport(extstyle.css)", - "@@ import import(extstyle.css)", - "@ @import import(extstyle.css)", - "@@importimport()", - "@@importimport() ()", - "@/* ... */import()", - "@im/* ... */port()", - "@ @import/* ... */import()", - "@ /* ... */ import()", - ] - for style_code in style_codes: - html = '' % style_code - s = lxml.html.fragment_fromstring(html) - - cleaned = lxml.html.tostring(clean_html(s)) - self.assertEqual( - b'', - cleaned, - "%s -> %s" % (style_code, cleaned)) - - def test_sneaky_schemes_in_style(self): - style_codes = [ - "javasjavascript:cript:", - "javascriptjavascript::", - "javascriptjavascript:: :", - "vbjavascript:cript:", - ] - for style_code in style_codes: - html = '' % style_code - s = lxml.html.fragment_fromstring(html) - - cleaned = lxml.html.tostring(clean_html(s)) - self.assertEqual( - b'', - cleaned, - "%s -> %s" % (style_code, cleaned)) - - def test_sneaky_urls_in_style(self): - style_codes = [ - "url(data:image/svg+xml;base64,...)", - "url(javasjavascript:cript:)", - "url(javasjavascript:cript: ::)", - "url(vbjavascript:cript:)", - "url(vbjavascript:cript: :)", - ] - for style_code in style_codes: - html = '' % style_code - s = lxml.html.fragment_fromstring(html) - - cleaned = lxml.html.tostring(clean_html(s)) - self.assertEqual( - b'', - cleaned, - "%s -> %s" % (style_code, cleaned)) - - def test_svg_data_links(self): - # Remove SVG images with potentially insecure content. - svg = b'' - gzout = io.BytesIO() - f = gzip.GzipFile(fileobj=gzout, mode='wb') - f.write(svg) - f.close() - svgz = gzout.getvalue() - svg_b64 = base64.b64encode(svg).decode('ASCII') - svgz_b64 = base64.b64encode(svgz).decode('ASCII') - urls = [ - "data:image/svg+xml;base64," + svg_b64, - "data:image/svg+xml-compressed;base64," + svgz_b64, - ] - for url in urls: - html = '' % url - s = lxml.html.fragment_fromstring(html) - - cleaned = lxml.html.tostring(clean_html(s)) - self.assertEqual( - b'', - cleaned, - "%s -> %s" % (url, cleaned)) - - def test_image_data_links(self): - data = b'123' - data_b64 = base64.b64encode(data).decode('ASCII') - urls = [ - "data:image/jpeg;base64," + data_b64, - "data:image/apng;base64," + data_b64, - "data:image/png;base64," + data_b64, - "data:image/gif;base64," + data_b64, - "data:image/webp;base64," + data_b64, - "data:image/bmp;base64," + data_b64, - "data:image/tiff;base64," + data_b64, - "data:image/x-icon;base64," + data_b64, - ] - for url in urls: - html = '' % url - s = lxml.html.fragment_fromstring(html) - - cleaned = lxml.html.tostring(clean_html(s)) - self.assertEqual( - html.encode("UTF-8"), - cleaned, - "%s -> %s" % (url, cleaned)) - - def test_image_data_links_in_style(self): - data = b'123' - data_b64 = base64.b64encode(data).decode('ASCII') - urls = [ - "data:image/jpeg;base64," + data_b64, - "data:image/apng;base64," + data_b64, - "data:image/png;base64," + data_b64, - "data:image/gif;base64," + data_b64, - "data:image/webp;base64," + data_b64, - "data:image/bmp;base64," + data_b64, - "data:image/tiff;base64," + data_b64, - "data:image/x-icon;base64," + data_b64, - ] - for url in urls: - html = '' % url - s = lxml.html.fragment_fromstring(html) - - cleaned = lxml.html.tostring(clean_html(s)) - self.assertEqual( - html.encode("UTF-8"), - cleaned, - "%s -> %s" % (url, cleaned)) - - def test_formaction_attribute_in_button_input(self): - # The formaction attribute overrides the form's action and should be - # treated as a malicious link attribute - html = ('
' - '') - expected = ('
' - '
') - cleaner = Cleaner( - forms=False, - safe_attrs_only=False, - ) - self.assertEqual( - expected, - cleaner.clean_html(html)) - - -def test_suite(): - suite = unittest.TestSuite() - suite.addTests([make_doctest('test_clean.txt')]) - suite.addTests([make_doctest('test_clean_embed.txt')]) - suite.addTests(unittest.makeSuite(CleanerTest)) - return suite diff --git a/src/lxml/html/tests/test_clean.txt b/src/lxml/html/tests/test_clean.txt deleted file mode 100644 index 18e6c7e61..000000000 --- a/src/lxml/html/tests/test_clean.txt +++ /dev/null @@ -1,221 +0,0 @@ ->>> import re ->>> from lxml.html import fromstring, tostring ->>> from lxml.html.clean import clean, clean_html, Cleaner ->>> from lxml.html import usedoctest - ->>> doc = ''' -... -... -... -... -... -... -... -... -... -... a link -... a control char link -... data -... another link -...

a paragraph

-...
secret EVIL!
-... of EVIL! -... -...
-... Password: -...
-... spam spam SPAM! -... -... Text -... -... -... ''' - ->>> print(re.sub('[\x00-\x07\x0E]', '', doc)) - - - - - - - - - - - a link - a control char link - data - another link -

a paragraph

-
secret EVIL!
- of EVIL! - -
- Password: -
- spam spam SPAM! - - Text - - - - ->>> print(tostring(fromstring(doc)).decode("utf-8")) - - - - - - - - - - - a link - a control char link - data - another link -

a paragraph

-
secret EVIL!
- of EVIL! - -
- Password: -
- spam spam SPAM! - - Text - - - - ->>> print(Cleaner(page_structure=False, comments=False).clean_html(doc)) - - - - - - - a link - a control char link - data - another link -

a paragraph

-
secret EVIL!
- of EVIL! - Password: - spam spam SPAM! - - Text - - - - ->>> print(Cleaner(page_structure=False, safe_attrs_only=False).clean_html(doc)) - - - - - - a link - a control char link - data - another link -

a paragraph

-
secret EVIL!
- of EVIL! - Password: - spam spam SPAM! - - Text - - - - ->>> print(Cleaner(style=True, inline_style=True, links=True, add_nofollow=True, page_structure=False, safe_attrs_only=False).clean_html(doc)) - - - - - a link - a control char link - data - another link -

a paragraph

-
secret EVIL!
- of EVIL! - Password: - spam spam SPAM! - Author - Text - - - - ->>> print(Cleaner(style=True, inline_style=False, links=True, add_nofollow=True, page_structure=False, safe_attrs_only=False).clean_html(doc)) - - - - - a link - a control char link - data - another link -

a paragraph

-
secret EVIL!
- of EVIL! - Password: - spam spam SPAM! - Author - Text - - - - ->>> print(Cleaner(links=False, page_structure=False, javascript=True, host_whitelist=['example.com'], whitelist_tags=None).clean_html(doc)) - - - - - - - - - a link - a control char link - data - another link -

a paragraph

-
secret EVIL!
- of EVIL! - Password: - spam spam SPAM! - - Text - - - diff --git a/src/lxml/html/tests/test_clean_embed.txt b/src/lxml/html/tests/test_clean_embed.txt deleted file mode 100644 index 59a40551d..000000000 --- a/src/lxml/html/tests/test_clean_embed.txt +++ /dev/null @@ -1,39 +0,0 @@ -THIS FAILS IN libxml2 2.6.29 AND 2.6.30 !! - - ->>> from lxml.html import fromstring, tostring ->>> from lxml.html.clean import clean, clean_html, Cleaner ->>> from lxml.html import usedoctest - ->>> def tostring(el): # work-around for Py3 'bytes' type -... from lxml.html import tostring -... s = tostring(el) -... if not isinstance(s, str): -... s = s.decode('UTF-8') -... return s - ->>> doc_embed = '''
-... -... -... -... -...
''' ->>> print(tostring(fromstring(doc_embed))) -
- - - - -
->>> print(Cleaner().clean_html(doc_embed)) -
-
->>> print(Cleaner(host_whitelist=['www.youtube.com']).clean_html(doc_embed)) -
- -
->>> print(Cleaner(host_whitelist=['www.youtube.com'], whitelist_tags=None).clean_html(doc_embed)) -
- - -
diff --git a/src/lxml/html/tests/test_diff.py b/src/lxml/html/tests/test_diff.py index c1adbd674..8157d582b 100644 --- a/src/lxml/html/tests/test_diff.py +++ b/src/lxml/html/tests/test_diff.py @@ -1,11 +1,11 @@ +import doctest import unittest -from lxml.tests.common_imports import make_doctest, doctest from lxml.html import diff def test_suite(): suite = unittest.TestSuite() - suite.addTests([make_doctest('test_diff.txt'), + suite.addTests([doctest.DocFileSuite('test_diff.txt'), doctest.DocTestSuite(diff)]) return suite diff --git a/src/lxml/html/tests/test_diff.txt b/src/lxml/html/tests/test_diff.txt index 9057a2b62..ce78e2f35 100644 --- a/src/lxml/html/tests/test_diff.txt +++ b/src/lxml/html/tests/test_diff.txt @@ -14,7 +14,7 @@ Example:: >>> from lxml.html.diff import htmldiff, html_annotate >>> html1 = '

This is some test text with some changes and some same stuff

' - >>> html2 = '''

This is some test textual writing with some changed stuff + >>> html2 = '''

This is some test textual writing with some changed stuff ... and some same stuff

''' >>> pdiff(html1, html2)

This is some test textual writing with some changed stuff @@ -46,7 +46,7 @@ Style tags are largely ignored in terms of differences, though markup is not eli

Hey there

Movement between paragraphs is ignored, as tag-based changes are generally ignored:: - >>> + >>> >>> pdiff('

Hello

World

', '

Hello World

')

Hello World

@@ -71,7 +71,7 @@ A test of empty elements: >>> pdiff('some
text', 'some
test') some
test

text
- + Whitespace is generally ignored for the diff but preserved during the diff: >>> print(htmldiff('

first\nsecond\nthird

', '

  first\n second\nthird

')) @@ -87,6 +87,27 @@ Whitespace is generally ignored for the diff but preserved during the diff: second third +Ensure we preserve the html structure on doing the diff: + + >>> a = "
some old text
more old text
" + >>> b = "
some old text
and new text
more old text
" + >>> pdiff(a, b) +
some old text
+ and new some old text
more + old text
+ >>> a = "

Some text that will change

Some tags will be added

" + >>> b = "

Some text that has changed a bit

All of this is new

" + >>> pdiff(a, b) +

Some text that has changed a bit

+

All of this is new

will + change

Some tags will be added

+ +The fine-grained diff above is a choice in lxml 6.0. We used to generate this: + +

Some text that has changed a bit

+

All of this is new

will + change

Some tags will be added

+ The sixteen combinations:: First "insert start" (del start/middle/end/none): @@ -141,7 +162,7 @@ Then no insert (del start/middle/end): A B C >>> pdiff('A

hey there how are you?

', 'A') A

hey there how are you?

- + Testing a larger document, to make sure there are not weird unnecessary parallels found: @@ -208,13 +229,13 @@ Now, a sequence of documents:

Hey Guy

+ Internals --------- - Some utility functions:: - >>> from lxml.html.diff import fixup_ins_del_tags, split_unbalanced, split_trailing_whitespace + >>> from lxml.html.diff import fixup_ins_del_tags, split_trailing_whitespace >>> def pfixup(text): ... print(fixup_ins_del_tags(text).strip()) >>> pfixup('

some text and more text and more

') @@ -227,21 +248,6 @@ Some utility functions:: ...
One tableMore stuff
''')
One tableMore stuff
- -Testing split_unbalanced:: - - >>> split_unbalanced(['', 'hey', '']) - ([], ['', 'hey', ''], []) - >>> split_unbalanced(['', 'hey']) - ([''], ['hey'], []) - >>> split_unbalanced(['Hey', '', 'You', '']) - ([], ['Hey', 'You'], ['', '']) - >>> split_unbalanced(['So', '', 'Hi', '', 'There', '']) - ([], ['So', 'Hi', '', 'There', ''], ['']) - >>> split_unbalanced(['So', '', 'Hi', '', 'There']) - ([''], ['So', 'Hi', 'There'], ['']) - - Testing split_trailing_whitespace:: >>> split_trailing_whitespace('test\n\n') diff --git a/src/lxml/html/tests/test_elementsoup.py b/src/lxml/html/tests/test_elementsoup.py index 553586b9f..7e4aa899b 100644 --- a/src/lxml/html/tests/test_elementsoup.py +++ b/src/lxml/html/tests/test_elementsoup.py @@ -1,5 +1,5 @@ import unittest, sys -from lxml.tests.common_imports import make_doctest, HelperTestCase +from ...tests.common_imports import make_doctest, HelperTestCase try: import lxml.html.soupparser @@ -50,7 +50,7 @@ def test_head_body(self): def test_wrap_html(self): # outside , parser should fix that - html = 'title</test></head><html><body/></html>' + html = '<head><title>title' res = b'title' tree = self.soupparser.fromstring(html) self.assertEqual(tostring(tree), res) @@ -118,9 +118,8 @@ def test_doctype_html5(self): def test_suite(): suite = unittest.TestSuite() if BS_INSTALLED: - suite.addTests([unittest.makeSuite(SoupParserTestCase)]) - if sys.version_info[0] < 3: - suite.addTests([make_doctest('../../../../doc/elementsoup.txt')]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(SoupParserTestCase)]) + #suite.addTests([make_doctest('elementsoup.txt')]) # FIXME: Py2-only ? return suite diff --git a/src/lxml/html/tests/test_feedparser_data.py b/src/lxml/html/tests/test_feedparser_data.py index 29a500ff3..ab4277409 100644 --- a/src/lxml/html/tests/test_feedparser_data.py +++ b/src/lxml/html/tests/test_feedparser_data.py @@ -6,10 +6,13 @@ # Python 3 from email import message_from_file as Message import unittest -from lxml.tests.common_imports import doctest from lxml.doctestcompare import LHTMLOutputChecker -from lxml.html.clean import clean, Cleaner +try: + from lxml.html.clean import clean, Cleaner + html_clean_available = True +except ImportError: + html_clean_available = False feed_dirs = [ os.path.join(os.path.dirname(__file__), 'feedparser-data'), @@ -29,10 +32,9 @@ def __init__(self, filename): unittest.TestCase.__init__(self) def parse(self): - f = open(self.filename, 'r') - headers = Message(f) - c = f.read() - f.close() + with open(self.filename) as f: + headers = Message(f) + c = f.read() if not c.strip(): c = headers.get_payload() if not headers.keys(): @@ -81,6 +83,11 @@ def shortDescription(self): def test_suite(): suite = unittest.TestSuite() + + if not html_clean_available: + print("Skipping tests in feedparser_data - external lxml_html_clean package is not installed") + return suite + for dir in feed_dirs: for fn in os.listdir(dir): fn = os.path.join(dir, fn) diff --git a/src/lxml/html/tests/test_formfill.py b/src/lxml/html/tests/test_formfill.py index 0f5351861..1e3bc7030 100644 --- a/src/lxml/html/tests/test_formfill.py +++ b/src/lxml/html/tests/test_formfill.py @@ -1,7 +1,7 @@ +import doctest import unittest -from lxml.tests.common_imports import make_doctest def test_suite(): suite = unittest.TestSuite() - suite.addTests([make_doctest('test_formfill.txt')]) + suite.addTests([doctest.DocFileSuite('test_formfill.txt')]) return suite diff --git a/src/lxml/html/tests/test_forms.py b/src/lxml/html/tests/test_forms.py index 37a0327fc..23b1e1c5f 100644 --- a/src/lxml/html/tests/test_forms.py +++ b/src/lxml/html/tests/test_forms.py @@ -1,9 +1,9 @@ +import doctest import unittest -from lxml.tests.common_imports import make_doctest def test_suite(): suite = unittest.TestSuite() - suite.addTests([make_doctest('test_forms.txt')]) + suite.addTests([doctest.DocFileSuite('test_forms.txt')]) return suite if __name__ == '__main__': diff --git a/src/lxml/html/tests/test_forms.txt b/src/lxml/html/tests/test_forms.txt index 5d7d51393..d0efcc408 100644 --- a/src/lxml/html/tests/test_forms.txt +++ b/src/lxml/html/tests/test_forms.txt @@ -43,10 +43,10 @@ ... ... ''', base_url='http://example.org/form.html') >>> h.base_url -u'http://example.org/form.html' +'http://example.org/form.html' >>> f = h.forms[0] >>> f.action -u'http://example.org/test' +'http://example.org/test' >>> f.method 'GET' diff --git a/src/lxml/html/tests/test_frames.py b/src/lxml/html/tests/test_frames.py index 2eeb844ee..cfb759cf5 100644 --- a/src/lxml/html/tests/test_frames.py +++ b/src/lxml/html/tests/test_frames.py @@ -1,7 +1,5 @@ import unittest, sys -from lxml.tests.common_imports import make_doctest, doctest import lxml.html -from lxml.html import html_parser, XHTML_NAMESPACE class FrameTest(unittest.TestCase): @@ -33,4 +31,4 @@ def test_parse_fromstring(self): def test_suite(): loader = unittest.TestLoader() - return loader.loadTestsFromModule(sys.modules[__name__]) \ No newline at end of file + return loader.loadTestsFromModule(sys.modules[__name__]) diff --git a/src/lxml/html/tests/test_html5parser.py b/src/lxml/html/tests/test_html5parser.py index 56afe98b7..a3b997178 100644 --- a/src/lxml/html/tests/test_html5parser.py +++ b/src/lxml/html/tests/test_html5parser.py @@ -1,5 +1,4 @@ import os -import imp try: from StringIO import StringIO except ImportError: # python 3 @@ -34,45 +33,14 @@ def path2url(path): except ImportError: html5lib = None - class BogusModules(object): - # See PEP 302 for details on how this works - def __init__(self, mocks): - self.mocks = mocks - - def find_module(self, fullname, path=None): - if fullname in self.mocks: - return self - return None - - def load_module(self, fullname): - mod = sys.modules.setdefault(fullname, imp.new_module(fullname)) - mod.__file__, mod.__loader__, mod.__path__ = "", self, [] - mod.__dict__.update(self.mocks[fullname]) - return mod - - # Fake just enough of html5lib so that html5parser.py is importable - # without errors. - sys.meta_path.append(BogusModules({ - 'html5lib': { - # A do-nothing HTMLParser class - 'HTMLParser': type('HTMLParser', (object,), { - '__init__': lambda self, **kw: None, - }), - }, - 'html5lib.treebuilders': { - }, - 'html5lib.treebuilders.etree_lxml': { - 'TreeBuilder': 'dummy treebuilder', - }, - })) - class Test_HTMLParser(unittest.TestCase): def make_one(self, **kwargs): + if html5lib is None: + raise unittest.SkipTest("html5lib is not installed") from lxml.html.html5parser import HTMLParser return HTMLParser(**kwargs) - @skipUnless(html5lib, 'html5lib is not installed') def test_integration(self): parser = self.make_one(strict=True) tree = parser.parse(XHTML_TEST_DOCUMENT) @@ -97,6 +65,8 @@ def test_integration(self): class Test_document_fromstring(unittest.TestCase): def call_it(self, *args, **kwargs): + if html5lib is None: + raise unittest.SkipTest("html5lib is not installed") from lxml.html.html5parser import document_fromstring return document_fromstring(*args, **kwargs) @@ -121,7 +91,6 @@ def test_raises_type_error_on_nonstring_input(self): not_a_string = None self.assertRaises(TypeError, self.call_it, not_a_string) - @skipUnless(html5lib, 'html5lib is not installed') def test_integration(self): elem = self.call_it(XHTML_TEST_DOCUMENT) self.assertEqual(elem.tag, xhtml_tag('html')) @@ -129,6 +98,8 @@ def test_integration(self): class Test_fragments_fromstring(unittest.TestCase): def call_it(self, *args, **kwargs): + if html5lib is None: + raise unittest.SkipTest("html5lib is not installed") from lxml.html.html5parser import fragments_fromstring return fragments_fromstring(*args, **kwargs) @@ -162,7 +133,6 @@ def test_no_leading_text_raises_error_if_leading_text(self): self.assertRaises(ParserError, self.call_it, '', parser=parser, no_leading_text=True) - @skipUnless(html5lib, 'html5lib is not installed') def test_integration(self): fragments = self.call_it('ac') self.assertEqual(len(fragments), 2) @@ -172,6 +142,8 @@ def test_integration(self): class Test_fragment_fromstring(unittest.TestCase): def call_it(self, *args, **kwargs): + if html5lib is None: + raise unittest.SkipTest("html5lib is not installed") from lxml.html.html5parser import fragment_fromstring return fragment_fromstring(*args, **kwargs) @@ -215,6 +187,8 @@ def test_raises_error_if_tail(self): class Test_fromstring(unittest.TestCase): def call_it(self, *args, **kwargs): + if html5lib is None: + raise unittest.SkipTest("html5lib is not installed") from lxml.html.html5parser import fromstring return fromstring(*args, **kwargs) @@ -230,7 +204,7 @@ def test_returns_whole_doc_if_input_contains_doctype(self): def test_returns_whole_doc_if_input_is_encoded(self): parser = DummyParser(root='the doc') - input = ''.encode('ascii') + input = b'' self.assertEqual(self.call_it(input, parser=parser), 'the doc') @@ -285,12 +259,10 @@ def test_raises_type_error_on_nonstring_input(self): not_a_string = None self.assertRaises(TypeError, self.call_it, not_a_string) - @skipUnless(html5lib, 'html5lib is not installed') def test_integration_whole_doc(self): elem = self.call_it(XHTML_TEST_DOCUMENT) self.assertEqual(elem.tag, xhtml_tag('html')) - @skipUnless(html5lib, 'html5lib is not installed') def test_integration_single_fragment(self): elem = self.call_it('

') self.assertEqual(elem.tag, xhtml_tag('p')) @@ -298,6 +270,8 @@ def test_integration_single_fragment(self): class Test_parse(unittest.TestCase): def call_it(self, *args, **kwargs): + if html5lib is None: + raise unittest.SkipTest("html5lib is not installed") from lxml.html.html5parser import parse return parse(*args, **kwargs) @@ -317,12 +291,9 @@ def make_temp_file(self, contents=''): def test_with_file_object(self): parser = DummyParser(doc='the doc') - fp = open(__file__) - try: + with open(__file__) as fp: self.assertEqual(self.call_it(fp, parser=parser), 'the doc') self.assertEqual(parser.parse_args, (fp,)) - finally: - fp.close() def test_with_file_name(self): parser = DummyParser(doc='the doc') @@ -359,7 +330,6 @@ def test_with_url(self): finally: os.unlink(tmpfile.name) - @skipUnless(html5lib, 'html5lib is not installed') def test_integration(self): doc = self.call_it(StringIO(XHTML_TEST_DOCUMENT)) root = doc.getroot() @@ -380,7 +350,7 @@ def __init__(self, namespaceHTMLElements=True): ElementMaker.__init__(self, **initargs) -class DummyParser(object): +class DummyParser: def __init__(self, doc=None, root=None, fragments=None, namespaceHTMLElements=True): self.doc = doc or DummyElementTree(root=root) @@ -398,12 +368,12 @@ def parseFragment(self, *args, **kwargs): return self.fragments -class DummyTreeBuilder(object): +class DummyTreeBuilder: def __init__(self, namespaceHTMLElements=True): self.namespaceHTMLElements = namespaceHTMLElements -class DummyElementTree(object): +class DummyElementTree: def __init__(self, root): self.root = root @@ -411,7 +381,7 @@ def getroot(self): return self.root -class DummyElement(object): +class DummyElement: def __init__(self, tag='tag', tail=None): self.tag = tag self.tail = tail diff --git a/src/lxml/html/tests/test_rewritelinks.py b/src/lxml/html/tests/test_rewritelinks.py index 100105fa4..c7665837f 100644 --- a/src/lxml/html/tests/test_rewritelinks.py +++ b/src/lxml/html/tests/test_rewritelinks.py @@ -1,9 +1,9 @@ +import doctest import unittest -from lxml.tests.common_imports import make_doctest def test_suite(): suite = unittest.TestSuite() - suite.addTests([make_doctest('test_rewritelinks.txt')]) + suite.addTests([doctest.DocFileSuite('test_rewritelinks.txt')]) return suite if __name__ == '__main__': diff --git a/src/lxml/html/tests/test_rewritelinks.txt b/src/lxml/html/tests/test_rewritelinks.txt index 9bd60af5c..0a25d2da4 100644 --- a/src/lxml/html/tests/test_rewritelinks.txt +++ b/src/lxml/html/tests/test_rewritelinks.txt @@ -138,6 +138,11 @@ link)``, which is awkward to test here, so we'll make a printer:: img src="/logo.gif" td style="/quoted.png"@23 +This also works directly on bytes input:: + + >>> print_iter(iterlinks(b'
lxml')) + a href="https://lxml.de/" + An application of ``iterlinks()`` is ``make_links_absolute()``:: >>> from lxml.html import make_links_absolute @@ -204,22 +209,21 @@ An application of ``iterlinks()`` is ``make_links_absolute()``:: -### Test disabled to support Py2.6 and earlier -#If the document contains invalid links, you may choose to "discard" or "ignore" -#them by passing the respective option into the ``handle_failures`` argument:: -# -# >>> html = lxml.html.fromstring ('''\ -# ...
-# ... test2 -# ...
''') -# -# >>> html.make_links_absolute(base_url="http://my.little.server/url/", -# ... handle_failures="discard") -# -# >>> print(lxml.html.tostring (html, pretty_print=True, encoding='unicode')) -#
-# test2 -#
+If the document contains invalid links, you may choose to "discard" or "ignore" +them by passing the respective option into the ``handle_failures`` argument:: + + >>> html = lxml.html.fromstring ('''\ + ...
+ ... test2 + ...
''') + + >>> html.make_links_absolute(base_url="http://my.little.server/url/", + ... handle_failures="discard") + + >>> print(lxml.html.tostring (html, pretty_print=True, encoding='unicode')) +
+ test2 +
Check if we can replace multiple links inside of the same text string:: diff --git a/src/lxml/html/tests/test_xhtml.py b/src/lxml/html/tests/test_xhtml.py index cc66170dd..5c71d8d28 100644 --- a/src/lxml/html/tests/test_xhtml.py +++ b/src/lxml/html/tests/test_xhtml.py @@ -1,9 +1,9 @@ +import doctest import unittest -from lxml.tests.common_imports import make_doctest def test_suite(): suite = unittest.TestSuite() - suite.addTests([make_doctest('test_xhtml.txt')]) + suite.addTests([doctest.DocFileSuite('test_xhtml.txt')]) return suite if __name__ == '__main__': diff --git a/src/lxml/includes/c14n.pxd b/src/lxml/includes/c14n.pxd index d075e90e2..8b1f3c4c5 100644 --- a/src/lxml/includes/c14n.pxd +++ b/src/lxml/includes/c14n.pxd @@ -1,13 +1,13 @@ from lxml.includes.tree cimport xmlDoc, xmlOutputBuffer, xmlChar from lxml.includes.xpath cimport xmlNodeSet -cdef extern from "libxml/c14n.h": +cdef extern from "libxml/c14n.h" nogil: cdef int xmlC14NDocDumpMemory(xmlDoc* doc, xmlNodeSet* nodes, int exclusive, xmlChar** inclusive_ns_prefixes, int with_comments, - xmlChar** doc_txt_ptr) nogil + xmlChar** doc_txt_ptr) cdef int xmlC14NDocSave(xmlDoc* doc, xmlNodeSet* nodes, @@ -15,12 +15,11 @@ cdef extern from "libxml/c14n.h": xmlChar** inclusive_ns_prefixes, int with_comments, char* filename, - int compression) nogil + int compression) cdef int xmlC14NDocSaveTo(xmlDoc* doc, xmlNodeSet* nodes, int exclusive, xmlChar** inclusive_ns_prefixes, int with_comments, - xmlOutputBuffer* buffer) nogil - + xmlOutputBuffer* buffer) diff --git a/src/lxml/includes/dtdvalid.pxd b/src/lxml/includes/dtdvalid.pxd index ae94dc63a..2ad49db11 100644 --- a/src/lxml/includes/dtdvalid.pxd +++ b/src/lxml/includes/dtdvalid.pxd @@ -2,8 +2,8 @@ from lxml.includes cimport tree from lxml.includes.tree cimport xmlDoc, xmlDtd cdef extern from "libxml/valid.h" nogil: - ctypedef void (*xmlValidityErrorFunc)(void * ctx, const char * msg, ...) - ctypedef void (*xmlValidityWarningFunc)(void * ctx, const char * msg, ...) + ctypedef void (*xmlValidityErrorFunc)(void * ctx, const char * msg, ...) noexcept + ctypedef void (*xmlValidityWarningFunc)(void * ctx, const char * msg, ...) noexcept ctypedef struct xmlValidCtxt: void *userData diff --git a/src/lxml/includes/etree_defs.h b/src/lxml/includes/etree_defs.h index e671fa85d..79f1e345d 100644 --- a/src/lxml/includes/etree_defs.h +++ b/src/lxml/includes/etree_defs.h @@ -5,29 +5,25 @@ #include "Python.h" #ifndef PY_VERSION_HEX # error the development package of Python (header files etc.) is not installed correctly -#else -# if PY_VERSION_HEX < 0x02070000 || PY_MAJOR_VERSION >= 3 && PY_VERSION_HEX < 0x03050000 -# error this version of lxml requires Python 2.7, 3.5 or later -# endif +#elif defined(Py_LIMITED_API) && Py_LIMITED_API < 0x030b0000 +# error building this version of lxml in the Limited API requires Python 3.11 or later +#elif PY_VERSION_HEX < 0x03090000 +# error this version of lxml requires Python 3.9 or later #endif #include "libxml/xmlversion.h" #ifndef LIBXML_VERSION # error the development package of libxml2 (header files etc.) is not installed correctly -#else -#if LIBXML_VERSION < 20700 -# error minimum required version of libxml2 is 2.7.0 -#endif +#elif LIBXML_VERSION < 20704 +# error minimum required version of libxml2 is 2.7.4 #endif #include "libxslt/xsltconfig.h" #ifndef LIBXSLT_VERSION # error the development package of libxslt (header files etc.) is not installed correctly -#else -#if LIBXSLT_VERSION < 10123 +#elif LIBXSLT_VERSION < 10123 # error minimum required version of libxslt is 1.1.23 #endif -#endif /* v_arg functions */ @@ -35,27 +31,16 @@ #define va_charptr(ap) va_arg(ap, char *) #ifdef PYPY_VERSION -# define IS_PYPY 1 -#else -# define IS_PYPY 0 -#endif - -#if PY_MAJOR_VERSION >= 3 -# define IS_PYTHON2 0 /* prefer for special casing Python 2.x */ -# define IS_PYTHON3 1 /* avoid */ +# define IS_PYPY (1) #else -# define IS_PYTHON2 1 -# define IS_PYTHON3 0 +# define IS_PYPY (0) #endif -#if IS_PYTHON2 -#ifndef LXML_UNICODE_STRINGS -#define LXML_UNICODE_STRINGS 0 -#endif -#else +/* unused */ +#define IS_PYTHON2 0 +#define IS_PYTHON3 1 #undef LXML_UNICODE_STRINGS #define LXML_UNICODE_STRINGS 1 -#endif #if !IS_PYPY # define PyWeakref_LockObject(obj) (NULL) @@ -68,21 +53,11 @@ # endif #endif -#if IS_PYPY -# undef PyFile_AsFile -# define PyFile_AsFile(o) (NULL) -# undef PyByteArray_Check -# define PyByteArray_Check(o) (0) -#elif !IS_PYTHON2 - /* Python 3+ doesn't have PyFile_*() anymore */ -# define PyFile_AsFile(o) (NULL) -#endif - #if IS_PYPY # ifndef PyUnicode_FromFormat # define PyUnicode_FromFormat PyString_FromFormat # endif -# if !IS_PYTHON2 && !defined(PyBytes_FromFormat) +# if !defined(PyBytes_FromFormat) # ifdef PyString_FromFormat # define PyBytes_FromFormat PyString_FromFormat # else @@ -113,11 +88,14 @@ static PyObject* PyBytes_FromFormat(const char* format, ...) { # endif #endif -/* PySlice_GetIndicesEx() has wrong signature in Py<=3.1 */ -#if PY_VERSION_HEX >= 0x03020000 -# define _lx_PySlice_GetIndicesEx(o, l, b, e, s, sl) PySlice_GetIndicesEx(o, l, b, e, s, sl) -#else -# define _lx_PySlice_GetIndicesEx(o, l, b, e, s, sl) PySlice_GetIndicesEx(((PySliceObject*)o), l, b, e, s, sl) +#if PY_VERSION_HEX >= 0x030B00A1 || defined(Py_LIMITED_API) +/* Python 3.12 doesn't have wstr Unicode strings any more. */ +#undef PyUnicode_GET_DATA_SIZE +#define PyUnicode_GET_DATA_SIZE(ustr) (0) +#undef PyUnicode_AS_DATA +#define PyUnicode_AS_DATA(ustr) (NULL) +#undef PyUnicode_IS_READY +#define PyUnicode_IS_READY(ustr) (1) #endif #ifdef WITHOUT_THREADING @@ -173,7 +151,18 @@ static PyObject* PyBytes_FromFormat(const char* format, ...) { # define HTML_PARSE_NODEFDTD 4 #endif #if LIBXML_VERSION < 20900 -# define XML_PARSE_BIG_LINES 4194304 +# define XML_PARSE_BIG_LINES 0x400000 +#endif +#if LIBXML_VERSION < 21300 +# define XML_PARSE_NO_XXE 0x800000 +#endif +#if LIBXML_VERSION < 21400 +# define XML_PARSE_UNZIP 0x1000000 +# define XML_PARSE_NO_SYS_CATALOG 0x2000000 +# define XML_PARSE_CATALOG_PI 0x4000000 +#endif +#if LIBXML_VERSION < 21500 +# define XML_PARSE_SKIP_IDS 0x8000000 #endif #include "libxml/tree.h" @@ -183,6 +172,10 @@ static PyObject* PyBytes_FromFormat(const char* format, ...) { # define xmlBufUse(buf) xmlBufferLength(buf) #endif +#if LIBXML_VERSION < 21500 +# define xmlCtxtIsStopped(p_ctxt) ((p_ctxt)->disableSAX != 0) +#endif + /* libexslt 1.1.25+ support EXSLT functions in XPath */ #if LIBXSLT_VERSION < 10125 #define exsltDateXpathCtxtRegister(ctxt, prefix) @@ -201,7 +194,7 @@ long _ftol2( double dblSource ) { return _ftol( dblSource ); } #ifdef __GNUC__ /* Test for GCC > 2.95 */ -#if __GNUC__ > 2 || (__GNUC__ == 2 && (__GNUC_MINOR__ > 95)) +#if __GNUC__ > 2 || (__GNUC__ == 2 && (__GNUC_MINOR__ > 95)) #define unlikely_condition(x) __builtin_expect((x), 0) #else /* __GNUC__ > 2 ... */ #define unlikely_condition(x) (x) @@ -210,15 +203,29 @@ long _ftol2( double dblSource ) { return _ftol( dblSource ); } #define unlikely_condition(x) (x) #endif /* __GNUC__ */ -#ifndef Py_TYPE - #define Py_TYPE(ob) (((PyObject*)(ob))->ob_type) -#endif -#define PY_NEW(T) \ - (((PyTypeObject*)(T))->tp_new( \ - (PyTypeObject*)(T), __pyx_empty_tuple, NULL)) - -#define _fqtypename(o) ((Py_TYPE(o))->tp_name) +#if defined(Py_LIMITED_API) + #define __lxml_typename(o) PyType_GetName(Py_TYPE(o)) + #define __lxml_fqtypename(o) PyType_GetQualName(Py_TYPE(o)) +#else + #ifndef Py_TYPE + #define Py_TYPE(ob) (((PyObject*)(ob))->ob_type) + #endif + static CYTHON_INLINE PyObject* __lxml_typename(PyObject* o) { + const char* c_name = (Py_TYPE(o))->tp_name; + const char* c_typename = c_name; + const char* c_pos = c_name; + while (*c_pos != '\0') { + if (*(c_pos++) == '.') + c_typename = c_pos; + } + return PyUnicode_DecodeUTF8(c_typename, c_pos - c_typename, NULL); + } + static CYTHON_INLINE PyObject* __lxml_fqtypename(PyObject* o) { + const char* c_name = (Py_TYPE(o))->tp_name; + return PyUnicode_DecodeUTF8(c_name, strlen(c_name), NULL); + } +#endif #define lxml_malloc(count, item_size) \ (unlikely_condition((size_t)(count) > (size_t) (PY_SSIZE_T_MAX / item_size)) ? NULL : \ @@ -230,21 +237,7 @@ long _ftol2( double dblSource ) { return _ftol( dblSource ); } #define lxml_free(mem) PyMem_Free(mem) -#if PY_MAJOR_VERSION < 3 -#define _isString(obj) (PyString_CheckExact(obj) || \ - PyUnicode_CheckExact(obj) || \ - PyType_IsSubtype(Py_TYPE(obj), &PyBaseString_Type)) -#else -/* builtin subtype type checks are almost as fast as exact checks in Py2.7+ - * and Unicode is more common in Py3 */ #define _isString(obj) (PyUnicode_Check(obj) || PyBytes_Check(obj)) -#endif - -#if PY_VERSION_HEX >= 0x03060000 -#define lxml_PyOS_FSPath(obj) (PyOS_FSPath(obj)) -#else -#define lxml_PyOS_FSPath(obj) (NULL) -#endif #define _isElement(c_node) \ (((c_node)->type == XML_ELEMENT_NODE) || \ @@ -306,7 +299,7 @@ static void* lxml_unpack_xmldoc_capsule(PyObject* capsule, int* is_owned) { * 'inclusive' is 1). The _ELEMENT_ variants will only stop on nodes * that match _isElement(), the normal variant will stop on every node * except text nodes. - * + * * To traverse the node and all of its children and siblings in Pyrex, call * cdef xmlNode* some_node * BEGIN_FOR_EACH_ELEMENT_FROM(some_node.parent, some_node, 1) diff --git a/src/lxml/includes/etreepublic.pxd b/src/lxml/includes/etreepublic.pxd index 94fe2e8d3..29ee5f6fd 100644 --- a/src/lxml/includes/etreepublic.pxd +++ b/src/lxml/includes/etreepublic.pxd @@ -4,20 +4,20 @@ from lxml.includes cimport tree from lxml.includes.tree cimport const_xmlChar cdef extern from "lxml-version.h": - cdef char* LXML_VERSION_STRING + cdef const char* LXML_VERSION_STRING cdef extern from "etree_defs.h": # test if c_node is considered an Element (i.e. Element, Comment, etc.) - cdef bint _isElement(tree.xmlNode* c_node) nogil + cdef bint _isElement(tree.xmlNode* c_node) noexcept nogil # return the namespace URI of the node or NULL - cdef const_xmlChar* _getNs(tree.xmlNode* node) nogil + cdef const_xmlChar* _getNs(tree.xmlNode* node) noexcept nogil # pair of macros for tree traversal cdef void BEGIN_FOR_EACH_ELEMENT_FROM(tree.xmlNode* tree_top, tree.xmlNode* start_node, - int start_node_inclusive) nogil - cdef void END_FOR_EACH_ELEMENT_FROM(tree.xmlNode* start_node) nogil + int start_node_inclusive) noexcept nogil + cdef void END_FOR_EACH_ELEMENT_FROM(tree.xmlNode* start_node) noexcept nogil cdef extern from "etree_api.h": @@ -49,6 +49,23 @@ cdef extern from "etree_api.h": cdef ElementClassLookup fallback cdef object (*_fallback_function)(object, _Document, tree.xmlNode*) + + ########################################################################## + # locking documents for reading and writing + + # read-only locking + cdef void lock_read(_Document doc) noexcept + cdef void unlock_read(_Document doc) noexcept + + # write locking (for modifications) + cdef void lock_write(_Document doc) noexcept + cdef void unlock_write(_Document doc) noexcept + + # write locking when moving parts between two different documents + cdef void lock_write2(_Document doc1, _Document doc2) noexcept + cdef void unlock_write2(_Document doc1, _Document doc2) noexcept + + ########################################################################## # creating Element objects @@ -101,12 +118,12 @@ cdef extern from "etree_api.h": # XML attribute access # return an attribute value for a C attribute on a C element node - cdef object attributeValue(tree.xmlNode* c_element, - tree.xmlAttr* c_attrib_node) + cdef str attributeValue(tree.xmlNode* c_element, + tree.xmlAttr* c_attrib_node) # return the value of the attribute with 'ns' and 'name' (or None) - cdef object attributeValueFromNsName(tree.xmlNode* c_element, - const_xmlChar* c_ns, const_xmlChar* c_name) + cdef str attributeValueFromNsName(tree.xmlNode* c_element, + const_xmlChar* c_ns, const_xmlChar* c_name) # return the value of attribute "{ns}name", or the default value cdef object getAttributeValue(_Element element, key, default) @@ -129,36 +146,36 @@ cdef extern from "etree_api.h": # delete an attribute based on name and namespace URI # returns -1 if the attribute was not found (no exception) cdef int delAttributeFromNsName(tree.xmlNode* c_element, - const_xmlChar* c_href, const_xmlChar* c_name) + const_xmlChar* c_href, const_xmlChar* c_name) noexcept ########################################################################## # XML node helper functions # check if the element has at least one child - cdef bint hasChild(tree.xmlNode* c_node) nogil + cdef bint hasChild(tree.xmlNode* c_node) noexcept nogil # find child element number 'index' (supports negative indexes) cdef tree.xmlNode* findChild(tree.xmlNode* c_node, - Py_ssize_t index) nogil + Py_ssize_t index) noexcept nogil # find child element number 'index' starting at first one cdef tree.xmlNode* findChildForwards(tree.xmlNode* c_node, - Py_ssize_t index) nogil + Py_ssize_t index) noexcept nogil # find child element number 'index' starting at last one cdef tree.xmlNode* findChildBackwards(tree.xmlNode* c_node, - Py_ssize_t index) nogil + Py_ssize_t index) noexcept nogil # return next/previous sibling element of the node - cdef tree.xmlNode* nextElement(tree.xmlNode* c_node) nogil - cdef tree.xmlNode* previousElement(tree.xmlNode* c_node) nogil + cdef tree.xmlNode* nextElement(tree.xmlNode* c_node) noexcept nogil + cdef tree.xmlNode* previousElement(tree.xmlNode* c_node) noexcept nogil ########################################################################## # iterators (DEPRECATED API, don't use in new code!) cdef class lxml.etree._ElementTagMatcher [ object LxmlElementTagMatcher ]: - cdef char* _href - cdef char* _name + cdef const char* _href + cdef const char* _name # store "{ns}tag" (or None) filter for this matcher or element iterator # ** unless _href *and* _name are set up 'by hand', this function *must* @@ -179,10 +196,10 @@ cdef extern from "etree_api.h": # check if a C node matches a tag name and namespace # (NULL allowed for each => always matches) - cdef int tagMatches(tree.xmlNode* c_node, const_xmlChar* c_href, const_xmlChar* c_name) + cdef int tagMatches(tree.xmlNode* c_node, const_xmlChar* c_href, const_xmlChar* c_name) noexcept - # convert a UTF-8 char* to a Python string or unicode string - cdef object pyunicode(const_xmlChar* s) + # convert a UTF-8 char* to a Python unicode string + cdef str pyunicode(const_xmlChar* s) # convert the string to UTF-8 using the normal lxml.etree semantics cdef bytes utf8(object s) @@ -194,22 +211,22 @@ cdef extern from "etree_api.h": cdef tuple getNsTagWithEmptyNs(object tag) # get the "{ns}tag" string for a C node - cdef object namespacedName(tree.xmlNode* c_node) + cdef str namespacedName(tree.xmlNode* c_node) # get the "{ns}tag" string for a href/tagname pair (c_ns may be NULL) - cdef object namespacedNameFromNsName(const_xmlChar* c_ns, const_xmlChar* c_tag) + cdef str namespacedNameFromNsName(const_xmlChar* c_ns, const_xmlChar* c_tag) # check if the node has a text value (which may be '') - cdef bint hasText(tree.xmlNode* c_node) nogil + cdef bint hasText(tree.xmlNode* c_node) noexcept nogil # check if the node has a tail value (which may be '') - cdef bint hasTail(tree.xmlNode* c_node) nogil + cdef bint hasTail(tree.xmlNode* c_node) noexcept nogil # get the text content of an element (or None) - cdef object textOf(tree.xmlNode* c_node) + cdef str textOf(tree.xmlNode* c_node) # get the tail content of an element (or None) - cdef object tailOf(tree.xmlNode* c_node) + cdef str tailOf(tree.xmlNode* c_node) # set the text value of an element cdef int setNodeText(tree.xmlNode* c_node, text) except -1 diff --git a/src/lxml/includes/htmlparser.pxd b/src/lxml/includes/htmlparser.pxd index 145a69a06..31dcc406c 100644 --- a/src/lxml/includes/htmlparser.pxd +++ b/src/lxml/includes/htmlparser.pxd @@ -4,7 +4,7 @@ from lxml.includes.tree cimport xmlDoc from lxml.includes.tree cimport xmlInputReadCallback, xmlInputCloseCallback from lxml.includes.xmlparser cimport xmlParserCtxt, xmlSAXHandler, xmlSAXHandlerV1 -cdef extern from "libxml/HTMLparser.h": +cdef extern from "libxml/HTMLparser.h" nogil: ctypedef enum htmlParserOption: HTML_PARSE_NOERROR # suppress error reports HTML_PARSE_NOWARNING # suppress warning reports @@ -24,33 +24,33 @@ cdef extern from "libxml/HTMLparser.h": xmlSAXHandlerV1 htmlDefaultSAXHandler cdef xmlParserCtxt* htmlCreateMemoryParserCtxt( - char* buffer, int size) nogil + char* buffer, int size) cdef xmlParserCtxt* htmlCreateFileParserCtxt( - char* filename, char* encoding) nogil + char* filename, char* encoding) cdef xmlParserCtxt* htmlCreatePushParserCtxt(xmlSAXHandler* sax, void* user_data, char* chunk, int size, - char* filename, int enc) nogil - cdef void htmlFreeParserCtxt(xmlParserCtxt* ctxt) nogil - cdef void htmlCtxtReset(xmlParserCtxt* ctxt) nogil - cdef int htmlCtxtUseOptions(xmlParserCtxt* ctxt, int options) nogil - cdef int htmlParseDocument(xmlParserCtxt* ctxt) nogil + char* filename, int enc) + cdef void htmlFreeParserCtxt(xmlParserCtxt* ctxt) + cdef void htmlCtxtReset(xmlParserCtxt* ctxt) + cdef int htmlCtxtUseOptions(xmlParserCtxt* ctxt, int options) + cdef int htmlParseDocument(xmlParserCtxt* ctxt) cdef int htmlParseChunk(xmlParserCtxt* ctxt, - char* chunk, int size, int terminate) nogil + char* chunk, int size, int terminate) cdef xmlDoc* htmlCtxtReadFile(xmlParserCtxt* ctxt, char* filename, const_char* encoding, - int options) nogil + int options) cdef xmlDoc* htmlCtxtReadDoc(xmlParserCtxt* ctxt, char* buffer, char* URL, const_char* encoding, - int options) nogil + int options) cdef xmlDoc* htmlCtxtReadIO(xmlParserCtxt* ctxt, xmlInputReadCallback ioread, xmlInputCloseCallback ioclose, void* ioctx, char* URL, const_char* encoding, - int options) nogil + int options) cdef xmlDoc* htmlCtxtReadMemory(xmlParserCtxt* ctxt, char* buffer, int size, char* filename, const_char* encoding, - int options) nogil + int options) diff --git a/src/lxml/includes/relaxng.pxd b/src/lxml/includes/relaxng.pxd index 28e9212d2..4913364da 100644 --- a/src/lxml/includes/relaxng.pxd +++ b/src/lxml/includes/relaxng.pxd @@ -1,12 +1,19 @@ from lxml.includes.tree cimport xmlDoc +from lxml.includes.xmlparser cimport xmlResourceLoader from lxml.includes.xmlerror cimport xmlStructuredErrorFunc -cdef extern from "libxml/relaxng.h": + +cdef extern from "libxml/relaxng.h" nogil: + """ + #if LIBXML_VERSION < 21400 + #define xmlRelaxNGSetResourceLoader(ctxt, loader, data) ((void) ((void) ctxt, (void) loader, (void) data)) + #endif + """ ctypedef struct xmlRelaxNG ctypedef struct xmlRelaxNGParserCtxt - + ctypedef struct xmlRelaxNGValidCtxt - + ctypedef enum xmlRelaxNGValidErr: XML_RELAXNG_OK = 0 XML_RELAXNG_ERR_MEMORY = 1 @@ -48,17 +55,20 @@ cdef extern from "libxml/relaxng.h": XML_RELAXNG_ERR_INTERNAL = 37 XML_RELAXNG_ERR_ELEMWRONG = 38 XML_RELAXNG_ERR_TEXTWRONG = 39 - - cdef xmlRelaxNGValidCtxt* xmlRelaxNGNewValidCtxt(xmlRelaxNG* schema) nogil - cdef int xmlRelaxNGValidateDoc(xmlRelaxNGValidCtxt* ctxt, xmlDoc* doc) nogil - cdef xmlRelaxNG* xmlRelaxNGParse(xmlRelaxNGParserCtxt* ctxt) nogil - cdef xmlRelaxNGParserCtxt* xmlRelaxNGNewParserCtxt(char* URL) nogil - cdef xmlRelaxNGParserCtxt* xmlRelaxNGNewDocParserCtxt(xmlDoc* doc) nogil - cdef void xmlRelaxNGFree(xmlRelaxNG* schema) nogil - cdef void xmlRelaxNGFreeParserCtxt(xmlRelaxNGParserCtxt* ctxt) nogil - cdef void xmlRelaxNGFreeValidCtxt(xmlRelaxNGValidCtxt* ctxt) nogil + + cdef xmlRelaxNGValidCtxt* xmlRelaxNGNewValidCtxt(xmlRelaxNG* schema) + cdef int xmlRelaxNGValidateDoc(xmlRelaxNGValidCtxt* ctxt, xmlDoc* doc) + cdef xmlRelaxNG* xmlRelaxNGParse(xmlRelaxNGParserCtxt* ctxt) + cdef xmlRelaxNGParserCtxt* xmlRelaxNGNewParserCtxt(char* URL) + cdef xmlRelaxNGParserCtxt* xmlRelaxNGNewDocParserCtxt(xmlDoc* doc) + cdef void xmlRelaxNGFree(xmlRelaxNG* schema) + cdef void xmlRelaxNGFreeParserCtxt(xmlRelaxNGParserCtxt* ctxt) + cdef void xmlRelaxNGFreeValidCtxt(xmlRelaxNGValidCtxt* ctxt) cdef void xmlRelaxNGSetValidStructuredErrors( - xmlRelaxNGValidCtxt* ctxt, xmlStructuredErrorFunc serror, void *ctx) nogil + xmlRelaxNGValidCtxt* ctxt, xmlStructuredErrorFunc serror, void *ctx) cdef void xmlRelaxNGSetParserStructuredErrors( - xmlRelaxNGParserCtxt* ctxt, xmlStructuredErrorFunc serror, void *ctx) nogil + xmlRelaxNGParserCtxt* ctxt, xmlStructuredErrorFunc serror, void *ctx) + + # 2.14+ + cdef void xmlRelaxNGSetResourceLoader(xmlRelaxNGParserCtxt* ctxt, xmlResourceLoader loader, void* vctxt) diff --git a/src/lxml/includes/schematron.pxd b/src/lxml/includes/schematron.pxd index f8e325284..181248afd 100644 --- a/src/lxml/includes/schematron.pxd +++ b/src/lxml/includes/schematron.pxd @@ -1,7 +1,7 @@ from lxml.includes cimport xmlerror from lxml.includes.tree cimport xmlDoc -cdef extern from "libxml/schematron.h": +cdef extern from "libxml/schematron.h" nogil: ctypedef struct xmlSchematron ctypedef struct xmlSchematronParserCtxt ctypedef struct xmlSchematronValidCtxt @@ -16,19 +16,19 @@ cdef extern from "libxml/schematron.h": XML_SCHEMATRON_OUT_IO = 1024 # output to I/O mechanism cdef xmlSchematronParserCtxt* xmlSchematronNewDocParserCtxt( - xmlDoc* doc) nogil + xmlDoc* doc) cdef xmlSchematronParserCtxt* xmlSchematronNewParserCtxt( char* filename) nogil cdef xmlSchematronValidCtxt* xmlSchematronNewValidCtxt( - xmlSchematron* schema, int options) nogil + xmlSchematron* schema, int options) - cdef xmlSchematron* xmlSchematronParse(xmlSchematronParserCtxt* ctxt) nogil + cdef xmlSchematron* xmlSchematronParse(xmlSchematronParserCtxt* ctxt) cdef int xmlSchematronValidateDoc(xmlSchematronValidCtxt* ctxt, - xmlDoc* instance) nogil + xmlDoc* instance) - cdef void xmlSchematronFreeParserCtxt(xmlSchematronParserCtxt* ctxt) nogil - cdef void xmlSchematronFreeValidCtxt(xmlSchematronValidCtxt* ctxt) nogil - cdef void xmlSchematronFree(xmlSchematron* schema) nogil + cdef void xmlSchematronFreeParserCtxt(xmlSchematronParserCtxt* ctxt) + cdef void xmlSchematronFreeValidCtxt(xmlSchematronValidCtxt* ctxt) + cdef void xmlSchematronFree(xmlSchematron* schema) cdef void xmlSchematronSetValidStructuredErrors( xmlSchematronValidCtxt* ctxt, xmlerror.xmlStructuredErrorFunc error_func, void *data) diff --git a/src/lxml/includes/tree.pxd b/src/lxml/includes/tree.pxd index 010af8090..ec5a7f7eb 100644 --- a/src/lxml/includes/tree.pxd +++ b/src/lxml/includes/tree.pxd @@ -3,25 +3,33 @@ from libc.string cimport const_char, const_uchar cdef extern from "lxml-version.h": # deprecated declaration, use etreepublic.pxd instead - cdef char* LXML_VERSION_STRING + cdef const char* LXML_VERSION_STRING cdef extern from "libxml/xmlversion.h": - cdef const_char* xmlParserVersion - cdef int LIBXML_VERSION + const char* xmlParserVersion + int LIBXML_VERSION -cdef extern from "libxml/xmlstring.h": + +cdef extern from "libxml/xmlstring.h" nogil: + """ + static CYTHON_INLINE int __lx_xmlStrEq(const xmlChar* str1, const xmlChar* str2) { + return (str1 == str2) || (strcmp((const char*) str1, (const char*) str2) == 0); + } + """ ctypedef unsigned char xmlChar ctypedef const xmlChar const_xmlChar "const xmlChar" - cdef int xmlStrlen(const_xmlChar* str) nogil - cdef xmlChar* xmlStrdup(const_xmlChar* cur) nogil - cdef int xmlStrncmp(const_xmlChar* str1, const_xmlChar* str2, int length) nogil - cdef int xmlStrcmp(const_xmlChar* str1, const_xmlChar* str2) nogil - cdef int xmlStrcasecmp(const xmlChar *str1, const xmlChar *str2) nogil - cdef const_xmlChar* xmlStrstr(const_xmlChar* str1, const_xmlChar* str2) nogil - cdef const_xmlChar* xmlStrchr(const_xmlChar* str1, xmlChar ch) nogil - cdef const_xmlChar* _xcstr "(const xmlChar*)PyBytes_AS_STRING" (object s) - -cdef extern from "libxml/encoding.h": + cdef xmlChar* xmlStrdup(const_xmlChar* cur) + cdef int xmlStrcasecmp(const xmlChar *str1, const xmlChar *str2) + cdef int xmlStrEqual "__lx_xmlStrEq" (const_xmlChar* str1, const_xmlChar* str2) + cdef const_xmlChar* _xcstr "(const xmlChar*)__Pyx_PyBytes_AsString" (object s) + # now unused, replaced by : + cdef int xmlStrlen(const_xmlChar* str) + cdef int xmlStrncmp(const_xmlChar* str1, const_xmlChar* str2, int length) + cdef int xmlStrcmp(const_xmlChar* str1, const_xmlChar* str2) + cdef const_xmlChar* xmlStrstr(const_xmlChar* str1, const_xmlChar* str2) + cdef const_xmlChar* xmlStrchr(const_xmlChar* str1, xmlChar ch) + +cdef extern from "libxml/encoding.h" nogil: ctypedef enum xmlCharEncoding: XML_CHAR_ENCODING_ERROR = -1 # No char encoding detected XML_CHAR_ENCODING_NONE = 0 # No char encoding detected @@ -48,41 +56,52 @@ cdef extern from "libxml/encoding.h": XML_CHAR_ENCODING_EUC_JP = 21 # EUC-JP XML_CHAR_ENCODING_ASCII = 22 # pure ASCII - ctypedef struct xmlCharEncodingHandler - cdef xmlCharEncodingHandler* xmlFindCharEncodingHandler(char* name) nogil + ctypedef struct xmlCharEncodingHandler: + char* name + + cdef xmlCharEncodingHandler* xmlFindCharEncodingHandler(char* name) cdef xmlCharEncodingHandler* xmlGetCharEncodingHandler( - xmlCharEncoding enc) nogil - cdef int xmlCharEncCloseFunc(xmlCharEncodingHandler* handler) nogil - cdef xmlCharEncoding xmlDetectCharEncoding(const_xmlChar* text, int len) nogil - cdef const_char* xmlGetCharEncodingName(xmlCharEncoding enc) nogil - cdef xmlCharEncoding xmlParseCharEncoding(char* name) nogil + xmlCharEncoding enc) + cdef int xmlCharEncCloseFunc(xmlCharEncodingHandler* handler) + cdef xmlCharEncoding xmlDetectCharEncoding(const_xmlChar* text, int len) + cdef const_char* xmlGetCharEncodingName(xmlCharEncoding enc) + cdef xmlCharEncoding xmlParseCharEncoding(char* name) ctypedef int (*xmlCharEncodingOutputFunc)( unsigned char *out_buf, int *outlen, const_uchar *in_buf, int *inlen) -cdef extern from "libxml/chvalid.h": - cdef int xmlIsChar_ch(char c) nogil - cdef int xmlIsCharQ(int ch) nogil +cdef extern from "libxml/chvalid.h" nogil: + cdef int xmlIsChar_ch(char c) + cdef int xmlIsCharQ(int ch) cdef extern from "libxml/hash.h": ctypedef struct xmlHashTable - ctypedef void (*xmlHashScanner)(void* payload, void* data, const_xmlChar* name) # may require GIL! + ctypedef void (*xmlHashScanner)(void* payload, void* data, const_xmlChar* name) noexcept # may require GIL! void xmlHashScan(xmlHashTable* table, xmlHashScanner f, void* data) nogil void* xmlHashLookup(xmlHashTable* table, const_xmlChar* name) nogil - ctypedef void (*xmlHashDeallocator)(void *payload, xmlChar *name) - cdef xmlHashTable* xmlHashCreate(int size) - cdef xmlHashTable* xmlHashCreateDict(int size, xmlDict *dict) - cdef int xmlHashSize(xmlHashTable* table) - cdef void xmlHashFree(xmlHashTable* table, xmlHashDeallocator f) + ctypedef void (*xmlHashDeallocator)(void *payload, xmlChar *name) noexcept + cdef xmlHashTable* xmlHashCreate(int size) nogil + cdef xmlHashTable* xmlHashCreateDict(int size, xmlDict *dict) nogil + cdef int xmlHashSize(xmlHashTable* table) nogil + cdef void xmlHashFree(xmlHashTable* table, xmlHashDeallocator f) nogil + -cdef extern from *: # actually "libxml/dict.h" +cdef extern from * nogil: # actually "libxml/dict.h" # libxml/dict.h appears to be broken to include in C ctypedef struct xmlDict - cdef const_xmlChar* xmlDictLookup(xmlDict* dict, const_xmlChar* name, int len) nogil - cdef const_xmlChar* xmlDictExists(xmlDict* dict, const_xmlChar* name, int len) nogil - cdef int xmlDictOwns(xmlDict* dict, const_xmlChar* name) nogil - cdef size_t xmlDictSize(xmlDict* dict) nogil -cdef extern from "libxml/tree.h": + cdef xmlDict* xmlDictCreate() + cdef xmlDict* xmlDictCreateSub(xmlDict* subdict) + cdef void xmlDictFree(xmlDict* sub) + cdef int xmlDictReference(xmlDict* dict) + cdef const_xmlChar* xmlDictLookup(xmlDict* dict, const_xmlChar* name, int len) + cdef const_xmlChar* xmlDictExists(xmlDict* dict, const_xmlChar* name, int len) + cdef int xmlDictOwns(xmlDict* dict, const_xmlChar* name) + cdef size_t xmlDictSize(xmlDict* dict) + cdef size_t xmlDictSetLimit(xmlDict* dict, size_t limit) + cdef size_t xmlDictGetUsage(xmlDict* dict) + + +cdef extern from "libxml/tree.h" nogil: ctypedef struct xmlDoc ctypedef struct xmlAttr ctypedef struct xmlNotationTable @@ -139,7 +158,7 @@ cdef extern from "libxml/tree.h": XML_ATTRIBUTE_NMTOKENS= 8 XML_ATTRIBUTE_ENUMERATION= 9 XML_ATTRIBUTE_NOTATION= 10 - + ctypedef enum xmlAttributeDefault: XML_ATTRIBUTE_NONE= 1 XML_ATTRIBUTE_REQUIRED= 2 @@ -154,6 +173,17 @@ cdef extern from "libxml/tree.h": XML_EXTERNAL_PARAMETER_ENTITY= 5 XML_INTERNAL_PREDEFINED_ENTITY= 6 + ctypedef enum xmlDocProperties: + XML_DOC_WELLFORMED = 1 # /* document is XML well formed */ + XML_DOC_NSVALID = 2 # /* document is Namespace valid */ + XML_DOC_OLD10 = 4 # /* parsed with old XML-1.0 parser */ + XML_DOC_DTDVALID = 8 # /* DTD validation was successful */ + XML_DOC_XINCLUDE = 16 # /* XInclude substitution was done */ + XML_DOC_USERBUILT = 32 # /* Document was built using the API + # and not by parsing an instance */ + XML_DOC_INTERNAL = 64 # /* built for internal processing */ + XML_DOC_HTML = 128 # /* parsed or built HTML document */ + ctypedef struct xmlNs: const_xmlChar* href const_xmlChar* prefix @@ -274,7 +304,8 @@ cdef extern from "libxml/tree.h": void* _private xmlDtd* intSubset xmlDtd* extSubset - + int properties + ctypedef struct xmlAttr: void* _private xmlElementType type @@ -293,7 +324,7 @@ cdef extern from "libxml/tree.h": const_xmlChar* name xmlAttr* attr xmlDoc* doc - + ctypedef struct xmlBuffer ctypedef struct xmlBuf # new in libxml2 2.9 @@ -304,101 +335,103 @@ cdef extern from "libxml/tree.h": int error const_xmlChar* XML_XML_NAMESPACE - - cdef void xmlFreeDoc(xmlDoc* cur) nogil - cdef void xmlFreeDtd(xmlDtd* cur) nogil - cdef void xmlFreeNode(xmlNode* cur) nogil - cdef void xmlFreeNsList(xmlNs* ns) nogil - cdef void xmlFreeNs(xmlNs* ns) nogil - cdef void xmlFree(void* buf) nogil - - cdef xmlNode* xmlNewNode(xmlNs* ns, const_xmlChar* name) nogil - cdef xmlNode* xmlNewDocText(xmlDoc* doc, const_xmlChar* content) nogil - cdef xmlNode* xmlNewDocComment(xmlDoc* doc, const_xmlChar* content) nogil - cdef xmlNode* xmlNewDocPI(xmlDoc* doc, const_xmlChar* name, const_xmlChar* content) nogil - cdef xmlNode* xmlNewReference(xmlDoc* doc, const_xmlChar* name) nogil - cdef xmlNode* xmlNewCDataBlock(xmlDoc* doc, const_xmlChar* text, int len) nogil - cdef xmlNs* xmlNewNs(xmlNode* node, const_xmlChar* href, const_xmlChar* prefix) nogil - cdef xmlNode* xmlAddChild(xmlNode* parent, xmlNode* cur) nogil - cdef xmlNode* xmlReplaceNode(xmlNode* old, xmlNode* cur) nogil - cdef xmlNode* xmlAddPrevSibling(xmlNode* cur, xmlNode* elem) nogil - cdef xmlNode* xmlAddNextSibling(xmlNode* cur, xmlNode* elem) nogil + + cdef void xmlFreeDoc(xmlDoc* cur) + cdef void xmlFreeDtd(xmlDtd* cur) + cdef void xmlFreeNode(xmlNode* cur) + cdef void xmlFreeNodeList(xmlNode* cur) + cdef void xmlFreeNsList(xmlNs* ns) + cdef void xmlFreeNs(xmlNs* ns) + cdef void xmlFree(void* buf) + + cdef xmlNode* xmlNewNode(xmlNs* ns, const_xmlChar* name) + cdef xmlNode* xmlNewDocText(xmlDoc* doc, const_xmlChar* content) + cdef xmlNode* xmlNewDocComment(xmlDoc* doc, const_xmlChar* content) + cdef xmlNode* xmlNewDocPI(xmlDoc* doc, const_xmlChar* name, const_xmlChar* content) + cdef xmlNode* xmlNewReference(xmlDoc* doc, const_xmlChar* name) + cdef xmlNode* xmlNewCDataBlock(xmlDoc* doc, const_xmlChar* text, int len) + cdef xmlNs* xmlNewNs(xmlNode* node, const_xmlChar* href, const_xmlChar* prefix) + cdef xmlNode* xmlAddChild(xmlNode* parent, xmlNode* cur) + cdef xmlNode* xmlReplaceNode(xmlNode* old, xmlNode* cur) + cdef xmlNode* xmlAddPrevSibling(xmlNode* cur, xmlNode* elem) + cdef xmlNode* xmlAddNextSibling(xmlNode* cur, xmlNode* elem) cdef xmlNode* xmlNewDocNode(xmlDoc* doc, xmlNs* ns, - const_xmlChar* name, const_xmlChar* content) nogil - cdef xmlDoc* xmlNewDoc(const_xmlChar* version) nogil - cdef xmlAttr* xmlNewProp(xmlNode* node, const_xmlChar* name, const_xmlChar* value) nogil + const_xmlChar* name, const_xmlChar* content) + cdef xmlDoc* xmlNewDoc(const_xmlChar* version) + cdef xmlAttr* xmlNewProp(xmlNode* node, const_xmlChar* name, const_xmlChar* value) cdef xmlAttr* xmlNewNsProp(xmlNode* node, xmlNs* ns, - const_xmlChar* name, const_xmlChar* value) nogil - cdef xmlChar* xmlGetNoNsProp(xmlNode* node, const_xmlChar* name) nogil - cdef xmlChar* xmlGetNsProp(xmlNode* node, const_xmlChar* name, const_xmlChar* nameSpace) nogil - cdef void xmlSetNs(xmlNode* node, xmlNs* ns) nogil - cdef xmlAttr* xmlSetProp(xmlNode* node, const_xmlChar* name, const_xmlChar* value) nogil + const_xmlChar* name, const_xmlChar* value) + cdef xmlChar* xmlGetNoNsProp(xmlNode* node, const_xmlChar* name) + cdef xmlChar* xmlGetNsProp(xmlNode* node, const_xmlChar* name, const_xmlChar* nameSpace) + cdef xmlChar* xmlGetProp(xmlNode* node, const_xmlChar* name) + cdef void xmlSetNs(xmlNode* node, xmlNs* ns) + cdef xmlAttr* xmlSetProp(xmlNode* node, const_xmlChar* name, const_xmlChar* value) cdef xmlAttr* xmlSetNsProp(xmlNode* node, xmlNs* ns, - const_xmlChar* name, const_xmlChar* value) nogil - cdef int xmlRemoveID(xmlDoc* doc, xmlAttr* cur) nogil - cdef int xmlRemoveProp(xmlAttr* cur) nogil - cdef void xmlFreePropList(xmlAttr* cur) nogil - cdef xmlChar* xmlGetNodePath(xmlNode* node) nogil - cdef void xmlDocDumpMemory(xmlDoc* cur, char** mem, int* size) nogil + const_xmlChar* name, const_xmlChar* value) + cdef int xmlRemoveID(xmlDoc* doc, xmlAttr* cur) + cdef int xmlRemoveProp(xmlAttr* cur) + cdef void xmlFreePropList(xmlAttr* cur) + cdef xmlChar* xmlGetNodePath(xmlNode* node) + cdef void xmlDocDumpMemory(xmlDoc* cur, char** mem, int* size) cdef void xmlDocDumpMemoryEnc(xmlDoc* cur, char** mem, int* size, - char* encoding) nogil + char* encoding) cdef int xmlSaveFileTo(xmlOutputBuffer* out, xmlDoc* cur, - char* encoding) nogil - - cdef void xmlUnlinkNode(xmlNode* cur) nogil - cdef xmlNode* xmlDocSetRootElement(xmlDoc* doc, xmlNode* root) nogil - cdef xmlNode* xmlDocGetRootElement(xmlDoc* doc) nogil - cdef void xmlSetTreeDoc(xmlNode* tree, xmlDoc* doc) nogil - cdef xmlAttr* xmlHasProp(xmlNode* node, const_xmlChar* name) nogil - cdef xmlAttr* xmlHasNsProp(xmlNode* node, const_xmlChar* name, const_xmlChar* nameSpace) nogil - cdef xmlChar* xmlNodeGetContent(xmlNode* cur) nogil - cdef int xmlNodeBufGetContent(xmlBuffer* buffer, xmlNode* cur) nogil - cdef xmlNs* xmlSearchNs(xmlDoc* doc, xmlNode* node, const_xmlChar* prefix) nogil - cdef xmlNs* xmlSearchNsByHref(xmlDoc* doc, xmlNode* node, const_xmlChar* href) nogil - cdef int xmlIsBlankNode(xmlNode* node) nogil - cdef long xmlGetLineNo(xmlNode* node) nogil - cdef void xmlElemDump(stdio.FILE* f, xmlDoc* doc, xmlNode* cur) nogil + char* encoding) + + cdef void xmlUnlinkNode(xmlNode* cur) + cdef xmlNode* xmlDocSetRootElement(xmlDoc* doc, xmlNode* root) + cdef xmlNode* xmlDocGetRootElement(xmlDoc* doc) + cdef void xmlSetTreeDoc(xmlNode* tree, xmlDoc* doc) + cdef xmlAttr* xmlHasProp(xmlNode* node, const_xmlChar* name) + cdef xmlAttr* xmlHasNsProp(xmlNode* node, const_xmlChar* name, const_xmlChar* nameSpace) + cdef xmlChar* xmlNodeGetContent(xmlNode* cur) + cdef int xmlNodeBufGetContent(xmlBuffer* buffer, xmlNode* cur) + cdef xmlNs* xmlSearchNs(xmlDoc* doc, xmlNode* node, const_xmlChar* prefix) + cdef xmlNs* xmlSearchNsByHref(xmlDoc* doc, xmlNode* node, const_xmlChar* href) + cdef int xmlIsBlankNode(xmlNode* node) + cdef long xmlGetLineNo(xmlNode* node) + cdef void xmlElemDump(stdio.FILE* f, xmlDoc* doc, xmlNode* cur) cdef void xmlNodeDumpOutput(xmlOutputBuffer* buf, xmlDoc* doc, xmlNode* cur, int level, - int format, const_char* encoding) nogil + int format, const_char* encoding) cdef void xmlBufAttrSerializeTxtContent(xmlOutputBuffer *buf, xmlDoc *doc, - xmlAttr *attr, const_xmlChar *string) nogil - cdef void xmlNodeSetName(xmlNode* cur, const_xmlChar* name) nogil - cdef void xmlNodeSetContent(xmlNode* cur, const_xmlChar* content) nogil - cdef xmlDtd* xmlCopyDtd(xmlDtd* dtd) nogil - cdef xmlDoc* xmlCopyDoc(xmlDoc* doc, int recursive) nogil - cdef xmlNode* xmlCopyNode(xmlNode* node, int extended) nogil - cdef xmlNode* xmlDocCopyNode(xmlNode* node, xmlDoc* doc, int extended) nogil - cdef int xmlReconciliateNs(xmlDoc* doc, xmlNode* tree) nogil - cdef xmlNs* xmlNewReconciliedNs(xmlDoc* doc, xmlNode* tree, xmlNs* ns) nogil - cdef xmlBuffer* xmlBufferCreate() nogil - cdef void xmlBufferWriteChar(xmlBuffer* buf, char* string) nogil - cdef void xmlBufferFree(xmlBuffer* buf) nogil - cdef const_xmlChar* xmlBufferContent(xmlBuffer* buf) nogil - cdef int xmlBufferLength(xmlBuffer* buf) nogil - cdef const_xmlChar* xmlBufContent(xmlBuf* buf) nogil # new in libxml2 2.9 - cdef size_t xmlBufUse(xmlBuf* buf) nogil # new in libxml2 2.9 - cdef int xmlKeepBlanksDefault(int val) nogil - cdef xmlChar* xmlNodeGetBase(xmlDoc* doc, xmlNode* node) nogil + xmlAttr *attr, const_xmlChar *string) + cdef void xmlNodeSetName(xmlNode* cur, const_xmlChar* name) + cdef void xmlNodeSetContent(xmlNode* cur, const_xmlChar* content) + cdef xmlDtd* xmlCopyDtd(xmlDtd* dtd) + cdef xmlDoc* xmlCopyDoc(xmlDoc* doc, int recursive) + cdef xmlNode* xmlCopyNode(xmlNode* node, int extended) + cdef xmlNode* xmlDocCopyNode(xmlNode* node, xmlDoc* doc, int extended) + cdef int xmlReconciliateNs(xmlDoc* doc, xmlNode* tree) + cdef xmlNs* xmlNewReconciliedNs(xmlDoc* doc, xmlNode* tree, xmlNs* ns) + cdef xmlBuffer* xmlBufferCreate() + cdef void xmlBufferWriteChar(xmlBuffer* buf, char* string) + cdef void xmlBufferFree(xmlBuffer* buf) + cdef const_xmlChar* xmlBufferContent(xmlBuffer* buf) + cdef int xmlBufferLength(xmlBuffer* buf) + cdef const_xmlChar* xmlBufContent(xmlBuf* buf) # new in libxml2 2.9 + cdef size_t xmlBufUse(xmlBuf* buf) # new in libxml2 2.9 + cdef int xmlKeepBlanksDefault(int val) + cdef xmlChar* xmlNodeGetBase(xmlDoc* doc, xmlNode* node) cdef xmlDtd* xmlCreateIntSubset(xmlDoc* doc, const_xmlChar* name, - const_xmlChar* ExternalID, const_xmlChar* SystemID) nogil - cdef void xmlNodeSetBase(xmlNode* node, const_xmlChar* uri) nogil - cdef int xmlValidateNCName(const_xmlChar* value, int space) nogil + const_xmlChar* ExternalID, const_xmlChar* SystemID) + cdef void xmlNodeSetBase(xmlNode* node, const_xmlChar* uri) + cdef int xmlValidateNCName(const_xmlChar* value, int space) -cdef extern from "libxml/uri.h": - cdef const_xmlChar* xmlBuildURI(const_xmlChar* href, const_xmlChar* base) nogil +cdef extern from "libxml/uri.h" nogil: + cdef const_xmlChar* xmlBuildURI(const_xmlChar* href, const_xmlChar* base) -cdef extern from "libxml/HTMLtree.h": +cdef extern from "libxml/HTMLtree.h" nogil: cdef void htmlNodeDumpFormatOutput(xmlOutputBuffer* buf, xmlDoc* doc, xmlNode* cur, - char* encoding, int format) nogil - cdef xmlDoc* htmlNewDoc(const_xmlChar* uri, const_xmlChar* externalID) nogil + char* encoding, int format) + cdef xmlDoc* htmlNewDoc(const_xmlChar* uri, const_xmlChar* externalID) -cdef extern from "libxml/valid.h": - cdef xmlAttr* xmlGetID(xmlDoc* doc, const_xmlChar* ID) nogil +cdef extern from "libxml/valid.h" nogil: + cdef xmlAttr* xmlGetID(xmlDoc* doc, const_xmlChar* ID) cdef void xmlDumpNotationTable(xmlBuffer* buffer, - xmlNotationTable* table) nogil - cdef int xmlValidateNameValue(const_xmlChar* value) nogil + xmlNotationTable* table) + cdef int xmlValidateNameValue(const_xmlChar* value) cdef extern from "libxml/xmlIO.h": cdef int xmlOutputBufferWrite(xmlOutputBuffer* out, @@ -411,26 +444,26 @@ cdef extern from "libxml/xmlIO.h": cdef int xmlOutputBufferClose(xmlOutputBuffer* out) nogil ctypedef int (*xmlInputReadCallback)(void* context, - char* buffer, int len) - ctypedef int (*xmlInputCloseCallback)(void* context) + char* buffer, int len) noexcept nogil + ctypedef int (*xmlInputCloseCallback)(void* context) noexcept nogil ctypedef int (*xmlOutputWriteCallback)(void* context, - char* buffer, int len) - ctypedef int (*xmlOutputCloseCallback)(void* context) + char* buffer, int len) noexcept + ctypedef int (*xmlOutputCloseCallback)(void* context) noexcept cdef xmlOutputBuffer* xmlAllocOutputBuffer( xmlCharEncodingHandler* encoder) nogil cdef xmlOutputBuffer* xmlOutputBufferCreateIO( xmlOutputWriteCallback iowrite, xmlOutputCloseCallback ioclose, - void * ioctx, + void * ioctx, xmlCharEncodingHandler* encoder) nogil cdef xmlOutputBuffer* xmlOutputBufferCreateFile( stdio.FILE* file, xmlCharEncodingHandler* encoder) nogil cdef xmlOutputBuffer* xmlOutputBufferCreateFilename( char* URI, xmlCharEncodingHandler* encoder, int compression) nogil -cdef extern from "libxml/xmlsave.h": +cdef extern from "libxml/xmlsave.h" nogil: ctypedef struct xmlSaveCtxt ctypedef enum xmlSaveOption: @@ -443,38 +476,35 @@ cdef extern from "libxml/xmlsave.h": XML_SAVE_AS_HTML = 64 # force HTML serialization on XML doc (2.7.2) cdef xmlSaveCtxt* xmlSaveToFilename(char* filename, char* encoding, - int options) nogil + int options) cdef xmlSaveCtxt* xmlSaveToBuffer(xmlBuffer* buffer, char* encoding, - int options) nogil # libxml2 2.6.23 - cdef long xmlSaveDoc(xmlSaveCtxt* ctxt, xmlDoc* doc) nogil - cdef long xmlSaveTree(xmlSaveCtxt* ctxt, xmlNode* node) nogil - cdef int xmlSaveClose(xmlSaveCtxt* ctxt) nogil - cdef int xmlSaveFlush(xmlSaveCtxt* ctxt) nogil - cdef int xmlSaveSetAttrEscape(xmlSaveCtxt* ctxt, void* escape_func) nogil - cdef int xmlSaveSetEscape(xmlSaveCtxt* ctxt, void* escape_func) nogil - -cdef extern from "libxml/globals.h": - cdef int xmlThrDefKeepBlanksDefaultValue(int onoff) nogil - cdef int xmlThrDefLineNumbersDefaultValue(int onoff) nogil - cdef int xmlThrDefIndentTreeOutput(int onoff) nogil - + int options) # libxml2 2.6.23 + cdef long xmlSaveDoc(xmlSaveCtxt* ctxt, xmlDoc* doc) + cdef long xmlSaveTree(xmlSaveCtxt* ctxt, xmlNode* node) + cdef int xmlSaveClose(xmlSaveCtxt* ctxt) + cdef int xmlSaveFlush(xmlSaveCtxt* ctxt) + cdef int xmlSaveSetAttrEscape(xmlSaveCtxt* ctxt, void* escape_func) + cdef int xmlSaveSetEscape(xmlSaveCtxt* ctxt, void* escape_func) + +cdef extern from "libxml/globals.h" nogil: + cdef int xmlThrDefKeepBlanksDefaultValue(int onoff) + cdef int xmlThrDefLineNumbersDefaultValue(int onoff) + cdef int xmlThrDefIndentTreeOutput(int onoff) + cdef extern from "libxml/xmlmemory.h" nogil: cdef void* xmlMalloc(size_t size) cdef int xmlMemBlocks() cdef int xmlMemUsed() - cdef void xmlMemDisplay(stdio.FILE* file) - cdef void xmlMemDisplayLast(stdio.FILE* file, long num_bytes) - cdef void xmlMemShow(stdio.FILE* file, int count) - -cdef extern from "etree_defs.h": - cdef bint _isElement(xmlNode* node) nogil - cdef bint _isElementOrXInclude(xmlNode* node) nogil - cdef const_xmlChar* _getNs(xmlNode* node) nogil + +cdef extern from "etree_defs.h" nogil: + cdef bint _isElement(xmlNode* node) + cdef bint _isElementOrXInclude(xmlNode* node) + cdef const_xmlChar* _getNs(xmlNode* node) cdef void BEGIN_FOR_EACH_ELEMENT_FROM(xmlNode* tree_top, xmlNode* start_node, - bint inclusive) nogil - cdef void END_FOR_EACH_ELEMENT_FROM(xmlNode* start_node) nogil + bint inclusive) + cdef void END_FOR_EACH_ELEMENT_FROM(xmlNode* start_node) cdef void BEGIN_FOR_EACH_FROM(xmlNode* tree_top, xmlNode* start_node, - bint inclusive) nogil - cdef void END_FOR_EACH_FROM(xmlNode* start_node) nogil + bint inclusive) + cdef void END_FOR_EACH_FROM(xmlNode* start_node) diff --git a/src/lxml/includes/uri.pxd b/src/lxml/includes/uri.pxd index 2b6bb79f3..f886a54b9 100644 --- a/src/lxml/includes/uri.pxd +++ b/src/lxml/includes/uri.pxd @@ -1,4 +1,4 @@ -cdef extern from "libxml/uri.h": +cdef extern from "libxml/uri.h" nogil: ctypedef struct xmlURI cdef xmlURI* xmlParseURI(char* str) diff --git a/src/lxml/includes/xinclude.pxd b/src/lxml/includes/xinclude.pxd index 4232d3e43..0fa762946 100644 --- a/src/lxml/includes/xinclude.pxd +++ b/src/lxml/includes/xinclude.pxd @@ -1,22 +1,34 @@ from lxml.includes.tree cimport xmlDoc, xmlNode +from lxml.includes.xmlparser cimport xmlResourceLoader -cdef extern from "libxml/xinclude.h": + +cdef extern from "libxml/xinclude.h" nogil: + """ + #if LIBXML_VERSION < 21400 + #define xmlXIncludeSetResourceLoader(ctxt, loader, data) ((void) ((void) ctxt, (void) loader, (void) data)) + #endif + """ ctypedef struct xmlXIncludeCtxt - cdef int xmlXIncludeProcess(xmlDoc* doc) nogil - cdef int xmlXIncludeProcessFlags(xmlDoc* doc, int parser_opts) nogil - cdef int xmlXIncludeProcessTree(xmlNode* doc) nogil - cdef int xmlXIncludeProcessTreeFlags(xmlNode* doc, int parser_opts) nogil + cdef int xmlXIncludeProcess(xmlDoc* doc) + cdef int xmlXIncludeProcessFlags(xmlDoc* doc, int parser_opts) + cdef int xmlXIncludeProcessTree(xmlNode* doc) + cdef int xmlXIncludeProcessTreeFlags(xmlNode* doc, int parser_opts) + + # libxml2 >= 2.14 + cdef void xmlXIncludeSetResourceLoader( + xmlXIncludeCtxt* ctxt, xmlResourceLoader loader, void* data) # libxml2 >= 2.7.4 cdef int xmlXIncludeProcessTreeFlagsData( - xmlNode* doc, int parser_opts, void* data) nogil + xmlNode* doc, int parser_opts, void* data) - cdef xmlXIncludeCtxt* xmlXIncludeNewContext(xmlDoc* doc) nogil - cdef int xmlXIncludeProcessNode(xmlXIncludeCtxt* ctxt, xmlNode* node) nogil - cdef int xmlXIncludeSetFlags(xmlXIncludeCtxt* ctxt, int flags) nogil + cdef xmlXIncludeCtxt* xmlXIncludeNewContext(xmlDoc* doc) + cdef void xmlXIncludeFreeContext(xmlXIncludeCtxt *ctxt) + cdef int xmlXIncludeProcessNode(xmlXIncludeCtxt* ctxt, xmlNode* node) + cdef int xmlXIncludeSetFlags(xmlXIncludeCtxt* ctxt, int flags) # libxml2 >= 2.6.27 cdef int xmlXIncludeProcessFlagsData( - xmlDoc* doc, int flags, void* data) nogil + xmlDoc* doc, int flags, void* data) diff --git a/src/lxml/includes/xmlerror.pxd b/src/lxml/includes/xmlerror.pxd index 13c8f3782..0249a45e2 100644 --- a/src/lxml/includes/xmlerror.pxd +++ b/src/lxml/includes/xmlerror.pxd @@ -5,23 +5,23 @@ cdef extern from "libxml/xmlerror.h": ctypedef enum xmlErrorLevel: - XML_ERR_NONE = 0 - XML_ERR_WARNING = 1 # A simple warning - XML_ERR_ERROR = 2 # A recoverable error + XML_ERR_NONE = 0 # Success + XML_ERR_WARNING = 1 # A warning + XML_ERR_ERROR = 2 # An error XML_ERR_FATAL = 3 # A fatal error ctypedef enum xmlErrorDomain: - XML_FROM_NONE = 0 + XML_FROM_NONE = 0 # Unknown XML_FROM_PARSER = 1 # The XML parser - XML_FROM_TREE = 2 # The tree module + XML_FROM_TREE = 2 # The tree module (unused) XML_FROM_NAMESPACE = 3 # The XML Namespace module - XML_FROM_DTD = 4 # The XML DTD validation with parser contex + XML_FROM_DTD = 4 # The XML DTD validation with parser context XML_FROM_HTML = 5 # The HTML parser - XML_FROM_MEMORY = 6 # The memory allocator + XML_FROM_MEMORY = 6 # The memory allocator (unused) XML_FROM_OUTPUT = 7 # The serialization code XML_FROM_IO = 8 # The Input/Output stack - XML_FROM_FTP = 9 # The FTP module - XML_FROM_HTTP = 10 # The HTTP module + XML_FROM_FTP = 9 # The FTP module (unused) + XML_FROM_HTTP = 10 # The HTTP module (unused) XML_FROM_XINCLUDE = 11 # The XInclude processing XML_FROM_XPATH = 12 # The XPath module XML_FROM_XPOINTER = 13 # The XPointer module @@ -33,20 +33,20 @@ cdef extern from "libxml/xmlerror.h": XML_FROM_RELAXNGV = 19 # The Relax-NG validator module XML_FROM_CATALOG = 20 # The Catalog module XML_FROM_C14N = 21 # The Canonicalization module - XML_FROM_XSLT = 22 # The XSLT engine from libxslt + XML_FROM_XSLT = 22 # The XSLT engine from libxslt (unused) XML_FROM_VALID = 23 # The XML DTD validation with valid context - XML_FROM_CHECK = 24 # The error checking module + XML_FROM_CHECK = 24 # The error checking module (unused) XML_FROM_WRITER = 25 # The xmlwriter module - XML_FROM_MODULE = 26 # The dynamically loaded module modul - XML_FROM_I18N = 27 # The module handling character conversion + XML_FROM_MODULE = 26 # The dynamically loaded module module (unused) + XML_FROM_I18N = 27 # The module handling character conversion (unused) XML_FROM_SCHEMATRONV = 28 # The Schematron validator module - XML_FROM_BUFFER = 29 # The buffers module - XML_FROM_URI = 30 # The URI module + XML_FROM_BUFFER = 29 # The buffers module (unused) + XML_FROM_URI = 30 # The URI module (unused) ctypedef enum xmlParserErrors: - XML_ERR_OK = 0 - XML_ERR_INTERNAL_ERROR = 1 - XML_ERR_NO_MEMORY = 2 + XML_ERR_OK = 0 # Success + XML_ERR_INTERNAL_ERROR = 1 # Internal assertion failure + XML_ERR_NO_MEMORY = 2 # Out of memory XML_ERR_DOCUMENT_START = 3 XML_ERR_DOCUMENT_EMPTY = 4 XML_ERR_DOCUMENT_END = 5 @@ -76,7 +76,7 @@ cdef extern from "libxml/xmlerror.h": XML_ERR_ENTITY_IS_EXTERNAL = 29 XML_ERR_ENTITY_IS_PARAMETER = 30 XML_ERR_UNKNOWN_ENCODING = 31 - XML_ERR_UNSUPPORTED_ENCODING = 32 + XML_ERR_UNSUPPORTED_ENCODING = 32 # Unsupported character encoding XML_ERR_STRING_NOT_STARTED = 33 XML_ERR_STRING_NOT_CLOSED = 34 XML_ERR_NS_DECL_ERROR = 35 @@ -157,6 +157,12 @@ cdef extern from "libxml/xmlerror.h": XML_ERR_NAME_TOO_LONG = 110 XML_ERR_USER_STOP = 111 XML_ERR_COMMENT_ABRUPTLY_ENDED = 112 + XML_WAR_ENCODING_MISMATCH = 113 + XML_ERR_RESOURCE_LIMIT = 114 # Internal resource limit like maximum amplification factor exceeded + XML_ERR_ARGUMENT = 115 # Invalid argument + XML_ERR_SYSTEM = 116 # Unexpected error from the OS or an external library + XML_ERR_REDECL_PREDEF_ENTITY = 117 + XML_ERR_INT_SUBSET_NOT_FINISHED = 118 XML_NS_ERR_XML_NAMESPACE = 200 XML_NS_ERR_UNDEFINED_NAMESPACE = 201 XML_NS_ERR_QNAME = 202 @@ -207,6 +213,7 @@ cdef extern from "libxml/xmlerror.h": XML_DTD_DUP_TOKEN = 541 XML_HTML_STRUCURE_ERROR = 800 XML_HTML_UNKNOWN_TAG = 801 + XML_HTML_INCORRECTLY_OPENED_COMMENT = 802 XML_RNGP_ANYNAME_ATTR_ANCESTOR = 1000 XML_RNGP_ATTR_CONFLICT = 1001 XML_RNGP_ATTRIBUTE_CHILDREN = 1002 @@ -385,7 +392,7 @@ cdef extern from "libxml/xmlerror.h": XML_IO_ENAMETOOLONG = 1521 XML_IO_ENFILE = 1522 XML_IO_ENODEV = 1523 - XML_IO_ENOENT = 1524 + XML_IO_ENOENT = 1524 # File not found XML_IO_ENOEXEC = 1525 XML_IO_ENOLCK = 1526 XML_IO_ENOMEM = 1527 @@ -418,6 +425,7 @@ cdef extern from "libxml/xmlerror.h": XML_IO_EADDRINUSE = 1554 XML_IO_EALREADY = 1555 XML_IO_EAFNOSUPPORT = 1556 + XML_IO_UNSUPPORTED_PROTOCOL = 1557 XML_XINCLUDE_RECURSION = 1600 XML_XINCLUDE_PARSE_VALUE = 1601 XML_XINCLUDE_ENTITY_DEF_MISMATCH = 1602 @@ -708,8 +716,8 @@ cdef extern from "libxml/xmlerror.h": XML_SCHEMAP_SRC_IMPORT_2 = 3066 XML_SCHEMAP_SRC_IMPORT_2_1 = 3067 XML_SCHEMAP_SRC_IMPORT_2_2 = 3068 - XML_SCHEMAP_INTERNAL = 3069 # 3069 non-W3C - XML_SCHEMAP_NOT_DETERMINISTIC = 3070 # 3070 non-W3C + XML_SCHEMAP_INTERNAL = 3069 + XML_SCHEMAP_NOT_DETERMINISTIC = 3070 XML_SCHEMAP_SRC_ATTRIBUTE_GROUP_1 = 3071 XML_SCHEMAP_SRC_ATTRIBUTE_GROUP_2 = 3072 XML_SCHEMAP_SRC_ATTRIBUTE_GROUP_3 = 3073 @@ -725,12 +733,12 @@ cdef extern from "libxml/xmlerror.h": XML_SCHEMAP_WARN_SKIP_SCHEMA = 3083 XML_SCHEMAP_WARN_UNLOCATED_SCHEMA = 3084 XML_SCHEMAP_WARN_ATTR_REDECL_PROH = 3085 - XML_SCHEMAP_WARN_ATTR_POINTLESS_PROH = 3086 # 3085 - XML_SCHEMAP_AG_PROPS_CORRECT = 3087 # 3086 - XML_SCHEMAP_COS_CT_EXTENDS_1_2 = 3088 # 3087 - XML_SCHEMAP_AU_PROPS_CORRECT = 3089 # 3088 - XML_SCHEMAP_A_PROPS_CORRECT_3 = 3090 # 3089 - XML_SCHEMAP_COS_ALL_LIMITED = 3091 # 3090 + XML_SCHEMAP_WARN_ATTR_POINTLESS_PROH = 3086 + XML_SCHEMAP_AG_PROPS_CORRECT = 3087 + XML_SCHEMAP_COS_CT_EXTENDS_1_2 = 3088 + XML_SCHEMAP_AU_PROPS_CORRECT = 3089 + XML_SCHEMAP_A_PROPS_CORRECT_3 = 3090 + XML_SCHEMAP_COS_ALL_LIMITED = 3091 XML_SCHEMATRONV_ASSERT = 4000 XML_SCHEMATRONV_REPORT = 4001 XML_MODULE_OPEN = 4900 @@ -823,7 +831,7 @@ cdef extern from "libxml/xmlerror.h": XML_RELAXNG_ERR_TEXTWRONG = 39 # --- END: GENERATED CONSTANTS --- -cdef extern from "libxml/xmlerror.h": +cdef extern from "libxml/xmlerror.h" nogil: ctypedef struct xmlError: int domain int code @@ -838,15 +846,15 @@ cdef extern from "libxml/xmlerror.h": int int2 void* node - ctypedef void (*xmlGenericErrorFunc)(void* ctxt, char* msg, ...) nogil + ctypedef void (*xmlGenericErrorFunc)(void* ctxt, char* msg, ...) noexcept ctypedef void (*xmlStructuredErrorFunc)(void* userData, - xmlError* error) nogil + const xmlError* error) noexcept cdef void xmlSetGenericErrorFunc( - void* ctxt, xmlGenericErrorFunc func) nogil + void* ctxt, xmlGenericErrorFunc func) cdef void xmlSetStructuredErrorFunc( - void* ctxt, xmlStructuredErrorFunc func) nogil + void* ctxt, xmlStructuredErrorFunc func) -cdef extern from "libxml/globals.h": +cdef extern from "libxml/globals.h" nogil: cdef xmlStructuredErrorFunc xmlStructuredError cdef void* xmlStructuredErrorContext diff --git a/src/lxml/includes/xmlparser.pxd b/src/lxml/includes/xmlparser.pxd index a196e34bd..beaf032fa 100644 --- a/src/lxml/includes/xmlparser.pxd +++ b/src/lxml/includes/xmlparser.pxd @@ -1,12 +1,12 @@ from libc.string cimport const_char from lxml.includes.tree cimport ( - xmlDoc, xmlNode, xmlDict, xmlDtd, xmlChar, const_xmlChar) + xmlDoc, xmlNode, xmlEntity, xmlDict, xmlDtd, xmlChar, const_xmlChar) from lxml.includes.tree cimport xmlInputReadCallback, xmlInputCloseCallback -from lxml.includes.xmlerror cimport xmlError, xmlStructuredErrorFunc +from lxml.includes.xmlerror cimport xmlError, xmlStructuredErrorFunc, xmlErrorLevel, xmlParserErrors -cdef extern from "libxml/parser.h": +cdef extern from "libxml/parser.h" nogil: ctypedef void (*startElementNsSAX2Func)(void* ctx, const_xmlChar* localname, const_xmlChar* prefix, @@ -15,43 +15,46 @@ cdef extern from "libxml/parser.h": const_xmlChar** namespaces, int nb_attributes, int nb_defaulted, - const_xmlChar** attributes) + const_xmlChar** attributes) noexcept ctypedef void (*endElementNsSAX2Func)(void* ctx, const_xmlChar* localname, const_xmlChar* prefix, - const_xmlChar* URI) + const_xmlChar* URI) noexcept - ctypedef void (*startElementSAXFunc)(void* ctx, const_xmlChar* name, const_xmlChar** atts) + ctypedef void (*startElementSAXFunc)(void* ctx, const_xmlChar* name, const_xmlChar** atts) noexcept - ctypedef void (*endElementSAXFunc)(void* ctx, const_xmlChar* name) + ctypedef void (*endElementSAXFunc)(void* ctx, const_xmlChar* name) noexcept - ctypedef void (*charactersSAXFunc)(void* ctx, const_xmlChar* ch, int len) + ctypedef void (*charactersSAXFunc)(void* ctx, const_xmlChar* ch, int len) noexcept - ctypedef void (*cdataBlockSAXFunc)(void* ctx, const_xmlChar* value, int len) + ctypedef void (*cdataBlockSAXFunc)(void* ctx, const_xmlChar* value, int len) noexcept - ctypedef void (*commentSAXFunc)(void* ctx, const_xmlChar* value) + ctypedef void (*commentSAXFunc)(void* ctx, const_xmlChar* value) noexcept - ctypedef void (*processingInstructionSAXFunc)(void* ctx, + ctypedef void (*processingInstructionSAXFunc)(void* ctx, const_xmlChar* target, - const_xmlChar* data) + const_xmlChar* data) noexcept - ctypedef void (*internalSubsetSAXFunc)(void* ctx, + ctypedef void (*internalSubsetSAXFunc)(void* ctx, const_xmlChar* name, const_xmlChar* externalID, - const_xmlChar* systemID) + const_xmlChar* systemID) noexcept - ctypedef void (*endDocumentSAXFunc)(void* ctx) + ctypedef void (*endDocumentSAXFunc)(void* ctx) noexcept - ctypedef void (*startDocumentSAXFunc)(void* ctx) + ctypedef void (*startDocumentSAXFunc)(void* ctx) noexcept - ctypedef void (*referenceSAXFunc)(void * ctx, const_xmlChar* name) + ctypedef void (*referenceSAXFunc)(void * ctx, const_xmlChar* name) noexcept + + ctypedef xmlEntity* (*getEntitySAXFunc)(void* ctx, const_xmlChar* name) noexcept cdef int XML_SAX2_MAGIC -cdef extern from "libxml/tree.h": +cdef extern from "libxml/tree.h" nogil: ctypedef struct xmlParserInput: int line + int col int length const_xmlChar* base const_xmlChar* cur @@ -76,6 +79,7 @@ cdef extern from "libxml/tree.h": charactersSAXFunc characters cdataBlockSAXFunc cdataBlock referenceSAXFunc reference + getEntitySAXFunc getEntity commentSAXFunc comment processingInstructionSAXFunc processingInstruction startDocumentSAXFunc startDocument @@ -93,13 +97,52 @@ cdef extern from "libxml/xmlIO.h" nogil: cdef xmlParserInputBuffer* xmlAllocParserInputBuffer(int enc) -cdef extern from "libxml/parser.h": +cdef extern from "libxml/parser.h" nogil: + + ctypedef enum xmlFeature: + XML_WITH_THREAD = 1 + XML_WITH_TREE = 2 + XML_WITH_OUTPUT = 3 + XML_WITH_PUSH = 4 + XML_WITH_READER = 5 + XML_WITH_PATTERN = 6 + XML_WITH_WRITER = 7 + XML_WITH_SAX1 = 8 + XML_WITH_FTP = 9 + XML_WITH_HTTP = 10 + XML_WITH_VALID = 11 + XML_WITH_HTML = 12 + XML_WITH_LEGACY = 13 + XML_WITH_C14N = 14 + XML_WITH_CATALOG = 15 + XML_WITH_XPATH = 16 + XML_WITH_XPTR = 17 + XML_WITH_XINCLUDE = 18 + XML_WITH_ICONV = 19 + XML_WITH_ISO8859X = 20 + XML_WITH_UNICODE = 21 + XML_WITH_REGEXP = 22 + XML_WITH_AUTOMATA = 23 + XML_WITH_EXPR = 24 + XML_WITH_SCHEMAS = 25 + XML_WITH_SCHEMATRON = 26 + XML_WITH_MODULES = 27 + XML_WITH_DEBUG = 28 + XML_WITH_DEBUG_MEM = 29 + XML_WITH_DEBUG_RUN = 30 + XML_WITH_ZLIB = 31 + XML_WITH_ICU = 32 + XML_WITH_LZMA = 33 + + cdef bint xmlHasFeature(xmlFeature feature) + + # START: Legacy, moved to tree.pxd + cdef xmlDict* xmlDictCreate() + cdef xmlDict* xmlDictCreateSub(xmlDict* subdict) + cdef void xmlDictFree(xmlDict* sub) + cdef int xmlDictReference(xmlDict* dict) + # END: Legacy, moved to tree.pxd - cdef xmlDict* xmlDictCreate() nogil - cdef xmlDict* xmlDictCreateSub(xmlDict* subdict) nogil - cdef void xmlDictFree(xmlDict* sub) nogil - cdef int xmlDictReference(xmlDict* dict) nogil - cdef int XML_COMPLETE_ATTRS # SAX option for adding DTD default attributes cdef int XML_SKIP_IDS # SAX option for not building an XML ID dict @@ -144,72 +187,89 @@ cdef extern from "libxml/parser.h": void* userData int* spaceTab int spaceMax + int nsNr bint html bint progressive int inSubset int charset xmlParserInput* input + int inputNr + xmlParserInput* inputTab[] ctypedef enum xmlParserOption: - XML_PARSE_RECOVER = 1 # recover on errors - XML_PARSE_NOENT = 2 # substitute entities - XML_PARSE_DTDLOAD = 4 # load the external subset - XML_PARSE_DTDATTR = 8 # default DTD attributes - XML_PARSE_DTDVALID = 16 # validate with the DTD - XML_PARSE_NOERROR = 32 # suppress error reports - XML_PARSE_NOWARNING = 64 # suppress warning reports - XML_PARSE_PEDANTIC = 128 # pedantic error reporting - XML_PARSE_NOBLANKS = 256 # remove blank nodes - XML_PARSE_SAX1 = 512 # use the SAX1 interface internally - XML_PARSE_XINCLUDE = 1024 # Implement XInclude substitution - XML_PARSE_NONET = 2048 # Forbid network access - XML_PARSE_NODICT = 4096 # Do not reuse the context dictionary - XML_PARSE_NSCLEAN = 8192 # remove redundant namespaces declarations - XML_PARSE_NOCDATA = 16384 # merge CDATA as text nodes - XML_PARSE_NOXINCNODE = 32768 # do not generate XINCLUDE START/END nodes + XML_PARSE_RECOVER = 0x1 # recover on errors + XML_PARSE_NOENT = 0x2 # substitute entities + XML_PARSE_DTDLOAD = 0x4 # load the external subset + XML_PARSE_DTDATTR = 0x8 # default DTD attributes + XML_PARSE_DTDVALID = 0x10 # validate with the DTD + XML_PARSE_NOERROR = 0x20 # suppress error reports + XML_PARSE_NOWARNING = 0x40 # suppress warning reports + XML_PARSE_PEDANTIC = 0x80 # pedantic error reporting + XML_PARSE_NOBLANKS = 0x100 # remove blank nodes + XML_PARSE_SAX1 = 0x200 # use the SAX1 interface internally + XML_PARSE_XINCLUDE = 0x400 # Implement XInclude substitution + XML_PARSE_NONET = 0x800 # Forbid network access + XML_PARSE_NODICT = 0x1000 # Do not reuse the context dictionary + XML_PARSE_NSCLEAN = 0x2000 # remove redundant namespaces declarations + XML_PARSE_NOCDATA = 0x4000 # merge CDATA as text nodes + XML_PARSE_NOXINCNODE = 0x8000 # do not generate XINCLUDE START/END nodes # libxml2 2.6.21+ only: - XML_PARSE_COMPACT = 65536 # compact small text nodes + XML_PARSE_COMPACT = 0x1_0000 # compact small text nodes # libxml2 2.7.0+ only: - XML_PARSE_OLD10 = 131072 # parse using XML-1.0 before update 5 - XML_PARSE_NOBASEFIX = 262144 # do not fixup XINCLUDE xml:base uris - XML_PARSE_HUGE = 524288 # relax any hardcoded limit from the parser + XML_PARSE_OLD10 = 0x2_0000 # parse using XML-1.0 before update 5 + XML_PARSE_NOBASEFIX = 0x4_0000 # do not fixup XINCLUDE xml:base uris + XML_PARSE_HUGE = 0x8_0000 # relax any hardcoded limit from the parser # libxml2 2.7.3+ only: - XML_PARSE_OLDSAX = 1048576 # parse using SAX2 interface before 2.7.0 + XML_PARSE_OLDSAX = 0x10_0000 # parse using SAX2 interface before 2.7.0 # libxml2 2.8.0+ only: - XML_PARSE_IGNORE_ENC = 2097152 # ignore internal document encoding hint + XML_PARSE_IGNORE_ENC = 0x20_0000 # ignore internal document encoding hint # libxml2 2.9.0+ only: - XML_PARSE_BIG_LINES = 4194304 # Store big lines numbers in text PSVI field - - cdef void xmlInitParser() nogil - cdef void xmlCleanupParser() nogil - - cdef int xmlLineNumbersDefault(int onoff) nogil - cdef xmlParserCtxt* xmlNewParserCtxt() nogil + XML_PARSE_BIG_LINES = 0x40_0000 # Store big lines numbers in text PSVI field + # libxml2 2.13.0+ only: + XML_PARSE_NO_XXE = 0x80_0000 # Disable loading of external DTDs or entities + # libxml2 2.14.0+ only: + XML_PARSE_UNZIP = 0x100_0000 # Enable input decompression (and potential gzip bombs) + XML_PARSE_NO_SYS_CATALOG = 0x200_0000 # Disable the global system XML catalog + XML_PARSE_CATALOG_PI = 0x400_0000 # Enable XML catalog processing instructions + # libxml2 2.15.0+ only: + XML_PARSE_SKIP_IDS = 0x800_0000 # Force the parser to ignore IDs + + cdef void xmlInitParser() + cdef void xmlCleanupParser() + + cdef int xmlLineNumbersDefault(int onoff) + cdef xmlParserCtxt* xmlNewParserCtxt() cdef xmlParserInput* xmlNewIOInputStream(xmlParserCtxt* ctxt, xmlParserInputBuffer* input, - int enc) nogil - cdef int xmlCtxtUseOptions(xmlParserCtxt* ctxt, int options) nogil - cdef void xmlFreeParserCtxt(xmlParserCtxt* ctxt) nogil - cdef void xmlCtxtReset(xmlParserCtxt* ctxt) nogil - cdef void xmlClearParserCtxt(xmlParserCtxt* ctxt) nogil + int enc) + cdef int xmlCtxtUseOptions(xmlParserCtxt* ctxt, int options) + cdef void xmlFreeParserCtxt(xmlParserCtxt* ctxt) + cdef void xmlCtxtReset(xmlParserCtxt* ctxt) + cdef void xmlClearParserCtxt(xmlParserCtxt* ctxt) cdef int xmlParseChunk(xmlParserCtxt* ctxt, - char* chunk, int size, int terminate) nogil + char* chunk, int size, int terminate) cdef xmlDoc* xmlCtxtReadDoc(xmlParserCtxt* ctxt, char* cur, char* URL, char* encoding, - int options) nogil + int options) cdef xmlDoc* xmlCtxtReadFile(xmlParserCtxt* ctxt, char* filename, char* encoding, - int options) nogil - cdef xmlDoc* xmlCtxtReadIO(xmlParserCtxt* ctxt, - xmlInputReadCallback ioread, - xmlInputCloseCallback ioclose, + int options) + cdef xmlDoc* xmlCtxtReadIO(xmlParserCtxt* ctxt, + xmlInputReadCallback ioread, + xmlInputCloseCallback ioclose, void* ioctx, char* URL, char* encoding, - int options) nogil + int options) cdef xmlDoc* xmlCtxtReadMemory(xmlParserCtxt* ctxt, char* buffer, int size, char* filename, const_char* encoding, - int options) nogil + int options) + + cdef void xmlErrParser(xmlParserCtxt* ctxt, xmlNode* node, + int domain, int code, xmlErrorLevel level, + const xmlChar *str1, const xmlChar *str2, const xmlChar *str3, + int int1, const char *msg, ...) + # iterparse: @@ -217,33 +277,61 @@ cdef extern from "libxml/parser.h": void* user_data, char* chunk, int size, - char* filename) nogil + char* filename) cdef int xmlCtxtResetPush(xmlParserCtxt* ctxt, char* chunk, int size, char* filename, - char* encoding) nogil + char* encoding) # entity loaders: ctypedef xmlParserInput* (*xmlExternalEntityLoader)( - const_char * URL, const_char * ID, xmlParserCtxt* context) nogil - cdef xmlExternalEntityLoader xmlGetExternalEntityLoader() nogil - cdef void xmlSetExternalEntityLoader(xmlExternalEntityLoader f) nogil + const_char * URL, const_char * ID, xmlParserCtxt* context) noexcept + cdef xmlExternalEntityLoader xmlGetExternalEntityLoader() + cdef void xmlSetExternalEntityLoader(xmlExternalEntityLoader f) + + cdef xmlEntity* xmlSAX2GetEntity(void* ctxt, const_xmlChar* name) noexcept # DTDs: - cdef xmlDtd* xmlParseDTD(const_xmlChar* ExternalID, const_xmlChar* SystemID) nogil + cdef xmlDtd* xmlParseDTD(const_xmlChar* ExternalID, const_xmlChar* SystemID) cdef xmlDtd* xmlIOParseDTD(xmlSAXHandler* sax, xmlParserInputBuffer* input, - int enc) nogil + int enc) + + +cdef extern from *: + """ + #if LIBXML_VERSION < 21400 + typedef xmlParserErrors (* xmlResourceLoader) ( + void *ctxt, const char *url, const char *publicId, int type, int flags, xmlParserInput **out); + #define xmlCtxtSetResourceLoader(ctxt, loader, data) ((void) ((void) ctxt, (void) loader, (void) data)) + #endif + """ + + ctypedef xmlParserErrors (*xmlResourceLoader)( + void *context, const char *url, const char *publicId, + int type, int flags, # actually "xmlResourceType type, xmlParserInputFlags flags" + xmlParserInput **out) + + cdef void xmlCtxtSetResourceLoader(xmlParserCtxt *c_ctxt, xmlResourceLoader c_loader, void* context) + -cdef extern from "libxml/parserInternals.h": +cdef extern from "libxml/parserInternals.h" nogil: + """ + #if LIBXML_VERSION < 21400 + #define xmlNewInputFromMemory(url, mem, size, flags) (NULL) + #endif + """ cdef xmlParserInput* xmlNewInputStream(xmlParserCtxt* ctxt) - cdef xmlParserInput* xmlNewStringInputStream(xmlParserCtxt* ctxt, - char* buffer) nogil - cdef xmlParserInput* xmlNewInputFromFile(xmlParserCtxt* ctxt, - char* filename) nogil - cdef void xmlFreeInputStream(xmlParserInput* input) nogil - cdef int xmlSwitchEncoding(xmlParserCtxt* ctxt, int enc) nogil + cdef xmlParserInput* xmlNewStringInputStream(xmlParserCtxt* ctxt, + char* buffer) + cdef xmlParserInput* xmlNewInputFromFile(xmlParserCtxt* ctxt, + char* filename) + cdef xmlParserInput* xmlNewInputFromMemory( + const char *url, const void *mem, size_t size, int flags) # actually "xmlParserInputFlags flags" + cdef void xmlFreeInputStream(xmlParserInput* input) + cdef int xmlSwitchEncoding(xmlParserCtxt* ctxt, int enc) + cdef bint xmlCtxtIsStopped(xmlParserCtxt* ctxt) diff --git a/src/lxml/includes/xmlschema.pxd b/src/lxml/includes/xmlschema.pxd index 8e93cc570..e06df06d6 100644 --- a/src/lxml/includes/xmlschema.pxd +++ b/src/lxml/includes/xmlschema.pxd @@ -1,8 +1,13 @@ from lxml.includes.tree cimport xmlDoc -from lxml.includes.xmlparser cimport xmlSAXHandler +from lxml.includes.xmlparser cimport xmlSAXHandler, xmlResourceLoader from lxml.includes.xmlerror cimport xmlStructuredErrorFunc -cdef extern from "libxml/xmlschemas.h": +cdef extern from "libxml/xmlschemas.h" nogil: + """ + #if LIBXML_VERSION < 21400 + #define xmlSchemaSetResourceLoader(ctxt, loader, data) ((void) ((void) ctxt, (void) loader, (void) data)) + #endif + """ ctypedef struct xmlSchema ctypedef struct xmlSchemaParserCtxt @@ -12,9 +17,12 @@ cdef extern from "libxml/xmlschemas.h": ctypedef enum xmlSchemaValidOption: XML_SCHEMA_VAL_VC_I_CREATE = 1 - cdef xmlSchemaValidCtxt* xmlSchemaNewValidCtxt(xmlSchema* schema) nogil + cdef void xmlSchemaSetResourceLoader(xmlSchemaParserCtxt* ctxt, + xmlResourceLoader loader, void *ctx) cdef void xmlSchemaSetParserStructuredErrors(xmlSchemaParserCtxt* ctxt, xmlStructuredErrorFunc serror, void *ctx) + + cdef xmlSchemaValidCtxt* xmlSchemaNewValidCtxt(xmlSchema* schema) nogil cdef void xmlSchemaSetValidStructuredErrors(xmlSchemaValidCtxt* ctxt, xmlStructuredErrorFunc serror, void *ctx) diff --git a/src/lxml/includes/xpath.pxd b/src/lxml/includes/xpath.pxd index d01735b68..22069eb7c 100644 --- a/src/lxml/includes/xpath.pxd +++ b/src/lxml/includes/xpath.pxd @@ -4,7 +4,8 @@ from lxml.includes cimport xmlerror from libc.string cimport const_char from lxml.includes.tree cimport xmlChar, const_xmlChar -cdef extern from "libxml/xpath.h": + +cdef extern from "libxml/xpath.h" nogil: ctypedef enum xmlXPathObjectType: XPATH_UNDEFINED = 0 XPATH_NODESET = 1 @@ -73,63 +74,63 @@ cdef extern from "libxml/xpath.h": ctypedef struct xmlXPathCompExpr - ctypedef void (*xmlXPathFunction)(xmlXPathParserContext* ctxt, int nargs) nogil + ctypedef void (*xmlXPathFunction)(xmlXPathParserContext* ctxt, int nargs) ctypedef xmlXPathFunction (*xmlXPathFuncLookupFunc)(void* ctxt, const_xmlChar* name, - const_xmlChar* ns_uri) nogil + const_xmlChar* ns_uri) - cdef xmlXPathContext* xmlXPathNewContext(tree.xmlDoc* doc) nogil + cdef xmlXPathContext* xmlXPathNewContext(tree.xmlDoc* doc) cdef xmlXPathObject* xmlXPathEvalExpression(const_xmlChar* str, - xmlXPathContext* ctxt) nogil + xmlXPathContext* ctxt) cdef xmlXPathObject* xmlXPathCompiledEval(xmlXPathCompExpr* comp, - xmlXPathContext* ctxt) nogil - cdef xmlXPathCompExpr* xmlXPathCompile(const_xmlChar* str) nogil + xmlXPathContext* ctxt) + cdef xmlXPathCompExpr* xmlXPathCompile(const_xmlChar* str) cdef xmlXPathCompExpr* xmlXPathCtxtCompile(xmlXPathContext* ctxt, - const_xmlChar* str) nogil - cdef void xmlXPathFreeContext(xmlXPathContext* ctxt) nogil - cdef void xmlXPathFreeCompExpr(xmlXPathCompExpr* comp) nogil - cdef void xmlXPathFreeObject(xmlXPathObject* obj) nogil + const_xmlChar* str) + cdef void xmlXPathFreeContext(xmlXPathContext* ctxt) + cdef void xmlXPathFreeCompExpr(xmlXPathCompExpr* comp) + cdef void xmlXPathFreeObject(xmlXPathObject* obj) cdef int xmlXPathRegisterNs(xmlXPathContext* ctxt, - const_xmlChar* prefix, const_xmlChar* ns_uri) nogil + const_xmlChar* prefix, const_xmlChar* ns_uri) - cdef xmlNodeSet* xmlXPathNodeSetCreate(tree.xmlNode* val) nogil - cdef void xmlXPathFreeNodeSet(xmlNodeSet* val) nogil + cdef xmlNodeSet* xmlXPathNodeSetCreate(tree.xmlNode* val) + cdef void xmlXPathFreeNodeSet(xmlNodeSet* val) -cdef extern from "libxml/xpathInternals.h": +cdef extern from "libxml/xpathInternals.h" nogil: cdef int xmlXPathRegisterFunc(xmlXPathContext* ctxt, const_xmlChar* name, - xmlXPathFunction f) nogil + xmlXPathFunction f) cdef int xmlXPathRegisterFuncNS(xmlXPathContext* ctxt, const_xmlChar* name, const_xmlChar* ns_uri, - xmlXPathFunction f) nogil + xmlXPathFunction f) cdef void xmlXPathRegisterFuncLookup(xmlXPathContext *ctxt, xmlXPathFuncLookupFunc f, - void *funcCtxt) nogil + void *funcCtxt) cdef int xmlXPathRegisterVariable(xmlXPathContext *ctxt, const_xmlChar* name, - xmlXPathObject* value) nogil + xmlXPathObject* value) cdef int xmlXPathRegisterVariableNS(xmlXPathContext *ctxt, const_xmlChar* name, const_xmlChar* ns_uri, - xmlXPathObject* value) nogil - cdef void xmlXPathRegisteredVariablesCleanup(xmlXPathContext *ctxt) nogil - cdef void xmlXPathRegisteredNsCleanup(xmlXPathContext *ctxt) nogil - cdef xmlXPathObject* valuePop (xmlXPathParserContext *ctxt) nogil - cdef int valuePush(xmlXPathParserContext* ctxt, xmlXPathObject *value) nogil + xmlXPathObject* value) + cdef void xmlXPathRegisteredVariablesCleanup(xmlXPathContext *ctxt) + cdef void xmlXPathRegisteredNsCleanup(xmlXPathContext *ctxt) + cdef xmlXPathObject* valuePop (xmlXPathParserContext *ctxt) + cdef int valuePush(xmlXPathParserContext* ctxt, xmlXPathObject *value) - cdef xmlXPathObject* xmlXPathNewCString(const_char *val) nogil - cdef xmlXPathObject* xmlXPathWrapCString(const_char * val) nogil - cdef xmlXPathObject* xmlXPathNewString(const_xmlChar *val) nogil - cdef xmlXPathObject* xmlXPathWrapString(const_xmlChar * val) nogil - cdef xmlXPathObject* xmlXPathNewFloat(double val) nogil - cdef xmlXPathObject* xmlXPathNewBoolean(int val) nogil - cdef xmlXPathObject* xmlXPathNewNodeSet(tree.xmlNode* val) nogil - cdef xmlXPathObject* xmlXPathNewValueTree(tree.xmlNode* val) nogil + cdef xmlXPathObject* xmlXPathNewCString(const_char *val) + cdef xmlXPathObject* xmlXPathWrapCString(const_char * val) + cdef xmlXPathObject* xmlXPathNewString(const_xmlChar *val) + cdef xmlXPathObject* xmlXPathWrapString(const_xmlChar * val) + cdef xmlXPathObject* xmlXPathNewFloat(double val) + cdef xmlXPathObject* xmlXPathNewBoolean(int val) + cdef xmlXPathObject* xmlXPathNewNodeSet(tree.xmlNode* val) + cdef xmlXPathObject* xmlXPathNewValueTree(tree.xmlNode* val) cdef void xmlXPathNodeSetAdd(xmlNodeSet* cur, - tree.xmlNode* val) nogil + tree.xmlNode* val) cdef void xmlXPathNodeSetAddUnique(xmlNodeSet* cur, - tree.xmlNode* val) nogil - cdef xmlXPathObject* xmlXPathWrapNodeSet(xmlNodeSet* val) nogil - cdef void xmlXPathErr(xmlXPathParserContext* ctxt, int error) nogil + tree.xmlNode* val) + cdef xmlXPathObject* xmlXPathWrapNodeSet(xmlNodeSet* val) + cdef void xmlXPathErr(xmlXPathParserContext* ctxt, int error) diff --git a/src/lxml/includes/xslt.pxd b/src/lxml/includes/xslt.pxd index 101fb7e78..742720968 100644 --- a/src/lxml/includes/xslt.pxd +++ b/src/lxml/includes/xslt.pxd @@ -11,7 +11,12 @@ cdef extern from "libxslt/xslt.h": cdef extern from "libxslt/xsltconfig.h": cdef int LIBXSLT_VERSION -cdef extern from "libxslt/xsltInternals.h": +cdef extern from "libxslt/xsltInternals.h" nogil: + """ + #if LIBXSLT_VERSION < 10134 + #define xsltParseStylesheetUser(style, doc) (-1) + #endif + """ ctypedef enum xsltTransformState: XSLT_STATE_OK # 0 XSLT_STATE_ERROR # 1 @@ -42,35 +47,37 @@ cdef extern from "libxslt/xsltInternals.h": ctypedef struct xsltTemplate - cdef xsltStylesheet* xsltParseStylesheetDoc(xmlDoc* doc) nogil - cdef void xsltFreeStylesheet(xsltStylesheet* sheet) nogil + cdef xsltStylesheet* xsltNewStylesheet() + cdef xsltStylesheet* xsltParseStylesheetDoc(xmlDoc* doc) + cdef int xsltParseStylesheetUser(xsltStylesheet* style, xmlDoc* doc) + cdef void xsltFreeStylesheet(xsltStylesheet* sheet) -cdef extern from "libxslt/imports.h": +cdef extern from "libxslt/imports.h" nogil: # actually defined in "etree_defs.h" cdef void LXML_GET_XSLT_ENCODING(const_xmlChar* result_var, xsltStylesheet* style) -cdef extern from "libxslt/extensions.h": +cdef extern from "libxslt/extensions.h" nogil: ctypedef void (*xsltTransformFunction)(xsltTransformContext* ctxt, xmlNode* context_node, xmlNode* inst, - void* precomp_unused) nogil + void* precomp_unused) noexcept cdef int xsltRegisterExtFunction(xsltTransformContext* ctxt, const_xmlChar* name, const_xmlChar* URI, - xmlXPathFunction function) nogil + xmlXPathFunction function) cdef int xsltRegisterExtModuleFunction(const_xmlChar* name, const_xmlChar* URI, - xmlXPathFunction function) nogil + xmlXPathFunction function) cdef int xsltUnregisterExtModuleFunction(const_xmlChar* name, const_xmlChar* URI) cdef xmlXPathFunction xsltExtModuleFunctionLookup( - const_xmlChar* name, const_xmlChar* URI) nogil - cdef int xsltRegisterExtPrefix(xsltStylesheet* style, - const_xmlChar* prefix, const_xmlChar* URI) nogil + const_xmlChar* name, const_xmlChar* URI) + cdef int xsltRegisterExtPrefix(xsltStylesheet* style, + const_xmlChar* prefix, const_xmlChar* URI) cdef int xsltRegisterExtElement(xsltTransformContext* ctxt, const_xmlChar* name, const_xmlChar* URI, - xsltTransformFunction function) nogil + xsltTransformFunction function) -cdef extern from "libxslt/documents.h": +cdef extern from "libxslt/documents.h" nogil: ctypedef enum xsltLoadType: XSLT_LOAD_START XSLT_LOAD_STYLESHEET @@ -79,56 +86,56 @@ cdef extern from "libxslt/documents.h": ctypedef xmlDoc* (*xsltDocLoaderFunc)(const_xmlChar* URI, xmlDict* dict, int options, void* ctxt, - xsltLoadType type) nogil + xsltLoadType type) noexcept cdef xsltDocLoaderFunc xsltDocDefaultLoader - cdef void xsltSetLoaderFunc(xsltDocLoaderFunc f) nogil + cdef void xsltSetLoaderFunc(xsltDocLoaderFunc f) -cdef extern from "libxslt/transform.h": +cdef extern from "libxslt/transform.h" nogil: cdef xmlDoc* xsltApplyStylesheet(xsltStylesheet* style, xmlDoc* doc, - const_char** params) nogil + const_char** params) cdef xmlDoc* xsltApplyStylesheetUser(xsltStylesheet* style, xmlDoc* doc, const_char** params, const_char* output, void* profile, - xsltTransformContext* context) nogil + xsltTransformContext* context) cdef void xsltProcessOneNode(xsltTransformContext* ctxt, xmlNode* contextNode, - xsltStackElem* params) nogil + xsltStackElem* params) cdef xsltTransformContext* xsltNewTransformContext(xsltStylesheet* style, - xmlDoc* doc) nogil - cdef void xsltFreeTransformContext(xsltTransformContext* context) nogil + xmlDoc* doc) + cdef void xsltFreeTransformContext(xsltTransformContext* context) cdef void xsltApplyOneTemplate(xsltTransformContext* ctxt, xmlNode* contextNode, xmlNode* list, xsltTemplate* templ, - xsltStackElem* params) nogil + xsltStackElem* params) -cdef extern from "libxslt/xsltutils.h": +cdef extern from "libxslt/xsltutils.h" nogil: cdef int xsltSaveResultToString(xmlChar** doc_txt_ptr, int* doc_txt_len, xmlDoc* result, - xsltStylesheet* style) nogil + xsltStylesheet* style) cdef int xsltSaveResultToFilename(const_char *URL, xmlDoc* result, xsltStylesheet* style, - int compression) nogil + int compression) cdef int xsltSaveResultTo(xmlOutputBuffer* buf, xmlDoc* result, - xsltStylesheet* style) nogil + xsltStylesheet* style) cdef xmlGenericErrorFunc xsltGenericError cdef void *xsltGenericErrorContext cdef void xsltSetGenericErrorFunc( - void* ctxt, void (*handler)(void* ctxt, char* msg, ...)) nogil + void* ctxt, void (*handler)(void* ctxt, char* msg, ...) nogil) cdef void xsltSetTransformErrorFunc( xsltTransformContext*, void* ctxt, - void (*handler)(void* ctxt, char* msg, ...) nogil) nogil - cdef void xsltTransformError(xsltTransformContext* ctxt, - xsltStylesheet* style, + void (*handler)(void* ctxt, char* msg, ...) nogil) + cdef void xsltTransformError(xsltTransformContext* ctxt, + xsltStylesheet* style, xmlNode* node, char* msg, ...) cdef void xsltSetCtxtParseOptions( xsltTransformContext* ctxt, int options) -cdef extern from "libxslt/security.h": +cdef extern from "libxslt/security.h" nogil: ctypedef struct xsltSecurityPrefs ctypedef enum xsltSecurityOption: XSLT_SECPREF_READ_FILE = 1 @@ -139,44 +146,44 @@ cdef extern from "libxslt/security.h": ctypedef int (*xsltSecurityCheck)(xsltSecurityPrefs* sec, xsltTransformContext* ctxt, - char* value) nogil + char* value) noexcept - cdef xsltSecurityPrefs* xsltNewSecurityPrefs() nogil - cdef void xsltFreeSecurityPrefs(xsltSecurityPrefs* sec) nogil + cdef xsltSecurityPrefs* xsltNewSecurityPrefs() + cdef void xsltFreeSecurityPrefs(xsltSecurityPrefs* sec) cdef int xsltSecurityForbid(xsltSecurityPrefs* sec, xsltTransformContext* ctxt, - char* value) nogil + char* value) cdef int xsltSecurityAllow(xsltSecurityPrefs* sec, xsltTransformContext* ctxt, - char* value) nogil + char* value) cdef int xsltSetSecurityPrefs(xsltSecurityPrefs* sec, xsltSecurityOption option, - xsltSecurityCheck func) nogil + xsltSecurityCheck func) cdef xsltSecurityCheck xsltGetSecurityPrefs( xsltSecurityPrefs* sec, - xsltSecurityOption option) nogil + xsltSecurityOption option) cdef int xsltSetCtxtSecurityPrefs(xsltSecurityPrefs* sec, - xsltTransformContext* ctxt) nogil - cdef xmlDoc* xsltGetProfileInformation(xsltTransformContext* ctxt) nogil + xsltTransformContext* ctxt) + cdef xmlDoc* xsltGetProfileInformation(xsltTransformContext* ctxt) -cdef extern from "libxslt/variables.h": +cdef extern from "libxslt/variables.h" nogil: cdef int xsltQuoteUserParams(xsltTransformContext* ctxt, const_char** params) cdef int xsltQuoteOneUserParam(xsltTransformContext* ctxt, const_xmlChar* name, const_xmlChar* value) -cdef extern from "libxslt/extra.h": +cdef extern from "libxslt/extra.h" nogil: const_xmlChar* XSLT_LIBXSLT_NAMESPACE const_xmlChar* XSLT_XALAN_NAMESPACE const_xmlChar* XSLT_SAXON_NAMESPACE const_xmlChar* XSLT_XT_NAMESPACE cdef xmlXPathFunction xsltFunctionNodeSet - cdef void xsltRegisterAllExtras() nogil + cdef void xsltRegisterAllExtras() -cdef extern from "libexslt/exslt.h": - cdef void exsltRegisterAll() nogil +cdef extern from "libexslt/exslt.h" nogil: + cdef void exsltRegisterAll() # libexslt 1.1.25+ const_xmlChar* EXSLT_DATE_NAMESPACE @@ -188,4 +195,3 @@ cdef extern from "libexslt/exslt.h": cdef int exsltSetsXpathCtxtRegister(xmlXPathContext* ctxt, const_xmlChar* prefix) cdef int exsltMathXpathCtxtRegister(xmlXPathContext* ctxt, const_xmlChar* prefix) cdef int exsltStrXpathCtxtRegister(xmlXPathContext* ctxt, const_xmlChar* prefix) - diff --git a/src/lxml/isoschematron/__init__.py b/src/lxml/isoschematron/__init__.py index 5967b1097..ac89fb62e 100644 --- a/src/lxml/isoschematron/__init__.py +++ b/src/lxml/isoschematron/__init__.py @@ -61,10 +61,16 @@ svrl_validation_errors = _etree.XPath( '//svrl:failed-assert', namespaces={'svrl': SVRL_NS}) - # RelaxNG validator for schematron schemas -schematron_schema_valid = _etree.RelaxNG( - file=os.path.join(_resources_dir, 'rng', 'iso-schematron.rng')) +schematron_schema_valid_supported = False +try: + schematron_schema_valid = _etree.RelaxNG( + file=os.path.join(_resources_dir, 'rng', 'iso-schematron.rng')) + schematron_schema_valid_supported = True +except _etree.RelaxNGParseError: + # Some distributions delete the file due to licensing issues. + def schematron_schema_valid(arg): + raise NotImplementedError("Validating the ISO schematron requires iso-schematron.rng") def stylesheet_params(**kwargs): @@ -153,6 +159,13 @@ class Schematron(_etree._Validator): report document gets stored and can be accessed as the ``validation_report`` property. + If ``validate_schema`` is set to False, the validation of the schema file + itself is disabled. Validation happens by default after building the full + schema, unless the schema validation file cannot be found at import time, + in which case the validation gets disabled. Some lxml distributions exclude + this file due to licensing issues. ISO-Schematron validation can then still + be used normally, but the schemas themselves cannot be validated. + Here is a usage example:: >>> from lxml import etree @@ -212,7 +225,7 @@ def _extract(self, element): schematron = None if element.tag == _xml_schema_root: schematron = self._extract_xsd(element) - elif element.nsmap[element.prefix] == RELAXNG_NS: + elif element.nsmap.get(element.prefix) == RELAXNG_NS: # RelaxNG does not have a single unique root element schematron = self._extract_rng(element) return schematron @@ -234,8 +247,9 @@ def _extract(self, element): def __init__(self, etree=None, file=None, include=True, expand=True, include_params={}, expand_params={}, compile_params={}, store_schematron=False, store_xslt=False, store_report=False, - phase=None, error_finder=ASSERTS_ONLY): - super(Schematron, self).__init__() + phase=None, error_finder=ASSERTS_ONLY, + validate_schema=schematron_schema_valid_supported): + super().__init__() self._store_report = store_report self._schematron = None @@ -273,7 +287,7 @@ def __init__(self, etree=None, file=None, include=True, expand=True, schematron = self._include(schematron, **include_params) if expand: schematron = self._expand(schematron, **expand_params) - if not schematron_schema_valid(schematron): + if validate_schema and not schematron_schema_valid(schematron): raise _etree.SchematronParseError( "invalid schematron schema: %s" % schematron_schema_valid.error_log) diff --git a/src/lxml/iterparse.pxi b/src/lxml/iterparse.pxi index 138c23a6a..593aad5f5 100644 --- a/src/lxml/iterparse.pxi +++ b/src/lxml/iterparse.pxi @@ -1,13 +1,13 @@ # iterparse -- event-driven parsing -DEF __ITERPARSE_CHUNK_SIZE = 32768 - cdef class iterparse: - u"""iterparse(self, source, events=("end",), tag=None, \ + """iterparse(self, source, events=("end",), tag=None, \ attribute_defaults=False, dtd_validation=False, \ load_dtd=False, no_network=True, remove_blank_text=False, \ - remove_comments=False, remove_pis=False, encoding=None, \ - html=False, recover=None, huge_tree=False, schema=None) + compact=True, resolve_entities='internal', remove_comments=False, \ + remove_pis=False, strip_cdata=True, encoding=None, \ + html=False, recover=None, huge_tree=False, schema=None, \ + chunk_size=65536) Incremental parser. @@ -34,26 +34,35 @@ cdef class iterparse: libxml2 parser configuration. A DTD will also be loaded if validation or attribute default values are requested. - Available boolean keyword arguments: - - attribute_defaults: read default attributes from DTD - - dtd_validation: validate (if DTD is available) - - load_dtd: use DTD for parsing - - no_network: prevent network access for related files - - remove_blank_text: discard blank text nodes - - remove_comments: discard comments - - remove_pis: discard processing instructions - - strip_cdata: replace CDATA sections by normal text content (default: True) - - compact: safe memory for short text content (default: True) - - resolve_entities: replace entities by their text value (default: True) - - huge_tree: disable security restrictions and support very deep trees - and very long text content (only affects libxml2 2.7+) - - html: parse input as HTML (default: XML) - - recover: try hard to parse through broken input (default: True for HTML, - False otherwise) - - Other keyword arguments: - - encoding: override the document encoding - - schema: an XMLSchema to validate against + **Available boolean keyword arguments:** + + - attribute_defaults: read default attributes from DTD + - dtd_validation: validate (if DTD is available) + - load_dtd: use DTD for parsing + - no_network: prevent network access for related files + - remove_blank_text: discard blank text nodes. In XML mode, without + DTD/schema, a heuristic preserves blank text nodes appearing after + non-blank content at the same level. In HTML mode, removal follows + built-in structural rules and does not necessarily require DTD/schema. + - remove_comments: discard comments + - remove_pis: discard processing instructions + - strip_cdata: replace CDATA sections by normal text content (default: + True for XML, ignored otherwise) + - compact: safe memory for short text content (default: True) + - resolve_entities: replace entities by their text value + (default: 'internal' only; True before lxml 6.1) + - huge_tree: disable security restrictions and support very deep trees + and very long text content + - html: parse input as HTML (default: XML) + - recover: try hard to parse through broken input (default: True for + HTML, False otherwise) + + **Other keyword arguments:** + + - encoding: override the document encoding + - schema: an XMLSchema to validate against + - chunk_size: the number of bytes to read from the 'source' in one chunk + (default: 65536) """ cdef _FeedParser _parser cdef object _tag @@ -62,24 +71,24 @@ cdef class iterparse: cdef object _source cdef object _filename cdef object _error + cdef object _chunk_size cdef bint _close_source_after_read - def __init__(self, source, events=(u"end",), *, tag=None, + def __init__(self, source, events=("end",), *, tag=None, attribute_defaults=False, dtd_validation=False, load_dtd=False, no_network=True, remove_blank_text=False, - compact=True, resolve_entities=True, remove_comments=False, + compact=True, resolve_entities='internal', remove_comments=False, remove_pis=False, strip_cdata=True, encoding=None, html=False, recover=None, huge_tree=False, collect_ids=True, - XMLSchema schema=None): + XMLSchema schema=None, int chunk_size=65536): if not hasattr(source, 'read'): source = _getFSPathOrObject(source) self._filename = source - if python.IS_PYTHON2: - source = _encodeFilename(source) - source = open(source, 'rb') + self._source = open(source, 'rb') self._close_source_after_read = True else: self._filename = _getFilenameForFile(source) + self._source = source self._close_source_after_read = False if recover is None: @@ -98,7 +107,6 @@ cdef class iterparse: remove_blank_text=remove_blank_text, remove_comments=remove_comments, remove_pis=remove_pis, - strip_cdata=strip_cdata, no_network=no_network, target=None, # TODO schema=schema, @@ -125,9 +133,9 @@ cdef class iterparse: target=None, # TODO compact=compact) + self._chunk_size = chunk_size self._events = parser.read_events() self._parser = parser - self._source = source @property def error_log(self): @@ -147,7 +155,7 @@ cdef class iterparse: return self._parser.version def set_element_class_lookup(self, ElementClassLookup lookup = None): - u"""set_element_class_lookup(self, lookup = None) + """set_element_class_lookup(self, lookup = None) Set a lookup scheme for element classes generated from this parser. @@ -156,7 +164,7 @@ cdef class iterparse: self._parser.set_element_class_lookup(lookup) def makeelement(self, _tag, attrib=None, nsmap=None, **_extra): - u"""makeelement(self, _tag, attrib=None, nsmap=None, **_extra) + """makeelement(self, _tag, attrib=None, nsmap=None, **_extra) Creates a new element associated with this parser. """ @@ -217,7 +225,7 @@ cdef class iterparse: @cython.final cdef bint _read_more_events(self, _SaxParserContext context) except -123: - data = self._source.read(__ITERPARSE_CHUNK_SIZE) + data = self._source.read(self._chunk_size) if not isinstance(data, bytes): self._close_source() raise TypeError("reading file objects must return bytes objects") @@ -239,7 +247,7 @@ cdef enum _IterwalkSkipStates: cdef class iterwalk: - u"""iterwalk(self, element_or_tree, events=("end",), tag=None) + """iterwalk(self, element_or_tree, events=("end",), tag=None) A tree walker that generates events from an existing tree as if it was parsing XML data with ``iterparse()``. @@ -260,7 +268,7 @@ cdef class iterwalk: cdef int _event_filter cdef _IterwalkSkipStates _skip_state - def __init__(self, element_or_tree, events=(u"end",), tag=None): + def __init__(self, element_or_tree, events=("end",), tag=None): cdef _Element root cdef int ns_count root = _rootNodeOrRaise(element_or_tree) @@ -276,20 +284,26 @@ cdef class iterwalk: if self._event_filter: self._index = 0 - if self._matcher is not None and self._event_filter & PARSE_EVENT_FILTER_START: - self._matcher.cacheTags(root._doc) - - # When processing an ElementTree, add events for the preceding comments/PIs. - if self._event_filter & (PARSE_EVENT_FILTER_COMMENT | PARSE_EVENT_FILTER_PI): - if isinstance(element_or_tree, _ElementTree): - self._include_siblings = root - for elem in list(root.itersiblings(preceding=True))[::-1]: - if self._event_filter & PARSE_EVENT_FILTER_COMMENT and elem.tag is Comment: - self._events.append((u'comment', elem)) - elif self._event_filter & PARSE_EVENT_FILTER_PI and elem.tag is PI: - self._events.append((u'pi', elem)) - - ns_count = self._start_node(root) + doc = root._doc + doc.lock_read() + try: + if self._matcher is not None and self._event_filter & PARSE_EVENT_FILTER_START: + self._matcher.cacheTags(root._doc) + + # When processing an ElementTree, add events for the preceding comments/PIs. + if self._event_filter & (PARSE_EVENT_FILTER_COMMENT | PARSE_EVENT_FILTER_PI): + if isinstance(element_or_tree, _ElementTree): + self._include_siblings = root + for elem in list(root.itersiblings(preceding=True))[::-1]: + if self._event_filter & PARSE_EVENT_FILTER_COMMENT and elem.tag is Comment: + self._events.append(('comment', elem)) + elif self._event_filter & PARSE_EVENT_FILTER_PI and elem.tag is PI: + self._events.append(('pi', elem)) + + ns_count = self._start_node(root) + finally: + doc.unlock_read() + self._node_stack.append( (root, ns_count) ) else: self._index = -1 @@ -299,52 +313,68 @@ cdef class iterwalk: def __next__(self): cdef xmlNode* c_child + cdef _Document doc cdef _Element node cdef _Element next_node cdef int ns_count = 0 if self._events: return self._next_event() - if self._matcher is not None and self._index >= 0: - node = self._node_stack[self._index][0] - self._matcher.cacheTags(node._doc) - - # find next node - while self._index >= 0: - node = self._node_stack[self._index][0] - if self._skip_state == IWSKIP_SKIP_NEXT: - c_child = NULL - else: - c_child = self._process_non_elements( - node._doc, _findChildForwards(node._c_node, 0)) - self._skip_state = IWSKIP_CANNOT_SKIP - - while c_child is NULL: - # back off through parents - self._index -= 1 - node = self._end_node() - if self._index < 0: - break - c_child = self._process_non_elements( - node._doc, _nextElement(node._c_node)) - - if c_child is not NULL: - next_node = _elementFactory(node._doc, c_child) - if self._event_filter & (PARSE_EVENT_FILTER_START | - PARSE_EVENT_FILTER_START_NS): - ns_count = self._start_node(next_node) - elif self._event_filter & PARSE_EVENT_FILTER_END_NS: - ns_count = _countNsDefs(next_node._c_node) - self._node_stack.append( (next_node, ns_count) ) - self._index += 1 - if self._events: - return self._next_event() - - if self._include_siblings is not None: - node, self._include_siblings = self._include_siblings, None - self._process_non_elements(node._doc, _nextElement(node._c_node)) - if self._events: - return self._next_event() + doc = None + try: + if self._matcher is not None and self._index >= 0: + node = self._node_stack[self._index][0] + doc = node._doc + doc.lock_read() + self._matcher.cacheTags(doc) + + # find next node + while self._index >= 0: + node = self._node_stack[self._index][0] + if doc is not node._doc: + # Normally, we should traverse only one doc. But if users move elements to other documents + # while we're still traversing them, we should handle a document switch as well. + if doc is not None: + doc.unlock_read() + doc = node._doc + doc.lock_read() + + if self._skip_state == IWSKIP_SKIP_NEXT: + c_child = NULL + else: + c_child = self._process_non_elements( + node._doc, _findChildForwards(node._c_node, 0)) + self._skip_state = IWSKIP_CANNOT_SKIP + + while c_child is NULL: + # back off through parents + self._index -= 1 + node = self._end_node() + if self._index < 0: + break + c_child = self._process_non_elements( + node._doc, _nextElement(node._c_node)) + + if c_child is not NULL: + next_node = _elementFactory(node._doc, c_child) + if self._event_filter & (PARSE_EVENT_FILTER_START | + PARSE_EVENT_FILTER_START_NS): + ns_count = self._start_node(next_node) + elif self._event_filter & PARSE_EVENT_FILTER_END_NS: + ns_count = _countNsDefs(next_node._c_node) + self._node_stack.append( (next_node, ns_count) ) + self._index += 1 + if self._events: + return self._next_event() + + if self._include_siblings is not None: + node, self._include_siblings = self._include_siblings, None + self._process_non_elements(node._doc, _nextElement(node._c_node)) + if self._events: + return self._next_event() + finally: + if doc is not None: + doc.unlock_read() raise StopIteration @@ -354,12 +384,12 @@ cdef class iterwalk: if c_node.type == tree.XML_COMMENT_NODE: if self._event_filter & PARSE_EVENT_FILTER_COMMENT: self._events.append( - (u"comment", _elementFactory(doc, c_node))) + ("comment", _elementFactory(doc, c_node))) c_node = _nextElement(c_node) elif c_node.type == tree.XML_PI_NODE: if self._event_filter & PARSE_EVENT_FILTER_PI: self._events.append( - (u"pi", _elementFactory(doc, c_node))) + ("pi", _elementFactory(doc, c_node))) c_node = _nextElement(c_node) else: break @@ -368,7 +398,7 @@ cdef class iterwalk: @cython.final cdef _next_event(self): if self._skip_state == IWSKIP_NEXT_IS_START: - if self._events[0][0] in (u'start', u'start-ns'): + if self._events[0][0] in ('start', 'start-ns'): self._skip_state = IWSKIP_CAN_SKIP return self._pop_event(0) @@ -395,7 +425,7 @@ cdef class iterwalk: ns_count = 0 if self._event_filter & PARSE_EVENT_FILTER_START: if self._matcher is None or self._matcher.matches(node._c_node): - self._events.append( (u"start", node) ) + self._events.append( ("start", node) ) self._skip_state = IWSKIP_NEXT_IS_START return ns_count @@ -406,21 +436,21 @@ cdef class iterwalk: node, ns_count = self._node_stack.pop() if self._event_filter & PARSE_EVENT_FILTER_END: if self._matcher is None or self._matcher.matches(node._c_node): - self._events.append( (u"end", node) ) + self._events.append( ("end", node) ) if self._event_filter & PARSE_EVENT_FILTER_END_NS and ns_count: - event = (u"end-ns", None) + event = ("end-ns", None) for i in range(ns_count): self._events.append(event) return node -cdef int _countNsDefs(xmlNode* c_node): +cdef int _countNsDefs(xmlNode* c_node) noexcept: cdef xmlNs* c_ns cdef int count count = 0 c_ns = c_node.nsDef while c_ns is not NULL: - count += 1 + count += (c_ns.href is not NULL) c_ns = c_ns.next return count @@ -431,9 +461,10 @@ cdef int _appendStartNsEvents(xmlNode* c_node, list event_list) except -1: count = 0 c_ns = c_node.nsDef while c_ns is not NULL: - ns_tuple = (funicode(c_ns.prefix) if c_ns.prefix is not NULL else '', - funicode(c_ns.href)) - event_list.append( (u"start-ns", ns_tuple) ) - count += 1 + if c_ns.href: + ns_tuple = (funicodeOrEmpty(c_ns.prefix), + funicode(c_ns.href)) + event_list.append( ("start-ns", ns_tuple) ) + count += 1 c_ns = c_ns.next return count diff --git a/src/lxml/lxml_endian.h b/src/lxml/lxml_endian.h index f53cb7ad7..d66baae72 100644 --- a/src/lxml/lxml_endian.h +++ b/src/lxml/lxml_endian.h @@ -1,8 +1,13 @@ #ifndef PY_BIG_ENDIAN +/* STOP INCLUDING THIS FILE ! DO NOT USE IT IN NEW CODE ! */ +/* Left only for legacy purposes - this file is no longer used. */ + #ifdef _MSC_VER +#pragma message ("Please do not include 'lxml_endian.h'. It is no longer used nor needed.") typedef unsigned __int32 uint32_t; #else +#warning Please do not include 'lxml_endian.h'. It is no longer used nor needed. #include #endif diff --git a/src/lxml/nsclasses.pxi b/src/lxml/nsclasses.pxi index 274277dcd..bad6591f3 100644 --- a/src/lxml/nsclasses.pxi +++ b/src/lxml/nsclasses.pxi @@ -1,21 +1,22 @@ # module-level API for namespace implementations -cdef class LxmlRegistryError(LxmlError): +class LxmlRegistryError(LxmlError): """Base class of lxml registry errors. """ -cdef class NamespaceRegistryError(LxmlRegistryError): +class NamespaceRegistryError(LxmlRegistryError): """Error registering a namespace extension. """ @cython.internal cdef class _NamespaceRegistry: - u"Dictionary-like namespace registry" + "Dictionary-like namespace registry" cdef object _ns_uri cdef bytes _ns_uri_utf cdef dict _entries - cdef char* _c_ns_uri_utf + cdef const char* _c_ns_uri_utf + def __cinit__(self, ns_uri): self._ns_uri = ns_uri if ns_uri is None: @@ -27,7 +28,7 @@ cdef class _NamespaceRegistry: self._entries = {} def update(self, class_dict_iterable): - u"""update(self, class_dict_iterable) + """update(self, class_dict_iterable) Forgivingly update the registry. @@ -38,7 +39,7 @@ cdef class _NamespaceRegistry: or if the name starts with '_', it will be silently discarded. This allows registrations at the module or class level using vars(), globals() etc.""" - if hasattr(class_dict_iterable, u'items'): + if hasattr(class_dict_iterable, 'items'): class_dict_iterable = class_dict_iterable.items() for name, item in class_dict_iterable: if (name is None or name[:1] != '_') and callable(item): @@ -47,27 +48,13 @@ cdef class _NamespaceRegistry: def __getitem__(self, name): if name is not None: name = _utf8(name) - return self._get(name) + return self._entries[name] def __delitem__(self, name): if name is not None: name = _utf8(name) del self._entries[name] - cdef object _get(self, object name): - cdef python.PyObject* dict_result - dict_result = python.PyDict_GetItem(self._entries, name) - if dict_result is NULL: - raise KeyError, u"Name not registered." - return dict_result - - cdef object _getForString(self, char* name): - cdef python.PyObject* dict_result - dict_result = python.PyDict_GetItem(self._entries, name) - if dict_result is NULL: - raise KeyError, u"Name not registered." - return dict_result - def __iter__(self): return iter(self._entries) @@ -104,21 +91,21 @@ cdef class _NamespaceRegistry: @cython.final @cython.internal cdef class _ClassNamespaceRegistry(_NamespaceRegistry): - u"Dictionary-like registry for namespace implementation classes" + "Dictionary-like registry for namespace implementation classes" def __setitem__(self, name, item): if not isinstance(item, type) or not issubclass(item, ElementBase): raise NamespaceRegistryError, \ - u"Registered element classes must be subtypes of ElementBase" + "Registered element classes must be subtypes of ElementBase" if name is not None: name = _utf8(name) self._entries[name] = item def __repr__(self): - return u"Namespace(%r)" % self._ns_uri + return "Namespace(%r)" % self._ns_uri cdef class ElementNamespaceClassLookup(FallbackElementClassLookup): - u"""ElementNamespaceClassLookup(self, fallback=None) + """ElementNamespaceClassLookup(self, fallback=None) Element class lookup scheme that searches the Element class in the Namespace registry. @@ -137,6 +124,7 @@ cdef class ElementNamespaceClassLookup(FallbackElementClassLookup): ... "Element implementation for 'movie' tag (explicit tag name) in schema namespace." """ cdef dict _namespace_registries + def __cinit__(self): self._namespace_registries = {} @@ -145,7 +133,7 @@ cdef class ElementNamespaceClassLookup(FallbackElementClassLookup): self._lookup_function = _find_nselement_class def get_namespace(self, ns_uri): - u"""get_namespace(self, ns_uri) + """get_namespace(self, ns_uri) Retrieve the namespace object associated with the given URI. Pass None for the empty namespace. @@ -155,46 +143,53 @@ cdef class ElementNamespaceClassLookup(FallbackElementClassLookup): ns_utf = _utf8(ns_uri) else: ns_utf = None - try: - return self._namespace_registries[ns_utf] - except KeyError: - registry = self._namespace_registries[ns_utf] = \ - _ClassNamespaceRegistry(ns_uri) - return registry + with cython.critical_section(self._namespace_registries): + try: + return self._namespace_registries[ns_utf] + except KeyError: + registry = self._namespace_registries[ns_utf] = \ + _ClassNamespaceRegistry(ns_uri) + return registry + cdef object _find_nselement_class(state, _Document doc, xmlNode* c_node): cdef python.PyObject* dict_result cdef ElementNamespaceClassLookup lookup - cdef _NamespaceRegistry registry + cdef _NamespaceRegistry registry = None if state is None: - return _lookupDefaultElementClass(None, doc, c_node) + return _lookupDefaultElementClassesOnly(None, doc, c_node) lookup = state if c_node.type != tree.XML_ELEMENT_NODE: return _callLookupFallback(lookup, doc, c_node) c_namespace_utf = _getNs(c_node) - if c_namespace_utf is not NULL: - dict_result = python.PyDict_GetItem( - lookup._namespace_registries, c_namespace_utf) - else: - dict_result = python.PyDict_GetItem( - lookup._namespace_registries, None) - if dict_result is not NULL: - registry = <_NamespaceRegistry>dict_result + # Use Python's own dict critical section to guard against changes. + with cython.critical_section(lookup._namespace_registries): + if c_namespace_utf is not NULL: + dict_result = python.PyDict_GetItemWithError( + lookup._namespace_registries, c_namespace_utf) + else: + dict_result = python.PyDict_GetItemWithError( + lookup._namespace_registries, None) + + if dict_result is not NULL: + registry = <_NamespaceRegistry>dict_result + + if registry is not None: classes = registry._entries if c_node.name is not NULL: - dict_result = python.PyDict_GetItem( - classes, c_node.name) - else: - dict_result = NULL - - if dict_result is NULL: - dict_result = python.PyDict_GetItem(classes, None) + with cython.critical_section(classes): + dict_result = python.PyDict_GetItemWithError( + classes, c_node.name) + if dict_result is not NULL: + return dict_result + dict_result = python.PyDict_GetItemWithError(classes, None) if dict_result is not NULL: return dict_result + return _callLookupFallback(lookup, doc, c_node) @@ -205,7 +200,7 @@ cdef dict __FUNCTION_NAMESPACE_REGISTRIES __FUNCTION_NAMESPACE_REGISTRIES = {} def FunctionNamespace(ns_uri): - u"""FunctionNamespace(ns_uri) + """FunctionNamespace(ns_uri) Retrieve the function namespace object associated with the given URI. @@ -226,26 +221,29 @@ def FunctionNamespace(ns_uri): ... return x + 3 """ ns_utf = _utf8(ns_uri) if ns_uri else None - try: - return __FUNCTION_NAMESPACE_REGISTRIES[ns_utf] - except KeyError: - registry = __FUNCTION_NAMESPACE_REGISTRIES[ns_utf] = \ - _XPathFunctionNamespaceRegistry(ns_uri) - return registry + with cython.critical_section(__FUNCTION_NAMESPACE_REGISTRIES): + try: + return __FUNCTION_NAMESPACE_REGISTRIES[ns_utf] + except KeyError: + registry = __FUNCTION_NAMESPACE_REGISTRIES[ns_utf] = \ + _XPathFunctionNamespaceRegistry(ns_uri) + return registry + @cython.internal cdef class _FunctionNamespaceRegistry(_NamespaceRegistry): def __setitem__(self, name, item): if not callable(item): raise NamespaceRegistryError, \ - u"Registered functions must be callable." + "Registered functions must be callable." if not name: raise ValueError, \ - u"extensions must have non empty names" + "extensions must have non empty names" self._entries[_utf8(name)] = item def __repr__(self): - return u"FunctionNamespace(%r)" % self._ns_uri + return "FunctionNamespace(%r)" % self._ns_uri + @cython.final @cython.internal @@ -254,7 +252,7 @@ cdef class _XPathFunctionNamespaceRegistry(_FunctionNamespaceRegistry): cdef bytes _prefix_utf property prefix: - u"Namespace prefix for extension functions." + "Namespace prefix for extension functions." def __del__(self): self._prefix = None # no prefix configured self._prefix_utf = None @@ -269,13 +267,14 @@ cdef class _XPathFunctionNamespaceRegistry(_FunctionNamespaceRegistry): self._prefix_utf = _utf8(prefix) if prefix is not None else None self._prefix = prefix + cdef list _find_all_extension_prefixes(): - u"Internal lookup function to find all function prefixes for XSLT/XPath." + "Internal lookup function to find all function prefixes for XSLT/XPath." cdef _XPathFunctionNamespaceRegistry registry - cdef list ns_prefixes = [] - for registry in __FUNCTION_NAMESPACE_REGISTRIES.itervalues(): - if registry._prefix_utf is not None: - if registry._ns_uri_utf is not None: - ns_prefixes.append( - (registry._prefix_utf, registry._ns_uri_utf)) + with cython.critical_section(__FUNCTION_NAMESPACE_REGISTRIES): + ns_prefixes = [ + (registry._prefix_utf, registry._ns_uri_utf) + for registry in __FUNCTION_NAMESPACE_REGISTRIES.values() + if registry._prefix_utf is not None and registry._ns_uri_utf is not None + ] return ns_prefixes diff --git a/src/lxml/objectify.pyx b/src/lxml/objectify.pyx index 376695a8b..03b82aadb 100644 --- a/src/lxml/objectify.pyx +++ b/src/lxml/objectify.pyx @@ -1,14 +1,12 @@ # cython: binding=True # cython: auto_pickle=False -# cython: language_level=2 +# cython: language_level=3 """ The ``lxml.objectify`` module implements a Python object API for XML. It is based on `lxml.etree`. """ -from __future__ import absolute_import - cimport cython from lxml.includes.etreepublic cimport _Document, _Element, ElementBase, ElementClassLookup @@ -20,16 +18,17 @@ from lxml.includes cimport tree cimport lxml.includes.etreepublic as cetree cimport libc.string as cstring_h # not to be confused with stdlib 'string' from libc.string cimport const_char - -__all__ = [u'BoolElement', u'DataElement', u'E', u'Element', u'ElementMaker', - u'FloatElement', u'IntElement', u'LongElement', u'NoneElement', - u'NumberElement', u'ObjectPath', u'ObjectifiedDataElement', - u'ObjectifiedElement', u'ObjectifyElementClassLookup', - u'PYTYPE_ATTRIBUTE', u'PyType', u'StringElement', u'SubElement', - u'XML', u'annotate', u'deannotate', u'dump', u'enable_recursive_str', - u'fromstring', u'getRegisteredTypes', u'makeparser', u'parse', - u'pyannotate', u'pytypename', u'set_default_parser', - u'set_pytype_attribute_tag', u'xsiannotate'] +from libc cimport limits + +__all__ = ['BoolElement', 'DataElement', 'E', 'Element', 'ElementMaker', + 'FloatElement', 'IntElement', 'NoneElement', + 'NumberElement', 'ObjectPath', 'ObjectifiedDataElement', + 'ObjectifiedElement', 'ObjectifyElementClassLookup', + 'PYTYPE_ATTRIBUTE', 'PyType', 'StringElement', 'SubElement', + 'XML', 'annotate', 'deannotate', 'dump', 'enable_recursive_str', + 'fromstring', 'getRegisteredTypes', 'makeparser', 'parse', + 'pyannotate', 'pytypename', 'set_default_parser', + 'set_pytype_attribute_tag', 'xsiannotate'] cdef object etree from lxml import etree @@ -45,25 +44,7 @@ cdef object re import re cdef tuple IGNORABLE_ERRORS = (ValueError, TypeError) -cdef object is_special_method = re.compile(u'__.*__$').match - - -# Duplicated from apihelpers.pxi, since dependencies obstruct -# including apihelpers.pxi. -cdef strrepr(s): - """Build a representation of strings which we can use in __repr__ - methods, e.g. _Element.__repr__(). - """ - return s.encode('unicode-escape') if python.IS_PYTHON2 else s - - -cdef object _typename(object t): - cdef const_char* c_name - c_name = python._fqtypename(t) - s = cstring_h.strrchr(c_name, c'.') - if s is not NULL: - c_name = s + 1 - return pyunicode(c_name) +cdef object is_special_method = re.compile('__.*__$').match # namespace/name for "pytype" hint attribute @@ -77,13 +58,13 @@ cdef const_xmlChar* _PYTYPE_ATTRIBUTE_NAME PYTYPE_ATTRIBUTE = None -cdef unicode TREE_PYTYPE_NAME = u"TREE" +cdef unicode TREE_PYTYPE_NAME = "TREE" -cdef tuple _unicodeAndUtf8(s): - return s, python.PyUnicode_AsUTF8String(s) +cdef tuple _unicodeAndUtf8(str s): + return s, s.encode('utf8') def set_pytype_attribute_tag(attribute_tag=None): - u"""set_pytype_attribute_tag(attribute_tag=None) + """set_pytype_attribute_tag(attribute_tag=None) Change name and namespace of the XML attribute that holds Python type information. @@ -93,24 +74,30 @@ def set_pytype_attribute_tag(attribute_tag=None): Default: "{http://codespeak.net/lxml/objectify/pytype}pytype" """ + if attribute_tag is None: + namespace, namespace_utf = _unicodeAndUtf8("http://codespeak.net/lxml/objectify/pytype") + attribute_name, attribute_name_utf = _unicodeAndUtf8("pytype") + else: + namespace_utf, attribute_name_utf = cetree.getNsTag(attribute_tag) + namespace = namespace_utf.decode('utf8') + attribute_name = attribute_name_utf.decode('utf8') + + cdef const_xmlChar *c_namespace = namespace_utf + cdef const_xmlChar *c_attribute = attribute_name_utf + global PYTYPE_ATTRIBUTE, _PYTYPE_NAMESPACE, _PYTYPE_ATTRIBUTE_NAME global PYTYPE_NAMESPACE, PYTYPE_NAMESPACE_UTF8 global PYTYPE_ATTRIBUTE_NAME, PYTYPE_ATTRIBUTE_NAME_UTF8 - if attribute_tag is None: - PYTYPE_NAMESPACE, PYTYPE_NAMESPACE_UTF8 = \ - _unicodeAndUtf8(u"http://codespeak.net/lxml/objectify/pytype") - PYTYPE_ATTRIBUTE_NAME, PYTYPE_ATTRIBUTE_NAME_UTF8 = \ - _unicodeAndUtf8(u"pytype") - else: - PYTYPE_NAMESPACE_UTF8, PYTYPE_ATTRIBUTE_NAME_UTF8 = \ - cetree.getNsTag(attribute_tag) - PYTYPE_NAMESPACE = PYTYPE_NAMESPACE_UTF8.decode('utf8') - PYTYPE_ATTRIBUTE_NAME = PYTYPE_ATTRIBUTE_NAME_UTF8.decode('utf8') - _PYTYPE_NAMESPACE = PYTYPE_NAMESPACE_UTF8 - _PYTYPE_ATTRIBUTE_NAME = PYTYPE_ATTRIBUTE_NAME_UTF8 - PYTYPE_ATTRIBUTE = cetree.namespacedNameFromNsName( - _PYTYPE_NAMESPACE, _PYTYPE_ATTRIBUTE_NAME) + with cython.critical_section(TREE_PYTYPE_NAME): # anything global and immutable, really + # First overwrite the C string pointers, then replace their old Python bytes targets. + _PYTYPE_NAMESPACE = c_namespace + _PYTYPE_ATTRIBUTE_NAME = c_attribute + + PYTYPE_NAMESPACE, PYTYPE_NAMESPACE_UTF8 = namespace, namespace_utf + PYTYPE_ATTRIBUTE_NAME, PYTYPE_ATTRIBUTE_NAME_UTF8 = attribute_name, attribute_name_utf + + PYTYPE_ATTRIBUTE = cetree.namespacedNameFromNsName(c_namespace, c_attribute) set_pytype_attribute_tag() @@ -118,23 +105,23 @@ set_pytype_attribute_tag() # namespaces for XML Schema cdef object XML_SCHEMA_NS, XML_SCHEMA_NS_UTF8 XML_SCHEMA_NS, XML_SCHEMA_NS_UTF8 = \ - _unicodeAndUtf8(u"http://www.w3.org/2001/XMLSchema") + _unicodeAndUtf8("http://www.w3.org/2001/XMLSchema") cdef const_xmlChar* _XML_SCHEMA_NS = _xcstr(XML_SCHEMA_NS_UTF8) cdef object XML_SCHEMA_INSTANCE_NS, XML_SCHEMA_INSTANCE_NS_UTF8 XML_SCHEMA_INSTANCE_NS, XML_SCHEMA_INSTANCE_NS_UTF8 = \ - _unicodeAndUtf8(u"http://www.w3.org/2001/XMLSchema-instance") + _unicodeAndUtf8("http://www.w3.org/2001/XMLSchema-instance") cdef const_xmlChar* _XML_SCHEMA_INSTANCE_NS = _xcstr(XML_SCHEMA_INSTANCE_NS_UTF8) -cdef object XML_SCHEMA_INSTANCE_NIL_ATTR = u"{%s}nil" % XML_SCHEMA_INSTANCE_NS -cdef object XML_SCHEMA_INSTANCE_TYPE_ATTR = u"{%s}type" % XML_SCHEMA_INSTANCE_NS +cdef object XML_SCHEMA_INSTANCE_NIL_ATTR = "{%s}nil" % XML_SCHEMA_INSTANCE_NS +cdef object XML_SCHEMA_INSTANCE_TYPE_ATTR = "{%s}type" % XML_SCHEMA_INSTANCE_NS ################################################################################ # Element class for the main API cdef class ObjectifiedElement(ElementBase): - u"""Main XML Element class. + """Main XML Element class. Element children are accessed as object attributes. Multiple children with the same name are available through a list index. Example:: @@ -148,7 +135,7 @@ cdef class ObjectifiedElement(ElementBase): subclasses. """ def __iter__(self): - u"""Iterate over self and all siblings with the same tag. + """Iterate over self and all siblings with the same tag. """ parent = self.getparent() if parent is None: @@ -159,7 +146,7 @@ cdef class ObjectifiedElement(ElementBase): if __RECURSIVE_STR: return _dump(self, 0) else: - return textOf(self._c_node) or u'' + return _lockedTextOf(self) or '' # pickle support for objectified Element def __reduce__(self): @@ -167,7 +154,7 @@ cdef class ObjectifiedElement(ElementBase): @property def text(self): - return textOf(self._c_node) + return _lockedTextOf(self) @property def __dict__(self): @@ -177,103 +164,142 @@ cdef class ObjectifiedElement(ElementBase): """ cdef _Element child cdef dict children - c_ns = tree._getNs(self._c_node) - tag = u"{%s}*" % pyunicode(c_ns) if c_ns is not NULL else None - children = {} - for child in etree.ElementChildIterator(self, tag=tag): - if c_ns is NULL and tree._getNs(child._c_node) is not NULL: - continue - name = pyunicode(child._c_node.name) - if name not in children: - children[name] = child + + doc = self._doc + cetree.lock_read(doc) + try: + c_ns = tree._getNs(self._c_node) + tag = "{%s}*" % pyunicode(c_ns) if c_ns is not NULL else None + children = {} + for child in etree.ElementChildIterator(self, tag=tag): + if c_ns is NULL and tree._getNs(child._c_node) is not NULL: + continue + name = pyunicode(child._c_node.name) + if name not in children: + children[name] = child + finally: + cetree.unlock_read(doc) + return children def __len__(self): - u"""Count self and siblings with the same tag. + """Count self and siblings with the same tag. """ return _countSiblings(self._c_node) def countchildren(self): - u"""countchildren(self) + """countchildren(self) Return the number of children of this element, regardless of their name. """ # copied from etree - cdef Py_ssize_t c + cdef Py_ssize_t count cdef tree.xmlNode* c_node - c = 0 - c_node = self._c_node.children - while c_node is not NULL: - if tree._isElement(c_node): - c += 1 - c_node = c_node.next - return c + count = 0 + + doc = self._doc + cetree.lock_read(doc) + try: + c_node = self._c_node.children + while c_node is not NULL: + count += tree._isElement(c_node) + c_node = c_node.next + finally: + cetree.unlock_read(doc) + + return count def getchildren(self): - u"""getchildren(self) + """getchildren(self) Returns a sequence of all direct children. The elements are returned in document order. """ cdef tree.xmlNode* c_node result = [] - c_node = self._c_node.children - while c_node is not NULL: - if tree._isElement(c_node): - result.append(cetree.elementFactory(self._doc, c_node)) - c_node = c_node.next + + doc = self._doc + cetree.lock_read(doc) + try: + c_node = self._c_node.children + while c_node is not NULL: + if tree._isElement(c_node): + result.append(cetree.elementFactory(doc, c_node)) + c_node = c_node.next + finally: + cetree.unlock_read(doc) + return result def __getattr__(self, tag): - u"""Return the (first) child with the given tag name. If no namespace + """Return the (first) child with the given tag name. If no namespace is provided, the child will be looked up in the same one as self. """ - if is_special_method(tag): - return object.__getattr__(self, tag) - return _lookupChildOrRaise(self, tag) + doc = self._doc + cetree.lock_read(doc) + try: + return _lookupChildOrRaise(self, tag) + finally: + cetree.unlock_read(doc) def __setattr__(self, tag, value): - u"""Set the value of the (first) child with the given tag name. If no + """Set the value of the (first) child with the given tag name. If no namespace is provided, the child will be looked up in the same one as self. """ cdef _Element element # properties are looked up /after/ __setattr__, so we must emulate them - if tag == u'text' or tag == u'pyval': + if tag == 'text' or tag == 'pyval': # read-only ! - raise TypeError, f"attribute '{tag}' of '{_typename(self)}' objects is not writable" - elif tag == u'tail': - cetree.setTailText(self._c_node, value) + raise TypeError, f"attribute '{tag}' of '{python._typename(self)}' objects is not writable" + elif tag == 'tail': + doc = self._doc + cetree.lock_write(doc) + try: + cetree.setTailText(self._c_node, value) + finally: + cetree.unlock_write(doc) return - elif tag == u'tag': + elif tag == 'tag': ElementBase.tag.__set__(self, value) return - elif tag == u'base': + elif tag == 'base': ElementBase.base.__set__(self, value) return - tag = _buildChildTag(self, tag) - element = _lookupChild(self, tag) - if element is None: - _appendValue(self, tag, value) - else: - _replaceElement(element, value) + + doc = self._doc + cetree.lock_write(doc) + try: + tag = _buildChildTag(self, tag) + element = _lookupChild(self, tag) + if element is None: + _appendValue(self, tag, value) + else: + _replaceElement(element, value) + finally: + cetree.unlock_write(doc) def __delattr__(self, tag): child = _lookupChildOrRaise(self, tag) self.remove(child) def addattr(self, tag, value): - u"""addattr(self, tag, value) + """addattr(self, tag, value) Add a child value to the element. As opposed to append(), it sets a data value, not an element. """ - _appendValue(self, _buildChildTag(self, tag), value) + doc = self._doc + cetree.lock_write(doc) + try: + _appendValue(self, _buildChildTag(self, tag), value) + finally: + cetree.unlock_write(doc) def __getitem__(self, key): - u"""Return a sibling, counting from the first child of the parent. The + """Return a sibling, counting from the first child of the parent. The method behaves like both a dict and a sequence. * If argument is an integer, returns the sibling at that position. @@ -288,30 +314,41 @@ cdef class ObjectifiedElement(ElementBase): cdef tree.xmlNode* c_parent cdef tree.xmlNode* c_node cdef Py_ssize_t c_index + + doc = self._doc if python._isString(key): - return _lookupChildOrRaise(self, key) + cetree.lock_read(doc) + try: + return _lookupChildOrRaise(self, key) + finally: + cetree.unlock_read(doc) elif isinstance(key, slice): return list(self)[key] + # normal item access - c_index = key # raises TypeError if necessary - c_self_node = self._c_node - c_parent = c_self_node.parent - if c_parent is NULL: - if c_index == 0 or c_index == -1: - return self - raise IndexError, unicode(key) - if c_index < 0: - c_node = c_parent.last - else: - c_node = c_parent.children - c_node = _findFollowingSibling( - c_node, tree._getNs(c_self_node), c_self_node.name, c_index) - if c_node is NULL: - raise IndexError, unicode(key) - return elementFactory(self._doc, c_node) + cetree.lock_read(doc) + try: + c_index = key # raises TypeError if necessary + c_self_node = self._c_node + c_parent = c_self_node.parent + if c_parent is NULL: + if c_index == 0 or c_index == -1: + return self + raise IndexError, str(key) + if c_index < 0: + c_node = c_parent.last + else: + c_node = c_parent.children + c_node = _findFollowingSibling( + c_node, tree._getNs(c_self_node), c_self_node.name, c_index) + if c_node is NULL: + raise IndexError, str(key) + return elementFactory(doc, c_node) + finally: + cetree.unlock_read(doc) def __setitem__(self, key, value): - u"""Set the value of a sibling, counting from the first child of the + """Set the value of a sibling, counting from the first child of the parent. Implements key assignment, item assignment and slice assignment. @@ -325,39 +362,50 @@ cdef class ObjectifiedElement(ElementBase): """ cdef _Element element cdef tree.xmlNode* c_node + if python._isString(key): - key = _buildChildTag(self, key) - element = _lookupChild(self, key) - if element is None: - _appendValue(self, key, value) - else: - _replaceElement(element, value) + doc = self._doc + cetree.lock_write(doc) + try: + key = _buildChildTag(self, key) + element = _lookupChild(self, key) + if element is None: + _appendValue(self, key, value) + else: + _replaceElement(element, value) + finally: + cetree.unlock_write(doc) return - if self._c_node.parent is NULL: - # the 'root[i] = ...' case - raise TypeError, u"assignment to root element is invalid" + doc = self._doc + cetree.lock_write(doc) + try: + if self._c_node.parent is NULL: + # the 'root[i] = ...' case + raise TypeError, "assignment to root element is invalid" - if isinstance(key, slice): - # slice assignment - _setSlice(key, self, value) - else: - # normal index assignment - if key < 0: - c_node = self._c_node.parent.last + if isinstance(key, slice): + # slice assignment + _setSlice(key, self, value) else: - c_node = self._c_node.parent.children - c_node = _findFollowingSibling( - c_node, tree._getNs(self._c_node), self._c_node.name, key) - if c_node is NULL: - raise IndexError, unicode(key) - element = elementFactory(self._doc, c_node) - _replaceElement(element, value) + # normal index assignment + if key < 0: + c_node = self._c_node.parent.last + else: + c_node = self._c_node.parent.children + c_node = _findFollowingSibling( + c_node, tree._getNs(self._c_node), self._c_node.name, key) + if c_node is NULL: + raise IndexError, unicode(key) + element = elementFactory(doc, c_node) + _replaceElement(element, value) + finally: + cetree.unlock_write(doc) def __delitem__(self, key): parent = self.getparent() if parent is None: - raise TypeError, u"deleting items not supported by root element" + raise TypeError, "deleting items not supported by root element" if isinstance(key, slice): # slice deletion del_items = list(self)[key] @@ -370,16 +418,21 @@ cdef class ObjectifiedElement(ElementBase): parent.remove(sibling) def descendantpaths(self, prefix=None): - u"""descendantpaths(self, prefix=None) + """descendantpaths(self, prefix=None) Returns a list of object path expressions for all descendants. """ if prefix is not None and not python._isString(prefix): - prefix = u'.'.join(prefix) - return _build_descendant_paths(self._c_node, prefix) + prefix = '.'.join(prefix) + doc = self._doc + cetree.lock_read(doc) + try: + return _build_descendant_paths(self._c_node, prefix) + finally: + cetree.unlock_read(doc) -cdef inline bint _tagMatches(tree.xmlNode* c_node, const_xmlChar* c_href, const_xmlChar* c_name): +cdef inline bint _tagMatches(tree.xmlNode* c_node, const_xmlChar* c_href, const_xmlChar* c_name) noexcept: if c_node.name != c_name: return 0 if c_href == NULL: @@ -387,10 +440,10 @@ cdef inline bint _tagMatches(tree.xmlNode* c_node, const_xmlChar* c_href, const_ c_node_href = tree._getNs(c_node) if c_node_href == NULL: return c_href[0] == c'\0' - return tree.xmlStrcmp(c_node_href, c_href) == 0 + return tree.xmlStrEqual(c_node_href, c_href) -cdef Py_ssize_t _countSiblings(tree.xmlNode* c_start_node): +cdef Py_ssize_t _countSiblings(tree.xmlNode* c_start_node) noexcept: cdef tree.xmlNode* c_node cdef Py_ssize_t count c_tag = c_start_node.name @@ -410,10 +463,10 @@ cdef Py_ssize_t _countSiblings(tree.xmlNode* c_start_node): c_node = c_node.prev return count + cdef tree.xmlNode* _findFollowingSibling(tree.xmlNode* c_node, const_xmlChar* href, const_xmlChar* name, - Py_ssize_t index): - cdef tree.xmlNode* (*next)(tree.xmlNode*) + Py_ssize_t index) noexcept: if index >= 0: next = cetree.nextElement else: @@ -428,13 +481,17 @@ cdef tree.xmlNode* _findFollowingSibling(tree.xmlNode* c_node, c_node = next(c_node) return NULL + cdef object _lookupChild(_Element parent, tag): cdef tree.xmlNode* c_result cdef tree.xmlNode* c_node c_node = parent._c_node ns, tag = cetree.getNsTagWithEmptyNs(tag) + c_tag_len = len( tag) + if c_tag_len > limits.INT_MAX: + return None c_tag = tree.xmlDictExists( - c_node.doc.dict, _xcstr(tag), python.PyBytes_GET_SIZE(tag)) + c_node.doc.dict, _xcstr(tag), c_tag_len) if c_tag is NULL: return None # not in the hash map => not in the tree if ns is None: @@ -447,18 +504,21 @@ cdef object _lookupChild(_Element parent, tag): return None return elementFactory(parent._doc, c_result) + cdef object _lookupChildOrRaise(_Element parent, tag): element = _lookupChild(parent, tag) if element is None: - raise AttributeError, u"no such child: " + _buildChildTag(parent, tag) + raise AttributeError, "no such child: " + _buildChildTag(parent, tag) return element + cdef object _buildChildTag(_Element parent, tag): ns, tag = cetree.getNsTag(tag) c_tag = _xcstr(tag) c_href = tree._getNs(parent._c_node) if ns is None else _xcstr(ns) return cetree.namespacedNameFromNsName(c_href, c_tag) + cdef _replaceElement(_Element element, value): cdef _Element new_element if isinstance(value, _Element): @@ -474,6 +534,7 @@ cdef _replaceElement(_Element element, value): _setElementValue(new_element, value) element.getparent().replace(element, new_element) + cdef _appendValue(_Element parent, tag, value): cdef _Element new_element if isinstance(value, _Element): @@ -491,10 +552,11 @@ cdef _appendValue(_Element parent, tag, value): _setElementValue(new_element, value) cetree.appendChildToElement(parent, new_element) + cdef _setElementValue(_Element element, value): if value is None: cetree.setAttributeValue( - element, XML_SCHEMA_INSTANCE_NIL_ATTR, u"true") + element, XML_SCHEMA_INSTANCE_NIL_ATTR, "true") elif isinstance(value, _Element): _replaceElement(element, value) return @@ -502,10 +564,10 @@ cdef _setElementValue(_Element element, value): cetree.delAttributeFromNsName( element._c_node, _XML_SCHEMA_INSTANCE_NS, "nil") if python._isString(value): - pytype_name = u"str" + pytype_name = "str" py_type = _PYTYPE_DICT.get(pytype_name) else: - pytype_name = _typename(value) + pytype_name = python._typename(value) py_type = _PYTYPE_DICT.get(pytype_name) if py_type is not None: value = py_type.stringify(value) @@ -518,6 +580,7 @@ cdef _setElementValue(_Element element, value): element._c_node, _PYTYPE_NAMESPACE, _PYTYPE_ATTRIBUTE_NAME) cetree.setNodeText(element._c_node, value) + cdef _setSlice(sliceobject, _Element target, items): cdef _Element parent cdef tree.xmlNode* c_node @@ -528,7 +591,7 @@ cdef _setSlice(sliceobject, _Element target, items): else: c_step = (sliceobject).step if c_step == 0: - raise ValueError, u"Invalid slice" + raise ValueError, "Invalid slice" cdef list del_items = target[sliceobject] # collect new values @@ -589,35 +652,41 @@ cdef _setSlice(sliceobject, _Element target, items): add(item) pos += 1 + ################################################################################ # Data type support in subclasses cdef class ObjectifiedDataElement(ObjectifiedElement): - u"""This is the base class for all data type Elements. Subclasses should + """This is the base class for all data type Elements. Subclasses should override the 'pyval' property and possibly the __str__ method. """ @property def pyval(self): - return textOf(self._c_node) + return _lockedTextOf(self) def __str__(self): - return textOf(self._c_node) or '' + return _lockedTextOf(self) or '' def __repr__(self): - return strrepr(textOf(self._c_node) or '') + return _lockedTextOf(self) or '' def _setText(self, s): - u"""For use in subclasses only. Don't use unless you know what you are + """For use in subclasses only. Don't use unless you know what you are doing. """ - cetree.setNodeText(self._c_node, s) + doc = self._doc + cetree.lock_write(doc) + try: + cetree.setNodeText(self._c_node, s) + finally: + cetree.unlock_write(doc) cdef class NumberElement(ObjectifiedDataElement): cdef object _parse_value def _setValueParser(self, function): - u"""Set the function that parses the Python value from a string. + """Set the function that parses the Python value from a string. Do not use this unless you know what you are doing. """ @@ -630,9 +699,6 @@ cdef class NumberElement(ObjectifiedDataElement): def __int__(self): return int(_parseNumber(self)) - def __long__(self): - return long(_parseNumber(self)) - def __float__(self): return float(_parseNumber(self)) @@ -771,21 +837,13 @@ cdef class IntElement(NumberElement): return int(_parseNumber(self)) -cdef class LongElement(NumberElement): - def _init(self): - self._parse_value = long - - def __index__(self): - return int(_parseNumber(self)) - - cdef class FloatElement(NumberElement): def _init(self): self._parse_value = float cdef class StringElement(ObjectifiedDataElement): - u"""String data class. + """String data class. Note that this class does *not* support the sequence protocol of strings: len(), iter(), str_attr[0], str_attr[0:1], etc. are *not* supported. @@ -793,67 +851,61 @@ cdef class StringElement(ObjectifiedDataElement): """ @property def pyval(self): - return textOf(self._c_node) or u'' + return _lockedTextOf(self) or '' def __repr__(self): - return repr(textOf(self._c_node) or u'') + return repr(_lockedTextOf(self) or '') def strlen(self): - text = textOf(self._c_node) - if text is None: - return 0 - else: - return len(text) + text = _lockedTextOf(self) + return len(text) if text is not None else 0 def __bool__(self): - return bool(textOf(self._c_node)) + return bool(_lockedTextOf(self)) def __richcmp__(self, other, int op): return _richcmpPyvals(self, other, op) def __hash__(self): - return hash(textOf(self._c_node) or u'') + return hash(_lockedTextOf(self) or '') def __add__(self, other): - text = _strValueOf(self) + text = _lockedTextOf(self) or '' other = _strValueOf(other) return text + other def __radd__(self, other): - text = _strValueOf(self) + text = _lockedTextOf(self) or '' other = _strValueOf(other) return other + text def __mul__(self, other): if isinstance(self, StringElement): - return (textOf((self)._c_node) or '') * _numericValueOf(other) + return (_lockedTextOf( self) or '') * _numericValueOf(other) elif isinstance(other, StringElement): - return _numericValueOf(self) * (textOf((other)._c_node) or '') + return _numericValueOf(self) * (_lockedTextOf( other) or '') else: return NotImplemented def __rmul__(self, other): - return _numericValueOf(other) * (textOf((self)._c_node) or '') + return _numericValueOf(other) * (_lockedTextOf( self) or '') def __mod__(self, other): - return (_strValueOf(self) or '') % other + return (_lockedTextOf(self) or '') % other def __int__(self): - return int(textOf(self._c_node)) - - def __long__(self): - return long(textOf(self._c_node)) + return int(_lockedTextOf(self)) def __float__(self): - return float(textOf(self._c_node)) + return float(_lockedTextOf(self)) def __complex__(self): - return complex(textOf(self._c_node)) + return complex(_lockedTextOf(self)) cdef class NoneElement(ObjectifiedDataElement): def __str__(self): - return u"None" + return "None" def __repr__(self): return "None" @@ -878,7 +930,7 @@ cdef class NoneElement(ObjectifiedDataElement): cdef class BoolElement(IntElement): - u"""Boolean type base on string values: 'true' or 'false'. + """Boolean type base on string values: 'true' or 'false'. Note that this inherits from IntElement to mimic the behaviour of Python's bool type. @@ -887,29 +939,29 @@ cdef class BoolElement(IntElement): self._parse_value = _parseBool # wraps as Python callable def __bool__(self): - return _parseBool(textOf(self._c_node)) + return _parseBool(_lockedTextOf(self)) def __int__(self): - return 0 + _parseBool(textOf(self._c_node)) + return 0 + _parseBool(_lockedTextOf(self)) def __float__(self): - return 0.0 + _parseBool(textOf(self._c_node)) + return 0.0 + _parseBool(_lockedTextOf(self)) def __richcmp__(self, other, int op): return _richcmpPyvals(self, other, op) def __hash__(self): - return hash(_parseBool(textOf(self._c_node))) + return hash(_parseBool(_lockedTextOf(self))) def __str__(self): - return unicode(_parseBool(textOf(self._c_node))) + return unicode(_parseBool(_lockedTextOf(self))) def __repr__(self): - return repr(_parseBool(textOf(self._c_node))) + return repr(_parseBool(_lockedTextOf(self))) @property def pyval(self): - return _parseBool(textOf(self._c_node)) + return _parseBool(_lockedTextOf(self)) cdef _checkBool(s): @@ -943,7 +995,7 @@ cdef inline int __parseBoolAsInt(text) except -2: cdef object _parseNumber(NumberElement element): - return element._parse_value(textOf(element._c_node)) + return element._parse_value(_lockedTextOf(element)) cdef enum NumberParserState: @@ -976,7 +1028,7 @@ cdef _checkNumber(bytes_unicode s, bint allow_float): cdef NumberParserState state = NPS_SPACE_PRE for c in s: - if c.isdigit() if (bytes_unicode is unicode) else c in b'0123456789': + if c in '0123456789': if state in (NPS_DIGITS, NPS_FRACTION, NPS_DIGITS_EXP): pass elif state in (NPS_SPACE_PRE, NPS_SIGN): @@ -988,7 +1040,7 @@ cdef _checkNumber(bytes_unicode s, bint allow_float): else: state = NPS_ERROR else: - if c == u'.': + if c == '.': if state in (NPS_SPACE_PRE, NPS_SIGN): state = NPS_POINT_LEAD elif state == NPS_DIGITS: @@ -997,14 +1049,14 @@ cdef _checkNumber(bytes_unicode s, bint allow_float): state = NPS_ERROR if not allow_float: state = NPS_ERROR - elif c in u'-+': + elif c in '-+': if state == NPS_SPACE_PRE: state = NPS_SIGN elif state == NPS_EXP: state = NPS_EXP_SIGN else: state = NPS_ERROR - elif c == u'E': + elif c == 'E': if state in (NPS_DIGITS, NPS_POINT, NPS_FRACTION): state = NPS_EXP else: @@ -1012,13 +1064,13 @@ cdef _checkNumber(bytes_unicode s, bint allow_float): if not allow_float: state = NPS_ERROR # Allow INF and NaN. XMLSchema requires case, we don't, like Python. - elif c in u'iI': + elif c in 'iI': state = NPS_INF1 if allow_float and state in (NPS_SPACE_PRE, NPS_SIGN) else NPS_ERROR - elif c in u'fF': + elif c in 'fF': state = NPS_INF3 if state == NPS_INF2 else NPS_ERROR - elif c in u'aA': + elif c in 'aA': state = NPS_NAN2 if state == NPS_NAN1 else NPS_ERROR - elif c in u'nN': + elif c in 'nN': # Python also allows [+-]NaN, so let's accept that. if state in (NPS_SPACE_PRE, NPS_SIGN): state = NPS_NAN1 if allow_float else NPS_ERROR @@ -1048,26 +1100,29 @@ cdef _checkNumber(bytes_unicode s, bint allow_float): cdef _checkInt(s): - if python.IS_PYTHON2 and type(s) is bytes: - return _checkNumber(s, allow_float=False) - else: - return _checkNumber(s, allow_float=False) + return _checkNumber(s, allow_float=False) cdef _checkFloat(s): - if python.IS_PYTHON2 and type(s) is bytes: - return _checkNumber(s, allow_float=True) - else: - return _checkNumber(s, allow_float=True) + return _checkNumber(s, allow_float=True) + + +cdef str _lockedTextOf(_Element element): + doc = element._doc + cetree.lock_read(doc) + try: + return textOf(element._c_node) + finally: + cetree.unlock_read(doc) cdef object _strValueOf(obj): if python._isString(obj): return obj if isinstance(obj, _Element): - return textOf((<_Element>obj)._c_node) or u'' + return _lockedTextOf(<_Element> obj) or '' if obj is None: - return u'' + return '' return unicode(obj) @@ -1092,7 +1147,7 @@ cdef _richcmpPyvals(left, right, int op): # Python type registry cdef class PyType: - u"""PyType(self, name, type_check, type_class, stringify=None) + """PyType(self, name, type_check, type_class, stringify=None) User defined type. Named type that contains a type check function, a type class that @@ -1119,27 +1174,25 @@ cdef class PyType: def __init__(self, name, type_check, type_class, stringify=None): if isinstance(name, bytes): name = (name).decode('ascii') - elif not isinstance(name, unicode): - raise TypeError, u"Type name must be a string" + elif not isinstance(name, str): + raise TypeError, f"Type name must be a string, got {repr(name)}" if type_check is not None and not callable(type_check): - raise TypeError, u"Type check function must be callable (or None)" + raise TypeError, "Type check function must be callable (or None)" if name != TREE_PYTYPE_NAME and \ not issubclass(type_class, ObjectifiedDataElement): raise TypeError, \ - u"Data classes must inherit from ObjectifiedDataElement" + "Data classes must inherit from ObjectifiedDataElement" self.name = name self._type = type_class self.type_check = type_check - if stringify is None: - stringify = unicode - self.stringify = stringify + self.stringify = stringify if stringify is not None else str self._schema_types = [] def __repr__(self): - return "PyType(%s, %s)" % (self.name, self._type.__name__) + return f"PyType({self.name}, {self._type.__name__})" def register(self, before=None, after=None): - u"""register(self, before=None, after=None) + """register(self, before=None, after=None) Register the type. @@ -1149,128 +1202,144 @@ cdef class PyType: ignored. Raises ValueError if the dependencies cannot be fulfilled. """ if self.name == TREE_PYTYPE_NAME: - raise ValueError, u"Cannot register tree type" + raise ValueError, "Cannot register tree type" + + cdef PyType pytype if self.type_check is not None: - for item in _TYPE_CHECKS: - if item[0] is self.type_check: - _TYPE_CHECKS.remove(item) - break entry = (self.type_check, self) - first_pos = 0 - last_pos = -1 - if before or after: - if before is None: - before = () - elif after is None: - after = () - for i, (check, pytype) in enumerate(_TYPE_CHECKS): - if last_pos == -1 and pytype.name in before: - last_pos = i - if pytype.name in after: - first_pos = i+1 - if last_pos == -1: - _TYPE_CHECKS.append(entry) - elif first_pos > last_pos: - raise ValueError, u"inconsistent before/after dependencies" - else: - _TYPE_CHECKS.insert(last_pos, entry) + with cython.critical_section(_TYPE_CHECKS): + for item in _TYPE_CHECKS: + if item[0] is self.type_check: + _TYPE_CHECKS.remove(item) + break + first_pos = 0 + last_pos = -1 + if before or after: + if before is None: + before = () + elif after is None: + after = () + for i, (check, pytype) in enumerate(_TYPE_CHECKS): + if last_pos == -1 and pytype.name in before: + last_pos = i + if pytype.name in after: + first_pos = i+1 + if last_pos == -1: + _TYPE_CHECKS.append(entry) + elif first_pos > last_pos: + raise ValueError, "inconsistent before/after dependencies" + else: + _TYPE_CHECKS.insert(last_pos, entry) + + with cython.critical_section(_PYTYPE_DICT): + _PYTYPE_DICT[self.name] = self - _PYTYPE_DICT[self.name] = self - for xs_type in self._schema_types: - _SCHEMA_TYPE_DICT[xs_type] = self + with cython.critical_section(_SCHEMA_TYPE_DICT): + for xs_type in self._schema_types: + _SCHEMA_TYPE_DICT[xs_type] = self def unregister(self): - u"unregister(self)" - if _PYTYPE_DICT.get(self.name) is self: - del _PYTYPE_DICT[self.name] - for xs_type, pytype in list(_SCHEMA_TYPE_DICT.items()): - if pytype is self: - del _SCHEMA_TYPE_DICT[xs_type] - if self.type_check is None: - return - try: - _TYPE_CHECKS.remove( (self.type_check, self) ) - except ValueError: - pass + "unregister(self)" + with cython.critical_section(_PYTYPE_DICT): + if _PYTYPE_DICT.get(self.name) is self: + del _PYTYPE_DICT[self.name] + with cython.critical_section(_SCHEMA_TYPE_DICT): + for xs_type, pytype in list(_SCHEMA_TYPE_DICT.items()): + if pytype is self: + del _SCHEMA_TYPE_DICT[xs_type] + if self.type_check is not None: + try: + with cython.critical_section(_TYPE_CHECKS): + _TYPE_CHECKS.remove( (self.type_check, self) ) + except ValueError: + pass property xmlSchemaTypes: - u"""The list of XML Schema datatypes this Python type maps to. + """The list of XML Schema datatypes this Python type maps to. Note that this must be set before registering the type! """ def __get__(self): return self._schema_types def __set__(self, types): - self._schema_types = list(map(unicode, types)) + self._schema_types = list(map(str, types)) cdef dict _PYTYPE_DICT = {} cdef dict _SCHEMA_TYPE_DICT = {} cdef list _TYPE_CHECKS = [] -cdef unicode _xml_bool(value): - return u"true" if value else u"false" -cdef unicode _xml_float(value): +cdef str _xml_bool(value): + return "true" if value else "false" + + +cdef str _xml_float(value): if _float_is_inf(value): if value > 0: - return u"INF" - return u"-INF" + return "INF" + return "-INF" if _float_is_nan(value): - return u"NaN" - return unicode(repr(value)) + return "NaN" + return repr(value) + cdef _pytypename(obj): - return u"str" if python._isString(obj) else _typename(obj) + return "str" if python._isString(obj) else python._typename(obj) + def pytypename(obj): - u"""pytypename(obj) + """pytypename(obj) Find the name of the corresponding PyType for a Python object. """ return _pytypename(obj) + cdef _registerPyTypes(): - pytype = PyType(u'int', _checkInt, IntElement) # wraps functions for Python - pytype.xmlSchemaTypes = (u"integer", u"int", u"short", u"byte", u"unsignedShort", - u"unsignedByte", u"nonPositiveInteger", - u"negativeInteger", u"long", u"nonNegativeInteger", - u"unsignedLong", u"unsignedInt", u"positiveInteger",) + pytype = PyType('int', _checkInt, IntElement) # wraps functions for Python + pytype.xmlSchemaTypes = ("integer", "int", "short", "byte", "unsignedShort", + "unsignedByte", "nonPositiveInteger", + "negativeInteger", "long", "nonNegativeInteger", + "unsignedLong", "unsignedInt", "positiveInteger",) pytype.register() # 'long' type just for backwards compatibility - pytype = PyType(u'long', None, IntElement) + pytype = PyType('long', None, IntElement) pytype.register() - pytype = PyType(u'float', _checkFloat, FloatElement, _xml_float) # wraps functions for Python - pytype.xmlSchemaTypes = (u"double", u"float") + pytype = PyType('float', _checkFloat, FloatElement, _xml_float) # wraps functions for Python + pytype.xmlSchemaTypes = ("double", "float") pytype.register() - pytype = PyType(u'bool', _checkBool, BoolElement, _xml_bool) # wraps functions for Python - pytype.xmlSchemaTypes = (u"boolean",) + pytype = PyType('bool', _checkBool, BoolElement, _xml_bool) # wraps functions for Python + pytype.xmlSchemaTypes = ("boolean",) pytype.register() - pytype = PyType(u'str', None, StringElement) - pytype.xmlSchemaTypes = (u"string", u"normalizedString", u"token", u"language", - u"Name", u"NCName", u"ID", u"IDREF", u"ENTITY", - u"NMTOKEN", ) + pytype = PyType('str', None, StringElement) + pytype.xmlSchemaTypes = ("string", "normalizedString", "token", "language", + "Name", "NCName", "ID", "IDREF", "ENTITY", + "NMTOKEN", ) pytype.register() # since lxml 2.0 - pytype = PyType(u'NoneType', None, NoneElement) + pytype = PyType('NoneType', None, NoneElement) pytype.register() # backwards compatibility - pytype = PyType(u'none', None, NoneElement) + pytype = PyType('none', None, NoneElement) pytype.register() + # non-registered PyType for inner tree elements cdef PyType TREE_PYTYPE = PyType(TREE_PYTYPE_NAME, None, ObjectifiedElement) + _registerPyTypes() + def getRegisteredTypes(): - u"""getRegisteredTypes() + """getRegisteredTypes() Returns a list of the currently registered PyType objects. @@ -1284,8 +1353,8 @@ def getRegisteredTypes(): check functions, you can simply register() it, which will append it to the end of the type list. """ - cdef list types = [] - cdef set known = set() + types = [] + known = set() for check, pytype in _TYPE_CHECKS: name = pytype.name if name not in known: @@ -1298,6 +1367,7 @@ def getRegisteredTypes(): types.append(pytype) return types + cdef PyType _guessPyType(value, PyType defaulttype): if value is None: return None @@ -1310,13 +1380,14 @@ cdef PyType _guessPyType(value, PyType defaulttype): pass return defaulttype + cdef object _guessElementClass(tree.xmlNode* c_node): value = textOf(c_node) if value is None: return None if value == '': return StringElement - + for type_check, pytype in _TYPE_CHECKS: try: type_check(value) @@ -1325,6 +1396,7 @@ cdef object _guessElementClass(tree.xmlNode* c_node): pass return None + ################################################################################ # adapted ElementMaker supports registered PyTypes @@ -1337,7 +1409,7 @@ cdef class _ObjectifyElementMakerCaller: cdef bint _annotate def __call__(self, *children, **attrib): - u"__call__(self, *children, **attrib)" + "__call__(self, *children, **attrib)" cdef _ObjectifyElementMakerCaller elementMaker cdef _Element element cdef _Element childElement @@ -1351,59 +1423,70 @@ cdef class _ObjectifyElementMakerCaller: pytype_name = None has_children = False has_string_value = False + for child in children: if child is None: if len(children) == 1: cetree.setAttributeValue( - element, XML_SCHEMA_INSTANCE_NIL_ATTR, u"true") + element, XML_SCHEMA_INSTANCE_NIL_ATTR, "true") + elif python._isString(child): _add_text(element, child) has_string_value = True + elif isinstance(child, _Element): - cetree.appendChildToElement(element, <_Element>child) + childElement = <_Element> child + doc = childElement._doc + cetree.lock_write(doc) + try: + cetree.appendChildToElement(element, childElement) + finally: + cetree.unlock_write(doc) has_children = True + elif isinstance(child, _ObjectifyElementMakerCaller): - elementMaker = <_ObjectifyElementMakerCaller>child + elementMaker = <_ObjectifyElementMakerCaller> child if elementMaker._element_factory is None: - cetree.makeSubElement(element, elementMaker._tag, - None, None, None, None) + cetree.makeSubElement(element, elementMaker._tag, None, None, None, None) else: - childElement = elementMaker._element_factory( - elementMaker._tag) + childElement = elementMaker._element_factory(elementMaker._tag) cetree.appendChildToElement(element, childElement) has_children = True + elif isinstance(child, dict): for name, value in child.items(): # keyword arguments in attrib take precedence if name in attrib: continue - pytype = _PYTYPE_DICT.get(_typename(value)) + pytype = _PYTYPE_DICT.get(python._typename(value)) if pytype is not None: value = (pytype).stringify(value) elif not python._isString(value): - value = unicode(value) + value = str(value) cetree.setAttributeValue(element, name, value) + else: if pytype_name is not None: # concatenation always makes the result a string has_string_value = True - pytype_name = _typename(child) - pytype = _PYTYPE_DICT.get(_typename(child)) + pytype_name = python._typename(child) + pytype = _PYTYPE_DICT.get(python._typename(child)) if pytype is not None: _add_text(element, (pytype).stringify(child)) else: has_string_value = True - child = unicode(child) + child = str(child) _add_text(element, child) if self._annotate and not has_children: if has_string_value: - cetree.setAttributeValue(element, PYTYPE_ATTRIBUTE, u"str") + cetree.setAttributeValue(element, PYTYPE_ATTRIBUTE, "str") elif pytype_name is not None: cetree.setAttributeValue(element, PYTYPE_ATTRIBUTE, pytype_name) return element + cdef _add_text(_Element elem, text): # add text to the tree in construction, either as element text or # tail text, depending on the current tree state @@ -1420,8 +1503,9 @@ cdef _add_text(_Element elem, text): text = old + text cetree.setNodeText(elem._c_node, text) + cdef class ElementMaker: - u"""ElementMaker(self, namespace=None, nsmap=None, annotate=True, makeelement=None) + """ElementMaker(self, namespace=None, nsmap=None, annotate=True, makeelement=None) An ElementMaker that can be used for constructing trees. @@ -1451,12 +1535,13 @@ cdef class ElementMaker: cdef object _nsmap cdef bint _annotate cdef dict _cache + def __init__(self, *, namespace=None, nsmap=None, annotate=True, makeelement=None): if nsmap is None: nsmap = _DEFAULT_NSMAP if annotate else {} self._nsmap = nsmap - self._namespace = None if namespace is None else u"{%s}" % namespace + self._namespace = None if namespace is None else "{%s}" % namespace self._annotate = annotate if makeelement is not None: if not callable(makeelement): @@ -1471,7 +1556,7 @@ cdef class ElementMaker: cdef _build_element_maker(self, tag, bint caching): cdef _ObjectifyElementMakerCaller element_maker element_maker = _ObjectifyElementMakerCaller.__new__(_ObjectifyElementMakerCaller) - if self._namespace is not None and tag[0] != u"{": + if self._namespace is not None and tag[0] != "{": element_maker._tag = self._namespace + tag else: element_maker._tag = tag @@ -1487,8 +1572,6 @@ cdef class ElementMaker: def __getattr__(self, tag): element_maker = self._cache.get(tag) if element_maker is None: - if is_special_method(tag): - return object.__getattr__(self, tag) return self._build_element_maker(tag, caching=True) return element_maker @@ -1504,8 +1587,9 @@ cdef class ElementMaker: cdef bint __RECURSIVE_STR = 0 # default: off + def enable_recursive_str(on=True): - u"""enable_recursive_str(on=True) + """enable_recursive_str(on=True) Enable a recursively generated tree representation for str(element), based on objectify.dump(element). @@ -1513,15 +1597,22 @@ def enable_recursive_str(on=True): global __RECURSIVE_STR __RECURSIVE_STR = on + def dump(_Element element not None): - u"""dump(_Element element not None) + """dump(_Element element not None) Return a recursively generated string representation of an element. """ - return _dump(element, 0) + doc = element._doc + cetree.lock_read(doc) + try: + return _dump(element, 0) + finally: + cetree.unlock_read(doc) + cdef object _dump(_Element element, int indent): - indentstr = u" " * indent + indentstr = " " * indent if isinstance(element, ObjectifiedDataElement): value = repr(element) else: @@ -1531,17 +1622,17 @@ cdef object _dump(_Element element, int indent): value = None else: value = repr(value) - result = f"{indentstr}{element.tag} = {value} [{_typename(element)}]\n" - xsi_ns = u"{%s}" % XML_SCHEMA_INSTANCE_NS - pytype_ns = u"{%s}" % PYTYPE_NAMESPACE + result = f"{indentstr}{element.tag} = {value} [{python._typename(element)}]\n" + xsi_ns = "{%s}" % XML_SCHEMA_INSTANCE_NS + pytype_ns = "{%s}" % PYTYPE_NAMESPACE for name, value in sorted(cetree.iterattributes(element, 3)): - if u'{' in name: + if '{' in name: if name == PYTYPE_ATTRIBUTE: if value == TREE_PYTYPE_NAME: continue else: - name = name.replace(pytype_ns, u'py:') - name = name.replace(xsi_ns, u'xsi:') + name = name.replace(pytype_ns, 'py:') + name = name.replace(xsi_ns, 'xsi:') result += f"{indentstr} * {name} = {value!r}\n" indent += 1 @@ -1560,10 +1651,7 @@ def __unpickleElementTree(data): return etree.ElementTree(fromstring(data)) cdef _setupPickle(elementTreeReduceFunction): - if python.IS_PYTHON2: - import copy_reg as copyreg - else: - import copyreg + import copyreg copyreg.pickle(etree._ElementTree, elementTreeReduceFunction, __unpickleElementTree) @@ -1571,19 +1659,22 @@ def pickleReduceElementTree(obj): return __unpickleElementTree, (etree.tostring(obj),) _setupPickle(pickleReduceElementTree) + del pickleReduceElementTree + ################################################################################ # Element class lookup cdef class ObjectifyElementClassLookup(ElementClassLookup): - u"""ObjectifyElementClassLookup(self, tree_class=None, empty_data_class=None) + """ObjectifyElementClassLookup(self, tree_class=None, empty_data_class=None) Element class lookup method that uses the objectify classes. """ cdef object empty_data_class cdef object tree_class + def __init__(self, tree_class=None, empty_data_class=None): - u"""Lookup mechanism for objectify. + """Lookup mechanism for objectify. The default Element classes can be replaced by passing subclasses of ObjectifiedElement and ObjectifiedDataElement as keyword arguments. @@ -1599,15 +1690,16 @@ cdef class ObjectifyElementClassLookup(ElementClassLookup): empty_data_class = StringElement self.empty_data_class = empty_data_class + cdef object _lookupElementClass(state, _Document doc, tree.xmlNode* c_node): - cdef ObjectifyElementClassLookup lookup - lookup = state + cdef ObjectifyElementClassLookup lookup = state + # if element has children => no data class if cetree.hasChild(c_node): return lookup.tree_class # if element is defined as xsi:nil, return NoneElement class - if u"true" == cetree.attributeValueFromNsName( + if "true" == cetree.attributeValueFromNsName( c_node, _XML_SCHEMA_INSTANCE_NS, "nil"): return NoneElement @@ -1628,8 +1720,8 @@ cdef object _lookupElementClass(state, _Document doc, tree.xmlNode* c_node): if value is not None: schema_type = _SCHEMA_TYPE_DICT.get(value) - if schema_type is None and u':' in value: - prefix, value = value.split(u':', 1) + if schema_type is None and ':' in value: + prefix, value = value.split(':', 1) schema_type = _SCHEMA_TYPE_DICT.get(value) if schema_type is not None: return schema_type._type @@ -1661,9 +1753,10 @@ cdef PyType _check_type(tree.xmlNode* c_node, PyType pytype): pass return None + def pyannotate(element_or_tree, *, ignore_old=False, ignore_xsi=False, empty_pytype=None): - u"""pyannotate(element_or_tree, ignore_old=False, ignore_xsi=False, empty_pytype=None) + """pyannotate(element_or_tree, ignore_old=False, ignore_xsi=False, empty_pytype=None) Recursively annotates the elements of an XML tree with 'pytype' attributes. @@ -1680,13 +1773,18 @@ def pyannotate(element_or_tree, *, ignore_old=False, ignore_xsi=False, ``empty_pytype`` keyword argument. The default is not to annotate empty elements. Pass 'str', for example, to make string values the default. """ - cdef _Element element element = cetree.rootNodeOrRaise(element_or_tree) - _annotate(element, 0, 1, ignore_xsi, ignore_old, None, empty_pytype) + doc = element._doc + cetree.lock_write(doc) + try: + _annotate(element, 0, 1, ignore_xsi, ignore_old, None, empty_pytype) + finally: + cetree.unlock_write(doc) + def xsiannotate(element_or_tree, *, ignore_old=False, ignore_pytype=False, empty_type=None): - u"""xsiannotate(element_or_tree, ignore_old=False, ignore_pytype=False, empty_type=None) + """xsiannotate(element_or_tree, ignore_old=False, ignore_pytype=False, empty_type=None) Recursively annotates the elements of an XML tree with 'xsi:type' attributes. @@ -1708,14 +1806,19 @@ def xsiannotate(element_or_tree, *, ignore_old=False, ignore_pytype=False, ``empty_type`` keyword argument. The default is not to annotate empty elements. Pass 'string', for example, to make string values the default. """ - cdef _Element element element = cetree.rootNodeOrRaise(element_or_tree) - _annotate(element, 1, 0, ignore_old, ignore_pytype, empty_type, None) + doc = element._doc + cetree.lock_write(doc) + try: + _annotate(element, 1, 0, ignore_old, ignore_pytype, empty_type, None) + finally: + cetree.unlock_write(doc) + def annotate(element_or_tree, *, ignore_old=True, ignore_xsi=False, empty_pytype=None, empty_type=None, annotate_xsi=0, annotate_pytype=1): - u"""annotate(element_or_tree, ignore_old=True, ignore_xsi=False, empty_pytype=None, empty_type=None, annotate_xsi=0, annotate_pytype=1) + """annotate(element_or_tree, ignore_old=True, ignore_xsi=False, empty_pytype=None, empty_type=None, annotate_xsi=0, annotate_pytype=1) Recursively annotates the elements of an XML tree with 'xsi:type' and/or 'py:pytype' attributes. @@ -1727,8 +1830,8 @@ def annotate(element_or_tree, *, ignore_old=True, ignore_xsi=False, If the 'ignore_xsi' keyword argument is False (the default), existing 'xsi:type' attributes will be used for the type annotation, if they fit the - element text values. - + element text values. + Note that the mapping from Python types to XSI types is usually ambiguous. Currently, only the first XSI type name in the corresponding PyType definition will be used for annotation. Thus, you should consider naming @@ -1743,12 +1846,16 @@ def annotate(element_or_tree, *, ignore_old=True, ignore_xsi=False, elements. Pass 'string', for example, to make string values the default. The keyword arguments 'annotate_xsi' (default: 0) and 'annotate_pytype' - (default: 1) control which kind(s) of annotation to use. + (default: 1) control which kind(s) of annotation to use. """ - cdef _Element element element = cetree.rootNodeOrRaise(element_or_tree) - _annotate(element, annotate_xsi, annotate_pytype, ignore_xsi, - ignore_old, empty_type, empty_pytype) + doc = element._doc + cetree.lock_write(doc) + try: + _annotate(element, annotate_xsi, annotate_pytype, ignore_xsi, + ignore_old, empty_type, empty_pytype) + finally: + cetree.unlock_write(doc) cdef _annotate(_Element element, bint annotate_xsi, bint annotate_pytype, @@ -1772,8 +1879,8 @@ cdef _annotate(_Element element, bint annotate_xsi, bint annotate_pytype, else: empty_pytype = None - StrType = _PYTYPE_DICT.get(u'str') - NoneType = _PYTYPE_DICT.get(u'NoneType') + StrType = _PYTYPE_DICT.get('str') + NoneType = _PYTYPE_DICT.get('NoneType') doc = element._doc c_node = element._c_node @@ -1784,6 +1891,7 @@ cdef _annotate(_Element element, bint annotate_xsi, bint annotate_pytype, empty_type_name, empty_pytype, StrType, NoneType) tree.END_FOR_EACH_ELEMENT_FROM(c_node) + cdef int _annotate_element(tree.xmlNode* c_node, _Document doc, bint annotate_xsi, bint annotate_pytype, bint ignore_xsi, bint ignore_pytype, @@ -1805,8 +1913,8 @@ cdef int _annotate_element(tree.xmlNode* c_node, _Document doc, c_node, _XML_SCHEMA_INSTANCE_NS, "type") if typename is not None: pytype = _SCHEMA_TYPE_DICT.get(typename) - if pytype is None and u':' in typename: - prefix, typename = typename.split(u':', 1) + if pytype is None and ':' in typename: + prefix, typename = typename.split(':', 1) pytype = _SCHEMA_TYPE_DICT.get(typename) if pytype is not None and pytype is not StrType: # StrType does not have a typecheck but is the default @@ -1878,7 +1986,7 @@ cdef int _annotate_element(tree.xmlNode* c_node, _Document doc, prefix, name = typename_utf8.split(b':', 1) if c_ns.prefix is NULL or c_ns.prefix[0] == c'\0': typename_utf8 = name - elif tree.xmlStrcmp(_xcstr(prefix), c_ns.prefix) != 0: + elif not tree.xmlStrEqual(_xcstr(prefix), c_ns.prefix): typename_utf8 = (c_ns.prefix) + b':' + name elif c_ns.prefix is not NULL and c_ns.prefix[0] != c'\0': typename_utf8 = (c_ns.prefix) + b':' + typename_utf8 @@ -1905,18 +2013,20 @@ cdef int _annotate_element(tree.xmlNode* c_node, _Document doc, return 0 + cdef object _strip_attributes = etree.strip_attributes cdef object _cleanup_namespaces = etree.cleanup_namespaces + def deannotate(element_or_tree, *, bint pytype=True, bint xsi=True, bint xsi_nil=False, bint cleanup_namespaces=False): - u"""deannotate(element_or_tree, pytype=True, xsi=True, xsi_nil=False, cleanup_namespaces=False) + """deannotate(element_or_tree, pytype=True, xsi=True, xsi_nil=False, cleanup_namespaces=False) Recursively de-annotate the elements of an XML tree by removing 'py:pytype' and/or 'xsi:type' attributes and/or 'xsi:nil' attributes. If the 'pytype' keyword argument is True (the default), 'py:pytype' - attributes will be removed. If the 'xsi' keyword argument is True (the + attributes will be removed. If the 'xsi' keyword argument is True (the default), 'xsi:type' attributes will be removed. If the 'xsi_nil' keyword argument is True (default: False), 'xsi:nil' attributes will be removed. @@ -1938,18 +2048,18 @@ def deannotate(element_or_tree, *, bint pytype=True, bint xsi=True, if cleanup_namespaces: _cleanup_namespaces(element_or_tree) + ################################################################################ # Module level parser setup -cdef object __DEFAULT_PARSER -__DEFAULT_PARSER = etree.XMLParser(remove_blank_text=True) +cdef object __DEFAULT_PARSER = etree.XMLParser(remove_blank_text=True) __DEFAULT_PARSER.set_element_class_lookup( ObjectifyElementClassLookup() ) -cdef object objectify_parser -objectify_parser = __DEFAULT_PARSER +cdef object objectify_parser = __DEFAULT_PARSER + def set_default_parser(new_parser = None): - u"""set_default_parser(new_parser = None) + """set_default_parser(new_parser = None) Replace the default parser used by objectify's Element() and fromstring() functions. @@ -1958,16 +2068,23 @@ def set_default_parser(new_parser = None): Call without arguments to reset to the original parser. """ - global objectify_parser if new_parser is None: - objectify_parser = __DEFAULT_PARSER - elif isinstance(new_parser, etree.XMLParser): + new_parser = __DEFAULT_PARSER + elif not isinstance(new_parser, etree.XMLParser): + raise TypeError, "parser must inherit from lxml.etree.XMLParser" + + global objectify_parser + with cython.critical_section(__DEFAULT_PARSER): objectify_parser = new_parser - else: - raise TypeError, u"parser must inherit from lxml.etree.XMLParser" + + +cdef _get_default_parser(): + with cython.critical_section(__DEFAULT_PARSER): + return objectify_parser + def makeparser(**kw): - u"""makeparser(remove_blank_text=True, **kw) + """makeparser(remove_blank_text=True, **kw) Create a new XML parser for objectify trees. @@ -1982,19 +2099,21 @@ def makeparser(**kw): parser.set_element_class_lookup( ObjectifyElementClassLookup() ) return parser + cdef _Element _makeElement(tag, text, attrib, nsmap): - return cetree.makeElement(tag, None, objectify_parser, text, None, attrib, nsmap) + return cetree.makeElement(tag, None, _get_default_parser(), text, None, attrib, nsmap) + ################################################################################ # Module level factory functions -cdef object _fromstring -_fromstring = etree.fromstring +cdef object _fromstring = etree.fromstring SubElement = etree.SubElement + def fromstring(xml, parser=None, *, base_url=None): - u"""fromstring(xml, parser=None, base_url=None) + """fromstring(xml, parser=None, base_url=None) Objectify specific version of the lxml.etree fromstring() function that uses the objectify parser. @@ -2006,11 +2125,12 @@ def fromstring(xml, parser=None, *, base_url=None): (DTD, XInclude, ...). """ if parser is None: - parser = objectify_parser + parser = _get_default_parser() return _fromstring(xml, parser, base_url=base_url) + def XML(xml, parser=None, *, base_url=None): - u"""XML(xml, parser=None, base_url=None) + """XML(xml, parser=None, base_url=None) Objectify specific version of the lxml.etree XML() literal factory that uses the objectify parser. @@ -2022,14 +2142,15 @@ def XML(xml, parser=None, *, base_url=None): (DTD, XInclude, ...). """ if parser is None: - parser = objectify_parser + parser = _get_default_parser() return _fromstring(xml, parser, base_url=base_url) -cdef object _parse -_parse = etree.parse + +cdef object _parse = etree.parse + def parse(f, parser=None, *, base_url=None): - u"""parse(f, parser=None, base_url=None) + """parse(f, parser=None, base_url=None) Parse a file or file-like object with the objectify parser. @@ -2040,19 +2161,22 @@ def parse(f, parser=None, *, base_url=None): up external entities (DTD, XInclude, ...) with relative paths. """ if parser is None: - parser = objectify_parser + parser = _get_default_parser() return _parse(f, parser, base_url=base_url) + cdef dict _DEFAULT_NSMAP = { "py" : PYTYPE_NAMESPACE, "xsi" : XML_SCHEMA_INSTANCE_NS, "xsd" : XML_SCHEMA_NS } + E = ElementMaker() + def Element(_tag, attrib=None, nsmap=None, *, _pytype=None, **_attributes): - u"""Element(_tag, attrib=None, nsmap=None, _pytype=None, **_attributes) + """Element(_tag, attrib=None, nsmap=None, _pytype=None, **_attributes) Objectify specific version of the lxml.etree Element() factory that always creates a structural (tree) element. @@ -2071,9 +2195,10 @@ def Element(_tag, attrib=None, nsmap=None, *, _pytype=None, **_attributes): _attributes[PYTYPE_ATTRIBUTE] = _pytype return _makeElement(_tag, None, _attributes, nsmap) + def DataElement(_value, attrib=None, nsmap=None, *, _pytype=None, _xsi=None, **_attributes): - u"""DataElement(_value, attrib=None, nsmap=None, _pytype=None, _xsi=None, **_attributes) + """DataElement(_value, attrib=None, nsmap=None, _pytype=None, _xsi=None, **_attributes) Create a new element from a Python value and XML attributes taken from keyword arguments or a dictionary passed as second argument. @@ -2118,23 +2243,23 @@ def DataElement(_value, attrib=None, nsmap=None, *, _pytype=None, _xsi=None, _pytype = _attributes.get(PYTYPE_ATTRIBUTE) if _xsi is not None: - if u':' in _xsi: - prefix, name = _xsi.split(u':', 1) + if ':' in _xsi: + prefix, name = _xsi.split(':', 1) ns = nsmap.get(prefix) if ns != XML_SCHEMA_NS: - raise ValueError, u"XSD types require the XSD namespace" + raise ValueError, "XSD types require the XSD namespace" elif nsmap is _DEFAULT_NSMAP: name = _xsi - _xsi = u'xsd:' + _xsi + _xsi = 'xsd:' + _xsi else: name = _xsi for prefix, ns in nsmap.items(): if ns == XML_SCHEMA_NS: if prefix is not None and prefix: - _xsi = prefix + u':' + _xsi + _xsi = prefix + ':' + _xsi break else: - raise ValueError, u"XSD types require the XSD namespace" + raise ValueError, "XSD types require the XSD namespace" _attributes[XML_SCHEMA_INSTANCE_TYPE_ATTR] = _xsi if _pytype is None: # allow using unregistered or even wrong xsi:type names @@ -2147,25 +2272,25 @@ def DataElement(_value, attrib=None, nsmap=None, *, _pytype=None, _xsi=None, if _pytype is None: _pytype = _pytypename(_value) - if _value is None and _pytype != u"str": - _pytype = _pytype or u"NoneType" + if _value is None and _pytype != "str": + _pytype = _pytype or "NoneType" strval = None elif python._isString(_value): strval = _value elif isinstance(_value, bool): if _value: - strval = u"true" + strval = "true" else: - strval = u"false" + strval = "false" else: py_type = _PYTYPE_DICT.get(_pytype) stringify = unicode if py_type is None else py_type.stringify strval = stringify(_value) - if _pytype is not None: - if _pytype == u"NoneType" or _pytype == u"none": + if _pytype is not None: + if _pytype == "NoneType" or _pytype == "none": strval = None - _attributes[XML_SCHEMA_INSTANCE_NIL_ATTR] = u"true" + _attributes[XML_SCHEMA_INSTANCE_NIL_ATTR] = "true" else: # check if type information from arguments is valid py_type = _PYTYPE_DICT.get(_pytype) @@ -2174,7 +2299,7 @@ def DataElement(_value, attrib=None, nsmap=None, *, _pytype=None, _xsi=None, py_type.type_check(strval) _attributes[PYTYPE_ATTRIBUTE] = _pytype - return _makeElement(u"value", strval, _attributes, nsmap) + return _makeElement("value", strval, _attributes, nsmap) ################################################################################ diff --git a/src/lxml/objectpath.pxi b/src/lxml/objectpath.pxi index 2e8d19227..6b0d41f25 100644 --- a/src/lxml/objectpath.pxi +++ b/src/lxml/objectpath.pxi @@ -11,7 +11,7 @@ cdef object _NO_DEFAULT = object() cdef class ObjectPath: - u"""ObjectPath(path) + """ObjectPath(path) Immutable object that represents a compiled object path. Example for a path: 'root.child[1].{other}child[25]' @@ -21,13 +21,17 @@ cdef class ObjectPath: cdef object _path_str cdef _ObjectPath* _c_path cdef Py_ssize_t _path_len + def __init__(self, path): + if self._c_path is not NULL: + raise RuntimeError("Repeated call to ObjectPath.__init__()") + if python._isString(path): self._path = _parse_object_path_string(path) self._path_str = path else: self._path = _parse_object_path_list(path) - self._path_str = u'.'.join(path) + self._path_str = '.'.join(path) self._path_len = len(self._path) self._c_path = _build_object_path_segments(self._path) self.find = self.__call__ @@ -40,7 +44,7 @@ cdef class ObjectPath: return self._path_str def __call__(self, _Element root not None, *_default): - u"""Follow the attribute path in the object structure and return the + """Follow the attribute path in the object structure and return the target attribute value. If it it not found, either returns a default value (if one was passed @@ -48,48 +52,69 @@ cdef class ObjectPath: """ if _default: if len(_default) > 1: - raise TypeError, u"invalid number of arguments: needs one or two" + raise TypeError, "invalid number of arguments: needs one or two" default = _default[0] else: default = _NO_DEFAULT - return _find_object_path(root, self._c_path, self._path_len, default) + + doc = root._doc + cetree.lock_read(doc) + try: + return _find_object_path(root, self._c_path, self._path_len, default) + finally: + cetree.unlock_read(doc) def hasattr(self, _Element root not None): - u"hasattr(self, root)" + "hasattr(self, root)" + doc = root._doc + cetree.lock_read(doc) try: _find_object_path(root, self._c_path, self._path_len, _NO_DEFAULT) except AttributeError: return False + finally: + cetree.unlock_read(doc) + return True def setattr(self, _Element root not None, value): - u"""setattr(self, root, value) + """setattr(self, root, value) Set the value of the target element in a subtree. If any of the children on the path does not exist, it is created. """ - _create_object_path(root, self._c_path, self._path_len, 1, value) + doc = root._doc + cetree.lock_write(doc) + try: + _create_object_path(root, self._c_path, self._path_len, 1, value) + finally: + cetree.unlock_write(doc) def addattr(self, _Element root not None, value): - u"""addattr(self, root, value) + """addattr(self, root, value) Append a value to the target element in a subtree. If any of the children on the path does not exist, it is created. """ - _create_object_path(root, self._c_path, self._path_len, 0, value) + doc = root._doc + cetree.lock_write(doc) + try: + _create_object_path(root, self._c_path, self._path_len, 0, value) + finally: + cetree.unlock_write(doc) cdef object __MATCH_PATH_SEGMENT = re.compile( - ur"(\.?)\s*(?:\{([^}]*)\})?\s*([^.{}\[\]\s]+)\s*(?:\[\s*([-0-9]+)\s*\])?", + r"(\.?)\s*(?:\{([^}]*)\})?\s*([^.{}\[\]\s]+)\s*(?:\[\s*([-0-9]+)\s*\])?", re.U).match cdef tuple _RELATIVE_PATH_SEGMENT = (None, None, 0) cdef list _parse_object_path_string(_path): - u"""Parse object path string into a (ns, name, index) list. + """Parse object path string into a (ns, name, index) list. """ cdef bint has_dot cdef unicode path @@ -101,7 +126,7 @@ cdef list _parse_object_path_string(_path): else: path = _path path = path.strip() - if path == u'.': + if path == '.': return [_RELATIVE_PATH_SEGMENT] path_pos = 0 while path: @@ -111,15 +136,15 @@ cdef list _parse_object_path_string(_path): dot, ns, name, index = match.groups() index = int(index) if index else 0 - has_dot = dot == u'.' + has_dot = dot == '.' if not new_path: if has_dot: # path '.child' => ignore root new_path.append(_RELATIVE_PATH_SEGMENT) elif index: - raise ValueError, u"index not allowed on root node" + raise ValueError, "index not allowed on root node" elif not has_dot: - raise ValueError, u"invalid path" + raise ValueError, "invalid path" if ns is not None: ns = python.PyUnicode_AsUTF8String(ns) name = python.PyUnicode_AsUTF8String(name) @@ -127,37 +152,37 @@ cdef list _parse_object_path_string(_path): path_pos = match.end() if not new_path or len(path) > path_pos: - raise ValueError, u"invalid path" + raise ValueError, "invalid path" return new_path cdef list _parse_object_path_list(path): - u"""Parse object path sequence into a (ns, name, index) list. + """Parse object path sequence into a (ns, name, index) list. """ new_path = [] for item in path: item = item.strip() - if not new_path and item == u'': + if not new_path and item == '': # path '.child' => ignore root ns = name = None index = 0 else: ns, name = cetree.getNsTag(item) - c_name = _xcstr(name) - index_pos = tree.xmlStrchr(c_name, c'[') + c_name = python._cstr(name) + index_pos = cstring_h.strchr(c_name, c'[') if index_pos is NULL: index = 0 else: - index_end = tree.xmlStrchr(index_pos + 1, c']') + index_end = cstring_h.strchr(index_pos + 1, c']') if index_end is NULL: - raise ValueError, u"index must be enclosed in []" + raise ValueError, "index must be enclosed in []" index = int(index_pos[1:index_end - index_pos]) if not new_path and index != 0: - raise ValueError, u"index not allowed on root node" + raise ValueError, "index not allowed on root node" name = c_name[:index_pos - c_name] new_path.append( (ns, name, index) ) if not new_path: - raise ValueError, u"invalid path" + raise ValueError, "invalid path" return new_path @@ -177,7 +202,7 @@ cdef _ObjectPath* _build_object_path_segments(list path_list) except NULL: cdef _find_object_path(_Element root, _ObjectPath* c_path, Py_ssize_t c_path_len, default_value): - u"""Follow the path to find the target element. + """Follow the path to find the target element. """ cdef tree.xmlNode* c_node cdef Py_ssize_t c_index @@ -221,7 +246,7 @@ cdef _find_object_path(_Element root, _ObjectPath* c_path, Py_ssize_t c_path_len cdef _create_object_path(_Element root, _ObjectPath* c_path, Py_ssize_t c_path_len, int replace, value): - u"""Follow the path to find the target element, build the missing children + """Follow the path to find the target element, build the missing children as needed and set the target element to 'value'. If replace is true, an existing value is replaced, otherwise the new value is added. """ @@ -230,7 +255,7 @@ cdef _create_object_path(_Element root, _ObjectPath* c_path, cdef tree.xmlNode* c_child cdef Py_ssize_t c_index if c_path_len == 1: - raise TypeError, u"cannot update root node" + raise TypeError, "cannot update root node" c_node = root._c_node c_name = c_path[0].name @@ -258,7 +283,7 @@ cdef _create_object_path(_Element root, _ObjectPath* c_path, if c_child is not NULL: c_node = c_child elif c_index != 0: - raise TypeError, u"creating indexed path attributes is not supported" + raise TypeError, "creating indexed path attributes is not supported" elif c_path_len == 1: _appendValue(cetree.elementFactory(root._doc, c_node), cetree.namespacedNameFromNsName(c_href, c_name), @@ -281,13 +306,13 @@ cdef _create_object_path(_Element root, _ObjectPath* c_path, cdef list _build_descendant_paths(tree.xmlNode* c_node, prefix_string): - u"""Returns a list of all descendant paths. + """Returns a list of all descendant paths. """ cdef list path, path_list tag = cetree.namespacedName(c_node) if prefix_string: - if prefix_string[-1] != u'.': - prefix_string += u'.' + if prefix_string[-1] != '.': + prefix_string += '.' prefix_string = prefix_string + tag else: prefix_string = tag @@ -299,12 +324,12 @@ cdef list _build_descendant_paths(tree.xmlNode* c_node, prefix_string): cdef int _recursive_build_descendant_paths(tree.xmlNode* c_node, list path, list path_list) except -1: - u"""Fills the list 'path_list' with all descendant paths, initial prefix + """Fills the list 'path_list' with all descendant paths, initial prefix being in the list 'path'. """ cdef tree.xmlNode* c_child tags = {} - path_list.append(u'.'.join(path)) + path_list.append('.'.join(path)) c_href = tree._getNs(c_node) c_child = c_node.children while c_child is not NULL: @@ -316,7 +341,7 @@ cdef int _recursive_build_descendant_paths(tree.xmlNode* c_node, tag = pyunicode(c_child.name) elif c_href is not NULL and tree._getNs(c_child) is NULL: # special case: parent has namespace, child does not - tag = u'{}' + pyunicode(c_child.name) + tag = '{}' + pyunicode(c_child.name) else: tag = cetree.namespacedName(c_child) count = tags.get(tag) diff --git a/src/lxml/parser.pxi b/src/lxml/parser.pxi index f5baf29b9..589cc71f3 100644 --- a/src/lxml/parser.pxi +++ b/src/lxml/parser.pxi @@ -3,6 +3,14 @@ from lxml.includes cimport xmlparser from lxml.includes cimport htmlparser +cdef object _GenericAlias +try: + from types import GenericAlias as _GenericAlias +except ImportError: + # Python 3.8 - we only need this as return value from "__class_getitem__" + def _GenericAlias(cls, item): + return f"{cls.__name__}[{item.__name__}]" + class ParseError(LxmlSyntaxError): """Syntax error while parsing an XML document. @@ -31,11 +39,63 @@ class XMLSyntaxError(ParseError): """Syntax error while parsing an XML document. """ -cdef class ParserError(LxmlError): +class ParserError(LxmlError): """Internal lxml parser error. """ +@cython.final +@cython.internal +cdef class _ParserDictionary: + # The string dictionary of a parser, shared by all of its parsed documents. + + cdef tree.xmlDict* _c_dict + + def __cinit__(self): + self._c_dict = tree.xmlDictCreate() + if not self._c_dict: + raise MemoryError() + + def __dealloc__(self): + tree.xmlDictFree(self._c_dict) + self._c_dict = NULL + + cdef void disableSizeLimit(self) noexcept: + tree.xmlDictSetLimit(self._c_dict, 0) + + cdef tree.xmlDict *getDict(self) noexcept: + return self._c_dict + + cdef tree.xmlDict *getDictRef(self) noexcept: + c_dict = self._c_dict + tree.xmlDictReference(c_dict) + return c_dict + + cdef size_t getDictSize(self) noexcept: + return tree.xmlDictSize(self._c_dict) + + cdef void initDictRef(self, tree.xmlDict** c_dict_ref) noexcept: + c_dict = c_dict_ref[0] + if c_dict is self._c_dict: + return + + c_dict_ref[0] = self.getDictRef() + if c_dict is not NULL: + tree.xmlDictFree(c_dict) + + cdef void initParserDict(self, xmlparser.xmlParserCtxt* pctxt) noexcept: + "Assure we always use the same string dictionary." + self.initDictRef(&pctxt.dict) + pctxt.dictNames = 1 + + #cdef void initXPathParserDict(self, xpath.xmlXPathContext* pctxt) noexcept: + # "Assure we always use the same string dictionary." + # self.initDictRef(&pctxt.dict) + + cdef void initDocDict(self, xmlDoc *c_doc) noexcept: + self.initDictRef(&c_doc.dict) + + @cython.final @cython.internal cdef class _ParserDictionaryContext: @@ -48,47 +108,41 @@ cdef class _ParserDictionaryContext: # __GLOBAL_PARSER_CONTEXT as defined below the class. # - cdef tree.xmlDict* _c_dict cdef _BaseParser _default_parser cdef list _implied_parser_contexts def __cinit__(self): - self._c_dict = NULL self._implied_parser_contexts = [] - def __dealloc__(self): - if self._c_dict is not NULL: - xmlparser.xmlDictFree(self._c_dict) - - cdef void initMainParserContext(self): - u"""Put the global context into the thread dictionary of the main + cdef int initMainParserContext(self) except -1: + """Put the global context into the thread dictionary of the main thread. To be called once and only in the main thread.""" thread_dict = python.PyThreadState_GetDict() if thread_dict is not NULL: - (thread_dict)[u"_ParserDictionaryContext"] = self + (thread_dict)["_ParserDictionaryContext"] = self cdef _ParserDictionaryContext _findThreadParserContext(self): - u"Find (or create) the _ParserDictionaryContext object for the current thread" + "Find (or create) the _ParserDictionaryContext object for the current thread" cdef _ParserDictionaryContext context thread_dict = python.PyThreadState_GetDict() if thread_dict is NULL: return self d = thread_dict - result = python.PyDict_GetItem(d, u"_ParserDictionaryContext") + result = python.PyDict_GetItem(d, "_ParserDictionaryContext") if result is not NULL: return result context = <_ParserDictionaryContext>_ParserDictionaryContext.__new__(_ParserDictionaryContext) - d[u"_ParserDictionaryContext"] = context + d["_ParserDictionaryContext"] = context return context - cdef void setDefaultParser(self, _BaseParser parser): - u"Set the default parser for the current thread" + cdef int setDefaultParser(self, _BaseParser parser) except -1: + "Set the default parser for the current thread" cdef _ParserDictionaryContext context context = self._findThreadParserContext() context._default_parser = parser cdef _BaseParser getDefaultParser(self): - u"Return (or create) the default parser of the current thread" + "Return (or create) the default parser of the current thread" cdef _ParserDictionaryContext context context = self._findThreadParserContext() if context._default_parser is None: @@ -98,50 +152,8 @@ cdef class _ParserDictionaryContext: context._default_parser = self._default_parser._copy() return context._default_parser - cdef tree.xmlDict* _getThreadDict(self, tree.xmlDict* default): - u"Return the thread-local dict or create a new one if necessary." - cdef _ParserDictionaryContext context - context = self._findThreadParserContext() - if context._c_dict is NULL: - # thread dict not yet set up => use default or create a new one - if default is not NULL: - context._c_dict = default - xmlparser.xmlDictReference(default) - return default - if self._c_dict is NULL: - self._c_dict = xmlparser.xmlDictCreate() - if context is not self: - context._c_dict = xmlparser.xmlDictCreateSub(self._c_dict) - return context._c_dict - - cdef void initThreadDictRef(self, tree.xmlDict** c_dict_ref): - c_dict = c_dict_ref[0] - c_thread_dict = self._getThreadDict(c_dict) - if c_dict is c_thread_dict: - return - if c_dict is not NULL: - xmlparser.xmlDictFree(c_dict) - c_dict_ref[0] = c_thread_dict - xmlparser.xmlDictReference(c_thread_dict) - - cdef void initParserDict(self, xmlparser.xmlParserCtxt* pctxt): - u"Assure we always use the same string dictionary." - self.initThreadDictRef(&pctxt.dict) - pctxt.dictNames = 1 - - cdef void initXPathParserDict(self, xpath.xmlXPathContext* pctxt): - u"Assure we always use the same string dictionary." - self.initThreadDictRef(&pctxt.dict) - - cdef void initDocDict(self, xmlDoc* result): - u"Store dict of last object parsed if no shared dict yet" - # XXX We also free the result dict here if there already was one. - # This case should only occur for new documents with empty dicts, - # otherwise we'd free data that's in use => segfault - self.initThreadDictRef(&result.dict) - cdef _ParserContext findImpliedContext(self): - u"""Return any current implied xml parser context for the current + """Return any current implied xml parser context for the current thread. This is used when the resolver functions are called with an xmlParserCtxt that was generated from within libxml2 (i.e. without a _ParserContext) - which happens when parsing @@ -156,21 +168,21 @@ cdef class _ParserDictionaryContext: return implied_context return None - cdef void pushImpliedContextFromParser(self, _BaseParser parser): - u"Push a new implied context object taken from the parser." + cdef int pushImpliedContextFromParser(self, _BaseParser parser) except -1: + "Push a new implied context object taken from the parser." if parser is not None: self.pushImpliedContext(parser._getParserContext()) else: self.pushImpliedContext(None) - cdef void pushImpliedContext(self, _ParserContext parser_context): - u"Push a new implied context object." + cdef int pushImpliedContext(self, _ParserContext parser_context) except -1: + "Push a new implied context object." cdef _ParserDictionaryContext context context = self._findThreadParserContext() context._implied_parser_contexts.append(parser_context) - cdef void popImpliedContext(self): - u"Pop the current implied context object." + cdef int popImpliedContext(self) except -1: + "Pop the current implied context object." cdef _ParserDictionaryContext context context = self._findThreadParserContext() context._implied_parser_contexts.pop() @@ -185,8 +197,15 @@ __GLOBAL_PARSER_CONTEXT.initMainParserContext() # name of Python Py_UNICODE encoding as known to libxml2 cdef const_char* _PY_UNICODE_ENCODING = NULL +cdef extern from *: + """ + #if defined(Py_LIMITED_API) + #define Py_UNICODE wchar_t + #endif + """ + cdef int _setupPythonUnicode() except -1: - u"""Sets _PY_UNICODE_ENCODING to the internal encoding name of Python unicode + """Sets _PY_UNICODE_ENCODING to the internal encoding name of Python unicode strings if libxml2 supports reading native Python unicode. This depends on iconv and the local Python installation, so we simply check if we find a matching encoding handler. @@ -216,13 +235,13 @@ cdef int _setupPythonUnicode() except -1: _PY_UNICODE_ENCODING = enc return 0 -cdef const_char* _findEncodingName(const_xmlChar* buffer, int size): - u"Work around bug in libxml2: find iconv name of encoding on our own." +cdef const_char* _findEncodingName(const_xmlChar* buffer, int size) noexcept: + "Work around bug in libxml2: find iconv name of encoding on our own." cdef tree.xmlCharEncoding enc enc = tree.xmlDetectCharEncoding(buffer, size) if enc == tree.XML_CHAR_ENCODING_UTF16LE: - if size >= 4 and (buffer[0] == '\xFF' and - buffer[1] == '\xFE' and + if size >= 4 and (buffer[0] == b'\xFF' and + buffer[1] == b'\xFE' and buffer[2] == 0 and buffer[3] == 0): return "UTF-32LE" # according to BOM else: @@ -239,7 +258,40 @@ cdef const_char* _findEncodingName(const_xmlChar* buffer, int size): # returns a constant char*, no need to free it return tree.xmlGetCharEncodingName(enc) -_setupPythonUnicode() +# Python 3.12 removed support for "Py_UNICODE". +if python.PY_VERSION_HEX < 0x030C0000: + _setupPythonUnicode() + + +cdef unicode _find_PyUCS4EncodingName(): + """ + Find a suitable encoding for Py_UCS4 PyUnicode strings in libxml2. + """ + ustring = "\U0001F92A" + cdef const xmlChar* buffer = python.PyUnicode_DATA(ustring) + cdef Py_ssize_t py_buffer_len = python.PyUnicode_GET_LENGTH(ustring) + + encoding_name = '' + cdef tree.xmlCharEncoding enc = tree.xmlDetectCharEncoding(buffer, py_buffer_len) + enchandler = tree.xmlGetCharEncodingHandler(enc) + if enchandler is not NULL: + try: + if enchandler.name: + encoding_name = enchandler.name.decode('UTF-8') + finally: + tree.xmlCharEncCloseFunc(enchandler) + else: + c_name = tree.xmlGetCharEncodingName(enc) + if c_name: + encoding_name = c_name.decode('UTF-8') + + + if encoding_name and not encoding_name.endswith('LE') and not encoding_name.endswith('BE'): + encoding_name += 'BE' if python.PY_BIG_ENDIAN else 'LE' + return encoding_name or None + +_pyucs4_encoding_name = _find_PyUCS4EncodingName() + ############################################################ ## support for file-like objects @@ -249,22 +301,20 @@ _setupPythonUnicode() @cython.internal cdef class _FileReaderContext: cdef object _filelike - cdef object _encoding - cdef object _url - cdef object _bytes + cdef bytes _encoding + cdef bytes _url + cdef bytes _bytes cdef _ExceptionContext _exc_context cdef Py_ssize_t _bytes_read - cdef char* _c_url + cdef const char* _c_url cdef bint _close_file_after_read def __cinit__(self, filelike, exc_context not None, url, encoding=None, bint close_file=False): self._exc_context = exc_context self._filelike = filelike self._close_file_after_read = close_file - self._encoding = encoding - if url is None: - self._c_url = NULL - else: + self._encoding = _utf8orNone(encoding) + if url is not None: url = _encodeFilename(url) self._c_url = _cstr(url) self._url = url @@ -283,57 +333,47 @@ cdef class _FileReaderContext: if close is not None: close() - cdef xmlparser.xmlParserInputBuffer* _createParserInputBuffer(self): - cdef stdio.FILE* c_stream - cdef xmlparser.xmlParserInputBuffer* c_buffer - c_buffer = xmlparser.xmlAllocParserInputBuffer(0) - c_stream = python.PyFile_AsFile(self._filelike) - if c_stream is NULL: + cdef xmlparser.xmlParserInputBuffer* _createParserInputBuffer(self) noexcept: + cdef xmlparser.xmlParserInputBuffer* c_buffer = xmlparser.xmlAllocParserInputBuffer(0) + if c_buffer: c_buffer.readcallback = _readFilelikeParser - c_buffer.context = self - else: - c_buffer.readcallback = _readFileParser - c_buffer.context = c_stream + c_buffer.context = self return c_buffer cdef xmlparser.xmlParserInput* _createParserInput( - self, xmlparser.xmlParserCtxt* ctxt): - cdef xmlparser.xmlParserInputBuffer* c_buffer - c_buffer = self._createParserInputBuffer() + self, xmlparser.xmlParserCtxt* ctxt) noexcept: + cdef xmlparser.xmlParserInputBuffer* c_buffer = self._createParserInputBuffer() + if not c_buffer: + return NULL return xmlparser.xmlNewIOInputStream(ctxt, c_buffer, 0) - cdef tree.xmlDtd* _readDtd(self): - cdef xmlparser.xmlParserInputBuffer* c_buffer - c_buffer = self._createParserInputBuffer() + cdef tree.xmlDtd* _readDtd(self) noexcept: + cdef xmlparser.xmlParserInputBuffer* c_buffer = self._createParserInputBuffer() + if not c_buffer: + return NULL with nogil: return xmlparser.xmlIOParseDTD(NULL, c_buffer, 0) - cdef xmlDoc* _readDoc(self, xmlparser.xmlParserCtxt* ctxt, int options): + cdef xmlDoc* _readDoc(self, xmlparser.xmlParserCtxt* ctxt, int options) noexcept: cdef xmlDoc* result - cdef char* c_encoding - cdef stdio.FILE* c_stream - cdef xmlparser.xmlInputReadCallback c_read_callback - cdef xmlparser.xmlInputCloseCallback c_close_callback - cdef void* c_callback_context - - if self._encoding is None: - c_encoding = NULL - else: - c_encoding = _cstr(self._encoding) + cdef void* c_callback_context = self + cdef const char* c_encoding - c_stream = python.PyFile_AsFile(self._filelike) - if c_stream is NULL: - c_read_callback = _readFilelikeParser - c_callback_context = self + if self._encoding is not None: + c_encoding = _cstr(self._encoding) else: - c_read_callback = _readFileParser - c_callback_context = c_stream + try: + self._bytes = self._readBytes(16) + self._bytes_read = _detectBOMEncoding(self._bytes, len(self._bytes), &c_encoding) + except: + self._exc_context._store_raised() + return NULL orig_options = ctxt.options with nogil: if ctxt.html: result = htmlparser.htmlCtxtReadIO( - ctxt, c_read_callback, NULL, c_callback_context, + ctxt, _readFilelikeParser, NULL, c_callback_context, self._c_url, c_encoding, options) if result is not NULL: if _fixHtmlDictNames(ctxt.dict, result) < 0: @@ -341,9 +381,10 @@ cdef class _FileReaderContext: result = NULL else: result = xmlparser.xmlCtxtReadIO( - ctxt, c_read_callback, NULL, c_callback_context, + ctxt, _readFilelikeParser, NULL, c_callback_context, self._c_url, c_encoding, options) ctxt.options = orig_options # work around libxml2 problem + try: self._close_file() except: @@ -351,14 +392,29 @@ cdef class _FileReaderContext: finally: return result # swallow any exceptions - cdef int copyToBuffer(self, char* c_buffer, int c_requested): + cdef bytes _readBytes(self, count): + data = self._filelike.read(count) + if isinstance(data, bytes): + return data + elif isinstance(data, str): + if self._encoding is None: + return ( data).encode('utf8') + else: + return python.PyUnicode_AsEncodedString( + data, _cstr(self._encoding), NULL) + else: + self._close_file() + raise TypeError, \ + "reading from file-like objects must return byte strings or unicode strings" + + cdef int copyToBuffer(self, char* c_buffer, int c_requested) noexcept: cdef int c_byte_count = 0 - cdef char* c_start + cdef const char* c_start cdef Py_ssize_t byte_count, remaining if self._bytes_read < 0: return 0 try: - byte_count = python.PyBytes_GET_SIZE(self._bytes) + byte_count = len(self._bytes) remaining = byte_count - self._bytes_read while c_requested > remaining: c_start = _cstr(self._bytes) + self._bytes_read @@ -367,20 +423,8 @@ cdef class _FileReaderContext: c_buffer += remaining c_requested -= remaining - self._bytes = self._filelike.read(c_requested) - if not isinstance(self._bytes, bytes): - if isinstance(self._bytes, unicode): - if self._encoding is None: - self._bytes = (self._bytes).encode('utf8') - else: - self._bytes = python.PyUnicode_AsEncodedString( - self._bytes, _cstr(self._encoding), NULL) - else: - self._close_file() - raise TypeError, \ - u"reading from file-like objects must return byte strings or unicode strings" - - remaining = python.PyBytes_GET_SIZE(self._bytes) + data = self._bytes = self._readBytes(c_requested) + remaining = len(data) if remaining == 0: self._bytes_read = -1 self._close_file() @@ -402,113 +446,211 @@ cdef class _FileReaderContext: finally: return c_byte_count # swallow any exceptions -cdef int _readFilelikeParser(void* ctxt, char* c_buffer, int c_size) with gil: +cdef int _readFilelikeParser(void* ctxt, char* c_buffer, int c_size) noexcept with gil: return (<_FileReaderContext>ctxt).copyToBuffer(c_buffer, c_size) -cdef int _readFileParser(void* ctxt, char* c_buffer, int c_size) nogil: - return stdio.fread(c_buffer, 1, c_size, ctxt) + +@cython.final +@cython.internal +cdef class _UnicodeStringReader: + cdef str _data + cdef size_t _pos + cdef size_t _remaining + + def __cinit__(self, unicode_string: str): + self._data = unicode_string + self._remaining = len(unicode_string) + + def read(self, Py_ssize_t count): + if self._remaining == 0: + return b'' + + if count > self._remaining: + # On negative or large counts, clip to the end of the string. + # Python's files do that for negative counts, too. + count = self._remaining + self._remaining = 0 + else: + self._remaining -= count + + cdef size_t pos = self._pos + self._pos = pos + count + + data = self._data[pos: pos+count].encode('utf8') + + if self._remaining == 0: + self._data = '' # a good time to clean up + return data + ############################################################ ## support for custom document loaders ############################################################ cdef xmlparser.xmlParserInput* _local_resolver(const_char* c_url, const_char* c_pubid, - xmlparser.xmlParserCtxt* c_context) with gil: - cdef _ResolverContext context + xmlparser.xmlParserCtxt* c_context) noexcept nogil: + # Legacy libxml2 pre-2.14 interface. cdef xmlparser.xmlParserInput* c_input + _local_resource_loader_internal(c_context, c_url, c_pubid, 0, 0, &c_input) + return c_input + + +cdef xmlerror.xmlParserErrors _local_resource_loader( + void *c_context_ptr, const char *c_url, const char *c_pubid, int type, int flags, xmlparser.xmlParserInput **c_input_out) noexcept nogil: + # actually "xmlResourceType type, xmlParserInputFlags flags" + # New libxml2 2.14+ interface. + + return _local_resource_loader_internal( + c_context_ptr, c_url, c_pubid, type, flags, c_input_out) + + +cdef xmlerror.xmlParserErrors _local_resource_loader_internal( + xmlparser.xmlParserCtxt* c_context, + const char *c_url, const char *c_pubid, int type, int flags, xmlparser.xmlParserInput **c_input_out) noexcept with gil: + + cdef _ResolverContext context + cdef xmlparser.xmlParserInput* c_input = NULL cdef _InputDocument doc_ref - cdef _FileReaderContext file_context - # if there is no _ParserContext associated with the xmlParserCtxt - # passed, check to see if the thread state object has an implied - # context. - if c_context._private is not NULL: - context = <_ResolverContext>c_context._private - else: + + # If there is no _ParserContext associated with the xmlParserCtxt passed, + # see if the thread state object has an implied context. + if c_context is NULL or c_context._private is NULL: context = __GLOBAL_PARSER_CONTEXT.findImpliedContext() + else: + context = <_ResolverContext> c_context._private - if context is None: - if __DEFAULT_ENTITY_LOADER is NULL: - return NULL + if context is not None: + try: + # parsing a related document (DTD etc.) => UTF-8 encoded URL? + url = _decodeFilename( c_url) if c_url is not NULL else None + pubid = funicodeOrNone( c_pubid) # always UTF-8 + + doc_ref = context._resolvers.resolve(url, pubid, context) + + if doc_ref is not None: + c_error = _resolve_doc_ref(doc_ref, c_context, context, url, &c_input) + if c_error != xmlerror.XML_ERR_OK: + return c_error + except: + context._store_raised() + c_input_out[0] = NULL + # Do not let libxml2 report an error itself. + return xmlerror.XML_ERR_OK + + if c_input is NULL and __DEFAULT_ENTITY_LOADER is not NULL: with nogil: # free the GIL as we might do serious I/O here (e.g. HTTP) c_input = __DEFAULT_ENTITY_LOADER(c_url, c_pubid, c_context) - return c_input - try: - if c_url is NULL: - url = None - else: - # parsing a related document (DTD etc.) => UTF-8 encoded URL? - url = _decodeFilename(c_url) - if c_pubid is NULL: - pubid = None - else: - pubid = funicode(c_pubid) # always UTF-8 + c_input_out[0] = c_input # may be NULL + return xmlerror.XML_ERR_OK - doc_ref = context._resolvers.resolve(url, pubid, context) - except: - context._store_raised() - return NULL - if doc_ref is not None: - if doc_ref._type == PARSER_DATA_STRING: - data = doc_ref._data_bytes - filename = doc_ref._filename - if not filename: +# returns 'xmlerror.xmlParserErrors' or -1 for exception +cdef int _resolve_doc_ref( + _InputDocument doc_ref, xmlparser.xmlParserCtxt* c_context, _ResolverContext context, url, + xmlparser.xmlParserInput** c_input_out) except -1: + + cdef xmlparser.xmlParserInput* c_input + cdef _FileReaderContext file_context + cdef xmlerror.xmlParserErrors c_return_code = xmlerror.XML_ERR_OK + + if doc_ref._type == PARSER_DATA_STRING: + data_bytes = doc_ref._data_bytes + data = data_bytes + filename = doc_ref._filename + if not filename: + filename = None + elif not isinstance(filename, bytes): + # most likely a text URL + filename = filename.encode('utf8') + if not isinstance(filename, bytes): filename = None - elif not isinstance(filename, bytes): - # most likely a text URL - filename = filename.encode('utf8') - if not isinstance(filename, bytes): - filename = None + if tree.LIBXML_VERSION >= 21400: + c_filename = tree.xmlStrdup(_xcstr(filename)) if filename is not None else NULL + c_input = xmlparser.xmlNewInputFromMemory( + c_filename, _xcstr(data_bytes), len(data_bytes), 0) + else: c_input = xmlparser.xmlNewInputStream(c_context) if c_input is not NULL: if filename is not None: c_input.filename = tree.xmlStrdup(_xcstr(filename)) - c_input.base = _xcstr(data) - c_input.length = python.PyBytes_GET_SIZE(data) + c_input.base = _xcstr(data_bytes) + c_input.length = len(data_bytes) c_input.cur = c_input.base c_input.end = c_input.base + c_input.length - elif doc_ref._type == PARSER_DATA_FILENAME: - data = None - c_filename = _cstr(doc_ref._filename) - with nogil: - # free the GIL as we might do serious I/O here - c_input = xmlparser.xmlNewInputFromFile( - c_context, c_filename) - elif doc_ref._type == PARSER_DATA_FILE: - file_context = _FileReaderContext(doc_ref._file, context, url, - None, doc_ref._close_file) - c_input = file_context._createParserInput(c_context) - data = file_context - else: - data = None - c_input = NULL - if data is not None: - context._storage.add(data) - if c_input is not NULL: - return c_input + if c_input is NULL: + c_return_code = xmlerror.xmlParserErrors.XML_ERR_NO_MEMORY - if __DEFAULT_ENTITY_LOADER is NULL: - return NULL + elif doc_ref._type == PARSER_DATA_FILENAME: + data = None + c_filename = _cstr(doc_ref._filename) + error_return = xmlerror.xmlParserErrors.XML_IO_ENOENT # Report "File not found" if this fails. + with nogil: + # free the GIL as we might do serious I/O here + c_input = xmlparser.xmlNewInputFromFile(c_context, c_filename) + + if c_input is NULL: + c_return_code = xmlerror.xmlParserErrors.XML_IO_ENOENT # File not found + + elif doc_ref._type == PARSER_DATA_FILE: + file_context = _FileReaderContext(doc_ref._file, context, url, + None, doc_ref._close_file) + c_input = file_context._createParserInput(c_context) + data = file_context + + else: + data = None + c_input = NULL + + if c_input is not NULL and data is not None: + context._storage.add(data) + + c_input_out[0] = c_input + return c_return_code - with nogil: - # free the GIL as we might do serious I/O here (e.g. HTTP) - c_input = __DEFAULT_ENTITY_LOADER(c_url, c_pubid, c_context) - return c_input cdef xmlparser.xmlExternalEntityLoader __DEFAULT_ENTITY_LOADER __DEFAULT_ENTITY_LOADER = xmlparser.xmlGetExternalEntityLoader() -cdef xmlparser.xmlExternalEntityLoader _register_document_loader() nogil: +cdef cython.pymutex _doc_loader_config_lock + +@cython.final +@cython.internal +cdef class _DocLoaderConfig: + cdef xmlparser.xmlExternalEntityLoader _old_entity_loader + + @cython.inline + cdef void __enter__(self) noexcept: + _doc_loader_config_lock.acquire() + self._old_entity_loader = xmlparser.xmlGetExternalEntityLoader() + xmlparser.xmlSetExternalEntityLoader( _local_resolver) + + def __exit__(self, exc_type, exc_value, exc_tb): + xmlparser.xmlSetExternalEntityLoader(self._old_entity_loader) + _doc_loader_config_lock.release() + + +cdef _DocLoaderConfig lxml_document_loader = _DocLoaderConfig() + + +cdef xmlparser.xmlExternalEntityLoader _register_resource_loader() noexcept nogil: + # libxml2 2.14 has per-context document loaders. + if tree.LIBXML_VERSION >= 21400: + return NULL + cdef xmlparser.xmlExternalEntityLoader old = xmlparser.xmlGetExternalEntityLoader() - xmlparser.xmlSetExternalEntityLoader(_local_resolver) + xmlparser.xmlSetExternalEntityLoader( _local_resolver) return old -cdef void _reset_document_loader(xmlparser.xmlExternalEntityLoader old) nogil: + +cdef void _reset_resource_loader(xmlparser.xmlExternalEntityLoader old) noexcept nogil: + # libxml2 2.14 has per-context document loaders. Nothing to reset for us. + if tree.LIBXML_VERSION >= 21400: + return xmlparser.xmlSetExternalEntityLoader(old) @@ -520,26 +662,21 @@ cdef void _reset_document_loader(xmlparser.xmlExternalEntityLoader old) nogil: @cython.internal cdef class _ParserContext(_ResolverContext): cdef _ErrorLog _error_log + cdef _ParserDictionary _dict cdef _ParserSchemaValidationContext _validator cdef xmlparser.xmlParserCtxt* _c_ctxt cdef xmlparser.xmlExternalEntityLoader _orig_loader - cdef python.PyThread_type_lock _lock cdef _Document _doc + cdef cython.pymutex _lock + cdef unsigned long _lock_owner_tid cdef bint _collect_ids def __cinit__(self): - self._c_ctxt = NULL self._collect_ids = True - if not config.ENABLE_THREADING: - self._lock = NULL - else: - self._lock = python.PyThread_allocate_lock() self._error_log = _ErrorLog() + self._dict = _ParserDictionary() def __dealloc__(self): - if config.ENABLE_THREADING and self._lock is not NULL: - python.PyThread_free_lock(self._lock) - self._lock = NULL if self._c_ctxt is not NULL: if self._validator is not NULL and self._validator is not None: # If the parser was not closed correctly (e.g. interrupted iterparse()), @@ -558,37 +695,59 @@ cdef class _ParserContext(_ResolverContext): _initParserContext(context, self._resolvers._copy(), NULL) return context - cdef void _initParserContext(self, xmlparser.xmlParserCtxt* c_ctxt): + cdef void _initParserContext(self, xmlparser.xmlParserCtxt* c_ctxt) noexcept: + """ + Connects the libxml2-level context to the lxml-level parser context. + """ self._c_ctxt = c_ctxt c_ctxt._private = self + xmlparser.xmlCtxtSetResourceLoader(c_ctxt, _local_resource_loader, NULL) + self._dict.initParserDict(c_ctxt) - cdef void _resetParserContext(self): + cdef void _resetParserContext(self) noexcept: if self._c_ctxt is not NULL: if self._c_ctxt.html: htmlparser.htmlCtxtReset(self._c_ctxt) self._c_ctxt.disableSAX = 0 # work around bug in libxml2 else: xmlparser.xmlClearParserCtxt(self._c_ctxt) + # work around bug in libxml2 [2.9.10 .. 2.9.14]: + # https://gitlab.gnome.org/GNOME/libxml2/-/issues/378 + self._c_ctxt.nsNr = 0 cdef int prepare(self, bint set_document_loader=True) except -1: cdef int result - if config.ENABLE_THREADING and self._lock is not NULL: - with nogil: - result = python.PyThread_acquire_lock( - self._lock, python.WAIT_LOCK) - if result == 0: - raise ParserError, u"parser locking failed" + cdef unsigned long current_tid + if config.ENABLE_THREADING: + current_tid = python.PyThread_get_thread_ident() + 1 # allow 0 == no thread + if self._lock_owner_tid == current_tid: + raise RuntimeError("Parser is already used by this thread. Raising to prevent deadlock.") + self._lock.acquire() + self._lock_owner_tid = current_tid self._error_log.clear() self._doc = None - self._c_ctxt.sax.serror = _receiveParserError - self._orig_loader = _register_document_loader() if set_document_loader else NULL + # Connect the lxml error log with libxml2's error handling. In the case of parsing + # HTML, ctxt->sax is not set to null, so this always works. The libxml2 function + # that does this is htmlInitParserCtxt in HTMLparser.c. For HTML (and possibly XML + # too), libxml2's SAX's serror is set to be the place where errors are sent when + # schannel is set to ctxt->sax->serror in xmlCtxtErrMemory in libxml2's + # parserInternals.c. + # Need a cast here because older libxml2 releases do not use 'const' in the functype. + self._c_ctxt.sax.serror = _receiveParserError + self._orig_loader = _register_resource_loader() if set_document_loader else NULL if self._validator is not None: - self._validator.connect(self._c_ctxt, self._error_log) + try: + self._validator.connect(self._c_ctxt, self._error_log) + except: + if config.ENABLE_THREADING: + self._lock_owner_tid = 0 + self._lock.release() + raise return 0 cdef int cleanup(self) except -1: if self._orig_loader is not NULL: - _reset_document_loader(self._orig_loader) + _reset_resource_loader(self._orig_loader) try: if self._validator is not None: self._validator.disconnect() @@ -597,8 +756,9 @@ cdef class _ParserContext(_ResolverContext): self._doc = None self._c_ctxt.sax.serror = NULL finally: - if config.ENABLE_THREADING and self._lock is not NULL: - python.PyThread_release_lock(self._lock) + if config.ENABLE_THREADING: + self._lock_owner_tid = 0 + self._lock.release() return 0 cdef object _handleParseResult(self, _BaseParser parser, @@ -623,10 +783,13 @@ cdef _initParserContext(_ParserContext context, if c_ctxt is not NULL: context._initParserContext(c_ctxt) -cdef void _forwardParserError(xmlparser.xmlParserCtxt* _parser_context, xmlerror.xmlError* error) with gil: +cdef void _forwardParserError(xmlparser.xmlParserCtxt* _parser_context, const xmlerror.xmlError* error) noexcept with gil: + """ + Add an error created by libxml2 to the lxml-level error_log. + """ (<_ParserContext>_parser_context._private)._error_log._receive(error) -cdef void _receiveParserError(void* c_context, xmlerror.xmlError* error) nogil: +cdef void _receiveParserError(void* c_context, const xmlerror.xmlError* error) noexcept nogil: if __DEBUG: if c_context is NULL or (c_context)._private is NULL: _forwardError(NULL, error) @@ -652,7 +815,7 @@ cdef int _raiseParseError(xmlparser.xmlParserCtxt* ctxt, filename, raise IOError, message elif error_log: raise error_log._buildParseException( - XMLSyntaxError, u"Document is not well formed") + XMLSyntaxError, "Document is not well formed") elif ctxt.lastError.message is not NULL: message = ctxt.lastError.message.strip() code = ctxt.lastError.code @@ -669,17 +832,22 @@ cdef xmlDoc* _handleParseResult(_ParserContext context, xmlparser.xmlParserCtxt* c_ctxt, xmlDoc* result, filename, bint recover, bint free_doc) except NULL: + # The C-level argument xmlDoc* result is passed in as NULL if the parser was not able + # to parse the document. cdef bint well_formed if result is not NULL: - __GLOBAL_PARSER_CONTEXT.initDocDict(result) + context._dict.initDocDict(result) if c_ctxt.myDoc is not NULL: if c_ctxt.myDoc is not result: - __GLOBAL_PARSER_CONTEXT.initDocDict(c_ctxt.myDoc) + context._dict.initDocDict(c_ctxt.myDoc) tree.xmlFreeDoc(c_ctxt.myDoc) c_ctxt.myDoc = NULL if result is not NULL: + # "wellFormed" in libxml2 is 0 if the parser found fatal errors. It still returns a + # parse result document if 'recover=True'. Here, we determine if we can present + # the document to the user or consider it incorrect or broken enough to raise an error. if (context._validator is not None and not context._validator.isvalid()): well_formed = 0 # actually not 'valid', but anyway ... @@ -690,7 +858,7 @@ cdef xmlDoc* _handleParseResult(_ParserContext context, # An encoding error occurred and libxml2 switched from UTF-8 # input to (undecoded) Latin-1, at some arbitrary point in the # document. Better raise an error than allowing for a broken - # tree with mixed encodings. + # tree with mixed encodings. This is fixed in libxml2 2.12. well_formed = 0 elif recover or (c_ctxt.wellFormed and c_ctxt.lastError.level < xmlerror.XML_ERR_ERROR): @@ -740,7 +908,7 @@ cdef xmlDoc* _handleParseResult(_ParserContext context, return result -cdef int _fixHtmlDictNames(tree.xmlDict* c_dict, xmlDoc* c_doc) nogil: +cdef int _fixHtmlDictNames(tree.xmlDict* c_dict, xmlDoc* c_doc) noexcept nogil: cdef xmlNode* c_node if c_doc is NULL: return 0 @@ -753,7 +921,7 @@ cdef int _fixHtmlDictNames(tree.xmlDict* c_dict, xmlDoc* c_doc) nogil: return 0 cdef int _fixHtmlDictSubtreeNames(tree.xmlDict* c_dict, xmlDoc* c_doc, - xmlNode* c_start_node) nogil: + xmlNode* c_start_node) noexcept nogil: """ Move names to the dict, iterating in document order, starting at c_start_node. This is used in incremental parsing after each chunk. @@ -772,7 +940,7 @@ cdef int _fixHtmlDictSubtreeNames(tree.xmlDict* c_dict, xmlDoc* c_doc, return 0 cdef inline int _fixHtmlDictNodeNames(tree.xmlDict* c_dict, - xmlNode* c_node) nogil: + xmlNode* c_node) noexcept nogil: cdef xmlNode* c_attr c_name = tree.xmlDictLookup(c_dict, c_node.name, -1) if c_name is NULL: @@ -791,41 +959,68 @@ cdef inline int _fixHtmlDictNodeNames(tree.xmlDict* c_dict, c_attr = c_attr.next return 0 + +cdef extern from *: + """ + typedef struct { + unsigned int for_html: 1; + unsigned int remove_comments: 1; + unsigned int remove_pis: 1; + unsigned int strip_cdata: 1; + unsigned int collect_ids: 1; + unsigned int resolve_external_entities: 1; + } __lxml_ParserFlags; + """ + ctypedef struct ParserFlags "__lxml_ParserFlags": + bint for_html + bint remove_comments + bint remove_pis + bint strip_cdata + bint collect_ids + bint resolve_external_entities + + @cython.internal cdef class _BaseParser: cdef ElementClassLookup _class_lookup cdef _ResolverRegistry _resolvers cdef _ParserContext _parser_context cdef _ParserContext _push_parser_context + cdef RWLock _lock cdef int _parse_options - cdef bint _for_html - cdef bint _remove_comments - cdef bint _remove_pis - cdef bint _strip_cdata - cdef bint _collect_ids + cdef ParserFlags _flags cdef XMLSchema _schema cdef bytes _filename cdef readonly object target cdef object _default_encoding cdef tuple _events_to_collect # (event_types, tag) + def __cinit__(self): + self._lock = RWLock() + def __init__(self, int parse_options, bint for_html, XMLSchema schema, remove_comments, remove_pis, strip_cdata, collect_ids, - target, encoding): + target, encoding, bint resolve_external_entities=True): cdef tree.xmlCharEncodingHandler* enchandler cdef int c_encoding if not isinstance(self, (XMLParser, HTMLParser)): - raise TypeError, u"This class cannot be instantiated" + raise TypeError, "This class cannot be instantiated" + + if not collect_ids and tree.LIBXML_VERSION >= 21500: + parse_options |= xmlparser.XML_PARSE_SKIP_IDS self._parse_options = parse_options + self._flags = ParserFlags( + for_html=for_html, + remove_comments=remove_comments, + remove_pis=remove_pis, + strip_cdata=strip_cdata, + collect_ids=collect_ids, + resolve_external_entities=resolve_external_entities, + ) + self.target = target - self._for_html = for_html - self._remove_comments = remove_comments - self._remove_pis = remove_pis - self._strip_cdata = strip_cdata - self._collect_ids = collect_ids self._schema = schema - self._resolvers = _ResolverRegistry() if encoding is None: @@ -851,35 +1046,64 @@ cdef class _BaseParser: cdef _ParserContext _getParserContext(self): cdef xmlparser.xmlParserCtxt* pctxt - if self._parser_context is None: - self._parser_context = self._createContext(self.target, None) - self._parser_context._collect_ids = self._collect_ids - if self._schema is not None: - self._parser_context._validator = \ - self._schema._newSaxValidator( - self._parse_options & xmlparser.XML_PARSE_DTDATTR) - pctxt = self._newParserCtxt() - _initParserContext(self._parser_context, self._resolvers, pctxt) - self._configureSaxContext(pctxt) - return self._parser_context + context = self._parser_context + if context is not None: + return context + + context = self._createContext(self.target, None) + + context._collect_ids = self._flags.collect_ids + if self._parse_options & xmlparser.XML_PARSE_HUGE: + context._dict.disableSizeLimit() + + if self._schema is not None: + context._validator = self._schema._newSaxValidator( + self._parse_options & xmlparser.XML_PARSE_DTDATTR) + + pctxt = self._newParserCtxt() + _initParserContext(context, self._resolvers, pctxt) + self._configureSaxContext(pctxt) + + with cython.critical_section(self): + if self._parser_context is None: + self._parser_context = context + else: + # Race condition, discard newly created context and reuse existing one. + context = self._parser_context + return context cdef _ParserContext _getPushParserContext(self): cdef xmlparser.xmlParserCtxt* pctxt - if self._push_parser_context is None: - self._push_parser_context = self._createContext( - self.target, self._events_to_collect) - self._push_parser_context._collect_ids = self._collect_ids - if self._schema is not None: - self._push_parser_context._validator = \ - self._schema._newSaxValidator( - self._parse_options & xmlparser.XML_PARSE_DTDATTR) - pctxt = self._newPushParserCtxt() - _initParserContext( - self._push_parser_context, self._resolvers, pctxt) - self._configureSaxContext(pctxt) - return self._push_parser_context + context = self._push_parser_context + if context is not None: + return context + + context = self._createContext(self.target, self._events_to_collect) + + context._collect_ids = self._flags.collect_ids + if self._parse_options & xmlparser.XML_PARSE_HUGE: + context._dict.disableSizeLimit() + + if self._schema is not None: + context._validator = self._schema._newSaxValidator( + self._parse_options & xmlparser.XML_PARSE_DTDATTR) + + pctxt = self._newPushParserCtxt() + _initParserContext(context, self._resolvers, pctxt) + self._configureSaxContext(pctxt) + + with cython.critical_section(self): + if self._push_parser_context is None: + self._push_parser_context = context + else: + # Race condition, discard newly created context and reuse existing one. + context = self._push_parser_context + return context cdef _ParserContext _createContext(self, target, events_to_collect): + """ + This method creates and configures the lxml-level parser. + """ cdef _SaxParserContext sax_context if target is not None: sax_context = _TargetParserContext(self) @@ -896,13 +1120,15 @@ cdef class _BaseParser: @cython.final cdef int _configureSaxContext(self, xmlparser.xmlParserCtxt* pctxt) except -1: - if self._remove_comments: + if self._flags.remove_comments: pctxt.sax.comment = NULL - if self._remove_pis: + if self._flags.remove_pis: pctxt.sax.processingInstruction = NULL - if self._strip_cdata: + if self._flags.strip_cdata: # hard switch-off for CDATA nodes => makes them plain text pctxt.sax.cdataBlock = NULL + if not self._flags.resolve_external_entities: + pctxt.sax.getEntity = _getInternalEntityOnly cdef int _registerHtmlErrorHandler(self, xmlparser.xmlParserCtxt* c_ctxt) except -1: cdef xmlparser.xmlSAXHandler* sax = c_ctxt.sax @@ -916,15 +1142,19 @@ cdef class _BaseParser: sizeof(htmlparser.htmlDefaultSAXHandler)) c_ctxt.sax = sax sax.initialized = xmlparser.XML_SAX2_MAGIC - sax.serror = _receiveParserError + # Need a cast here because older libxml2 releases do not use 'const' in the functype. + sax.serror = _receiveParserError sax.startElementNs = NULL sax.endElementNs = NULL sax._private = NULL return 0 cdef xmlparser.xmlParserCtxt* _newParserCtxt(self) except NULL: + """ + Create and initialise a libxml2-level parser context. + """ cdef xmlparser.xmlParserCtxt* c_ctxt - if self._for_html: + if self._flags.for_html: c_ctxt = htmlparser.htmlCreateMemoryParserCtxt('dummy', 5) if c_ctxt is not NULL: self._registerHtmlErrorHandler(c_ctxt) @@ -937,8 +1167,8 @@ cdef class _BaseParser: cdef xmlparser.xmlParserCtxt* _newPushParserCtxt(self) except NULL: cdef xmlparser.xmlParserCtxt* c_ctxt - cdef char* c_filename = _cstr(self._filename) if self._filename is not None else NULL - if self._for_html: + cdef const char* c_filename = _cstr(self._filename) if self._filename is not None else NULL + if self._flags.for_html: c_ctxt = htmlparser.htmlCreatePushParserCtxt( NULL, NULL, NULL, 0, c_filename, tree.XML_CHAR_ENCODING_NONE) if c_ctxt is not NULL: @@ -954,6 +1184,14 @@ cdef class _BaseParser: c_ctxt.sax.startDocument = _initSaxDocument return c_ctxt + @cython.final + cdef void initDocDict(self, tree.xmlDoc *c_doc) noexcept: + self._getParserContext()._dict.initDocDict(c_doc) + + @cython.final + cdef tree.xmlDict* getDict(self) noexcept: + return self._getParserContext()._dict.getDict() + @property def error_log(self): """The error log of the last parser run. @@ -970,14 +1208,19 @@ cdef class _BaseParser: @property def version(self): """The version of the underlying XML parser.""" - return u"libxml2 %d.%d.%d" % LIBXML_VERSION + return "libxml2 %d.%d.%d" % LIBXML_VERSION - def setElementClassLookup(self, ElementClassLookup lookup = None): - u":deprecated: use ``parser.set_element_class_lookup(lookup)`` instead." - self.set_element_class_lookup(lookup) + @property + def dict_size(self): + cdef size_t size = 0 + if self._parser_context is not None: + size += self._parser_context._dict.getDictSize() + if self._push_parser_context is not None: + size += self._push_parser_context._dict.getDictSize() + return size def set_element_class_lookup(self, ElementClassLookup lookup = None): - u"""set_element_class_lookup(self, lookup = None) + """set_element_class_lookup(self, lookup = None) Set a lookup scheme for element classes generated from this parser. @@ -986,14 +1229,11 @@ cdef class _BaseParser: self._class_lookup = lookup cdef _BaseParser _copy(self): - u"Create a new parser with the same configuration." + "Create a new parser with the same configuration." cdef _BaseParser parser parser = self.__class__() parser._parse_options = self._parse_options - parser._for_html = self._for_html - parser._remove_comments = self._remove_comments - parser._remove_pis = self._remove_pis - parser._strip_cdata = self._strip_cdata + parser._flags = self._flags parser._filename = self._filename parser._resolvers = self._resolvers parser.target = self.target @@ -1004,24 +1244,25 @@ cdef class _BaseParser: return parser def copy(self): - u"""copy(self) + """copy(self) Create a new parser with the same configuration. """ return self._copy() def makeelement(self, _tag, attrib=None, nsmap=None, **_extra): - u"""makeelement(self, _tag, attrib=None, nsmap=None, **_extra) + """makeelement(self, _tag, attrib=None, nsmap=None, **_extra) Creates a new element associated with this parser. """ - return _makeElement(_tag, NULL, None, self, None, None, - attrib, nsmap, _extra) + return _makeElement( + _tag, NULL, doc=None, parser=self, + text=None, tail=None, attrib=attrib, nsmap=nsmap, extra_attrs=_extra) # internal parser methods - cdef xmlDoc* _parseUnicodeDoc(self, utext, char* c_filename) except NULL: - u"""Parse unicode document, share dictionary if possible. + cdef xmlDoc* _parseUnicodeDoc(self, utext, const char* c_filename) except NULL: + """Parse unicode document, share dictionary if possible. """ cdef _ParserContext context cdef xmlDoc* result @@ -1030,14 +1271,16 @@ cdef class _BaseParser: cdef int buffer_len, c_kind cdef const_char* c_text cdef const_char* c_encoding = _PY_UNICODE_ENCODING - cdef bint is_pep393_string = ( - python.PEP393_ENABLED and python.PyUnicode_IS_READY(utext)) - if is_pep393_string: + if python.PyUnicode_IS_READY(utext): + # PEP-393 string c_text = python.PyUnicode_DATA(utext) py_buffer_len = python.PyUnicode_GET_LENGTH(utext) c_kind = python.PyUnicode_KIND(utext) if c_kind == 1: - c_encoding = 'ISO-8859-1' + if python.PyUnicode_MAX_CHAR_VALUE(utext) <= 127: + c_encoding = 'UTF-8' + else: + c_encoding = 'ISO-8859-1' elif c_kind == 2: py_buffer_len *= 2 if python.PY_BIG_ENDIAN: @@ -1047,12 +1290,13 @@ cdef class _BaseParser: elif c_kind == 4: py_buffer_len *= 4 if python.PY_BIG_ENDIAN: - c_encoding = 'UCS-4BE' + c_encoding = 'UTF-32BE' # actually UCS-4 else: - c_encoding = 'UCS-4LE' + c_encoding = 'UTF-32LE' # actually UCS-4 else: assert False, f"Illegal Unicode kind {c_kind}" else: + # old Py_UNICODE string py_buffer_len = python.PyUnicode_GET_DATA_SIZE(utext) c_text = python.PyUnicode_AS_DATA(utext) assert 0 <= py_buffer_len <= limits.INT_MAX @@ -1062,10 +1306,9 @@ cdef class _BaseParser: context.prepare() try: pctxt = context._c_ctxt - __GLOBAL_PARSER_CONTEXT.initParserDict(pctxt) orig_options = pctxt.options with nogil: - if self._for_html: + if self._flags.for_html: result = htmlparser.htmlCtxtReadMemory( pctxt, c_text, buffer_len, c_filename, c_encoding, self._parse_options) @@ -1083,48 +1326,28 @@ cdef class _BaseParser: finally: context.cleanup() - cdef xmlDoc* _parseDoc(self, char* c_text, int c_len, - char* c_filename) except NULL: - u"""Parse document, share dictionary if possible. + cdef xmlDoc* _parseDoc(self, const char* c_text, int c_len, const char* c_filename) except NULL: + """Parse document, share dictionary if possible. """ cdef _ParserContext context cdef xmlDoc* result cdef xmlparser.xmlParserCtxt* pctxt - cdef char* c_encoding + cdef const char* c_encoding cdef tree.xmlCharEncoding enc context = self._getParserContext() context.prepare() try: - pctxt = context._c_ctxt - __GLOBAL_PARSER_CONTEXT.initParserDict(pctxt) - - if self._default_encoding is None: - c_encoding = NULL - # libxml2 (at least 2.9.3) does not recognise UTF-32 BOMs - # NOTE: limit to problematic cases because it changes character offsets - if c_len >= 4 and (c_text[0] == '\xFF' and c_text[1] == '\xFE' and - c_text[2] == 0 and c_text[3] == 0): - c_encoding = "UTF-32LE" - c_text += 4 - c_len -= 4 - elif c_len >= 4 and (c_text[0] == 0 and c_text[1] == 0 and - c_text[2] == '\xFE' and c_text[3] == '\xFF'): - c_encoding = "UTF-32BE" - c_text += 4 - c_len -= 4 - else: - # no BOM => try to determine encoding - enc = tree.xmlDetectCharEncoding(c_text, c_len) - if enc == tree.XML_CHAR_ENCODING_UCS4LE: - c_encoding = 'UTF-32LE' - elif enc == tree.XML_CHAR_ENCODING_UCS4BE: - c_encoding = 'UTF-32BE' - else: + if self._default_encoding is not None: c_encoding = _cstr(self._default_encoding) + else: + bom_offset = _detectBOMEncoding(c_text, c_len, &c_encoding) + c_text += bom_offset + c_len -= bom_offset + pctxt = context._c_ctxt orig_options = pctxt.options with nogil: - if self._for_html: + if self._flags.for_html: result = htmlparser.htmlCtxtReadMemory( pctxt, c_text, c_len, c_filename, c_encoding, self._parse_options) @@ -1142,27 +1365,23 @@ cdef class _BaseParser: finally: context.cleanup() - cdef xmlDoc* _parseDocFromFile(self, char* c_filename) except NULL: + cdef xmlDoc* _parseDocFromFile(self, const char* c_filename) except NULL: cdef _ParserContext context cdef xmlDoc* result cdef xmlparser.xmlParserCtxt* pctxt - cdef char* c_encoding + cdef const char* c_encoding = NULL result = NULL context = self._getParserContext() context.prepare() try: - pctxt = context._c_ctxt - __GLOBAL_PARSER_CONTEXT.initParserDict(pctxt) - - if self._default_encoding is None: - c_encoding = NULL - else: + if self._default_encoding is not None: c_encoding = _cstr(self._default_encoding) + pctxt = context._c_ctxt orig_options = pctxt.options with nogil: - if self._for_html: + if self._flags.for_html: result = htmlparser.htmlCtxtReadFile( pctxt, c_filename, c_encoding, self._parse_options) if result is not NULL: @@ -1178,25 +1397,20 @@ cdef class _BaseParser: finally: context.cleanup() - cdef xmlDoc* _parseDocFromFilelike(self, filelike, filename, - encoding) except NULL: + cdef xmlDoc* _parseDocFromFilelike(self, filelike, filename, encoding) except NULL: cdef _ParserContext context cdef _FileReaderContext file_context cdef xmlDoc* result - cdef xmlparser.xmlParserCtxt* pctxt - cdef char* c_filename if not filename: filename = None context = self._getParserContext() context.prepare() try: - pctxt = context._c_ctxt - __GLOBAL_PARSER_CONTEXT.initParserDict(pctxt) file_context = _FileReaderContext( filelike, context, filename, encoding or self._default_encoding) - result = file_context._readDoc(pctxt, self._parse_options) + result = file_context._readDoc(context._c_ctxt, self._parse_options) return context._handleParseResultDoc( self, result, filename) @@ -1204,7 +1418,59 @@ cdef class _BaseParser: context.cleanup() -cdef void _initSaxDocument(void* ctxt) with gil: +cdef tree.xmlEntity* _getInternalEntityOnly(void* ctxt, const_xmlChar* name) noexcept nogil: + """ + Callback function to intercept the entity resolution when external entity loading is disabled. + """ + cdef tree.xmlEntity* entity = xmlparser.xmlSAX2GetEntity(ctxt, name) + if not entity: + return NULL + if entity.etype not in ( + tree.xmlEntityType.XML_EXTERNAL_GENERAL_PARSED_ENTITY, + tree.xmlEntityType.XML_EXTERNAL_GENERAL_UNPARSED_ENTITY, + tree.xmlEntityType.XML_EXTERNAL_PARAMETER_ENTITY): + return entity + + # Reject all external entities and fail the parsing instead. There is currently + # no way in libxml2 to just prevent the entity resolution in this case. + cdef xmlerror.xmlError c_error + cdef xmlerror.xmlStructuredErrorFunc err_func + cdef xmlparser.xmlParserInput* parser_input + cdef void* err_context + + c_ctxt = ctxt + err_func = xmlerror.xmlStructuredError + if err_func: + parser_input = c_ctxt.input + # Copied from xmlVErrParser() in libxml2: get current input from stack. + if parser_input and parser_input.filename is NULL and c_ctxt.inputNr > 1: + parser_input = c_ctxt.inputTab[c_ctxt.inputNr - 2] + + c_error = xmlerror.xmlError( + domain=xmlerror.xmlErrorDomain.XML_FROM_PARSER, + code=xmlerror.xmlParserErrors.XML_ERR_EXT_ENTITY_STANDALONE, + level=xmlerror.xmlErrorLevel.XML_ERR_FATAL, + message=b"External entity resolution is disabled for security reasons " + b"when resolving '&%s;'. Use 'XMLParser(resolve_entities=True)' " + b"if you consider it safe to enable it.", + file= parser_input.filename, + node=entity, + str1= name, + str2=NULL, + str3=NULL, + line=parser_input.line if parser_input else 0, + int1=0, + int2=parser_input.col if parser_input else 0, + ) + err_context = xmlerror.xmlStructuredErrorContext + err_func(err_context, &c_error) + + c_ctxt.wellFormed = 0 + # The entity was looked up and does not need to be freed. + return NULL + + +cdef void _initSaxDocument(void* ctxt) noexcept with gil: xmlparser.xmlSAX2StartDocument(ctxt) c_ctxt = ctxt c_doc = c_ctxt.myDoc @@ -1213,8 +1479,8 @@ cdef void _initSaxDocument(void* ctxt) with gil: if c_doc and c_ctxt.dict and not c_doc.dict: # I have no idea why libxml2 disables this - we need it c_ctxt.dictNames = 1 + tree.xmlDictReference(c_ctxt.dict) c_doc.dict = c_ctxt.dict - xmlparser.xmlDictReference(c_ctxt.dict) # set up XML ID hash table if c_ctxt._private: @@ -1223,10 +1489,11 @@ cdef void _initSaxDocument(void* ctxt) with gil: # keep the global parser dict from filling up with XML IDs if c_doc and not c_doc.ids: # memory errors are not fatal here - c_dict = xmlparser.xmlDictCreate() + c_dict = tree.xmlDictCreate() if c_dict: + tree.xmlDictSetLimit(c_dict, 0) c_doc.ids = tree.xmlHashCreateDict(0, c_dict) - xmlparser.xmlDictFree(c_dict) + tree.xmlDictFree(c_dict) else: c_doc.ids = tree.xmlHashCreate(0) else: @@ -1237,12 +1504,40 @@ cdef void _initSaxDocument(void* ctxt) with gil: c_doc.ids = NULL +cdef int _detectBOMEncoding(const char* c_text, int c_len, const char** c_encoding): + """Detect the text encoding and return the BOM length to skip, if any.""" + # libxml2 (at least 2.9.3) does not recognise UTF-32 BOMs + # NOTE: limit to problematic cases because it changes character offsets + cdef const char* c_enc = NULL + cdef int offset = 0 + if c_len >= 4 and (c_text[0] == b'\xFF' and c_text[1] == b'\xFE' and + c_text[2] == 0 and c_text[3] == 0): + c_enc = "UTF-32LE" + offset = 4 + elif c_len >= 4 and (c_text[0] == 0 and c_text[1] == 0 and + c_text[2] == b'\xFE' and c_text[3] == b'\xFF'): + c_enc = "UTF-32BE" + offset = 4 + else: + # no BOM => try to determine encoding + enc = tree.xmlDetectCharEncoding(c_text, c_len) + if enc == tree.XML_CHAR_ENCODING_UCS4LE: + c_enc = 'UTF-32LE' + elif enc == tree.XML_CHAR_ENCODING_UCS4BE: + c_enc = 'UTF-32BE' + + if c_encoding is not NULL: + c_encoding[0] = c_enc + return offset + + ############################################################ ## ET feed parser ############################################################ cdef class _FeedParser(_BaseParser): cdef bint _feed_parser_running + cdef cython.pymutex _feed_lock @property def feed_error_log(self): @@ -1253,8 +1548,8 @@ cdef class _FeedParser(_BaseParser): """ return self._getPushParserContext()._error_log.copy() - cpdef feed(self, data): - u"""feed(self, data) + def feed(self, data): + """feed(self, data) Feeds data to the parser. The argument should be an 8-bit string buffer containing encoded data, although Unicode is supported as long @@ -1271,6 +1566,11 @@ cdef class _FeedParser(_BaseParser): usage. You can use the same parser as a feed parser and in the ``parse()`` function concurrently. """ + with self._feed_lock: + self._feed(data) + + @cython.final + cdef _feed(self, data): cdef _ParserContext context cdef bytes bstring cdef xmlparser.xmlParserCtxt* pctxt @@ -1287,7 +1587,7 @@ cdef class _FeedParser(_BaseParser): else: c_encoding = self._default_encoding char_data = _cstr(data) - py_buffer_len = python.PyBytes_GET_SIZE(data) + py_buffer_len = len( data) ustart = 0 elif isinstance(data, unicode): c_encoding = b"UTF-8" @@ -1295,7 +1595,7 @@ cdef class _FeedParser(_BaseParser): py_buffer_len = len( data) ustart = 0 else: - raise TypeError, u"Parsing requires string data" + raise TypeError, "Parsing requires string data" context = self._getPushParserContext() pctxt = context._c_ctxt @@ -1313,8 +1613,8 @@ cdef class _FeedParser(_BaseParser): buffer_len = 0 if char_data is not NULL: buffer_len = 4 if py_buffer_len > 4 else py_buffer_len - orig_loader = _register_document_loader() - if self._for_html: + orig_loader = _register_resource_loader() + if self._flags.for_html: error = _htmlCtxtResetPush( pctxt, char_data, buffer_len, c_filename, c_encoding, self._parse_options) @@ -1322,12 +1622,11 @@ cdef class _FeedParser(_BaseParser): xmlparser.xmlCtxtUseOptions(pctxt, self._parse_options) error = xmlparser.xmlCtxtResetPush( pctxt, char_data, buffer_len, c_filename, c_encoding) - _reset_document_loader(orig_loader) + _reset_resource_loader(orig_loader) py_buffer_len -= buffer_len char_data += buffer_len if error: raise MemoryError() - __GLOBAL_PARSER_CONTEXT.initParserDict(pctxt) #print pctxt.charset, 'NONE' if c_encoding is NULL else c_encoding @@ -1365,7 +1664,7 @@ cdef class _FeedParser(_BaseParser): else: error = 0 - if not pctxt.wellFormed and pctxt.disableSAX and context._has_raised(): + if not pctxt.wellFormed and xmlparser.xmlCtxtIsStopped(pctxt) and context._has_raised(): # propagate Python exceptions immediately recover = 0 error = 1 @@ -1377,8 +1676,8 @@ cdef class _FeedParser(_BaseParser): finally: context.cleanup() - cpdef close(self): - u"""close(self) + def close(self): + """close(self) Terminates feeding data to this parser. This tells the parser to process any remaining data in the feed buffer, and then returns the @@ -1388,8 +1687,13 @@ cdef class _FeedParser(_BaseParser): the ``feed()`` method. It should only be called when using the feed parser interface, all other usage is undefined. """ + with self._feed_lock: + return self._close() + + @cython.final + cdef _close(self): if not self._feed_parser_running: - raise XMLSyntaxError(u"no element found", + raise XMLSyntaxError("no element found", xmlerror.XML_ERR_INTERNAL_ERROR, 0, 0, self._filename) @@ -1397,12 +1701,12 @@ cdef class _FeedParser(_BaseParser): pctxt = context._c_ctxt self._feed_parser_running = 0 - if self._for_html: + if self._flags.for_html: htmlparser.htmlParseChunk(pctxt, NULL, 0, 1) else: xmlparser.xmlParseChunk(pctxt, NULL, 0, 1) - if (pctxt.recovery and not pctxt.disableSAX and + if (pctxt.recovery and not xmlparser.xmlCtxtIsStopped(pctxt) and isinstance(context, _SaxParserContext)): # apply any left-over 'end' events (<_SaxParserContext>context).flushEvents() @@ -1421,24 +1725,26 @@ cdef class _FeedParser(_BaseParser): cdef (int, int) _parse_data_chunk(xmlparser.xmlParserCtxt* c_ctxt, const char* char_data, int buffer_len): fixup_error = 0 - with nogil: - if c_ctxt.html: - c_node = c_ctxt.node # last node where the parser stopped - orig_loader = _register_document_loader() + if c_ctxt.html: + c_node = c_ctxt.node # last node where the parser stopped + with nogil: + old_loader = _register_resource_loader() error = htmlparser.htmlParseChunk(c_ctxt, char_data, buffer_len, 0) - _reset_document_loader(orig_loader) - # and now for the fun part: move node names to the dict - if c_ctxt.myDoc: + _reset_resource_loader(old_loader) + # and now for the fun part: move node names to the dict + if c_ctxt.myDoc: + with nogil: fixup_error = _fixHtmlDictSubtreeNames( c_ctxt.dict, c_ctxt.myDoc, c_node) if c_ctxt.myDoc.dict and c_ctxt.myDoc.dict is not c_ctxt.dict: - xmlparser.xmlDictFree(c_ctxt.myDoc.dict) + tree.xmlDictReference(c_ctxt.dict) + tree.xmlDictFree(c_ctxt.myDoc.dict) c_ctxt.myDoc.dict = c_ctxt.dict - xmlparser.xmlDictReference(c_ctxt.dict) - else: - orig_loader = _register_document_loader() + else: + with nogil: + old_loader = _register_resource_loader() error = xmlparser.xmlParseChunk(c_ctxt, char_data, buffer_len, 0) - _reset_document_loader(orig_loader) + _reset_resource_loader(old_loader) return (error, fixup_error) @@ -1454,7 +1760,8 @@ cdef int _htmlCtxtResetPush(xmlparser.xmlParserCtxt* c_ctxt, return error # fix libxml2 setup for HTML - c_ctxt.progressive = 1 + if tree.LIBXML_VERSION < 21400: + c_ctxt.progressive = 1 # TODO: remove c_ctxt.html = 1 htmlparser.htmlCtxtUseOptions(c_ctxt, parse_options) @@ -1472,10 +1779,15 @@ _XML_DEFAULT_PARSE_OPTIONS = ( xmlparser.XML_PARSE_NONET | xmlparser.XML_PARSE_COMPACT | xmlparser.XML_PARSE_BIG_LINES - ) +) cdef class XMLParser(_FeedParser): - u"""XMLParser(self, encoding=None, attribute_defaults=False, dtd_validation=False, load_dtd=False, no_network=True, ns_clean=False, recover=False, schema: XMLSchema =None, huge_tree=False, remove_blank_text=False, resolve_entities=True, remove_comments=False, remove_pis=False, strip_cdata=True, collect_ids=True, target=None, compact=True) + """XMLParser(self, encoding=None, attribute_defaults=False, dtd_validation=False, \ + load_dtd=False, no_network=True, decompress=False, ns_clean=False, \ + recover=False, schema: XMLSchema =None, huge_tree=False, \ + remove_blank_text=False, resolve_entities='internal', \ + remove_comments=False, remove_pis=False, strip_cdata=True, \ + collect_ids=True, target=None, compact=True) The XML parser. @@ -1491,41 +1803,51 @@ cdef class XMLParser(_FeedParser): additionally provide an XMLSchema from which the default attributes can be read). - Available boolean keyword arguments: - - - attribute_defaults - inject default attributes from DTD or XMLSchema - - dtd_validation - validate against a DTD referenced by the document - - load_dtd - use DTD for parsing - - no_network - prevent network access for related files (default: True) - - ns_clean - clean up redundant namespace declarations - - recover - try hard to parse through broken XML - - remove_blank_text - discard blank text nodes that appear ignorable - - remove_comments - discard comments - - remove_pis - discard processing instructions - - strip_cdata - replace CDATA sections by normal text content (default: True) - - compact - save memory for short text content (default: True) - - collect_ids - use a hash table of XML IDs for fast access (default: True, always True with DTD validation) - - resolve_entities - replace entities by their text value (default: True) - - huge_tree - disable security restrictions and support very deep trees - and very long text content (only affects libxml2 2.7+) - - Other keyword arguments: - - - encoding - override the document encoding - - target - a parser target object that will receive the parse events - - schema - an XMLSchema to validate against + **Available boolean keyword arguments:** + + - attribute_defaults: inject default attributes from DTD or XMLSchema + - dtd_validation: validate against a DTD referenced by the document + - load_dtd: use DTD for parsing + - no_network: prevent network access for related files (default: True) + - decompress: automatically decompress gzip input (default: False, + changed in lxml 6.0, disabling only affects libxml2 2.15+) + - ns_clean: clean up redundant namespace declarations + - recover: try hard to parse through broken XML + - remove_blank_text: discard blank text nodes that appear ignorable. + Without DTD/schema, a heuristic preserves blank text nodes appearing + after non-blank content at the same level. + - remove_comments: discard comments + - remove_pis: discard processing instructions + - strip_cdata: replace CDATA sections by normal text content + (default: True) + - compact: save memory for short text content (default: True) + - collect_ids: use a hash table of XML IDs for fast access + (default: True, always True with DTD validation) + - huge_tree: disable security restrictions and support very deep trees + and very long text content + + **Other keyword arguments:** + + - resolve_entities: replace entities by their text value. False for keeping + the entity references, True for resolving them, and 'internal' for resolving + internal definitions only (no external file/URL access). The default used + to be True and was changed to 'internal' in lxml 5.0. + - encoding: override the document encoding (note: libiconv encoding name) + - target: a parser target object that will receive the parse events + - schema: an XMLSchema to validate against Note that you should avoid sharing parsers between threads. While this is not harmful, it is more efficient to use separate parsers. This does not apply to the default parser. """ def __init__(self, *, encoding=None, attribute_defaults=False, - dtd_validation=False, load_dtd=False, no_network=True, + dtd_validation=False, load_dtd=False, no_network=True, decompress=False, ns_clean=False, recover=False, XMLSchema schema=None, - huge_tree=False, remove_blank_text=False, resolve_entities=True, + huge_tree=False, remove_blank_text=False, resolve_entities='internal', remove_comments=False, remove_pis=False, strip_cdata=True, collect_ids=True, target=None, compact=True): cdef int parse_options + cdef bint resolve_external = True parse_options = _XML_DEFAULT_PARSE_OPTIONS if load_dtd: parse_options = parse_options | xmlparser.XML_PARSE_DTDLOAD @@ -1550,12 +1872,20 @@ cdef class XMLParser(_FeedParser): parse_options = parse_options ^ xmlparser.XML_PARSE_COMPACT if not resolve_entities: parse_options = parse_options ^ xmlparser.XML_PARSE_NOENT + elif resolve_entities == 'internal': + resolve_external = False if not strip_cdata: parse_options = parse_options ^ xmlparser.XML_PARSE_NOCDATA + if decompress: + parse_options |= xmlparser.XML_PARSE_UNZIP - _BaseParser.__init__(self, parse_options, 0, schema, + _BaseParser.__init__(self, parse_options, False, schema, remove_comments, remove_pis, strip_cdata, - collect_ids, target, encoding) + collect_ids, target, encoding, resolve_external) + + # Allow subscripting XMLParser in type annotations (PEP 560) + def __class_getitem__(cls, item): + return _GenericAlias(cls, item) cdef class XMLPullParser(XMLParser): @@ -1588,10 +1918,10 @@ cdef class XMLPullParser(XMLParser): cdef class ETCompatXMLParser(XMLParser): - u"""ETCompatXMLParser(self, encoding=None, attribute_defaults=False, \ - dtd_validation=False, load_dtd=False, no_network=True, \ + """ETCompatXMLParser(self, encoding=None, attribute_defaults=False, \ + dtd_validation=False, load_dtd=False, no_network=True, decompress=False, \ ns_clean=False, recover=False, schema=None, \ - huge_tree=False, remove_blank_text=False, resolve_entities=True, \ + huge_tree=False, remove_blank_text=False, resolve_entities='internal', \ remove_comments=True, remove_pis=True, strip_cdata=True, \ target=None, compact=True) @@ -1601,11 +1931,14 @@ cdef class ETCompatXMLParser(XMLParser): This parser has ``remove_comments`` and ``remove_pis`` enabled by default and thus ignores comments and processing instructions. + + The default value of ``resolve_entities`` used to be True and was changed to + 'internal' in lxml 6.1. """ def __init__(self, *, encoding=None, attribute_defaults=False, - dtd_validation=False, load_dtd=False, no_network=True, + dtd_validation=False, load_dtd=False, no_network=True, decompress=False, ns_clean=False, recover=False, schema=None, - huge_tree=False, remove_blank_text=False, resolve_entities=True, + huge_tree=False, remove_blank_text=False, resolve_entities='internal', remove_comments=True, remove_pis=True, strip_cdata=True, target=None, compact=True): XMLParser.__init__(self, @@ -1613,6 +1946,7 @@ cdef class ETCompatXMLParser(XMLParser): dtd_validation=dtd_validation, load_dtd=load_dtd, no_network=no_network, + decompress=decompress, ns_clean=ns_clean, recover=recover, remove_blank_text=remove_blank_text, @@ -1624,7 +1958,8 @@ cdef class ETCompatXMLParser(XMLParser): strip_cdata=strip_cdata, target=target, encoding=encoding, - schema=schema) + schema=schema, + ) # ET 1.2 compatible name XMLTreeBuilder = ETCompatXMLParser @@ -1636,7 +1971,7 @@ __DEFAULT_XML_PARSER = XMLParser() __GLOBAL_PARSER_CONTEXT.setDefaultParser(__DEFAULT_XML_PARSER) def set_default_parser(_BaseParser parser=None): - u"""set_default_parser(parser=None) + """set_default_parser(parser=None) Set a default parser for the current thread. This parser is used globally whenever no parser is supplied to the various parse functions of @@ -1652,7 +1987,7 @@ def set_default_parser(_BaseParser parser=None): __GLOBAL_PARSER_CONTEXT.setDefaultParser(parser) def get_default_parser(): - u"get_default_parser()" + "get_default_parser()" return __GLOBAL_PARSER_CONTEXT.getDefaultParser() ############################################################ @@ -1666,10 +2001,12 @@ _HTML_DEFAULT_PARSE_OPTIONS = ( htmlparser.HTML_PARSE_COMPACT ) +cdef object _UNUSED = object() + cdef class HTMLParser(_FeedParser): - u"""HTMLParser(self, encoding=None, remove_blank_text=False, \ - remove_comments=False, remove_pis=False, strip_cdata=True, \ - no_network=True, target=None, schema: XMLSchema =None, \ + """HTMLParser(self, encoding=None, remove_blank_text=False, \ + remove_comments=False, remove_pis=False, \ + no_network=True, decompress=False, target=None, schema: XMLSchema =None, \ recover=True, compact=True, collect_ids=True, huge_tree=False) The HTML parser. @@ -1679,32 +2016,36 @@ cdef class HTMLParser(_FeedParser): the capabilities of libxml2. Use the 'recover' option to switch this off. - Available boolean keyword arguments: - - - recover - try hard to parse through broken HTML (default: True) - - no_network - prevent network access for related files (default: True) - - remove_blank_text - discard empty text nodes that are ignorable (i.e. not actual text content) - - remove_comments - discard comments - - remove_pis - discard processing instructions - - strip_cdata - replace CDATA sections by normal text content (default: True) - - compact - save memory for short text content (default: True) - - default_doctype - add a default doctype even if it is not found in the HTML (default: True) - - collect_ids - use a hash table of XML IDs for fast access (default: True) - - huge_tree - disable security restrictions and support very deep trees - and very long text content (only affects libxml2 2.7+) - - Other keyword arguments: - - - encoding - override the document encoding - - target - a parser target object that will receive the parse events - - schema - an XMLSchema to validate against + **Available boolean keyword arguments:** + + - recover: try hard to parse through broken HTML (default: True) + - no_network: prevent network access for related files (default: True) + - decompress: automatically decompress gzip input (default: False, + changed in lxml 6.0, disabling only affects libxml2 2.15+) + - remove_blank_text: discard empty text nodes that are ignorable (i.e. + not actual text content). Preserves spacing in mixed-content elements + per HTML structural rules. + - remove_comments: discard comments + - remove_pis: discard processing instructions + - compact: save memory for short text content (default: True) + - default_doctype: add a default doctype even if it is not found in the + HTML (default: True) + - collect_ids: use a hash table of XML IDs for fast access (default: True) + - huge_tree: disable security restrictions and support very deep trees and + very long text content + + **Other keyword arguments:** + + - encoding: override the document encoding (note: libiconv encoding name) + - target: a parser target object that will receive the parse events + - schema: an XMLSchema to validate against Note that you should avoid sharing parsers between threads for performance reasons. """ def __init__(self, *, encoding=None, remove_blank_text=False, - remove_comments=False, remove_pis=False, strip_cdata=True, - no_network=True, target=None, XMLSchema schema=None, + remove_comments=False, remove_pis=False, strip_cdata=_UNUSED, + no_network=True, decompress=False, target=None, XMLSchema schema=None, recover=True, compact=True, default_doctype=True, collect_ids=True, huge_tree=False): cdef int parse_options @@ -1721,11 +2062,22 @@ cdef class HTMLParser(_FeedParser): parse_options = parse_options ^ htmlparser.HTML_PARSE_NODEFDTD if huge_tree: parse_options = parse_options | xmlparser.XML_PARSE_HUGE - - _BaseParser.__init__(self, parse_options, 1, schema, + if decompress: + parse_options |= xmlparser.XML_PARSE_UNZIP + + if strip_cdata is not _UNUSED: + import warnings + warnings.warn( + "The 'strip_cdata' option of HTMLParser() has never done anything and will eventually be removed.", + DeprecationWarning) + _BaseParser.__init__(self, parse_options, True, schema, remove_comments, remove_pis, strip_cdata, collect_ids, target, encoding) + # Allow subscripting HTMLParser in type annotations (PEP 560) + def __class_getitem__(cls, item): + return _GenericAlias(cls, item) + cdef HTMLParser __DEFAULT_HTML_PARSER __DEFAULT_HTML_PARSER = HTMLParser() @@ -1765,10 +2117,7 @@ cdef class HTMLPullParser(HTMLParser): ############################################################ cdef xmlDoc* _parseDoc(text, filename, _BaseParser parser) except NULL: - cdef char* c_filename - cdef char* c_text - cdef Py_ssize_t c_len - cdef bint is_pep393_string + cdef const char* c_filename if parser is None: parser = __GLOBAL_PARSER_CONTEXT.getDefaultParser() if not filename: @@ -1776,40 +2125,59 @@ cdef xmlDoc* _parseDoc(text, filename, _BaseParser parser) except NULL: else: filename_utf = _encodeFilenameUTF8(filename) c_filename = _cstr(filename_utf) - if isinstance(text, unicode): - is_pep393_string = ( - python.PEP393_ENABLED and python.PyUnicode_IS_READY(text)) - if is_pep393_string: - c_len = python.PyUnicode_GET_LENGTH(text) * python.PyUnicode_KIND(text) - else: - c_len = python.PyUnicode_GET_DATA_SIZE(text) - if c_len > limits.INT_MAX: - return (<_BaseParser>parser)._parseDocFromFilelike( - StringIO(text), filename, None) - if _PY_UNICODE_ENCODING is NULL and not is_pep393_string: - text = (text).encode('utf8') - return (<_BaseParser>parser)._parseDocFromFilelike( - BytesIO(text), filename, "UTF-8") - return (<_BaseParser>parser)._parseUnicodeDoc(text, c_filename) + if isinstance(text, bytes): + if python.IN_LIMITED_API: + return parser._parseDocFromFilelike(BytesIO(text), filename, None) + return _parseDoc_bytes( text, filename, c_filename, parser) + elif isinstance(text, unicode): + if python.IN_LIMITED_API: + return parser._parseDocFromFilelike(_UnicodeStringReader(text), filename, b'utf-8') + return _parseDoc_unicode( text, filename, c_filename, parser) + else: + return _parseDoc_charbuffer(text, filename, c_filename, parser) + + +cdef xmlDoc* _parseDoc_unicode(text: str, filename, const char* c_filename, _BaseParser parser) except NULL: + cdef Py_ssize_t c_len + if python.PyUnicode_IS_READY(text): + # PEP-393 Unicode string + c_len = python.PyUnicode_GET_LENGTH(text) * python.PyUnicode_KIND(text) else: - c_len = python.PyBytes_GET_SIZE(text) - if c_len > limits.INT_MAX: - return (<_BaseParser>parser)._parseDocFromFilelike( - BytesIO(text), filename, None) - c_text = _cstr(text) - return (<_BaseParser>parser)._parseDoc(c_text, c_len, c_filename) + # old Py_UNICODE string + c_len = python.PyUnicode_GET_DATA_SIZE(text) + if c_len > limits.INT_MAX: + return parser._parseDocFromFilelike(_UnicodeStringReader(text), filename, None) + return parser._parseUnicodeDoc(text, c_filename) + + +cdef xmlDoc* _parseDoc_bytes(text: bytes, filename, const char* c_filename, _BaseParser parser) except NULL: + cdef Py_ssize_t c_len = len(text) + if c_len > limits.INT_MAX: + return parser._parseDocFromFilelike(BytesIO(text), filename, None) + return parser._parseDoc(text, c_len, c_filename) + + +cdef xmlDoc* _parseDoc_charbuffer(text, filename, const char* c_filename, _BaseParser parser) except NULL: + cdef const unsigned char[::1] data = memoryview(text).cast('B') # cast to 'unsigned char' buffer + cdef Py_ssize_t c_len = len(data) + if c_len > limits.INT_MAX: + return parser._parseDocFromFilelike(BytesIO(text), filename, None) + return parser._parseDoc(&data[0], c_len, c_filename) + cdef xmlDoc* _parseDocFromFile(filename8, _BaseParser parser) except NULL: if parser is None: parser = __GLOBAL_PARSER_CONTEXT.getDefaultParser() return (<_BaseParser>parser)._parseDocFromFile(_cstr(filename8)) + cdef xmlDoc* _parseDocFromFilelike(source, filename, _BaseParser parser) except NULL: if parser is None: parser = __GLOBAL_PARSER_CONTEXT.getDefaultParser() return (<_BaseParser>parser)._parseDocFromFilelike(source, filename, None) + cdef xmlDoc* _newXMLDoc() except NULL: cdef xmlDoc* result result = tree.xmlNewDoc(NULL) @@ -1817,7 +2185,6 @@ cdef xmlDoc* _newXMLDoc() except NULL: raise MemoryError() if result.encoding is NULL: result.encoding = tree.xmlStrdup("UTF-8") - __GLOBAL_PARSER_CONTEXT.initDocDict(result) return result cdef xmlDoc* _newHTMLDoc() except NULL: @@ -1825,10 +2192,11 @@ cdef xmlDoc* _newHTMLDoc() except NULL: result = tree.htmlNewDoc(NULL, NULL) if result is NULL: raise MemoryError() - __GLOBAL_PARSER_CONTEXT.initDocDict(result) return result + cdef xmlDoc* _copyDoc(xmlDoc* c_doc, int recursive) except NULL: + """Return a copy of c_doc, without moving the names into the dict.""" cdef xmlDoc* result if recursive: with nogil: @@ -1837,27 +2205,40 @@ cdef xmlDoc* _copyDoc(xmlDoc* c_doc, int recursive) except NULL: result = tree.xmlCopyDoc(c_doc, 0) if result is NULL: raise MemoryError() - __GLOBAL_PARSER_CONTEXT.initDocDict(result) + tree.xmlDictReference(c_doc.dict) + result.dict = c_doc.dict return result + cdef xmlDoc* _copyDocRoot(xmlDoc* c_doc, xmlNode* c_new_root) except NULL: - u"Recursively copy the document and make c_new_root the new root node." + """Recursively copy the document and make c_new_root the new root node.""" cdef xmlDoc* result cdef xmlNode* c_node result = tree.xmlCopyDoc(c_doc, 0) # non recursive - __GLOBAL_PARSER_CONTEXT.initDocDict(result) + if not result: + raise MemoryError() + + assert result.dict is NULL + tree.xmlDictReference(c_doc.dict) + result.dict = c_doc.dict + with nogil: c_node = tree.xmlDocCopyNode(c_new_root, result, 1) # recursive if c_node is NULL: + tree.xmlFreeDoc(result) raise MemoryError() + tree.xmlDocSetRootElement(result, c_node) + # Copy the tail text after setting the root element since libxml2 otherwise unlinks the tail. _copyTail(c_new_root.next, c_node) return result + cdef xmlNode* _copyNodeToDoc(xmlNode* c_node, xmlDoc* c_doc) except NULL: - u"Recursively copy the element into the document. c_doc is not modified." + """Recursively copy the element into the document. c_doc is not modified.""" cdef xmlNode* c_root - c_root = tree.xmlDocCopyNode(c_node, c_doc, 1) # recursive + with nogil: + c_root = tree.xmlDocCopyNode(c_node, c_doc, 1) # recursive if c_root is NULL: raise MemoryError() _copyTail(c_node.next, c_root) @@ -1887,16 +2268,16 @@ cdef _Document _parseDocument(source, _BaseParser parser, base_url): else: url = _getFilenameForFile(source) - if hasattr(source, u'getvalue') and hasattr(source, u'tell'): + if hasattr(source, 'getvalue') and hasattr(source, 'tell'): # StringIO - reading from start? if source.tell() == 0: return _parseMemoryDocument(source.getvalue(), url, parser) # Support for file-like objects (urlgrabber.urlopen, ...) - if hasattr(source, u'read'): + if hasattr(source, 'read'): return _parseFilelikeDocument(source, url, parser) - raise TypeError, f"cannot parse from '{python._fqtypename(source).decode('UTF-8')}'" + raise TypeError, f"cannot parse from '{python._fqtypename(source)}'" cdef _Document _parseDocumentFromURL(url, _BaseParser parser): c_doc = _parseDocFromFile(url, parser) @@ -1906,10 +2287,8 @@ cdef _Document _parseMemoryDocument(text, url, _BaseParser parser): if isinstance(text, unicode): if _hasEncodingDeclaration(text): raise ValueError( - u"Unicode strings with encoding declaration are not supported. " - u"Please use bytes input or XML fragments without declaration.") - elif not isinstance(text, bytes): - raise ValueError, u"can only parse strings" + "Unicode strings with encoding declaration are not supported. " + "Please use bytes input or XML fragments without declaration.") c_doc = _parseDoc(text, url, parser) return _documentFactory(c_doc, parser) diff --git a/src/lxml/parsertarget.pxi b/src/lxml/parsertarget.pxi index 941e03229..9b31c3e3a 100644 --- a/src/lxml/parsertarget.pxi +++ b/src/lxml/parsertarget.pxi @@ -119,15 +119,14 @@ cdef class _PythonSaxParserTarget(_SaxParserTarget): @cython.final @cython.internal -@cython.no_gc_clear # Required because parent class uses it - Cython bug. cdef class _TargetParserContext(_SaxParserContext): - u"""This class maps SAX2 events to the ET parser target interface. + """This class maps SAX2 events to the ET parser target interface. """ cdef object _python_target cdef int _setTarget(self, target) except -1: self._python_target = target if not isinstance(target, _SaxParserTarget) or \ - hasattr(target, u'__dict__'): + hasattr(target, '__dict__'): target = _PythonSaxParserTarget(target) self._setSaxParserTarget(target) return 0 @@ -138,7 +137,7 @@ cdef class _TargetParserContext(_SaxParserContext): context._setTarget(self._python_target) return context - cdef void _cleanupTargetParserContext(self, xmlDoc* result): + cdef void _cleanupTargetParserContext(self, xmlDoc* result) noexcept: if self._c_ctxt.myDoc is not NULL: if self._c_ctxt.myDoc is not result and \ self._c_ctxt.myDoc._private is NULL: @@ -157,15 +156,8 @@ cdef class _TargetParserContext(_SaxParserContext): if not self._c_ctxt.wellFormed and not recover: _raiseParseError(self._c_ctxt, filename, self._error_log) except: - if python.IS_PYTHON2: - exc = sys.exc_info() - # Python 2 can't chain exceptions - try: self._python_target.close() - except: pass - raise exc[0], exc[1], exc[2] - else: - self._python_target.close() - raise + self._python_target.close() + raise return self._python_target.close() cdef xmlDoc* _handleParseResultDoc(self, _BaseParser parser, @@ -181,14 +173,7 @@ cdef class _TargetParserContext(_SaxParserContext): if not self._c_ctxt.wellFormed and not recover: _raiseParseError(self._c_ctxt, filename, self._error_log) except: - if python.IS_PYTHON2: - exc = sys.exc_info() - # Python 2 can't chain exceptions - try: self._python_target.close() - except: pass - raise exc[0], exc[1], exc[2] - else: - self._python_target.close() - raise + self._python_target.close() + raise parse_result = self._python_target.close() raise _TargetParserResult(parse_result) diff --git a/src/lxml/proxy.pxi b/src/lxml/proxy.pxi index 3c6e30689..7fda0de79 100644 --- a/src/lxml/proxy.pxi +++ b/src/lxml/proxy.pxi @@ -4,48 +4,80 @@ # structure of the respective node to avoid multiple instantiation of # the Python class. -@cython.linetrace(False) -@cython.profile(False) -cdef inline _Element getProxy(xmlNode* c_node): - u"""Get a proxy for a given node. +cdef extern from *: """ - #print "getProxy for:", c_node - if c_node is not NULL and c_node._private is not NULL: - return <_Element>c_node._private - else: - return None + #if !(CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX >= 0x030e0000 && defined(Py_GIL_DISABLED)) + #define PyUnstable_EnableTryIncRef(obj) + #endif + + static CYTHON_INLINE PyObject* _lx__getProxy(xmlNode *c_node) { + PyObject *proxy; + if (c_node && c_node->_private) { + proxy = c_node->_private; + #if CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX >= 0x030e0000 && defined(Py_GIL_DISABLED) + /* In FT-Python, prevent concurrent disposal and resurrection. */ + if (PyUnstable_TryIncRef(proxy)) { + return proxy; + } else { + /* NOTE: potential race condition, requires outside guard. */ + c_node->_private = NULL; + proxy = Py_None; + } + #endif + } else { + proxy = Py_None; + } + Py_INCREF(proxy); + return proxy; + } + """ + _Element getProxy "_lx__getProxy" (xmlNode* c_node) + + void PyUnstable_EnableTryIncRef(object o) + # Enables subsequent uses of PyUnstable_TryIncRef() on obj. + # The caller must hold a strong reference to obj when calling this. + # + # Added in CPython 3.14. @cython.linetrace(False) @cython.profile(False) -cdef inline bint hasProxy(xmlNode* c_node): - if c_node._private is NULL: - return False - return True +cdef inline bint hasProxy(xmlNode* c_node) noexcept: + return c_node._private is not NULL @cython.linetrace(False) @cython.profile(False) cdef inline int _registerProxy(_Element proxy, _Document doc, xmlNode* c_node) except -1: - u"""Register a proxy and type for the node it's proxying for. + """Register a proxy and type for the node it's proxying for. """ #print "registering for:", proxy._c_node - assert not hasProxy(c_node), u"double registering proxy!" + assert not hasProxy(c_node), "double registering proxy!" + c_node._private = proxy proxy._doc = doc proxy._c_node = c_node - c_node._private = proxy + PyUnstable_EnableTryIncRef(proxy) return 0 @cython.linetrace(False) @cython.profile(False) cdef inline int _unregisterProxy(_Element proxy) except -1: - u"""Unregister a proxy for the node it's proxying for. + """Unregister a proxy for the node it's proxying for. """ cdef xmlNode* c_node = proxy._c_node - assert c_node._private is proxy, u"Tried to unregister unknown proxy" - c_node._private = NULL + #if c_node._private is not proxy: + # import tracemalloc + # print(tracemalloc.get_object_traceback(proxy)) + #assert c_node._private is NULL or c_node._private is proxy, \ + # f"Tried to unregister unknown proxy 0x{ c_node._private:x}, should be 0x{ proxy:x}" + + if c_node._private is proxy: + c_node._private = NULL + proxy._c_node = NULL + proxy._doc = None + proxy._tag = None return 0 @@ -71,7 +103,9 @@ cdef xmlDoc* _plainFakeRootDoc(xmlDoc* c_base_doc, xmlNode* c_node, return c_base_doc c_doc = _copyDoc(c_base_doc, 0) # non recursive! - c_new_root = tree.xmlDocCopyNode(c_node, c_doc, 2) # non recursive! + c_new_root = tree.xmlDocCopyNode(c_node, c_doc, 2) if c_doc is not NULL else NULL # non recursive! + if c_new_root is NULL: + raise MemoryError() tree.xmlDocSetRootElement(c_doc, c_new_root) _copyParentNamespaces(c_node, c_new_root) @@ -91,7 +125,7 @@ cdef xmlDoc* _plainFakeRootDoc(xmlDoc* c_base_doc, xmlNode* c_node, c_doc.children = c_new_root return c_doc -cdef void _destroyFakeDoc(xmlDoc* c_base_doc, xmlDoc* c_doc): +cdef void _destroyFakeDoc(xmlDoc* c_base_doc, xmlDoc* c_doc) noexcept: # delete a temporary document cdef xmlNode* c_child cdef xmlNode* c_parent @@ -112,7 +146,7 @@ cdef void _destroyFakeDoc(xmlDoc* c_base_doc, xmlDoc* c_doc): tree.xmlFreeDoc(c_doc) cdef _Element _fakeDocElementFactory(_Document doc, xmlNode* c_element): - u"""Special element factory for cases where we need to create a fake + """Special element factory for cases where we need to create a fake root document, but still need to instantiate arbitrary nodes from it. If we instantiate the fake root node, things will turn bad when it's destroyed. @@ -130,8 +164,21 @@ cdef _Element _fakeDocElementFactory(_Document doc, xmlNode* c_element): ################################################################################ # support for freeing tree elements when proxy objects are destroyed -cdef int attemptDeallocation(xmlNode* c_node): - u"""Attempt deallocation of c_node (or higher up in tree). +cdef int freeSubtree(xmlNode* c_node) noexcept: + """Deallocate the c_node, its following siblings and all children. + """ + if c_node is NULL: + #print "not freeing, node is NULL" + return 0 + #print "freeing:", c_top.name + + # Free the complete list of all siblings. + tree.xmlFreeNodeList(c_node) + return 1 + + +cdef int attemptDeallocation(xmlNode* c_node) noexcept: + """Attempt deallocation of c_node (or higher up in tree). """ cdef xmlNode* c_top # could be we actually aren't referring to the tree at all @@ -139,21 +186,18 @@ cdef int attemptDeallocation(xmlNode* c_node): #print "not freeing, node is NULL" return 0 c_top = getDeallocationTop(c_node) - if c_top is not NULL: - #print "freeing:", c_top.name - _removeText(c_top.next) # tail - tree.xmlFreeNode(c_top) - return 1 - return 0 + return freeSubtree(c_top) + -cdef xmlNode* getDeallocationTop(xmlNode* c_node): - u"""Return the top of the tree that can be deallocated, or NULL. +cdef xmlNode* getDeallocationTop(xmlNode* c_node) noexcept: + """Return the left-most sibling at the top of the tree that can be deallocated, or NULL. """ cdef xmlNode* c_next #print "trying to do deallocating:", c_node.type if hasProxy(c_node): #print "Not freeing: proxies still exist" return NULL + while c_node.parent is not NULL: c_node = c_node.parent #print "checking:", c_current.type @@ -165,25 +209,32 @@ cdef xmlNode* getDeallocationTop(xmlNode* c_node): if hasProxy(c_node): #print "Not freeing: proxies still exist" return NULL + # see whether we have children to deallocate if not canDeallocateChildNodes(c_node): return NULL + # see whether we have siblings to deallocate - c_next = c_node.prev + c_next = c_node.next while c_next: if _isElement(c_next): if hasProxy(c_next) or not canDeallocateChildNodes(c_next): return NULL - c_next = c_next.prev - c_next = c_node.next + c_next = c_next.next + # Now check the preceding siblings and find the first node. + c_next = c_node.prev while c_next: if _isElement(c_next): if hasProxy(c_next) or not canDeallocateChildNodes(c_next): return NULL - c_next = c_next.next + c_node = c_next + c_next = c_next.prev + + # Return the left-most node at the top of the tree. return c_node -cdef int canDeallocateChildNodes(xmlNode* c_parent): + +cdef int canDeallocateChildNodes(xmlNode* c_parent) noexcept: cdef xmlNode* c_node c_node = c_parent.children tree.BEGIN_FOR_EACH_ELEMENT_FROM(c_parent, c_node, 1) @@ -195,8 +246,8 @@ cdef int canDeallocateChildNodes(xmlNode* c_parent): ################################################################################ # fix _Document references and namespaces when a node changes documents -cdef void _copyParentNamespaces(xmlNode* c_from_node, xmlNode* c_to_node) nogil: - u"""Copy the namespaces of all ancestors of c_from_node to c_to_node. +cdef void _copyParentNamespaces(xmlNode* c_from_node, xmlNode* c_to_node) noexcept nogil: + """Copy the namespaces of all ancestors of c_from_node to c_to_node. """ cdef xmlNode* c_parent cdef xmlNs* c_ns @@ -250,7 +301,7 @@ cdef inline int _appendToNsCache(_nscache* c_ns_cache, cdef int _stripRedundantNamespaceDeclarations(xmlNode* c_element, _nscache* c_ns_cache, xmlNs** c_del_ns_list) except -1: - u"""Removes namespace declarations from an element that are already + """Removes namespace declarations from an element that are already defined in its parents. Does not free the xmlNs's, just prepends them to the c_del_ns_list. """ @@ -278,7 +329,7 @@ cdef int _stripRedundantNamespaceDeclarations(xmlNode* c_element, _nscache* c_ns cdef void _cleanUpFromNamespaceAdaptation(xmlNode* c_start_node, - _nscache* c_ns_cache, xmlNs* c_del_ns_list): + _nscache* c_ns_cache, xmlNs* c_del_ns_list) noexcept: # Try to recover from exceptions with really bad timing. We were in the middle # of ripping out xmlNS-es and likely ran out of memory. Try to fix up the tree # by re-adding the original xmlNs declarations (which might still be used in some @@ -297,7 +348,7 @@ cdef void _cleanUpFromNamespaceAdaptation(xmlNode* c_start_node, cdef int moveNodeToDocument(_Document doc, xmlDoc* c_source_doc, xmlNode* c_element) except -1: - u"""Fix the xmlNs pointers of a node and its subtree that were moved. + """Fix the xmlNs pointers of a node and its subtree that were moved. Originally copied from libxml2's xmlReconciliateNs(). Expects libxml2 doc pointers of node to be correct already, but fixes @@ -326,18 +377,25 @@ cdef int moveNodeToDocument(_Document doc, xmlDoc* c_source_doc, step 1), but freed only after the complete subtree was traversed and all occurrences were replaced by tree-internal pointers. """ + if not tree._isElementOrXInclude(c_element): + return 0 + + doc.lock_proxies() + try: + return moveNodeToDocument_locked(doc, c_source_doc, c_element) + finally: + doc.unlock_proxies() + + +cdef int moveNodeToDocument_locked(_Document doc, xmlDoc* c_source_doc, xmlNode* c_element) except -1: cdef xmlNode* c_start_node cdef xmlNode* c_node cdef xmlDoc* c_doc = doc._c_doc cdef tree.xmlAttr* c_attr - cdef char* c_name cdef _nscache c_ns_cache = [NULL, 0, 0] cdef xmlNs* c_del_ns_list = NULL cdef proxy_count = 0 - if not tree._isElementOrXInclude(c_element): - return 0 - c_start_node = c_element tree.BEGIN_FOR_EACH_FROM(c_element, c_element, 1) @@ -382,7 +440,7 @@ cdef int moveNodeToDocument(_Document doc, xmlDoc* c_source_doc, # 4) fix _Document references # (and potentially deallocate the source document) if proxy_count > 0: - if proxy_count == 1 and c_start_node._private is not NULL: + if proxy_count == 1 and hasProxy(c_start_node): proxy = getProxy(c_start_node) if proxy is not None: if proxy._doc is not doc: @@ -395,7 +453,7 @@ cdef int moveNodeToDocument(_Document doc, xmlDoc* c_source_doc, return 0 -cdef void _setTreeDoc(xmlNode* c_node, xmlDoc* c_doc): +cdef void _setTreeDoc(xmlNode* c_node, xmlDoc* c_doc) noexcept: """Adaptation of 'xmlSetTreeDoc()' that deep-fixes the document links iteratively. It avoids https://gitlab.gnome.org/GNOME/libxml2/issues/42 """ @@ -413,7 +471,7 @@ cdef void _setTreeDoc(xmlNode* c_node, xmlDoc* c_doc): tree.END_FOR_EACH_FROM(c_node) -cdef inline void _fixDocChildren(xmlNode* c_child, xmlDoc* c_doc): +cdef inline void _fixDocChildren(xmlNode* c_child, xmlDoc* c_doc) noexcept: while c_child: c_child.doc = c_doc if c_child.children: @@ -451,25 +509,25 @@ cdef int _fixCNs(_Document doc, xmlNode* c_start_node, xmlNode* c_node, return 0 -cdef void fixElementDocument(xmlNode* c_element, _Document doc, - size_t proxy_count): +cdef int fixElementDocument(xmlNode* c_element, _Document doc, + size_t proxy_count) except -1: cdef xmlNode* c_node = c_element cdef _Element proxy = None # init-to-None required due to fake-loop below tree.BEGIN_FOR_EACH_FROM(c_element, c_node, 1) - if c_node._private is not NULL: + if hasProxy(c_node): proxy = getProxy(c_node) if proxy is not None: if proxy._doc is not doc: proxy._doc = doc proxy_count -= 1 if proxy_count == 0: - return + return 0 tree.END_FOR_EACH_FROM(c_node) cdef void fixThreadDictNames(xmlNode* c_element, tree.xmlDict* c_src_dict, - tree.xmlDict* c_dict) nogil: + tree.xmlDict* c_dict) noexcept nogil: # re-assign the names of tags and attributes # # this should only be called when the element is based on a @@ -492,7 +550,7 @@ cdef void fixThreadDictNames(xmlNode* c_element, cdef inline void _fixThreadDictPtr(const_xmlChar** c_ptr, tree.xmlDict* c_src_dict, - tree.xmlDict* c_dict) nogil: + tree.xmlDict* c_dict) noexcept nogil: c_str = c_ptr[0] if c_str and c_src_dict and tree.xmlDictOwns(c_src_dict, c_str): # return value can be NULL on memory error, but we don't handle that here @@ -503,7 +561,7 @@ cdef inline void _fixThreadDictPtr(const_xmlChar** c_ptr, cdef void fixThreadDictNamesForNode(xmlNode* c_element, tree.xmlDict* c_src_dict, - tree.xmlDict* c_dict) nogil: + tree.xmlDict* c_dict) noexcept nogil: cdef xmlNode* c_node = c_element tree.BEGIN_FOR_EACH_FROM(c_element, c_node, 1) if c_node.type in (tree.XML_ELEMENT_NODE, tree.XML_XINCLUDE_START): @@ -523,7 +581,7 @@ cdef void fixThreadDictNamesForNode(xmlNode* c_element, cdef inline void fixThreadDictNamesForAttributes(tree.xmlAttr* c_attr, tree.xmlDict* c_src_dict, - tree.xmlDict* c_dict) nogil: + tree.xmlDict* c_dict) noexcept nogil: cdef xmlNode* c_child cdef xmlNode* c_node = c_attr while c_node is not NULL: @@ -539,7 +597,7 @@ cdef inline void fixThreadDictNamesForAttributes(tree.xmlAttr* c_attr, cdef inline void fixThreadDictContentForNode(xmlNode* c_node, tree.xmlDict* c_src_dict, - tree.xmlDict* c_dict) nogil: + tree.xmlDict* c_dict) noexcept nogil: if c_node.content is not NULL and \ c_node.content is not &c_node.properties: if tree.xmlDictOwns(c_src_dict, c_node.content): @@ -549,7 +607,7 @@ cdef inline void fixThreadDictContentForNode(xmlNode* c_node, cdef inline void fixThreadDictNsForNode(xmlNode* c_node, tree.xmlDict* c_src_dict, - tree.xmlDict* c_dict) nogil: + tree.xmlDict* c_dict) noexcept nogil: cdef xmlNs* c_ns = c_node.nsDef while c_ns is not NULL: _fixThreadDictPtr(&c_ns.href, c_src_dict, c_dict) @@ -559,7 +617,7 @@ cdef inline void fixThreadDictNsForNode(xmlNode* c_node, cdef void fixThreadDictNamesForDtd(tree.xmlDtd* c_dtd, tree.xmlDict* c_src_dict, - tree.xmlDict* c_dict) nogil: + tree.xmlDict* c_dict) noexcept nogil: cdef xmlNode* c_node cdef tree.xmlElement* c_element cdef tree.xmlAttribute* c_attribute @@ -574,7 +632,10 @@ cdef void fixThreadDictNamesForDtd(tree.xmlDtd* c_dtd, _fixThreadDictPtr(&c_element.content.prefix, c_src_dict, c_dict) c_attribute = c_element.attributes while c_attribute: - _fixThreadDictPtr(&c_attribute.defaultValue, c_src_dict, c_dict) + if tree.LIBXML_VERSION < 21500: + # libxml2 2.15 no longer stores default values in the dict. + # See https://gitlab.gnome.org/GNOME/libxml2/-/commit/24628f25 + _fixThreadDictPtr(&c_attribute.defaultValue, c_src_dict, c_dict) _fixThreadDictPtr(&c_attribute.name, c_src_dict, c_dict) _fixThreadDictPtr(&c_attribute.prefix, c_src_dict, c_dict) _fixThreadDictPtr(&c_attribute.elem, c_src_dict, c_dict) diff --git a/src/lxml/public-api.pxi b/src/lxml/public-api.pxi index 1c4a552a2..124445f20 100644 --- a/src/lxml/public-api.pxi +++ b/src/lxml/public-api.pxi @@ -1,7 +1,7 @@ # Public C API for lxml.etree cdef public api _Element deepcopyNodeToDocument(_Document doc, xmlNode* c_root): - u"Recursively copy the element into the document. doc is not modified." + "Recursively copy the element into the document. doc is not modified." cdef xmlNode* c_node c_node = _copyNodeToDoc(c_root, doc._c_doc) return _elementFactory(doc, c_node) @@ -30,19 +30,19 @@ cdef public api _Element elementFactory(_Document doc, xmlNode* c_node): cdef public api _Element makeElement(tag, _Document doc, parser, text, tail, attrib, nsmap): - return _makeElement(tag, NULL, doc, parser, text, tail, attrib, nsmap, None) + return _makeElement(tag, NULL, doc, parser, text, tail, attrib, nsmap) cdef public api _Element makeSubElement(_Element parent, tag, text, tail, attrib, nsmap): _assertValidNode(parent) - return _makeSubElement(parent, tag, text, tail, attrib, nsmap, None) + return _makeSubElement(parent, tag, text, tail, attrib, nsmap) cdef public api void setElementClassLookupFunction( _element_class_lookup_function function, state): _setElementClassLookupFunction(function, state) cdef public api object lookupDefaultElementClass(state, doc, xmlNode* c_node): - return _lookupDefaultElementClass(state, doc, c_node) + return _lookupDefaultElementClasses(state, doc, c_node) cdef public api object lookupNamespaceElementClass(state, doc, xmlNode* c_node): return _find_nselement_class(state, doc, c_node) @@ -51,7 +51,7 @@ cdef public api object callLookupFallback(FallbackElementClassLookup lookup, _Document doc, xmlNode* c_node): return _callLookupFallback(lookup, doc, c_node) -cdef public api int tagMatches(xmlNode* c_node, const_xmlChar* c_href, const_xmlChar* c_name): +cdef public api int tagMatches(xmlNode* c_node, const_xmlChar* c_href, const_xmlChar* c_name) noexcept: if c_node is NULL: return -1 return _tagMatches(c_node, c_href, c_name) @@ -62,18 +62,18 @@ cdef public api _Document documentOrRaise(object input): cdef public api _Element rootNodeOrRaise(object input): return _rootNodeOrRaise(input) -cdef public api bint hasText(xmlNode* c_node): +cdef public api bint hasText(xmlNode* c_node) noexcept: return _hasText(c_node) -cdef public api bint hasTail(xmlNode* c_node): +cdef public api bint hasTail(xmlNode* c_node) noexcept: return _hasTail(c_node) -cdef public api object textOf(xmlNode* c_node): +cdef public api unicode textOf(xmlNode* c_node): if c_node is NULL: return None return _collectText(c_node.children) -cdef public api object tailOf(xmlNode* c_node): +cdef public api unicode tailOf(xmlNode* c_node): if c_node is NULL: return None return _collectText(c_node.next) @@ -88,10 +88,10 @@ cdef public api int setTailText(xmlNode* c_node, text) except -1: raise ValueError return _setTailText(c_node, text) -cdef public api object attributeValue(xmlNode* c_element, xmlAttr* c_attrib_node): +cdef public api unicode attributeValue(xmlNode* c_element, xmlAttr* c_attrib_node): return _attributeValue(c_element, c_attrib_node) -cdef public api object attributeValueFromNsName(xmlNode* c_element, +cdef public api unicode attributeValueFromNsName(xmlNode* c_element, const_xmlChar* ns, const_xmlChar* name): return _attributeValueFromNsName(c_element, ns, name) @@ -115,25 +115,25 @@ cdef public api int delAttribute(_Element element, key) except -1: return _delAttribute(element, key) cdef public api int delAttributeFromNsName(tree.xmlNode* c_element, - const_xmlChar* c_href, const_xmlChar* c_name): + const_xmlChar* c_href, const_xmlChar* c_name) noexcept: return _delAttributeFromNsName(c_element, c_href, c_name) -cdef public api bint hasChild(xmlNode* c_node): +cdef public api bint hasChild(xmlNode* c_node) noexcept: return _hasChild(c_node) -cdef public api xmlNode* findChild(xmlNode* c_node, Py_ssize_t index): +cdef public api xmlNode* findChild(xmlNode* c_node, Py_ssize_t index) noexcept: return _findChild(c_node, index) -cdef public api xmlNode* findChildForwards(xmlNode* c_node, Py_ssize_t index): +cdef public api xmlNode* findChildForwards(xmlNode* c_node, Py_ssize_t index) noexcept: return _findChildForwards(c_node, index) -cdef public api xmlNode* findChildBackwards(xmlNode* c_node, Py_ssize_t index): +cdef public api xmlNode* findChildBackwards(xmlNode* c_node, Py_ssize_t index) noexcept: return _findChildBackwards(c_node, index) -cdef public api xmlNode* nextElement(xmlNode* c_node): +cdef public api xmlNode* nextElement(xmlNode* c_node) noexcept: return _nextElement(c_node) -cdef public api xmlNode* previousElement(xmlNode* c_node): +cdef public api xmlNode* previousElement(xmlNode* c_node) noexcept: return _previousElement(c_node) cdef public api void appendChild(_Element parent, _Element child): @@ -143,7 +143,7 @@ cdef public api void appendChild(_Element parent, _Element child): cdef public api int appendChildToElement(_Element parent, _Element child) except -1: return _appendChild(parent, child) -cdef public api object pyunicode(const_xmlChar* s): +cdef public api unicode pyunicode(const_xmlChar* s): if s is NULL: raise TypeError return funicode(s) @@ -157,10 +157,10 @@ cdef public api tuple getNsTag(object tag): cdef public api tuple getNsTagWithEmptyNs(object tag): return _getNsTagWithEmptyNs(tag) -cdef public api object namespacedName(xmlNode* c_node): +cdef public api unicode namespacedName(xmlNode* c_node): return _namespacedName(c_node) -cdef public api object namespacedNameFromNsName(const_xmlChar* href, const_xmlChar* name): +cdef public api unicode namespacedNameFromNsName(const_xmlChar* href, const_xmlChar* name): return _namespacedNameFromNsName(href, name) cdef public api void iteratorStoreNext(_ElementIterator iterator, _Element node): @@ -176,3 +176,24 @@ cdef public api tree.xmlNs* findOrBuildNodeNsPrefix( if doc is None: raise TypeError return doc._findOrBuildNodeNs(c_node, href, prefix, 0) + + +# Document locking + +cdef public api void lock_read(_Document doc) noexcept: + doc.lock_read() + +cdef public api void unlock_read(_Document doc) noexcept: + doc.unlock_read() + +cdef public api void lock_write(_Document doc) noexcept: + doc.lock_write() + +cdef public api void unlock_write(_Document doc) noexcept: + doc.unlock_write() + +cdef public api void lock_write2(_Document doc1, _Document doc2) noexcept: + doc1.lock_write_with(doc2) + +cdef public api void unlock_write2(_Document doc1, _Document doc2) noexcept: + doc1.unlock_write_with(doc2) diff --git a/src/lxml/python.pxd b/src/lxml/python.pxd index 79aadc920..e1fdd471b 100644 --- a/src/lxml/python.pxd +++ b/src/lxml/python.pxd @@ -2,13 +2,16 @@ from libc cimport stdio from libc.string cimport const_char cimport cython -cdef extern from *: - cdef bint PEP393_ENABLED "CYTHON_PEP393_ENABLED" cdef extern from "Python.h": """ - #if defined(CYTHON_PEP393_ENABLED) && CYTHON_PEP393_ENABLED - #if PY_VERSION_HEX >= 0x030C0000 + #if defined(Py_LIMITED_API) + #define LXML_IN_LIMITED_API 1 + #else + #define LXML_IN_LIMITED_API 0 + #endif + + #if defined(Py_LIMITED_API) || PY_VERSION_HEX >= 0x030C0000 #undef PyUnicode_IS_READY #define PyUnicode_IS_READY(s) (1) #undef PyUnicode_READY @@ -20,17 +23,33 @@ cdef extern from "Python.h": #undef PyUnicode_GET_SIZE #define PyUnicode_GET_SIZE(s) (0) #endif - #elif PY_VERSION_HEX <= 0x03030000 - #define PyUnicode_IS_READY(op) (0) - #define PyUnicode_GET_LENGTH(u) PyUnicode_GET_SIZE(u) - #define PyUnicode_KIND(u) (sizeof(Py_UNICODE)) - #define PyUnicode_DATA(u) ((void*)PyUnicode_AS_UNICODE(u)) + + #if defined(Py_LIMITED_API) + #undef PyUnicode_MAX_CHAR_VALUE + #define PyUnicode_MAX_CHAR_VALUE(s) (0) + #undef PyUnicode_GET_LENGTH + #define PyUnicode_GET_LENGTH(s) (0) + #undef PyUnicode_KIND + #define PyUnicode_KIND(s) (0) + #undef PyUnicode_DATA + #define PyUnicode_DATA(s) (0) + #endif + + #if !defined(Py_mod_gil) && (PY_VERSION_HEX < 0x030d0000 || defined(Py_LIMITED_API) && Py_LIMITED_API < 0x030d0000) + #define Py_mod_gil 4 + #define Py_MOD_GIL_USED NULL + #define Py_MOD_GIL_NOT_USED NULL #endif + """ ctypedef struct PyObject - cdef int PY_SSIZE_T_MAX - cdef int PY_VERSION_HEX + cdef const Py_ssize_t PY_SSIZE_T_MIN + cdef const Py_ssize_t PY_SSIZE_T_MAX + cdef const int PY_VERSION_HEX + cdef const bint IN_LIMITED_API "LXML_IN_LIMITED_API" + cdef bint PY_BIG_ENDIAN + cdef bint PY_LITTLE_ENDIAN cdef void Py_INCREF(object o) cdef void Py_DECREF(object o) @@ -57,12 +76,12 @@ cdef extern from "Python.h": cdef char* PyUnicode_AS_DATA(object ustring) cdef Py_ssize_t PyUnicode_GET_DATA_SIZE(object ustring) cdef Py_ssize_t PyUnicode_GET_SIZE(object ustring) + cdef Py_UCS4 PyUnicode_MAX_CHAR_VALUE(object ustring) cdef bytes PyBytes_FromStringAndSize(char* s, Py_ssize_t size) cdef bytes PyBytes_FromFormat(char* format, ...) cdef Py_ssize_t PyBytes_GET_SIZE(object s) cdef object PyNumber_Int(object value) - cdef Py_ssize_t PyInt_AsSsize_t(object value) cdef Py_ssize_t PyTuple_GET_SIZE(object t) cdef object PyTuple_GET_ITEM(object o, Py_ssize_t pos) @@ -73,13 +92,11 @@ cdef extern from "Python.h": cdef void PyList_SET_ITEM(object l, Py_ssize_t index, object value) cdef int PyList_Insert(object l, Py_ssize_t index, object o) except -1 cdef object PyList_AsTuple(object l) - cdef void PyList_Clear(object l) cdef PyObject* PyDict_GetItemString(object d, char* key) cdef PyObject* PyDict_GetItem(object d, object key) - cdef void PyDict_Clear(object d) + cdef PyObject* PyDict_GetItemWithError(object d, object key) except? NULL cdef object PyDictProxy_New(object d) - cdef Py_ssize_t PyDict_Size(object d) cdef object PySequence_List(object o) cdef object PySequence_Tuple(object o) @@ -87,15 +104,14 @@ cdef extern from "Python.h": cdef bint PySequence_Check(object instance) cdef bint PyType_Check(object instance) cdef bint PyTuple_CheckExact(object instance) + cdef bint PyIndex_Check(object instance) - cdef int _PyEval_SliceIndex(object value, Py_ssize_t* index) except 0 - cdef int PySlice_GetIndicesEx "_lx_PySlice_GetIndicesEx" ( + cdef int PySlice_GetIndicesEx( object slice, Py_ssize_t length, Py_ssize_t *start, Py_ssize_t *stop, Py_ssize_t *step, Py_ssize_t *slicelength) except -1 cdef object PyObject_RichCompare(object o1, object o2, int op) - cdef int PyObject_RichCompareBool(object o1, object o2, int op) PyObject* PyWeakref_NewRef(object ob, PyObject* callback) except NULL # used for PyPy only object PyWeakref_LockObject(PyObject* ob) # PyPy only @@ -104,52 +120,62 @@ cdef extern from "Python.h": cdef void* PyMem_Realloc(void* p, size_t size) cdef void PyMem_Free(void* p) + const int Py_mod_gil + const void* Py_MOD_GIL_USED + const void* Py_MOD_GIL_NOT_USED + + ctypedef struct PyModuleDef_Slot: + int slot + void* value + + ctypedef struct PyModuleDef: + PyModuleDef_Slot* m_slots + + cdef PyModuleDef* PyModule_GetDef(object module) except? NULL + # always returns NULL to pass on the exception cdef object PyErr_SetFromErrno(object type) + cdef void PyException_SetContext(object exception, object context) + cdef PyObject* PyException_GetContext(object exception) cdef PyObject* PyThreadState_GetDict() # some handy functions - cdef char* _cstr "PyBytes_AS_STRING" (object s) - cdef char* __cstr "PyBytes_AS_STRING" (PyObject* s) + cdef const char* _cstr "__Pyx_PyBytes_AsString" (object s) + cdef const char* __cstr "__Pyx_PyBytes_AsString" (PyObject* s) # Py_buffer related flags - cdef int PyBUF_SIMPLE - cdef int PyBUF_WRITABLE - cdef int PyBUF_LOCK - cdef int PyBUF_FORMAT - cdef int PyBUF_ND - cdef int PyBUF_STRIDES - cdef int PyBUF_C_CONTIGUOUS - cdef int PyBUF_F_CONTIGUOUS - cdef int PyBUF_ANY_CONTIGUOUS - cdef int PyBUF_INDIRECT + cdef const int PyBUF_SIMPLE + cdef const int PyBUF_WRITABLE + cdef const int PyBUF_LOCK + cdef const int PyBUF_FORMAT + cdef const int PyBUF_ND + cdef const int PyBUF_STRIDES + cdef const int PyBUF_C_CONTIGUOUS + cdef const int PyBUF_F_CONTIGUOUS + cdef const int PyBUF_ANY_CONTIGUOUS + cdef const int PyBUF_INDIRECT + cdef extern from "pythread.h": ctypedef void* PyThread_type_lock cdef PyThread_type_lock PyThread_allocate_lock() cdef void PyThread_free_lock(PyThread_type_lock lock) cdef int PyThread_acquire_lock(PyThread_type_lock lock, int mode) nogil - cdef void PyThread_release_lock(PyThread_type_lock lock) - cdef long PyThread_get_thread_ident() + cdef void PyThread_release_lock(PyThread_type_lock lock) nogil + cdef unsigned long PyThread_get_thread_ident() ctypedef enum __WaitLock: WAIT_LOCK NOWAIT_LOCK -cdef extern from "includes/etree_defs.h": # redefines some functions as macros +cdef extern from "etree_defs.h": # redefines some functions as macros cdef void* lxml_malloc(size_t count, size_t item_size) cdef void* lxml_realloc(void* mem, size_t count, size_t item_size) cdef void lxml_free(void* mem) cdef void* lxml_unpack_xmldoc_capsule(object capsule, bint* is_owned) except? NULL cdef bint _isString(object obj) - cdef const_char* _fqtypename(object t) - cdef object PY_NEW(object t) - cdef bint LXML_UNICODE_STRINGS - cdef bint IS_PYTHON2 - cdef bint IS_PYTHON3 # legacy, avoid + cdef str _typename "__lxml_typename" (object t) + cdef str _fqtypename "__lxml_fqtypename" (object t) cdef bint IS_PYPY - cdef object PY_FSPath "lxml_PyOS_FSPath" (object obj) - -cdef extern from "lxml_endian.h": - cdef bint PY_BIG_ENDIAN # defined in later Py3.x versions + cdef object PyOS_FSPath(object obj) diff --git a/src/lxml/readonlytree.pxi b/src/lxml/readonlytree.pxi index cc25f98ea..3e74bb59e 100644 --- a/src/lxml/readonlytree.pxi +++ b/src/lxml/readonlytree.pxi @@ -2,17 +2,19 @@ @cython.internal cdef class _ReadOnlyProxy: - u"A read-only proxy class suitable for PIs/Comments (for internal use only!)." - cdef bint _free_after_use + "A read-only proxy class suitable for PIs/Comments (for internal use only!)." cdef xmlNode* _c_node cdef _ReadOnlyProxy _source_proxy cdef list _dependent_proxies + cdef RWLock _lock + cdef bint _free_after_use + def __cinit__(self): self._c_node = NULL self._free_after_use = 0 cdef int _assertNode(self) except -1: - u"""This is our way of saying: this proxy is invalid! + """This is our way of saying: this proxy is invalid! """ if not self._c_node: raise ReferenceError("Proxy invalidated!") @@ -21,8 +23,8 @@ cdef class _ReadOnlyProxy: cdef int _raise_unsupported_type(self) except -1: raise TypeError(f"Unsupported node type: {self._c_node.type}") - cdef void free_after_use(self): - u"""Should the xmlNode* be freed when releasing the proxy? + cdef void free_after_use(self) noexcept: + """Should the xmlNode* be freed when releasing the proxy? """ self._free_after_use = 1 @@ -30,77 +32,99 @@ cdef class _ReadOnlyProxy: def tag(self): """Element tag """ - self._assertNode() - if self._c_node.type == tree.XML_ELEMENT_NODE: - return _namespacedName(self._c_node) - elif self._c_node.type == tree.XML_PI_NODE: - return ProcessingInstruction - elif self._c_node.type == tree.XML_COMMENT_NODE: - return Comment - elif self._c_node.type == tree.XML_ENTITY_REF_NODE: - return Entity - else: - self._raise_unsupported_type() + self._lock.lock_read() + try: + self._assertNode() + if self._c_node.type == tree.XML_ELEMENT_NODE: + return _namespacedName(self._c_node) + elif self._c_node.type == tree.XML_PI_NODE: + return ProcessingInstruction + elif self._c_node.type == tree.XML_COMMENT_NODE: + return Comment + elif self._c_node.type == tree.XML_ENTITY_REF_NODE: + return Entity + else: + self._raise_unsupported_type() + finally: + self._lock.unlock_read() @property def text(self): """Text before the first subelement. This is either a string or the value None, if there was no text. """ - self._assertNode() - if self._c_node.type == tree.XML_ELEMENT_NODE: - return _collectText(self._c_node.children) - elif self._c_node.type in (tree.XML_PI_NODE, - tree.XML_COMMENT_NODE): - if self._c_node.content is NULL: - return '' + self._lock.lock_read() + try: + self._assertNode() + if self._c_node.type == tree.XML_ELEMENT_NODE: + return _collectText(self._c_node.children) + elif self._c_node.type in (tree.XML_PI_NODE, + tree.XML_COMMENT_NODE): + if self._c_node.content is NULL: + return '' + else: + return funicode(self._c_node.content) + elif self._c_node.type == tree.XML_ENTITY_REF_NODE: + return f'&{funicode(self._c_node.name)};' else: - return funicode(self._c_node.content) - elif self._c_node.type == tree.XML_ENTITY_REF_NODE: - return f'&{funicode(self._c_node.name)};' - else: - self._raise_unsupported_type() - + self._raise_unsupported_type() + finally: + self._lock.unlock_read() + @property def tail(self): """Text after this element's end tag, but before the next sibling element's start tag. This is either a string or the value None, if there was no text. """ - self._assertNode() - return _collectText(self._c_node.next) + self._lock.lock_read() + try: + self._assertNode() + return _collectText(self._c_node.next) + finally: + self._lock.unlock_read() @property def sourceline(self): """Original line number as found by the parser or None if unknown. """ cdef long line - self._assertNode() - line = tree.xmlGetLineNo(self._c_node) + + self._lock.lock_read() + try: + self._assertNode() + line = tree.xmlGetLineNo(self._c_node) + finally: + self._lock.unlock_read() + if line > 0: return line else: return None def __repr__(self): - self._assertNode() - if self._c_node.type == tree.XML_ELEMENT_NODE: - return "" % (strrepr(self.tag), id(self)) - elif self._c_node.type == tree.XML_COMMENT_NODE: - return "" % strrepr(self.text) - elif self._c_node.type == tree.XML_ENTITY_NODE: - return "&%s;" % strrepr(funicode(self._c_node.name)) - elif self._c_node.type == tree.XML_PI_NODE: - text = self.text - if text: - return "" % (strrepr(self.target), text) + self._lock.lock_read() + try: + self._assertNode() + if self._c_node.type == tree.XML_ELEMENT_NODE: + return "" % (self.tag, id(self)) + elif self._c_node.type == tree.XML_COMMENT_NODE: + return "" % self.text + elif self._c_node.type == tree.XML_ENTITY_NODE: + return "&%s;" % funicode(self._c_node.name) + elif self._c_node.type == tree.XML_PI_NODE: + text = self.text + if text: + return "" % (self.target, text) + else: + return "" % self.target else: - return "" % strrepr(self.target) - else: - self._raise_unsupported_type() + self._raise_unsupported_type() + finally: + self._lock.unlock_read() def __getitem__(self, x): - u"""Returns the subelement at the given position or the requested + """Returns the subelement at the given position or the requested slice. """ cdef xmlNode* c_node = NULL @@ -108,68 +132,88 @@ cdef class _ReadOnlyProxy: cdef Py_ssize_t c, i cdef _node_to_node_function next_element cdef list result - self._assertNode() - if isinstance(x, slice): - # slicing - if _isFullSlice(x): - return _collectChildren(self) - _findChildSlice(x, self._c_node, &c_node, &step, &slicelength) - if c_node is NULL: - return [] - if step > 0: - next_element = _nextElement + + self._lock.lock_read() + try: + self._assertNode() + if isinstance(x, slice): + # slicing + if _isFullSlice(x): + return _collectChildren(self) + _findChildSlice(x, self._c_node, &c_node, &step, &slicelength) + if c_node is NULL: + return [] + if step > 0: + next_element = _nextElement + else: + step = -step if step != python.PY_SSIZE_T_MIN else python.PY_SSIZE_T_MAX + next_element = _previousElement + result = [] + c = 0 + while c_node is not NULL and c < slicelength: + result.append(_newReadOnlyProxy(self._source_proxy, c_node)) + result.append(_elementFactory(self._doc, c_node)) + c = c + 1 + for i from 0 <= i < step: + c_node = next_element(c_node) + return result else: - step = -step - next_element = _previousElement - result = [] - c = 0 - while c_node is not NULL and c < slicelength: - result.append(_newReadOnlyProxy(self._source_proxy, c_node)) - result.append(_elementFactory(self._doc, c_node)) - c = c + 1 - for i from 0 <= i < step: - c_node = next_element(c_node) - return result - else: - # indexing - c_node = _findChild(self._c_node, x) - if c_node is NULL: - raise IndexError, u"list index out of range" - return _newReadOnlyProxy(self._source_proxy, c_node) + # indexing + c_node = _findChild(self._c_node, x) + if c_node is NULL: + raise IndexError, "list index out of range" + return _newReadOnlyProxy(self._source_proxy, c_node) + finally: + self._lock.unlock_read() def __len__(self): - u"""Returns the number of subelements. + """Returns the number of subelements. """ - cdef Py_ssize_t c + cdef Py_ssize_t count cdef xmlNode* c_node - self._assertNode() - c = 0 - c_node = self._c_node.children - while c_node is not NULL: - if tree._isElement(c_node): - c = c + 1 - c_node = c_node.next - return c - def __nonzero__(self): + self._lock.lock_read() + try: + self._assertNode() + count = 0 + c_node = self._c_node.children + while c_node is not NULL: + count += tree._isElement(c_node) + c_node = c_node.next + finally: + self._lock.unlock_read() + return count + + def __bool__(self): cdef xmlNode* c_node - self._assertNode() - c_node = _findChildBackwards(self._c_node, 0) + self._lock.lock_read() + try: + self._assertNode() + c_node = _findChildBackwards(self._c_node, 0) + finally: + self._lock.unlock_read() + return c_node != NULL def __deepcopy__(self, memo): - u"__deepcopy__(self, memo)" + "__deepcopy__(self, memo)" return self.__copy__() - + cpdef __copy__(self): - u"__copy__(self)" + "__copy__(self)" cdef xmlDoc* c_doc cdef xmlNode* c_node cdef _Document new_doc - if self._c_node is NULL: - return self - c_doc = _copyDocRoot(self._c_node.doc, self._c_node) # recursive - new_doc = _documentFactory(c_doc, None) + + self._lock.lock_read() + try: + if self._c_node is NULL: + return self + c_doc = _copyDocRoot(self._c_node.doc, self._c_node) # recursive + new_doc = _documentFactory(c_doc, None) + finally: + self._lock.unlock_read() + root = new_doc.getroot() if root is not None: return root @@ -185,7 +229,7 @@ cdef class _ReadOnlyProxy: return iter(self.getchildren()) def iterchildren(self, tag=None, *, reversed=False): - u"""iterchildren(self, tag=None, reversed=False) + """iterchildren(self, tag=None, reversed=False) Iterate over the children of this element. """ @@ -197,49 +241,72 @@ cdef class _ReadOnlyProxy: return iter(children) cpdef getchildren(self): - u"""Returns all subelements. The elements are returned in document + """Returns all subelements. The elements are returned in document order. """ cdef xmlNode* c_node cdef list result - self._assertNode() - result = [] - c_node = self._c_node.children - while c_node is not NULL: - if tree._isElement(c_node): - result.append(_newReadOnlyProxy(self._source_proxy, c_node)) - c_node = c_node.next + + self._lock.lock_read() + try: + self._assertNode() + result = [] + c_node = self._c_node.children + while c_node is not NULL: + if tree._isElement(c_node): + result.append(_newReadOnlyProxy(self._source_proxy, c_node)) + c_node = c_node.next + finally: + self._lock.unlock_read() + return result def getparent(self): - u"""Returns the parent of this element or None for the root element. + """Returns the parent of this element or None for the root element. """ cdef xmlNode* c_parent - self._assertNode() - c_parent = self._c_node.parent - if c_parent is NULL or not tree._isElement(c_parent): - return None - else: - return _newReadOnlyProxy(self._source_proxy, c_parent) + + self._lock.lock_read() + try: + self._assertNode() + c_parent = self._c_node.parent + if c_parent is NULL or not tree._isElement(c_parent): + return None + else: + return _newReadOnlyProxy(self._source_proxy, c_parent) + finally: + self._lock.unlock_read() def getnext(self): - u"""Returns the following sibling of this element or None. + """Returns the following sibling of this element or None. """ cdef xmlNode* c_node - self._assertNode() - c_node = _nextElement(self._c_node) - if c_node is not NULL: - return _newReadOnlyProxy(self._source_proxy, c_node) + + self._lock.lock_read() + try: + self._assertNode() + c_node = _nextElement(self._c_node) + if c_node is not NULL: + return _newReadOnlyProxy(self._source_proxy, c_node) + finally: + self._lock.unlock_read() + return None def getprevious(self): - u"""Returns the preceding sibling of this element or None. + """Returns the preceding sibling of this element or None. """ cdef xmlNode* c_node - self._assertNode() - c_node = _previousElement(self._c_node) - if c_node is not NULL: - return _newReadOnlyProxy(self._source_proxy, c_node) + + self._lock.lock_read() + try: + self._assertNode() + c_node = _previousElement(self._c_node) + if c_node is not NULL: + return _newReadOnlyProxy(self._source_proxy, c_node) + finally: + self._lock.unlock_read() + return None @@ -249,26 +316,34 @@ cdef class _ReadOnlyPIProxy(_ReadOnlyProxy): """A read-only proxy for processing instructions (for internal use only!)""" @property def target(self): - self._assertNode() - return funicode(self._c_node.name) + self._lock.lock_read() + try: + self._assertNode() + return funicode(self._c_node.name) + finally: + self._lock.unlock_read() @cython.final @cython.internal cdef class _ReadOnlyEntityProxy(_ReadOnlyProxy): """A read-only proxy for entity references (for internal use only!)""" - property name: - def __get__(self): + @property + def name(self): + self._lock.lock_read() + try: + self._assertNode() return funicode(self._c_node.name) - - def __set__(self, value): - value_utf = _utf8(value) - if u'&' in value or u';' in value: - raise ValueError(f"Invalid entity name '{value}'") - tree.xmlNodeSetName(self._c_node, _xcstr(value_utf)) + finally: + self._lock.unlock_read() @property def text(self): - return f'&{funicode(self._c_node.name)};' + self._lock.lock_read() + try: + self._assertNode() + return f'&{funicode(self._c_node.name)};' + finally: + self._lock.unlock_read() @cython.internal @@ -277,17 +352,28 @@ cdef class _ReadOnlyElementProxy(_ReadOnlyProxy): @property def attrib(self): - self._assertNode() - return dict(_collectAttributes(self._c_node, 3)) + self._lock.lock_read() + try: + self._assertNode() + attributes = _collectAttributes(self._c_node, 3) + finally: + self._lock.unlock_read() + + return dict(attributes) @property def prefix(self): """Namespace prefix or None. """ - self._assertNode() - if self._c_node.ns is not NULL: - if self._c_node.ns.prefix is not NULL: - return funicode(self._c_node.ns.prefix) + self._lock.lock_read() + try: + self._assertNode() + if self._c_node.ns is not NULL: + if self._c_node.ns.prefix is not NULL: + return funicode(self._c_node.ns.prefix) + finally: + self._lock.unlock_read() + return None @property @@ -298,38 +384,58 @@ cdef class _ReadOnlyElementProxy(_ReadOnlyProxy): Note that changing the returned dict has no effect on the Element. """ - self._assertNode() - return _build_nsmap(self._c_node) + self._lock.lock_read() + try: + self._assertNode() + return _build_nsmap(self._c_node) + finally: + self._lock.unlock_read() def get(self, key, default=None): - u"""Gets an element attribute. + """Gets an element attribute. """ - self._assertNode() - return _getNodeAttributeValue(self._c_node, key, default) + self._lock.lock_read() + try: + self._assertNode() + return _getNodeAttributeValue(self._c_node, key, default) + finally: + self._lock.unlock_read() def keys(self): - u"""Gets a list of attribute names. The names are returned in an + """Gets a list of attribute names. The names are returned in an arbitrary order (just like for an ordinary Python dictionary). """ - self._assertNode() - return _collectAttributes(self._c_node, 1) + self._lock.lock_read() + try: + self._assertNode() + return _collectAttributes(self._c_node, 1) + finally: + self._lock.unlock_read() def values(self): - u"""Gets element attributes, as a sequence. The attributes are returned + """Gets element attributes, as a sequence. The attributes are returned in an arbitrary order. """ - self._assertNode() - return _collectAttributes(self._c_node, 2) + self._lock.lock_read() + try: + self._assertNode() + return _collectAttributes(self._c_node, 2) + finally: + self._lock.unlock_read() def items(self): - u"""Gets element attributes, as a sequence. The attributes are returned + """Gets element attributes, as a sequence. The attributes are returned in an arbitrary order. """ - self._assertNode() - return _collectAttributes(self._c_node, 3) + self._lock.lock_read() + try: + self._assertNode() + return _collectAttributes(self._c_node, 3) + finally: + self._lock.unlock_read() -cdef _ReadOnlyProxy _newReadOnlyProxy( - _ReadOnlyProxy source_proxy, xmlNode* c_node): + +cdef _ReadOnlyProxy _newReadOnlyProxy(_ReadOnlyProxy source_proxy, xmlNode* c_node): cdef _ReadOnlyProxy el if c_node.type == tree.XML_ELEMENT_NODE: el = _ReadOnlyElementProxy.__new__(_ReadOnlyElementProxy) @@ -340,19 +446,24 @@ cdef _ReadOnlyProxy _newReadOnlyProxy( el = _ReadOnlyProxy.__new__(_ReadOnlyProxy) else: raise TypeError(f"Unsupported element type: {c_node.type}") + el._c_node = c_node _initReadOnlyProxy(el, source_proxy) return el + cdef inline _initReadOnlyProxy(_ReadOnlyProxy el, _ReadOnlyProxy source_proxy): if source_proxy is None: el._source_proxy = el + el._lock = RWLock.__new__(RWLock) el._dependent_proxies = [el] else: el._source_proxy = source_proxy + el._lock = source_proxy._lock source_proxy._dependent_proxies.append(el) + cdef _freeReadOnlyProxies(_ReadOnlyProxy sourceProxy): cdef xmlNode* c_node cdef _ReadOnlyProxy el @@ -367,6 +478,7 @@ cdef _freeReadOnlyProxies(_ReadOnlyProxy sourceProxy): tree.xmlFreeNode(c_node) del sourceProxy._dependent_proxies[:] + # opaque wrapper around non-element nodes, e.g. the document node # # This class does not imply any restrictions on modifiability or @@ -376,19 +488,20 @@ cdef _freeReadOnlyProxies(_ReadOnlyProxy sourceProxy): cdef class _OpaqueNodeWrapper: cdef tree.xmlNode* _c_node def __init__(self): - raise TypeError, u"This type cannot be instantiated from Python" + raise TypeError, "This type cannot be instantiated from Python" + @cython.final @cython.internal cdef class _OpaqueDocumentWrapper(_OpaqueNodeWrapper): cdef int _assertNode(self) except -1: - u"""This is our way of saying: this proxy is invalid! + """This is our way of saying: this proxy is invalid! """ - assert self._c_node is not NULL, u"Proxy invalidated!" + assert self._c_node is not NULL, "Proxy invalidated!" return 0 cpdef append(self, other_element): - u"""Append a copy of an Element to the list of children. + """Append a copy of an Element to the list of children. """ cdef xmlNode* c_next cdef xmlNode* c_node @@ -396,7 +509,7 @@ cdef class _OpaqueDocumentWrapper(_OpaqueNodeWrapper): c_node = _roNodeOf(other_element) if c_node.type == tree.XML_ELEMENT_NODE: if tree.xmlDocGetRootElement(self._c_node) is not NULL: - raise ValueError, u"cannot append, document already has a root element" + raise ValueError, "cannot append, document already has a root element" elif c_node.type not in (tree.XML_PI_NODE, tree.XML_COMMENT_NODE): raise TypeError, f"unsupported element type for top-level node: {c_node.type}" c_node = _copyNodeToDoc(c_node, self._c_node) @@ -405,13 +518,14 @@ cdef class _OpaqueDocumentWrapper(_OpaqueNodeWrapper): _moveTail(c_next, c_node) def extend(self, elements): - u"""Append a copy of all Elements from a sequence to the list of + """Append a copy of all Elements from a sequence to the list of children. """ self._assertNode() for element in elements: self.append(element) + cdef _OpaqueNodeWrapper _newOpaqueAppendOnlyNodeWrapper(xmlNode* c_node): cdef _OpaqueNodeWrapper node if c_node.type in (tree.XML_DOCUMENT_NODE, tree.XML_HTML_DOCUMENT_NODE): @@ -421,11 +535,13 @@ cdef _OpaqueNodeWrapper _newOpaqueAppendOnlyNodeWrapper(xmlNode* c_node): node._c_node = c_node return node -# element proxies that allow restricted modification + +# Element proxies that allow restricted modification. +# No locking, requires a thread-local document. @cython.internal cdef class _ModifyContentOnlyProxy(_ReadOnlyProxy): - u"""A read-only proxy that allows changing the text content. + """A read-only proxy that allows changing the text content. """ property text: def __get__(self): @@ -472,7 +588,7 @@ cdef class _ModifyContentOnlyEntityProxy(_ModifyContentOnlyProxy): def __set__(self, value): value = _utf8(value) - assert u'&' not in value and u';' not in value, \ + assert '&' not in value and ';' not in value, \ f"Invalid entity name '{value}'" c_text = _xcstr(value) tree.xmlNodeSetName(self._c_node, c_text) @@ -481,11 +597,11 @@ cdef class _ModifyContentOnlyEntityProxy(_ModifyContentOnlyProxy): @cython.final @cython.internal cdef class _AppendOnlyElementProxy(_ReadOnlyElementProxy): - u"""A read-only element that allows adding children and changing the + """A read-only element that allows adding children and changing the text content (i.e. everything that adds to the subtree). """ cpdef append(self, other_element): - u"""Append a copy of an Element to the list of children. + """Append a copy of an Element to the list of children. """ cdef xmlNode* c_next cdef xmlNode* c_node @@ -495,9 +611,9 @@ cdef class _AppendOnlyElementProxy(_ReadOnlyElementProxy): c_next = c_node.next tree.xmlAddChild(self._c_node, c_node) _moveTail(c_next, c_node) - + def extend(self, elements): - u"""Append a copy of all Elements from a sequence to the list of + """Append a copy of all Elements from a sequence to the list of children. """ self._assertNode() @@ -546,7 +662,7 @@ cdef xmlNode* _roNodeOf(element) except NULL: raise TypeError, f"invalid argument type {type(element)}" if c_node is NULL: - raise TypeError, u"invalid element" + raise TypeError, "invalid element" return c_node cdef xmlNode* _nonRoNodeOf(element) except NULL: @@ -561,5 +677,5 @@ cdef xmlNode* _nonRoNodeOf(element) except NULL: raise TypeError, f"invalid argument type {type(element)}" if c_node is NULL: - raise TypeError, u"invalid element" + raise TypeError, "invalid element" return c_node diff --git a/src/lxml/relaxng.pxi b/src/lxml/relaxng.pxi index 6a82a295f..0472c9352 100644 --- a/src/lxml/relaxng.pxi +++ b/src/lxml/relaxng.pxi @@ -15,15 +15,15 @@ cdef int _require_rnc2rng() except -1: return 0 -cdef class RelaxNGError(LxmlError): +class RelaxNGError(LxmlError): """Base class for RelaxNG errors. """ -cdef class RelaxNGParseError(RelaxNGError): +class RelaxNGParseError(RelaxNGError): """Error while parsing an XML document as RelaxNG. """ -cdef class RelaxNGValidateError(RelaxNGError): +class RelaxNGValidateError(RelaxNGError): """Error while validating an XML document with a RelaxNG schema. """ @@ -32,17 +32,18 @@ cdef class RelaxNGValidateError(RelaxNGError): # RelaxNG cdef class RelaxNG(_Validator): - u"""RelaxNG(self, etree=None, file=None) + """RelaxNG(self, etree=None, file=None) Turn a document into a Relax NG validator. Either pass a schema as Element or ElementTree, or pass a file or filename through the ``file`` keyword argument. """ cdef relaxng.xmlRelaxNG* _c_schema - def __cinit__(self): - self._c_schema = NULL def __init__(self, etree=None, *, file=None): + if self._c_schema is not NULL: + raise RuntimeError("Repeated call to RelaxNG.__init__()") + cdef _Document doc cdef _Element root_node cdef xmlDoc* fake_c_doc = NULL @@ -51,7 +52,12 @@ cdef class RelaxNG(_Validator): if etree is not None: doc = _documentOrRaise(etree) root_node = _rootNodeOrRaise(etree) - fake_c_doc = _fakeRootDoc(doc._c_doc, root_node._c_node) + doc.lock_fakedoc() + try: + fake_c_doc = _fakeRootDoc(doc._c_doc, root_node._c_node) + except: + doc.unlock_fakedoc() + raise parser_ctxt = relaxng.xmlRelaxNGNewDocParserCtxt(fake_c_doc) elif file is not None: if _isString(file): @@ -63,10 +69,8 @@ cdef class RelaxNG(_Validator): else: doc = None filename = _encodeFilename(file) - with self._error_log: - orig_loader = _register_document_loader() + with self._error_log, lxml_document_loader: parser_ctxt = relaxng.xmlRelaxNGNewParserCtxt(_cstr(filename)) - _reset_document_loader(orig_loader) elif (_getFilenameForFile(file) or '')[-4:].lower() == '.rnc': _require_rnc2rng() rng_data_utf8 = _utf8(_rnc2rng.dumps(_rnc2rng.load(file))) @@ -77,38 +81,42 @@ cdef class RelaxNG(_Validator): doc = _parseDocument(file, parser=None, base_url=None) parser_ctxt = relaxng.xmlRelaxNGNewDocParserCtxt(doc._c_doc) else: - raise RelaxNGParseError, u"No tree or file given" + raise RelaxNGParseError, "No tree or file given" if parser_ctxt is NULL: if fake_c_doc is not NULL: _destroyFakeDoc(doc._c_doc, fake_c_doc) + doc.unlock_fakedoc() raise RelaxNGParseError( self._error_log._buildExceptionMessage( - u"Document is not parsable as Relax NG"), + "Document is not parsable as Relax NG"), self._error_log) + # Need a cast here because older libxml2 releases do not use 'const' in the functype. relaxng.xmlRelaxNGSetParserStructuredErrors( - parser_ctxt, _receiveError, self._error_log) + parser_ctxt, _receiveError, self._error_log) _connectGenericErrorLog(self._error_log, xmlerror.XML_FROM_RELAXNGP) + old_resource_loader = _register_relaxng_resource_loader(parser_ctxt) self._c_schema = relaxng.xmlRelaxNGParse(parser_ctxt) + _reset_resource_loader(old_resource_loader) _connectGenericErrorLog(None) relaxng.xmlRelaxNGFreeParserCtxt(parser_ctxt) + if fake_c_doc is not NULL: + _destroyFakeDoc(doc._c_doc, fake_c_doc) + doc.unlock_fakedoc() + if self._c_schema is NULL: - if fake_c_doc is not NULL: - _destroyFakeDoc(doc._c_doc, fake_c_doc) raise RelaxNGParseError( self._error_log._buildExceptionMessage( - u"Document is not valid Relax NG"), + "Document is not valid Relax NG"), self._error_log) - if fake_c_doc is not NULL: - _destroyFakeDoc(doc._c_doc, fake_c_doc) def __dealloc__(self): relaxng.xmlRelaxNGFree(self._c_schema) def __call__(self, etree): - u"""__call__(self, etree) + """__call__(self, etree) Validate doc using Relax NG. @@ -127,22 +135,25 @@ cdef class RelaxNG(_Validator): if valid_ctxt is NULL: raise MemoryError() + doc.lock_fakedoc() try: self._error_log.clear() + # Need a cast here because older libxml2 releases do not use 'const' in the functype. relaxng.xmlRelaxNGSetValidStructuredErrors( - valid_ctxt, _receiveError, self._error_log) + valid_ctxt, _receiveError, self._error_log) _connectGenericErrorLog(self._error_log, xmlerror.XML_FROM_RELAXNGV) c_doc = _fakeRootDoc(doc._c_doc, root_node._c_node) with nogil: ret = relaxng.xmlRelaxNGValidateDoc(valid_ctxt, c_doc) _destroyFakeDoc(doc._c_doc, c_doc) finally: + doc.unlock_fakedoc() _connectGenericErrorLog(None) relaxng.xmlRelaxNGFreeValidCtxt(valid_ctxt) if ret == -1: raise RelaxNGValidateError( - u"Internal error in Relax NG validation", + "Internal error in Relax NG validation", self._error_log) if ret == 0: return True @@ -161,3 +172,11 @@ cdef class RelaxNG(_Validator): _require_rnc2rng() rng_str = utf8(_rnc2rng.dumps(_rnc2rng.loads(src))) return cls(_parseMemoryDocument(rng_str, parser=None, url=base_url)) + + +cdef xmlparser.xmlExternalEntityLoader _register_relaxng_resource_loader(relaxng.xmlRelaxNGParserCtxt *rng_ctxt) noexcept nogil: + if tree.LIBXML_VERSION < 21400: + return _register_resource_loader() + # libxml2 2.14 has per-context document loaders. + relaxng.xmlRelaxNGSetResourceLoader(rng_ctxt, _local_resource_loader, NULL) + return NULL diff --git a/src/lxml/rwlock.pxi b/src/lxml/rwlock.pxi new file mode 100644 index 000000000..f1987f6e6 --- /dev/null +++ b/src/lxml/rwlock.pxi @@ -0,0 +1,438 @@ +"""Read-write lock implementation. +""" + +cdef extern from * nogil: + """ +#include +#include + +#ifndef LXML_ATOMICS_ENABLED + #define LXML_ATOMICS_ENABLED 1 +#endif + +#ifndef LXML_LOCK_PERFORMANCE + #define LXML_LOCK_PERFORMANCE 0 +#endif + +#define __lxml_atomic_int_type int32_t +#define __lxml_nonatomic_int_type int32_t + +/* For standard C atomics, get the headers first so we have ATOMIC_INT_LOCK_FREE */ +/* defined when we decide to use them. */ +#if LXML_ATOMICS_ENABLED && (defined(__STDC_VERSION__) && \ + (__STDC_VERSION__ >= 201112L) && \ + !defined(__STDC_NO_ATOMICS__)) + #include +#endif + +#if LXML_ATOMICS_ENABLED && defined(Py_ATOMIC_H) + /* "Python.h" included "pyatomics.h" */ + + #define __lxml_atomic_compare_exchange(value, expected, desired) _Py_atomic_compare_exchange_int32((value), (expected), (desired)) + #define __lxml_atomic_add(value, arg) _Py_atomic_add_int32((value), (arg)) + #define __lxml_atomic_incr(value) __lxml_atomic_add((value), 1) + #define __lxml_atomic_decr(value) __lxml_atomic_add((value), -1) + #define __lxml_atomic_load(value) _Py_atomic_load_int32((value)) + + #if defined(LXML_DEBUG_ATOMICS) && defined(_MSC_VER) + #pragma message ("Using pyatomics.h atomics") + #elif defined(LXML_DEBUG_ATOMICS) + #warning "Using pyatomics.h atomics" + #endif + +#elif LXML_ATOMICS_ENABLED && (defined(__STDC_VERSION__) && \ + (__STDC_VERSION__ >= 201112L) && \ + !defined(__STDC_NO_ATOMICS__) && \ + ATOMIC_INT_LOCK_FREE == 2) + /* C11 atomics are available and ATOMIC_INT_LOCK_FREE is definitely on */ + #undef __lxml_atomic_int_type + #define __lxml_atomic_int_type _Atomic __lxml_nonatomic_int_type + + #define __lxml_atomic_compare_exchange(value, expected, desired) atomic_compare_exchange_strong((value), (expected), (desired)) + #define __lxml_atomic_add(value, arg) atomic_fetch_add_explicit((value), (arg), memory_order_seq_cst) + #define __lxml_atomic_incr(value) __lxml_atomic_add((value), 1) + #define __lxml_atomic_decr(value) __lxml_atomic_add((value), -1) + #define __lxml_atomic_load(value) atomic_load((value)) + + #if defined(LXML_DEBUG_ATOMICS) && defined(_MSC_VER) + #pragma message ("Using standard C11 atomics") + #elif defined(LXML_DEBUG_ATOMICS) + #warning "Using standard C11 atomics" + #endif + +#elif LXML_ATOMICS_ENABLED && (__GNUC__ >= 5 || (__GNUC__ == 4 && \ + (__GNUC_MINOR__ > 1 || \ + (__GNUC_MINOR__ == 1 && __GNUC_PATCHLEVEL__ >= 2)))) + + /* gcc >= 4.1.2 */ + #define __lxml_atomic_add(value, arg) __sync_fetch_and_add((value), (arg)) + #define __lxml_atomic_incr(value) __sync_fetch_and_add((value), 1) + #define __lxml_atomic_decr(value) __sync_fetch_and_sub((value), 1) + #define __lxml_atomic_load(value) __sync_fetch_and_add((value), 0) + + /* UNUSED + static int __lxml_atomic_compare_exchange(__lxml_atomic_int_type *value, __lxml_nonatomic_int_type *expected, __lxml_nonatomic_int_type desired) { + __lxml_nonatomic_int_type old_value = __sync_val_compare_and_swap(value, *expected, desired); + if (old_value != *expected) { + *expected = old_value; + return 0; + } + return 1; + } + */ + + #ifdef LXML_DEBUG_ATOMICS + #warning "Using GNU atomics" + #endif + +#elif LXML_ATOMICS_ENABLED && defined(_MSC_VER) + /* msvc */ + #include + + #pragma intrinsic (_InterlockedExchangeAdd, _InterlockedCompareExchange) + + #define __lxml_atomic_add(value, arg) _InterlockedExchangeAdd((value), (arg)) + #define __lxml_atomic_incr(value) __lxml_atomic_add((value), 1) + #define __lxml_atomic_decr(value) __lxml_atomic_add((value), -1) + #define __lxml_atomic_load(value) (*(value)) + + /* UNUSED + static int __lxml_atomic_compare_exchange(__lxml_atomic_int_type *value, __lxml_nonatomic_int_type *expected, __lxml_nonatomic_int_type desired) { + __lxml_nonatomic_int_type old_value = _InterlockedCompareExchange(value, *expected, desired); + if (old_value != *expected) { + *expected = old_value; + return 0; + } + return 1; + } + */ + + #ifdef LXML_DEBUG_ATOMICS + #pragma message ("Using MSVC atomics") + #endif + +#elif PY_VERSION_HEX >= 0x030d0000 + /* Python critical section */ + #undef LXML_ATOMICS_ENABLED + #define LXML_ATOMICS_ENABLED 0 + + static __lxml_nonatomic_int_type __lxml_atomic_add_cs(PyObject *cs, __lxml_atomic_int_type *value, __lxml_nonatomic_int_type arg) { + __lxml_nonatomic_int_type old_value; + Py_BEGIN_CRITICAL_SECTION(cs); + old_value = *value; + *value = old_value + arg; + Py_END_CRITICAL_SECTION(); + return old_value; + } + + #define __lxml_atomic_add(value, arg) __lxml_atomic_add_cs(__pyx_v_self, value, arg) + #define __lxml_atomic_incr(value) __lxml_atomic_add((value), 1) + #define __lxml_atomic_decr(value) __lxml_atomic_add((value), -1) + #define __lxml_atomic_load(value) (*(value)) + + /* UNUSED + static int __lxml_atomic_compare_exchange_cs(PyObject *cs, __lxml_atomic_int_type *value, __lxml_nonatomic_int_type *expected, __lxml_nonatomic_int_type desired) { + __lxml_nonatomic_int_type old_value; + int retval; + Py_BEGIN_CRITICAL_SECTION(cs); + old_value = *value; + if (old_value == *expected) { + *value = desired; + retval = 1; + } else { + *expected = old_value; + retval = 0; + } + Py_END_CRITICAL_SECTION(); + return retval; + } + + #define __lxml_atomic_compare_exchange(value, expected, desired) __lxml_atomic_compare_exchange_cs(__pyx_v_self, (value), (expected), (desired)) + */ + + #if defined(LXML_DEBUG_ATOMICS) && defined(_MSC_VER) + #pragma message ("Not using atomics, using CPython critical section") + #elif defined(LXML_DEBUG_ATOMICS) + #warning "Not using atomics, using CPython critical section" + #endif + +#else + #undef LXML_ATOMICS_ENABLED + #define LXML_ATOMICS_ENABLED 0 + + #define __lxml_atomic_add(value, arg) ((*(value)) += (arg), (*(value) - (arg))) + #define __lxml_atomic_incr(value) (*(value))++ + #define __lxml_atomic_decr(value) (*(value))-- + #define __lxml_atomic_load(value) (*(value)) + + /* UNUSED + static int __lxml_atomic_compare_exchange(__lxml_atomic_int_type *value, __lxml_nonatomic_int_type *expected, __lxml_nonatomic_int_type desired) { + __lxml_nonatomic_int_type old_value = *value; + if (old_value == *expected) { + *value = desired; + return 1; + } else { + *expected = old_value; + return 0; + } + } + */ + + #if defined(LXML_DEBUG_ATOMICS) && defined(_MSC_VER) + #pragma message ("Not using atomics, using the GIL") + #elif defined(LXML_DEBUG_ATOMICS) + #warning "Not using atomics, using the GIL" + #endif +#endif + +#if LXML_LOCK_PERFORMANCE + #define __lxml_inc_counter(counter) __lxml_atomic_incr((counter)) + typedef struct { + __lxml_atomic_int_type read_acquired; + __lxml_atomic_int_type write_acquired; + __lxml_atomic_int_type write_reentry; + __lxml_atomic_int_type read_wait_on_writer; + __lxml_atomic_int_type write_wait_on_reader; + __lxml_atomic_int_type write_wait_on_writer; + } __lxml_lock_perf; +#else + /* Fake counter struct that just provides all attributes. */ + #define __lxml_inc_counter(counter) + typedef struct { + unsigned int read_acquired: 1; + unsigned int write_acquired: 1; + unsigned int write_reentry: 1; + unsigned int read_wait_on_writer: 1; + unsigned int write_wait_on_reader: 1; + unsigned int write_wait_on_writer: 1; + } __lxml_lock_perf; +#endif + """ + const bint ATOMICS_ENABLED "LXML_ATOMICS_ENABLED" + ctypedef long atomic_int "__lxml_atomic_int_type" + ctypedef long nonatomic_int "__lxml_nonatomic_int_type" + + nonatomic_int atomic_add "__lxml_atomic_add" (atomic_int *value, nonatomic_int arg) noexcept + nonatomic_int atomic_incr "__lxml_atomic_incr" (atomic_int *value) noexcept + nonatomic_int atomic_decr "__lxml_atomic_decr" (atomic_int *value) noexcept + nonatomic_int atomic_load "__lxml_atomic_load" (atomic_int *value) noexcept + int atomic_compare_exchange "__lxml_atomic_compare_exchange" (atomic_int *value, nonatomic_int *expected, nonatomic_int desired) noexcept + + const bint COUNT_LOCK_PERFORMANCE "LXML_LOCK_PERFORMANCE" + void inc_perf_counter "__lxml_inc_counter" (atomic_int *counter) + + ctypedef struct lock_perf "__lxml_lock_perf": + atomic_int read_acquired + atomic_int write_acquired + atomic_int write_reentry + atomic_int read_wait_on_writer + atomic_int write_wait_on_reader + atomic_int write_wait_on_writer + + +cdef const nonatomic_int max_lock_reader_count = 1 << 30 + + +@cython.final +@cython.internal +@cython.profile(False) +@cython.linetrace(False) +cdef class RWLock: + """Writer-preferring Read-Write lock. + + Uses atomics to avoid locking in the non-congested case. + Uses 'PyMutex' to block readers and writers while writing. + """ + cdef unsigned long _write_locked_id + cdef atomic_int _reader_count + cdef atomic_int _readers_departing + cdef atomic_int _readers_waiting + cdef cython.pymutex _readers_wait_lock + cdef cython.pymutex _readers_wait_guard + cdef cython.pymutex _writer_wait_lock + cdef cython.pymutex _writer_lock + cdef int _writer_reentry + cdef lock_perf _perf_counters + + @cython.inline + @cython.profile(False) + cdef unsigned long _my_lock_id(self) noexcept: + # "+1" to make sure that "== 0" really means "no thread waiting". + return python.PyThread_get_thread_ident() + 1 + + @cython.inline + @cython.profile(False) + cdef bint _owns_write_lock(self, unsigned long lock_id) noexcept nogil: + return lock_id == self._write_locked_id + + def get_perf_counters(self) -> dict: + """Return the current performance counters as dict. + """ + if COUNT_LOCK_PERFORMANCE: + return self._perf_counters + else: + return {} + + # Read locking. + + cdef void _unblock_readers(self, nonatomic_int waiting_readers) noexcept: + while atomic_load(&self._readers_waiting) == 0: + # Wait for the first reader to acquire the lock. + with nogil: pass + + # Signal to the reader that we noticed it taking the lock. + waiting_already = atomic_add(&self._readers_waiting, waiting_readers) + assert waiting_already == -1, waiting_already + + # Unlock the first reader. + self._readers_wait_lock.release() + + cdef void _wait_for_pending_readers_to_start(self): + """Wait for any pending readers to start running after a writer unblocked them.""" + while atomic_load(&self._readers_waiting) > 0: + with nogil: pass + + cdef void _wait_to_read(self) noexcept: + # Guard against concurrent readers. + with self._readers_wait_guard: + inc_perf_counter(&self._perf_counters.read_wait_on_writer) + + # Wait for writer to unblock us. + with self._readers_wait_lock: + # Signal the writer that we have acquired the lock. + readers_waiting = atomic_decr(&self._readers_waiting) + + if readers_waiting == 0: + # We acquired the lock first and notified the writer about it. + # Wait for the writer to release the lock to us. + self._readers_wait_lock.acquire() + + inc_perf_counter(&self._perf_counters.read_acquired) + + cdef void lock_read(self) noexcept: + readers_before = atomic_incr(&self._reader_count) + if readers_before >= 0: + # Only readers active => go! + inc_perf_counter(&self._perf_counters.read_acquired) + return + + if self._owns_write_lock(self._my_lock_id()): + # I own the write lock => ignore the read lock and read! + atomic_decr(&self._reader_count) + inc_perf_counter(&self._perf_counters.read_acquired) + return + + # A writer is waiting => wait for lock to become free. + self._wait_to_read() + + cdef void unlock_read(self) noexcept: + readers_before = atomic_decr(&self._reader_count) + assert readers_before != 0 + if readers_before >= 0: + return + + if self._owns_write_lock(self._my_lock_id()): + # I own the write lock and ignored the read lock => undo the read claim. + atomic_incr(&self._reader_count) + return + + # A writer is waiting. + readers_departing = atomic_decr(&self._readers_departing) + if readers_departing == 1: + # No more readers after us, notify the waiting writer. + self._notify_writer() + + # Write locking. + + @cython.inline + cdef void _notify_writer(self) noexcept: + self._writer_wait_lock.release() + + cdef void _wait_for_readers_to_finish(self, nonatomic_int readers) noexcept: + self._writer_wait_lock.acquire() + + inc_perf_counter(&self._perf_counters.write_wait_on_reader) + + # Push current readers to '_readers_departing' and wait for them to exit. + # Note that they might have departed already, making "self._readers_departing" negative + # before we add to it. + readers_departing = atomic_add(&self._readers_departing, readers) + readers + if readers_departing > 0: + # Wait for the readers to finish. + self._writer_wait_lock.acquire() + self._writer_wait_lock.release() + + cdef void lock_write(self) noexcept: + my_lock_id = self._my_lock_id() + + if self._owns_write_lock(my_lock_id): + inc_perf_counter(&self._perf_counters.write_reentry) + self._writer_reentry += 1 + return + + if COUNT_LOCK_PERFORMANCE: + if not self._owns_write_lock(0): + # Risks race conditions if a writer finishes between now and us acquiring + # the writer lock below, but that seems acceptable for a performance counter. + inc_perf_counter(&self._perf_counters.write_wait_on_writer) + + self._writer_lock.acquire() + + self._wait_for_pending_readers_to_start() + + # Claim the lock and block new readers if no writers are waiting. + readers = atomic_add(&self._reader_count, -max_lock_reader_count) + + if readers != 0: + if readers > 0: + self._wait_for_readers_to_finish(readers) + else: + assert readers >= 0, "Writer claimed the lock but did not acquire it!" + + # No readers, no writers => go. + self._write_locked_id = my_lock_id + inc_perf_counter(&self._perf_counters.write_acquired) + + cdef void unlock_write(self) noexcept: + assert self._owns_write_lock(self._my_lock_id()), f"{self._write_locked_id} != {self._my_lock_id()}" + assert atomic_load(&self._reader_count) < 0, atomic_load(&self._reader_count) + + if self._writer_reentry > 0: + self._writer_reentry -= 1 + return + + self._write_locked_id = 0 + + readers = atomic_add(&self._reader_count, max_lock_reader_count) + max_lock_reader_count + if readers > 0: + self._unblock_readers(readers) + + self._writer_lock.release() + + # Double lock locking. + + cdef void lock_write_with(self, RWLock second_lock) noexcept: + """Acquire two locks for writing at the same time. + """ + # Avoid deadlocks by deterministically locking an arbitrary lock first. + if self is second_lock: + self.lock_write() + elif self < second_lock: + second_lock.lock_write() + self.lock_write() + else: + self.lock_write() + second_lock.lock_write() + + cdef void unlock_write_with(self, RWLock second_lock) noexcept: + """Release two locks for writing after locking them at the same time. + """ + # Avoid deadlocks by deterministically locking an arbitrary lock first. + if self is second_lock: + self.unlock_write() + elif self < second_lock: + self.unlock_write() + second_lock.unlock_write() + else: + second_lock.unlock_write() + self.unlock_write() diff --git a/src/lxml/sax.py b/src/lxml/sax.py index 02ee3bf39..2648f6ac6 100644 --- a/src/lxml/sax.py +++ b/src/lxml/sax.py @@ -1,5 +1,3 @@ -# cython: language_level=2 - """ SAX-based adapter to copy trees from/to the Python standard library. @@ -12,13 +10,19 @@ See https://lxml.de/sax.html """ -from __future__ import absolute_import from xml.sax.handler import ContentHandler from lxml import etree from lxml.etree import ElementTree, SubElement from lxml.etree import Comment, ProcessingInstruction +try: + from types import GenericAlias as _GenericAlias +except ImportError: + # Python 3.8 - we only need this as return value from "__class_getitem__" + def _GenericAlias(cls, item): + return f"{cls.__name__}[{item.__name__}]" + class SaxError(etree.LxmlError): """General SAX error. @@ -26,7 +30,7 @@ class SaxError(etree.LxmlError): def _getNsTag(tag): - if tag[0] == '{': + if tag[0] == '{' and '}' in tag: return tuple(tag[1:].split('}', 1)) else: return None, tag @@ -135,9 +139,7 @@ def endElementNS(self, ns_name, qname): def startElement(self, name, attributes=None): if attributes: - attributes = dict( - [((None, k), v) for k, v in attributes.items()] - ) + attributes = {(None, k): v for k, v in attributes.items()} self.startElementNS((None, name), name, attributes) def endElement(self, name): @@ -148,15 +150,20 @@ def characters(self, data): try: # if there already is a child element, we must append to its tail last_element = last_element[-1] - last_element.tail = (last_element.tail or '') + data except IndexError: # otherwise: append to the text last_element.text = (last_element.text or '') + data + else: + last_element.tail = (last_element.tail or '') + data ignorableWhitespace = characters + # Allow subscripting sax.ElementTreeContentHandler in type annotations (PEP 560) + def __class_getitem__(cls, item): + return _GenericAlias(cls, item) + -class ElementTreeProducer(object): +class ElementTreeProducer: """Produces SAX events for an element and children. """ def __init__(self, element_or_tree, content_handler): diff --git a/src/lxml/saxparser.pxi b/src/lxml/saxparser.pxi index 49e72beaf..01a66a384 100644 --- a/src/lxml/saxparser.pxi +++ b/src/lxml/saxparser.pxi @@ -7,6 +7,8 @@ class XMLSyntaxAssertionError(XMLSyntaxError, AssertionError): This class may get replaced by a plain XMLSyntaxError in a future version. """ + def __init__(self, message): + XMLSyntaxError.__init__(self, message, None, 0, 1) ctypedef enum _SaxParserEvents: @@ -29,8 +31,7 @@ ctypedef enum _ParseEventFilter: cdef int _buildParseEventFilter(events) except -1: - cdef int event_filter - event_filter = 0 + cdef int event_filter = 0 for event in events: if event == 'start': event_filter |= PARSE_EVENT_FILTER_START @@ -51,8 +52,6 @@ cdef int _buildParseEventFilter(events) except -1: cdef class _SaxParserTarget: cdef int _sax_event_filter - def __cinit__(self): - self._sax_event_filter = 0 cdef _handleSaxStart(self, tag, attrib, nsmap): return None @@ -74,9 +73,8 @@ cdef class _SaxParserTarget: #@cython.final @cython.internal -@cython.no_gc_clear # Required because parent class uses it - Cython bug. cdef class _SaxParserContext(_ParserContext): - u"""This class maps SAX2 events to parser target events. + """This class maps SAX2 events to parser target events. """ cdef _SaxParserTarget _target cdef _BaseParser _parser @@ -107,17 +105,17 @@ cdef class _SaxParserContext(_ParserContext): self._parser = parser self.events_iterator = _ParseEventsIterator() - cdef void _setSaxParserTarget(self, _SaxParserTarget target): + cdef void _setSaxParserTarget(self, _SaxParserTarget target) noexcept: self._target = target - cdef void _initParserContext(self, xmlparser.xmlParserCtxt* c_ctxt): + cdef void _initParserContext(self, xmlparser.xmlParserCtxt* c_ctxt) noexcept: _ParserContext._initParserContext(self, c_ctxt) if self._target is not None: self._connectTarget(c_ctxt) elif self._event_filter: self._connectEvents(c_ctxt) - cdef void _connectTarget(self, xmlparser.xmlParserCtxt* c_ctxt): + cdef void _connectTarget(self, xmlparser.xmlParserCtxt* c_ctxt) noexcept: """Wrap original SAX2 callbacks to call into parser target. """ sax = c_ctxt.sax @@ -163,7 +161,7 @@ cdef class _SaxParserContext(_ParserContext): sax.reference = NULL c_ctxt.replaceEntities = 1 - cdef void _connectEvents(self, xmlparser.xmlParserCtxt* c_ctxt): + cdef void _connectEvents(self, xmlparser.xmlParserCtxt* c_ctxt) noexcept: """Wrap original SAX2 callbacks to collect parse events without parser target. """ sax = c_ctxt.sax @@ -218,7 +216,7 @@ cdef class _SaxParserContext(_ParserContext): finally: self._parser = None # clear circular reference ASAP if self._matcher is not None: - self._matcher.cacheTags(self._doc, True) # force entry in libxml2 dict + self._matcher.cacheTags(self._doc, force_into_dict=True) return 0 cdef int pushEvent(self, event, xmlNode* c_node) except -1: @@ -239,7 +237,7 @@ cdef class _SaxParserContext(_ParserContext): while self._ns_stack: _pushSaxNsEndEvents(self) - cdef void _handleSaxException(self, xmlparser.xmlParserCtxt* c_ctxt): + cdef void _handleSaxException(self, xmlparser.xmlParserCtxt* c_ctxt) noexcept: if c_ctxt.errNo == xmlerror.XML_ERR_OK: c_ctxt.errNo = xmlerror.XML_ERR_INTERNAL_ERROR # stop parsing immediately @@ -294,11 +292,11 @@ cdef void _handleSaxStart( const_xmlChar* c_namespace, int c_nb_namespaces, const_xmlChar** c_namespaces, int c_nb_attributes, int c_nb_defaulted, - const_xmlChar** c_attributes) with gil: + const_xmlChar** c_attributes) noexcept with gil: cdef int i cdef size_t c_len c_ctxt = ctxt - if c_ctxt._private is NULL or c_ctxt.disableSAX: + if c_ctxt._private is NULL or xmlparser.xmlCtxtIsStopped(c_ctxt): return context = <_SaxParserContext>c_ctxt._private cdef int event_filter = context._event_filter @@ -319,6 +317,12 @@ cdef void _handleSaxStart( c_nb_defaulted, c_attributes) if c_ctxt.html: _fixHtmlDictNodeNames(c_ctxt.dict, c_ctxt.node) + # The HTML parser in libxml2 reports the missing opening tags when it finds + # misplaced ones, but with tag names from C string constants that ignore the + # parser dict. Thus, we need to intern the name ourselves. + c_localname = tree.xmlDictLookup(c_ctxt.dict, c_localname, -1) + if c_localname is NULL: + raise MemoryError() if event_filter & PARSE_EVENT_FILTER_END_NS: context._ns_stack.append(declared_namespaces) @@ -336,11 +340,11 @@ cdef void _handleSaxTargetStart( const_xmlChar* c_namespace, int c_nb_namespaces, const_xmlChar** c_namespaces, int c_nb_attributes, int c_nb_defaulted, - const_xmlChar** c_attributes) with gil: + const_xmlChar** c_attributes) noexcept with gil: cdef int i cdef size_t c_len c_ctxt = ctxt - if c_ctxt._private is NULL or c_ctxt.disableSAX: + if c_ctxt._private is NULL or xmlparser.xmlCtxtIsStopped(c_ctxt): return context = <_SaxParserContext>c_ctxt._private @@ -358,9 +362,6 @@ cdef void _handleSaxTargetStart( if sax_event_filter & SAX_EVENT_START_NS: for prefix, uri in declared_namespaces: context._target._handleSaxStartNs(prefix, uri) - #if not context._target._sax_event_filter & SAX_EVENT_START: - # # *Only* collecting start-ns events. - # return else: declared_namespaces = None @@ -407,15 +408,21 @@ cdef void _handleSaxTargetStart( cdef void _handleSaxStartNoNs(void* ctxt, const_xmlChar* c_name, - const_xmlChar** c_attributes) with gil: + const_xmlChar** c_attributes) noexcept with gil: c_ctxt = ctxt - if c_ctxt._private is NULL or c_ctxt.disableSAX: + if c_ctxt._private is NULL or xmlparser.xmlCtxtIsStopped(c_ctxt): return context = <_SaxParserContext>c_ctxt._private try: context._origSaxStartNoNs(c_ctxt, c_name, c_attributes) if c_ctxt.html: _fixHtmlDictNodeNames(c_ctxt.dict, c_ctxt.node) + # The HTML parser in libxml2 reports the missing opening tags when it finds + # misplaced ones, but with tag names from C string constants that ignore the + # parser dict. Thus, we need to intern the name ourselves. + c_name = tree.xmlDictLookup(c_ctxt.dict, c_name, -1) + if c_name is NULL: + raise MemoryError() if context._event_filter & (PARSE_EVENT_FILTER_END | PARSE_EVENT_FILTER_START): _pushSaxStartEvent(context, c_ctxt, NULL, c_name, None) @@ -426,9 +433,9 @@ cdef void _handleSaxStartNoNs(void* ctxt, const_xmlChar* c_name, cdef void _handleSaxTargetStartNoNs(void* ctxt, const_xmlChar* c_name, - const_xmlChar** c_attributes) with gil: + const_xmlChar** c_attributes) noexcept with gil: c_ctxt = ctxt - if c_ctxt._private is NULL or c_ctxt.disableSAX: + if c_ctxt._private is NULL or xmlparser.xmlCtxtIsStopped(c_ctxt): return context = <_SaxParserContext>c_ctxt._private try: @@ -483,9 +490,9 @@ cdef int _pushSaxStartEvent(_SaxParserContext context, cdef void _handleSaxEnd(void* ctxt, const_xmlChar* c_localname, const_xmlChar* c_prefix, - const_xmlChar* c_namespace) with gil: + const_xmlChar* c_namespace) noexcept with gil: c_ctxt = ctxt - if c_ctxt._private is NULL or c_ctxt.disableSAX: + if c_ctxt._private is NULL or xmlparser.xmlCtxtIsStopped(c_ctxt): return context = <_SaxParserContext>c_ctxt._private try: @@ -506,9 +513,9 @@ cdef void _handleSaxEnd(void* ctxt, const_xmlChar* c_localname, return # swallow any further exceptions -cdef void _handleSaxEndNoNs(void* ctxt, const_xmlChar* c_name) with gil: +cdef void _handleSaxEndNoNs(void* ctxt, const_xmlChar* c_name) noexcept with gil: c_ctxt = ctxt - if c_ctxt._private is NULL or c_ctxt.disableSAX: + if c_ctxt._private is NULL or xmlparser.xmlCtxtIsStopped(c_ctxt): return context = <_SaxParserContext>c_ctxt._private try: @@ -558,10 +565,10 @@ cdef int _pushSaxEndEvent(_SaxParserContext context, return 0 -cdef void _handleSaxData(void* ctxt, const_xmlChar* c_data, int data_len) with gil: +cdef void _handleSaxData(void* ctxt, const_xmlChar* c_data, int data_len) noexcept with gil: # can only be called if parsing with a target c_ctxt = ctxt - if c_ctxt._private is NULL or c_ctxt.disableSAX: + if c_ctxt._private is NULL or xmlparser.xmlCtxtIsStopped(c_ctxt): return context = <_SaxParserContext>c_ctxt._private try: @@ -575,10 +582,10 @@ cdef void _handleSaxData(void* ctxt, const_xmlChar* c_data, int data_len) with g cdef void _handleSaxTargetDoctype(void* ctxt, const_xmlChar* c_name, const_xmlChar* c_public, - const_xmlChar* c_system) with gil: + const_xmlChar* c_system) noexcept with gil: # can only be called if parsing with a target c_ctxt = ctxt - if c_ctxt._private is NULL or c_ctxt.disableSAX: + if c_ctxt._private is NULL or xmlparser.xmlCtxtIsStopped(c_ctxt): return context = <_SaxParserContext>c_ctxt._private try: @@ -592,9 +599,9 @@ cdef void _handleSaxTargetDoctype(void* ctxt, const_xmlChar* c_name, return # swallow any further exceptions -cdef void _handleSaxStartDocument(void* ctxt) with gil: +cdef void _handleSaxStartDocument(void* ctxt) noexcept with gil: c_ctxt = ctxt - if c_ctxt._private is NULL or c_ctxt.disableSAX: + if c_ctxt._private is NULL or xmlparser.xmlCtxtIsStopped(c_ctxt): return context = <_SaxParserContext>c_ctxt._private context._origSaxStartDocument(ctxt) @@ -608,10 +615,10 @@ cdef void _handleSaxStartDocument(void* ctxt) with gil: cdef void _handleSaxTargetPI(void* ctxt, const_xmlChar* c_target, - const_xmlChar* c_data) with gil: + const_xmlChar* c_data) noexcept with gil: # can only be called if parsing with a target c_ctxt = ctxt - if c_ctxt._private is NULL or c_ctxt.disableSAX: + if c_ctxt._private is NULL or xmlparser.xmlCtxtIsStopped(c_ctxt): return context = <_SaxParserContext>c_ctxt._private try: @@ -627,10 +634,10 @@ cdef void _handleSaxTargetPI(void* ctxt, const_xmlChar* c_target, cdef void _handleSaxPIEvent(void* ctxt, const_xmlChar* target, - const_xmlChar* data) with gil: + const_xmlChar* data) noexcept with gil: # can only be called when collecting pi events c_ctxt = ctxt - if c_ctxt._private is NULL or c_ctxt.disableSAX: + if c_ctxt._private is NULL or xmlparser.xmlCtxtIsStopped(c_ctxt): return context = <_SaxParserContext>c_ctxt._private context._origSaxPI(ctxt, target, data) @@ -645,10 +652,10 @@ cdef void _handleSaxPIEvent(void* ctxt, const_xmlChar* target, return # swallow any further exceptions -cdef void _handleSaxTargetComment(void* ctxt, const_xmlChar* c_data) with gil: +cdef void _handleSaxTargetComment(void* ctxt, const_xmlChar* c_data) noexcept with gil: # can only be called if parsing with a target c_ctxt = ctxt - if c_ctxt._private is NULL or c_ctxt.disableSAX: + if c_ctxt._private is NULL or xmlparser.xmlCtxtIsStopped(c_ctxt): return context = <_SaxParserContext>c_ctxt._private try: @@ -661,10 +668,10 @@ cdef void _handleSaxTargetComment(void* ctxt, const_xmlChar* c_data) with gil: return # swallow any further exceptions -cdef void _handleSaxComment(void* ctxt, const_xmlChar* text) with gil: +cdef void _handleSaxComment(void* ctxt, const_xmlChar* text) noexcept with gil: # can only be called when collecting comment events c_ctxt = ctxt - if c_ctxt._private is NULL or c_ctxt.disableSAX: + if c_ctxt._private is NULL or xmlparser.xmlCtxtIsStopped(c_ctxt): return context = <_SaxParserContext>c_ctxt._private context._origSaxComment(ctxt, text) @@ -679,7 +686,7 @@ cdef void _handleSaxComment(void* ctxt, const_xmlChar* text) with gil: return # swallow any further exceptions -cdef inline xmlNode* _findLastEventNode(xmlparser.xmlParserCtxt* c_ctxt): +cdef inline xmlNode* _findLastEventNode(xmlparser.xmlParserCtxt* c_ctxt) noexcept: # this mimics what libxml2 creates for comments/PIs if c_ctxt.inSubset == 1: return c_ctxt.myDoc.intSubset.last @@ -698,8 +705,8 @@ cdef inline xmlNode* _findLastEventNode(xmlparser.xmlParserCtxt* c_ctxt): ############################################################ cdef class TreeBuilder(_SaxParserTarget): - u"""TreeBuilder(self, element_factory=None, parser=None, - comment_factory=None, pi_factory=None, + """TreeBuilder(self, element_factory=None, parser=None, \ + comment_factory=None, pi_factory=None, \ insert_comments=True, insert_pis=True) Parser target that builds a tree from parse event callbacks. @@ -746,12 +753,12 @@ cdef class TreeBuilder(_SaxParserTarget): cdef int _flush(self) except -1: if self._data: if self._last is not None: - text = u"".join(self._data) + text = "".join(self._data) if self._in_tail: - assert self._last.tail is None, u"internal error (tail)" + assert self._last.tail is None, "internal error (tail)" self._last.tail = text else: - assert self._last.text is None, u"internal error (text)" + assert self._last.text is None, "internal error (text)" self._last.text = text del self._data[:] return 0 @@ -767,10 +774,13 @@ cdef class TreeBuilder(_SaxParserTarget): _appendChild(self._element_stack[-1], self._last) elif self._element_stack: self._last = _makeSubElement( - self._element_stack[-1], tag, None, None, attrib, nsmap, None) + self._element_stack[-1], tag, + text=None, tail=None, attrib=attrib, nsmap=nsmap) else: self._last = _makeElement( - tag, NULL, None, self._parser, None, None, attrib, nsmap, None) + tag, NULL, + doc=None, parser=self._parser, + text=None, tail=None, attrib=attrib, nsmap=nsmap) self._element_stack.append(self._last) self._in_tail = 0 return self._last @@ -811,7 +821,7 @@ cdef class TreeBuilder(_SaxParserTarget): # Python level event handlers def close(self): - u"""close(self) + """close(self) Flushes the builder buffers, and returns the toplevel document element. Raises XMLSyntaxError on inconsistencies. @@ -824,7 +834,7 @@ cdef class TreeBuilder(_SaxParserTarget): return self._last def data(self, data): - u"""data(self, data) + """data(self, data) Adds text to the current element. The value should be either an 8-bit string containing ASCII text, or a Unicode string. @@ -832,7 +842,7 @@ cdef class TreeBuilder(_SaxParserTarget): self._handleSaxData(data) def start(self, tag, attrs, nsmap=None): - u"""start(self, tag, attrs, nsmap=None) + """start(self, tag, attrs, nsmap=None) Opens a new element. """ @@ -841,7 +851,7 @@ cdef class TreeBuilder(_SaxParserTarget): return self._handleSaxStart(tag, attrs, nsmap) def end(self, tag): - u"""end(self, tag) + """end(self, tag) Closes the current element. """ @@ -851,7 +861,7 @@ cdef class TreeBuilder(_SaxParserTarget): return element def pi(self, target, data=None): - u"""pi(self, target, data=None) + """pi(self, target, data=None) Creates a processing instruction using the factory, appends it (unless disabled) and returns it. @@ -859,7 +869,7 @@ cdef class TreeBuilder(_SaxParserTarget): return self._handleSaxPi(target, data) def comment(self, comment): - u"""comment(self, comment) + """comment(self, comment) Creates a comment using the factory, appends it (unless disabled) and returns it. diff --git a/src/lxml/schematron.pxi b/src/lxml/schematron.pxi index dfd2cc05f..d0368555e 100644 --- a/src/lxml/schematron.pxi +++ b/src/lxml/schematron.pxi @@ -2,15 +2,15 @@ from lxml.includes cimport schematron -cdef class SchematronError(LxmlError): +class SchematronError(LxmlError): """Base class of all Schematron errors. """ -cdef class SchematronParseError(SchematronError): +class SchematronParseError(SchematronError): """Error while parsing an XML document as Schematron schema. """ -cdef class SchematronValidateError(SchematronError): +class SchematronValidateError(SchematronError): """Error while validating an XML document with a Schematron schema. """ @@ -19,7 +19,7 @@ cdef class SchematronValidateError(SchematronError): # Schematron cdef class Schematron(_Validator): - u"""Schematron(self, etree=None, file=None) + """Schematron(self, etree=None, file=None) A Schematron validator. Pass a root Element or an ElementTree to turn it into a validator. @@ -69,24 +69,35 @@ cdef class Schematron(_Validator): """ cdef schematron.xmlSchematron* _c_schema cdef xmlDoc* _c_schema_doc - def __cinit__(self): - self._c_schema = NULL - self._c_schema_doc = NULL def __init__(self, etree=None, *, file=None): + if self._c_schema is not NULL: + raise RuntimeError("Repeated call to Schematron.__init__()") + cdef _Document doc cdef _Element root_node cdef xmlNode* c_node - cdef char* c_href cdef schematron.xmlSchematronParserCtxt* parser_ctxt = NULL _Validator.__init__(self) if not config.ENABLE_SCHEMATRON: raise SchematronError, \ - u"lxml.etree was compiled without Schematron support." + "lxml.etree was compiled without Schematron support." + + import warnings + warnings.warn( + "The (non-ISO) Schematron feature is deprecated and will be removed from libxml2 and lxml. " + "Use 'lxml.isoschematron' instead.", + DeprecationWarning, + ) + if etree is not None: doc = _documentOrRaise(etree) root_node = _rootNodeOrRaise(etree) - self._c_schema_doc = _copyDocRoot(doc._c_doc, root_node._c_node) + doc.lock_read() + try: + self._c_schema_doc = _copyDocRoot(doc._c_doc, root_node._c_node) + finally: + doc.unlock_read() parser_ctxt = schematron.xmlSchematronNewDocParserCtxt(self._c_schema_doc) elif file is not None: filename = _getFilenameForFile(file) @@ -94,12 +105,10 @@ cdef class Schematron(_Validator): # XXX assume a string object filename = file filename = _encodeFilename(filename) - with self._error_log: - orig_loader = _register_document_loader() + with self._error_log, lxml_document_loader: parser_ctxt = schematron.xmlSchematronNewParserCtxt(_cstr(filename)) - _reset_document_loader(orig_loader) else: - raise SchematronParseError, u"No tree or file given" + raise SchematronParseError, "No tree or file given" if parser_ctxt is NULL: if self._c_schema_doc is not NULL: @@ -108,16 +117,14 @@ cdef class Schematron(_Validator): raise MemoryError() try: - with self._error_log: - orig_loader = _register_document_loader() + with self._error_log, lxml_document_loader: self._c_schema = schematron.xmlSchematronParse(parser_ctxt) - _reset_document_loader(orig_loader) finally: schematron.xmlSchematronFreeParserCtxt(parser_ctxt) if self._c_schema is NULL: raise SchematronParseError( - u"Document is not a valid Schematron schema", + "Document is not a valid Schematron schema", self._error_log) def __dealloc__(self): @@ -126,7 +133,7 @@ cdef class Schematron(_Validator): tree.xmlFreeDoc(self._c_schema_doc) def __call__(self, etree): - u"""__call__(self, etree) + """__call__(self, etree) Validate doc using Schematron. @@ -146,20 +153,23 @@ cdef class Schematron(_Validator): if valid_ctxt is NULL: raise MemoryError() + doc.lock_fakedoc() try: self._error_log.clear() + # Need a cast here because older libxml2 releases do not use 'const' in the functype. schematron.xmlSchematronSetValidStructuredErrors( - valid_ctxt, _receiveError, self._error_log) + valid_ctxt, _receiveError, self._error_log) c_doc = _fakeRootDoc(doc._c_doc, root_node._c_node) with nogil: ret = schematron.xmlSchematronValidateDoc(valid_ctxt, c_doc) _destroyFakeDoc(doc._c_doc, c_doc) finally: + doc.unlock_fakedoc() schematron.xmlSchematronFreeValidCtxt(valid_ctxt) if ret == -1: raise SchematronValidateError( - u"Internal error in Schematron validation", + "Internal error in Schematron validation", self._error_log) if ret == 0: return True diff --git a/src/lxml/serializer.pxi b/src/lxml/serializer.pxi index 79a02829e..5b3051d2a 100644 --- a/src/lxml/serializer.pxi +++ b/src/lxml/serializer.pxi @@ -4,7 +4,7 @@ cdef object GzipFile from gzip import GzipFile -cdef class SerialisationError(LxmlError): +class SerialisationError(LxmlError): """A libxml2 error that occurred during serialisation. """ @@ -50,7 +50,7 @@ cdef _textToString(xmlNode* c_node, encoding, bint with_tail): if error_result < 0 or c_text is NULL: tree.xmlBufferFree(c_buffer) - raise SerialisationError, u"Error during serialisation (out of memory?)" + raise SerialisationError, "Error during serialisation (out of memory?)" try: needs_conversion = 0 @@ -59,8 +59,8 @@ cdef _textToString(xmlNode* c_node, encoding, bint with_tail): elif encoding is not None: # Python prefers lower case encoding names encoding = encoding.lower() - if encoding not in (u'utf8', u'utf-8'): - if encoding == u'ascii': + if encoding not in ('utf8', 'utf-8'): + if encoding == 'ascii': if isutf8l(c_text, tree.xmlBufferLength(c_buffer)): # will raise a decode error below needs_conversion = 1 @@ -83,7 +83,7 @@ cdef _textToString(xmlNode* c_node, encoding, bint with_tail): cdef _tostring(_Element element, encoding, doctype, method, bint write_xml_declaration, bint write_complete_document, bint pretty_print, bint with_tail, int standalone): - u"""Serialize an element to an encoded string representation of its XML + """Serialize an element to an encoded string representation of its XML tree. """ cdef tree.xmlOutputBuffer* c_buffer @@ -99,7 +99,12 @@ cdef _tostring(_Element element, encoding, doctype, method, _assertValidNode(element) c_method = _findOutputMethod(method) if c_method == OUTPUT_METHOD_TEXT: - return _textToString(element._c_node, encoding, with_tail) + doc = element._doc + doc.lock_read() + try: + return _textToString(element._c_node, encoding, with_tail) + finally: + doc.unlock_read() if encoding is None or encoding is unicode: c_enc = NULL else: @@ -122,15 +127,18 @@ cdef _tostring(_Element element, encoding, doctype, method, tree.xmlCharEncCloseFunc(enchandler) raise MemoryError() + doc = element._doc + doc.lock_read() with nogil: _writeNodeToBuffer(c_buffer, element._c_node, c_enc, c_doctype, c_method, - write_xml_declaration, write_complete_document, - pretty_print, with_tail, standalone) + write_xml_declaration, write_complete_document, + pretty_print, with_tail, standalone) tree.xmlOutputBufferFlush(c_buffer) if c_buffer.conv is not NULL: c_result_buffer = c_buffer.conv else: c_result_buffer = c_buffer.buffer + doc.unlock_read() error_result = c_buffer.error if error_result != xmlerror.XML_ERR_OK: @@ -150,6 +158,7 @@ cdef _tostring(_Element element, encoding, doctype, method, _raiseSerialisationError(error_result) return result + cdef bytes _tostringC14N(element_or_tree, bint exclusive, bint with_comments, inclusive_ns_prefixes): cdef xmlDoc* c_doc cdef xmlChar* c_buffer = NULL @@ -157,38 +166,52 @@ cdef bytes _tostringC14N(element_or_tree, bint exclusive, bint with_comments, in cdef bytes result cdef _Document doc cdef _Element element - cdef xmlChar **c_inclusive_ns_prefixes + cdef xmlChar **c_inclusive_ns_prefixes = NULL if isinstance(element_or_tree, _Element): - _assertValidNode(<_Element>element_or_tree) - doc = (<_Element>element_or_tree)._doc - c_doc = _plainFakeRootDoc(doc._c_doc, (<_Element>element_or_tree)._c_node, 0) + element = <_Element> element_or_tree + _assertValidNode(element) + doc = element._doc + doc.lock_fakedoc() + try: + c_doc = _plainFakeRootDoc(doc._c_doc, element._c_node, 0) + except: + doc.unlock_fakedoc() + raise else: doc = _documentOrRaise(element_or_tree) _assertValidDoc(doc) + element = None + doc.lock_read() c_doc = doc._c_doc - c_inclusive_ns_prefixes = _convert_ns_prefixes(c_doc.dict, inclusive_ns_prefixes) if inclusive_ns_prefixes else NULL try: - with nogil: - byte_count = c14n.xmlC14NDocDumpMemory( - c_doc, NULL, exclusive, c_inclusive_ns_prefixes, with_comments, &c_buffer) + c_inclusive_ns_prefixes = _convert_ns_prefixes(c_doc.dict, inclusive_ns_prefixes) if inclusive_ns_prefixes else NULL + with nogil: + byte_count = c14n.xmlC14NDocDumpMemory( + c_doc, NULL, exclusive, c_inclusive_ns_prefixes, with_comments, &c_buffer) finally: - _destroyFakeDoc(doc._c_doc, c_doc) - if c_inclusive_ns_prefixes is not NULL: - python.lxml_free(c_inclusive_ns_prefixes) + if element is None: + doc.unlock_read() + else: + _destroyFakeDoc(doc._c_doc, c_doc) + doc.unlock_fakedoc() + + if c_inclusive_ns_prefixes is not NULL: + python.lxml_free(c_inclusive_ns_prefixes) if byte_count < 0 or c_buffer is NULL: if c_buffer is not NULL: tree.xmlFree(c_buffer) - raise C14NError, u"C14N failed" + raise C14NError, "C14N failed" try: result = c_buffer[:byte_count] finally: tree.xmlFree(c_buffer) return result + cdef _raiseSerialisationError(int error_result): if error_result == xmlerror.XML_ERR_NO_MEMORY: raise MemoryError() @@ -197,21 +220,22 @@ cdef _raiseSerialisationError(int error_result): message = f"unknown error {error_result}" raise SerialisationError, message + ############################################################ -# low-level serialisation functions +# low-level serialisation functions (no locking) cdef void _writeDoctype(tree.xmlOutputBuffer* c_buffer, - const_xmlChar* c_doctype) nogil: - tree.xmlOutputBufferWrite(c_buffer, tree.xmlStrlen(c_doctype), - c_doctype) + const_xmlChar* c_doctype) noexcept nogil: + tree.xmlOutputBufferWrite(c_buffer, cstring_h.strlen( c_doctype), c_doctype) tree.xmlOutputBufferWriteString(c_buffer, "\n") + cdef void _writeNodeToBuffer(tree.xmlOutputBuffer* c_buffer, xmlNode* c_node, const_char* encoding, const_xmlChar* c_doctype, int c_method, bint write_xml_declaration, bint write_complete_document, bint pretty_print, bint with_tail, - int standalone) nogil: + int standalone) noexcept nogil: cdef xmlNode* c_nsdecl_node cdef xmlDoc* c_doc = c_node.doc if write_xml_declaration and c_method == OUTPUT_METHOD_XML: @@ -267,9 +291,10 @@ cdef void _writeNodeToBuffer(tree.xmlOutputBuffer* c_buffer, if pretty_print: tree.xmlOutputBufferWrite(c_buffer, 1, "\n") + cdef void _writeDeclarationToBuffer(tree.xmlOutputBuffer* c_buffer, const_xmlChar* version, const_char* encoding, - int standalone) nogil: + int standalone) noexcept nogil: if version is NULL: version = "1.0" tree.xmlOutputBufferWrite(c_buffer, 15, "\n") + cdef void _writeDtdToBuffer(tree.xmlOutputBuffer* c_buffer, xmlDoc* c_doc, const_xmlChar* c_root_name, - int c_method, const_char* encoding) nogil: + int c_method, const_char* encoding) noexcept nogil: cdef tree.xmlDtd* c_dtd cdef xmlNode* c_node - cdef char* quotechar + cdef char quotechar c_dtd = c_doc.intSubset if not c_dtd or not c_dtd.name: return @@ -299,7 +325,7 @@ cdef void _writeDtdToBuffer(tree.xmlOutputBuffer* c_buffer, if tree.xmlStrcasecmp(c_root_name, c_dtd.name) != 0: return else: - if tree.xmlStrcmp(c_root_name, c_dtd.name) != 0: + if not tree.xmlStrEqual(c_root_name, c_dtd.name): return tree.xmlOutputBufferWrite(c_buffer, 10, " sys_url, b'"') else c'"' + tree.xmlOutputBufferWrite(c_buffer, 1, "echar) tree.xmlOutputBufferWriteString(c_buffer, sys_url) - tree.xmlOutputBufferWrite(c_buffer, 1, quotechar) + tree.xmlOutputBufferWrite(c_buffer, 1, "echar) if (not c_dtd.entities and not c_dtd.elements and not c_dtd.attributes and not c_dtd.notations and @@ -354,9 +377,10 @@ cdef void _writeDtdToBuffer(tree.xmlOutputBuffer* c_buffer, c_node = c_node.next tree.xmlOutputBufferWrite(c_buffer, 3, "]>\n") + cdef void _writeTail(tree.xmlOutputBuffer* c_buffer, xmlNode* c_node, - const_char* encoding, int c_method, bint pretty_print) nogil: - u"Write the element tail." + const_char* encoding, int c_method, bint pretty_print) noexcept nogil: + "Write the element tail." c_node = c_node.next while c_node and not c_buffer.error and c_node.type in ( tree.XML_TEXT_NODE, tree.XML_CDATA_SECTION_NODE): @@ -368,8 +392,9 @@ cdef void _writeTail(tree.xmlOutputBuffer* c_buffer, xmlNode* c_node, c_buffer, c_node.doc, c_node, 0, pretty_print, encoding) c_node = c_node.next + cdef void _writePrevSiblings(tree.xmlOutputBuffer* c_buffer, xmlNode* c_node, - const_char* encoding, bint pretty_print) nogil: + const_char* encoding, bint pretty_print) noexcept nogil: cdef xmlNode* c_sibling if c_node.parent and _isElement(c_node.parent): return @@ -386,8 +411,9 @@ cdef void _writePrevSiblings(tree.xmlOutputBuffer* c_buffer, xmlNode* c_node, tree.xmlOutputBufferWriteString(c_buffer, "\n") c_sibling = c_sibling.next + cdef void _writeNextSiblings(tree.xmlOutputBuffer* c_buffer, xmlNode* c_node, - const_char* encoding, bint pretty_print) nogil: + const_char* encoding, bint pretty_print) noexcept nogil: cdef xmlNode* c_sibling if c_node.parent and _isElement(c_node.parent): return @@ -403,99 +429,17 @@ cdef void _writeNextSiblings(tree.xmlOutputBuffer* c_buffer, xmlNode* c_node, c_sibling = c_sibling.next -# copied and adapted from libxml2 -cdef unsigned char *xmlSerializeHexCharRef(unsigned char *out, int val): - cdef xmlChar *ptr - cdef xmlChar c - - out[0] = '&' - out += 1 - - out[0] = '#' - out += 1 - - out[0] = 'x' - out += 1 - - if val < 0x10: - ptr = out - elif val < 0x100: - ptr = out + 1 - elif val < 0x1000: - ptr = out + 2 - elif val < 0x10000: - ptr = out + 3 - elif val < 0x100000: - ptr = out + 4 - else: - ptr = out + 5 - - out = ptr + 1 - while val > 0: - c = (val & 0xF) - - if c == 0: - ptr[0] = '0' - elif c == 1: - ptr[0] = '1' - elif c == 2: - ptr[0] = '2' - elif c == 3: - ptr[0] = '3' - elif c == 4: - ptr[0] = '4' - elif c == 5: - ptr[0] = '5' - elif c == 6: - ptr[0] = '6' - elif c == 7: - ptr[0] = '7' - elif c == 8: - ptr[0] = '8' - elif c == 9: - ptr[0] = '9' - elif c == 0xA: - ptr[0] = 'A' - elif c == 0xB: - ptr[0] = 'B' - elif c == 0xC: - ptr[0] = 'C' - elif c == 0xD: - ptr[0] = 'D' - elif c == 0xE: - ptr[0] = 'E' - elif c == 0xF: - ptr[0] = 'F' - else: - ptr[0] = '0' - - ptr -= 1 - - val >>= 4 - - out[0] = ';' - out += 1 - out[0] = 0 - - return out - - # copied and adapted from libxml2 (xmlBufAttrSerializeTxtContent()) cdef _write_attr_string(tree.xmlOutputBuffer* buf, const char *string): cdef const char *base cdef const char *cur - cdef const unsigned char *ucur - - cdef unsigned char tmp[12] - cdef int val = 0 - cdef int l if string == NULL: return base = cur = string while cur[0] != 0: - if cur[0] == '\n': + if cur[0] == b'\n': if base != cur: tree.xmlOutputBufferWrite(buf, cur - base, base) @@ -503,7 +447,7 @@ cdef _write_attr_string(tree.xmlOutputBuffer* buf, const char *string): cur += 1 base = cur - elif cur[0] == '\r': + elif cur[0] == b'\r': if base != cur: tree.xmlOutputBufferWrite(buf, cur - base, base) @@ -511,7 +455,7 @@ cdef _write_attr_string(tree.xmlOutputBuffer* buf, const char *string): cur += 1 base = cur - elif cur[0] == '\t': + elif cur[0] == b'\t': if base != cur: tree.xmlOutputBufferWrite(buf, cur - base, base) @@ -519,7 +463,7 @@ cdef _write_attr_string(tree.xmlOutputBuffer* buf, const char *string): cur += 1 base = cur - elif cur[0] == '"': + elif cur[0] == b'"': if base != cur: tree.xmlOutputBufferWrite(buf, cur - base, base) @@ -527,7 +471,7 @@ cdef _write_attr_string(tree.xmlOutputBuffer* buf, const char *string): cur += 1 base = cur - elif cur[0] == '<': + elif cur[0] == b'<': if base != cur: tree.xmlOutputBufferWrite(buf, cur - base, base) @@ -535,14 +479,14 @@ cdef _write_attr_string(tree.xmlOutputBuffer* buf, const char *string): cur += 1 base = cur - elif cur[0] == '>': + elif cur[0] == b'>': if base != cur: tree.xmlOutputBufferWrite(buf, cur - base, base) tree.xmlOutputBufferWrite(buf, 4, ">") cur += 1 base = cur - elif cur[0] == '&': + elif cur[0] == b'&': if base != cur: tree.xmlOutputBufferWrite(buf, cur - base, base) @@ -550,68 +494,63 @@ cdef _write_attr_string(tree.xmlOutputBuffer* buf, const char *string): cur += 1 base = cur - elif (cur[0] >= 0x80) and (cur[1] != 0): - - if base != cur: - tree.xmlOutputBufferWrite(buf, cur - base, base) - - ucur = cur - - if ucur[0] < 0xC0: - # invalid UTF-8 sequence - val = ucur[0] - l = 1 - - elif ucur[0] < 0xE0: - val = (ucur[0]) & 0x1F - val <<= 6 - val |= (ucur[1]) & 0x3F - l = 2 - - elif (ucur[0] < 0xF0) and (ucur[2] != 0): - val = (ucur[0]) & 0x0F - val <<= 6 - val |= (ucur[1]) & 0x3F - val <<= 6 - val |= (ucur[2]) & 0x3F - l = 3 - - elif (ucur[0] < 0xF8) and (ucur[2] != 0) and (ucur[3] != 0): - val = (ucur[0]) & 0x07 - val <<= 6 - val |= (ucur[1]) & 0x3F - val <<= 6 - val |= (ucur[2]) & 0x3F - val <<= 6 - val |= (ucur[3]) & 0x3F - l = 4 - else: - # invalid UTF-8 sequence - val = ucur[0] - l = 1 - - if (l == 1) or (not tree.xmlIsCharQ(val)): - raise ValueError(f"Invalid character: {val:X}") - - # We could do multiple things here. Just save - # as a char ref - xmlSerializeHexCharRef(tmp, val) - tree.xmlOutputBufferWrite(buf, len(tmp), tmp) - cur += l - base = cur - else: + # Leave further encoding and escaping to the buffer encoder. cur += 1 if base != cur: tree.xmlOutputBufferWrite(buf, cur - base, base) +cdef void _write_cdata_section(tree.xmlOutputBuffer* buf, const char* c_data, const char* c_end): + tree.xmlOutputBufferWrite(buf, 9, " limits.INT_MAX: + tree.xmlOutputBufferWrite(buf, limits.INT_MAX, c_data) + c_data += limits.INT_MAX + tree.xmlOutputBufferWrite(buf, c_end - c_data, c_data) + tree.xmlOutputBufferWrite(buf, 3, "]]>") + + +cdef _write_cdata_string(tree.xmlOutputBuffer* buf, bytes bstring): + cdef const char* c_data = bstring + cdef const char* c_end = c_data + len(bstring) + cdef const char* c_pos = c_data + cdef bint nothing_written = True + + while True: + c_pos = cstring_h.memchr(c_pos, b']', c_end - c_pos) + if not c_pos: + break + c_pos += 1 + next_char = c_pos[0] + c_pos += 1 + if next_char != b']': + continue + # Found ']]', c_pos points to next character. + while c_pos[0] == b']': + c_pos += 1 + if c_pos[0] != b'>': + if c_pos == c_end: + break + # c_pos[0] is neither ']' nor '>', continue with next character. + c_pos += 1 + continue + + # Write section up to ']]' and start next block at trailing '>'. + _write_cdata_section(buf, c_data, c_pos) + nothing_written = False + c_data = c_pos + c_pos += 1 + + if nothing_written or c_data < c_end: + _write_cdata_section(buf, c_data, c_end) + + ############################################################ # output to file-like objects cdef object io_open -from io import open +from io import open as io_open cdef object gzip import gzip @@ -643,21 +582,68 @@ def _open_utf8_file(file, compression=0): yield utf8_writer(file) +@cython.final +@cython.internal +cdef class _SlidingMemoryView: + cdef char *buffer + cdef Py_ssize_t length + cdef stdint.uintptr_t used + + cdef int point_buffer(self, char* data, Py_ssize_t length): + if self.used: + raise RuntimeError, f"Buffer object is not free to use, {self.used:d} references found" + self.buffer = data + self.length = length + return 0 + + def __getbuffer__(self, Py_buffer* info, int flags): + assert (flags | PyBUF_READ | PyBUF_FORMAT | PyBUF_ND | PyBUF_STRIDES) == (PyBUF_SIMPLE | PyBUF_READ | PyBUF_FORMAT | PyBUF_ND | PyBUF_STRIDES), flags + assert self.buffer is not NULL + + self.used += 1 + + info.obj = self + info.buf = self.buffer + info.len = self.length + info.itemsize = 1 + info.readonly = 1 + info.ndim = 1 + info.format = b'B' + info.shape = &info.itemsize + info.strides = &info.itemsize + info.suboffsets = NULL + info.internal = NULL + + def __releasebuffer__(self, Py_buffer* info): + info.buf = NULL + self.used -= 1 + + @cython.final @cython.internal cdef class _FilelikeWriter: cdef object _filelike cdef object _close_filelike + cdef _SlidingMemoryView _mview cdef _ExceptionContext _exc_context cdef _ErrorLog error_log - def __cinit__(self, filelike, exc_context=None, compression=None, close=False): + + def __cinit__(self, filelike, exc_context=None, compression=None, bint close=False): + cdef bint use_mview if compression is not None and compression > 0: filelike = GzipFile( fileobj=filelike, mode='wb', compresslevel=compression) - self._close_filelike = filelike.close - elif close: - self._close_filelike = filelike.close + use_mview = True + close = True + else: + use_mview = not python.IS_PYPY and isinstance(filelike, (BufferedWriter, BytesIO)) + self._filelike = filelike + if close: + self._close_filelike = filelike.close + if not python.IS_PYPY and use_mview: + self._mview = _SlidingMemoryView() + if exc_context is None: self._exc_context = _ExceptionContext() else: @@ -671,22 +657,27 @@ cdef class _FilelikeWriter: _writeFilelikeWriter, _closeFilelikeWriter, self, enchandler) if c_buffer is NULL: - raise IOError, u"Could not create I/O writer context." + raise IOError, "Could not create I/O writer context." return c_buffer - cdef int write(self, char* c_buffer, int size): + cdef int write(self, char* c_buffer, int size) noexcept: try: if self._filelike is None: - raise IOError, u"File is already closed" - py_buffer = c_buffer[:size] - self._filelike.write(py_buffer) + raise IOError, "File is already closed" + # If we don't know what the 'filelike' does with the data object, we pass it as plain 'bytes' copy. + if self._mview is None: + py_buffer = c_buffer[:size] + self._filelike.write(py_buffer) + else: + self._mview.point_buffer(c_buffer, size) + self._filelike.write(self._mview) except: size = -1 self._exc_context._store_raised() finally: return size # and swallow any further exceptions - cdef int close(self): + cdef int close(self) noexcept: retval = 0 try: if self._close_filelike is not None: @@ -699,12 +690,15 @@ cdef class _FilelikeWriter: finally: return retval # and swallow any further exceptions -cdef int _writeFilelikeWriter(void* ctxt, char* c_buffer, int length): + +cdef int _writeFilelikeWriter(void* ctxt, char* c_buffer, int length) noexcept: return (<_FilelikeWriter>ctxt).write(c_buffer, length) -cdef int _closeFilelikeWriter(void* ctxt): + +cdef int _closeFilelikeWriter(void* ctxt) noexcept: return (<_FilelikeWriter>ctxt).close() + cdef _tofilelike(f, _Element element, encoding, doctype, method, bint write_xml_declaration, bint write_doctype, bint pretty_print, bint with_tail, int standalone, @@ -718,7 +712,12 @@ cdef _tofilelike(f, _Element element, encoding, doctype, method, c_method = _findOutputMethod(method) if c_method == OUTPUT_METHOD_TEXT: - data = _textToString(element._c_node, encoding, with_tail) + doc = element._doc + doc.lock_read() + try: + data = _textToString(element._c_node, encoding, with_tail) + finally: + doc.unlock_read() if compression: bytes_out = BytesIO() with GzipFile(fileobj=bytes_out, mode='wb', compresslevel=compression) as gzip_file: @@ -745,15 +744,20 @@ cdef _tofilelike(f, _Element element, encoding, doctype, method, c_doctype = _xcstr(doctype) writer = _create_output_buffer(f, c_enc, compression, &c_buffer, close=False) - if writer is None: - with nogil: + doc = element._doc + doc.lock_read() + try: + if writer is None: + with nogil: + error_result = _serialise_node( + c_buffer, c_doctype, c_enc, element._c_node, c_method, + write_xml_declaration, write_doctype, pretty_print, with_tail, standalone) + else: error_result = _serialise_node( c_buffer, c_doctype, c_enc, element._c_node, c_method, write_xml_declaration, write_doctype, pretty_print, with_tail, standalone) - else: - error_result = _serialise_node( - c_buffer, c_doctype, c_enc, element._c_node, c_method, - write_xml_declaration, write_doctype, pretty_print, with_tail, standalone) + finally: + doc.unlock_read() if writer is not None: writer._exc_context._raise_if_stored() @@ -764,7 +768,7 @@ cdef _tofilelike(f, _Element element, encoding, doctype, method, cdef int _serialise_node(tree.xmlOutputBuffer* c_buffer, const_xmlChar* c_doctype, const_char* c_enc, xmlNode* c_node, int c_method, bint write_xml_declaration, bint write_doctype, bint pretty_print, - bint with_tail, int standalone) nogil: + bint with_tail, int standalone) noexcept nogil: _writeNodeToBuffer( c_buffer, c_node, c_enc, c_doctype, c_method, write_xml_declaration, write_doctype, pretty_print, with_tail, standalone) @@ -784,12 +788,19 @@ cdef _FilelikeWriter _create_output_buffer( cdef tree.xmlOutputBuffer* c_buffer cdef _FilelikeWriter writer cdef bytes filename8 + enchandler = tree.xmlFindCharEncodingHandler(c_enc) if enchandler is NULL: raise LookupError( f"unknown encoding: '{c_enc.decode('UTF-8') if c_enc is not NULL else u''}'") try: f = _getFSPathOrObject(f) + + if c_compression and not HAS_ZLIB_COMPRESSION and _isString(f): + # Let "_FilelikeWriter" fall back to Python's GzipFile. + f = open(f, mode="wb") + close = True + if _isString(f): filename8 = _encodeFilename(f) if b'%' in filename8 and ( @@ -809,13 +820,14 @@ cdef _FilelikeWriter _create_output_buffer( c_buffer = writer._createOutputBuffer(enchandler) else: raise TypeError( - f"File or filename expected, got '{python._fqtypename(f).decode('UTF-8')}'") + f"File or filename expected, got '{python._fqtypename(f)}'") except: tree.xmlCharEncCloseFunc(enchandler) raise c_buffer_ret[0] = c_buffer return writer + cdef xmlChar **_convert_ns_prefixes(tree.xmlDict* c_dict, ns_prefixes) except NULL: cdef size_t i, num_ns_prefixes = len(ns_prefixes) # Need to allocate one extra memory block to handle last NULL entry @@ -826,7 +838,10 @@ cdef xmlChar **_convert_ns_prefixes(tree.xmlDict* c_dict, ns_prefixes) except NU try: for prefix in ns_prefixes: prefix_utf = _utf8(prefix) - c_prefix = tree.xmlDictExists(c_dict, _xcstr(prefix_utf), len(prefix_utf)) + c_prefix_len = len(prefix_utf) + if c_prefix_len > limits.INT_MAX: + raise ValueError("Prefix too long") + c_prefix = tree.xmlDictExists(c_dict, _xcstr(prefix_utf), c_prefix_len) if c_prefix: # unknown prefixes do not need to get serialised c_ns_prefixes[i] = c_prefix @@ -838,24 +853,35 @@ cdef xmlChar **_convert_ns_prefixes(tree.xmlDict* c_dict, ns_prefixes) except NU c_ns_prefixes[i] = NULL # append end marker return c_ns_prefixes + cdef _tofilelikeC14N(f, _Element element, bint exclusive, bint with_comments, int compression, inclusive_ns_prefixes): cdef _FilelikeWriter writer = None cdef tree.xmlOutputBuffer* c_buffer cdef xmlChar **c_inclusive_ns_prefixes = NULL - cdef char* c_filename + cdef const char* c_filename cdef xmlDoc* c_base_doc - cdef xmlDoc* c_doc + cdef xmlDoc* c_doc = NULL cdef int bytes_count, error = 0 - c_base_doc = element._c_node.doc - c_doc = _fakeRootDoc(c_base_doc, element._c_node) + doc = element._doc + doc.lock_fakedoc() try: + c_base_doc = element._c_node.doc + c_doc = _fakeRootDoc(c_base_doc, element._c_node) + c_inclusive_ns_prefixes = ( _convert_ns_prefixes(c_doc.dict, inclusive_ns_prefixes) if inclusive_ns_prefixes else NULL) f = _getFSPathOrObject(f) + + close = False + if compression and not HAS_ZLIB_COMPRESSION and _isString(f): + # Let "_FilelikeWriter" fall back to Python's GzipFile. + f = open(f, mode="wb") + close = True + if _isString(f): filename8 = _encodeFilename(f) c_filename = _cstr(filename8) @@ -864,7 +890,7 @@ cdef _tofilelikeC14N(f, _Element element, bint exclusive, bint with_comments, c_doc, NULL, exclusive, c_inclusive_ns_prefixes, with_comments, c_filename, compression) elif hasattr(f, 'write'): - writer = _FilelikeWriter(f, compression=compression) + writer = _FilelikeWriter(f, compression=compression, close=close) c_buffer = writer._createOutputBuffer(NULL) try: with writer.error_log: @@ -878,9 +904,11 @@ cdef _tofilelikeC14N(f, _Element element, bint exclusive, bint with_comments, elif error != -1: error = xmlerror.XML_ERR_OK else: - raise TypeError(f"File or filename expected, got '{python._fqtypename(f).decode('UTF-8')}'") + raise TypeError(f"File or filename expected, got '{python._fqtypename(f)}'") finally: - _destroyFakeDoc(c_base_doc, c_doc) + if c_doc is not NULL: + _destroyFakeDoc(c_base_doc, c_doc) + doc.unlock_fakedoc() if c_inclusive_ns_prefixes is not NULL: python.lxml_free(c_inclusive_ns_prefixes) @@ -888,7 +916,7 @@ cdef _tofilelikeC14N(f, _Element element, bint exclusive, bint with_comments, writer._exc_context._raise_if_stored() if error < 0: - message = u"C14N failed" + message = "C14N failed" if writer is not None: errors = writer.error_log if len(errors): @@ -965,7 +993,7 @@ cdef _tree_to_target(element, target): return target.close() -cdef object _looks_like_prefix_name = re.compile('^\w+:\w+$', re.UNICODE).match +cdef object _looks_like_prefix_name = re.compile(r'^\w+:\w+$', re.UNICODE).match cdef class C14NWriterTarget: @@ -974,17 +1002,17 @@ cdef class C14NWriterTarget: Serialises parse events to XML C14N 2.0. - Configuration options: - - - *with_comments*: set to true to include comments - - *strip_text*: set to true to strip whitespace before and after text content - - *rewrite_prefixes*: set to true to replace namespace prefixes by "n{number}" - - *qname_aware_tags*: a set of qname aware tag names in which prefixes - should be replaced in text content - - *qname_aware_attrs*: a set of qname aware attribute names in which prefixes - should be replaced in text content - - *exclude_attrs*: a set of attribute names that should not be serialised - - *exclude_tags*: a set of tag names that should not be serialised + **Configuration options:** + + - with_comments: set to true to include comments + - strip_text: set to true to strip whitespace before and after text content + - rewrite_prefixes: set to true to replace namespace prefixes by "n{number}" + - qname_aware_tags: a set of qname aware tag names in which prefixes should + be replaced in text content + - qname_aware_attrs: a set of qname aware attribute names in which prefixes + should be replaced in text content + - exclude_attrs: a set of attribute names that should not be serialised + - exclude_tags: a set of tag names that should not be serialised """ cdef object _write cdef list _data @@ -1095,13 +1123,13 @@ cdef class C14NWriterTarget: self._data.append(data) cdef _flush(self): - data = u''.join(self._data) + cdef unicode data = ''.join(self._data) del self._data[:] if self._strip_text and not self._preserve_space[-1]: data = data.strip() if self._pending_start is not None: (tag, attrs, new_namespaces), self._pending_start = self._pending_start, None - qname_text = data if u':' in data and _looks_like_prefix_name(data) else None + qname_text = data if ':' in data and _looks_like_prefix_name(data) else None self._start(tag, attrs, new_namespaces, qname_text) if qname_text is not None: return @@ -1164,7 +1192,7 @@ cdef class C14NWriterTarget: # Write namespace declarations in prefix order ... if new_namespaces: attr_list = [ - (u'xmlns:' + prefix if prefix else u'xmlns', uri) + ('xmlns:' + prefix if prefix else 'xmlns', uri) for uri, prefix in new_namespaces ] attr_list.sort() @@ -1189,10 +1217,10 @@ cdef class C14NWriterTarget: # Write the tag. write = self._write - write(u'<' + parsed_qnames[tag][0]) + write('<' + parsed_qnames[tag][0]) if attr_list: - write(u''.join([f' {k}="{_escape_attrib_c14n(v)}"' for k, v in attr_list])) - write(u'>') + write(''.join([f' {k}="{_escape_attrib_c14n(v)}"' for k, v in attr_list])) + write('>') # Write the resolved qname text content. if qname_text is not None: @@ -1219,24 +1247,24 @@ cdef class C14NWriterTarget: if self._ignored_depth: return if self._root_done: - self._write(u'\n') + self._write('\n') elif self._root_seen and self._data: self._flush() self._write(f'') if not self._root_seen: - self._write(u'\n') + self._write('\n') def pi(self, target, data): if self._ignored_depth: return if self._root_done: - self._write(u'\n') + self._write('\n') elif self._root_seen and self._data: self._flush() self._write( f'' if data else f'') if not self._root_seen: - self._write(u'\n') + self._write('\n') def close(self): return None @@ -1249,44 +1277,79 @@ cdef _raise_serialization_error(text): cdef unicode _escape_cdata_c14n(stext): # escape character data cdef unicode text + cdef Py_UCS4 ch + cdef Py_ssize_t start = 0, pos = 0 + cdef list substrings = None try: - # it's worth avoiding do-nothing calls for strings that are - # shorter than 500 character, or so. assume that's, by far, - # the most common case in most applications. text = unicode(stext) - if u'&' in text: - text = text.replace(u'&', u'&') - if u'<' in text: - text = text.replace(u'<', u'<') - if u'>' in text: - text = text.replace(u'>', u'>') - if u'\r' in text: - text = text.replace(u'\r', u' ') - return text except (TypeError, AttributeError): - _raise_serialization_error(stext) + return _raise_serialization_error(stext) + + for pos, ch in enumerate(text): + if ch == '&': + escape = '&' + elif ch == '<': + escape = '<' + elif ch == '>': + escape = '>' + elif ch == '\r': + escape = ' ' + else: + continue + + if substrings is None: + substrings = [] + if pos > start: + substrings.append(text[start:pos]) + substrings.append(escape) + start = pos + 1 + + if substrings is None: + return text + if pos >= start: + substrings.append(text[start:pos+1]) + return ''.join(substrings) cdef unicode _escape_attrib_c14n(stext): # escape attribute value cdef unicode text + cdef Py_UCS4 ch + cdef Py_ssize_t start = 0, pos = 0 + cdef list substrings = None try: text = unicode(stext) - if u'&' in text: - text = text.replace(u'&', u'&') - if u'<' in text: - text = text.replace(u'<', u'<') - if u'"' in text: - text = text.replace(u'"', u'"') - if u'\t' in text: - text = text.replace(u'\t', u' ') - if u'\n' in text: - text = text.replace(u'\n', u' ') - if u'\r' in text: - text = text.replace(u'\r', u' ') - return text except (TypeError, AttributeError): - _raise_serialization_error(stext) + return _raise_serialization_error(stext) + + for pos, ch in enumerate(text): + if ch == '&': + escape = '&' + elif ch == '<': + escape = '<' + elif ch == '"': + escape = '"' + elif ch == '\t': + escape = ' ' + elif ch == '\n': + escape = ' ' + elif ch == '\r': + escape = ' ' + else: + continue + + if substrings is None: + substrings = [] + if pos > start: + substrings.append(text[start:pos]) + substrings.append(escape) + start = pos + 1 + + if substrings is None: + return text + if pos >= start: + substrings.append(text[start:pos+1]) + return ''.join(substrings) # incremental serialisation @@ -1416,9 +1479,11 @@ cdef class _IncrementalFileWriter: self._status = WRITER_STARTING self._element_stack = [] if encoding is None: + # We always need a document encoding to make the attribute serialisation + # of libxml2 identical to ours. encoding = b'ASCII' self._encoding = encoding - self._c_encoding = _cstr(encoding) if encoding is not None else NULL + self._c_encoding = _cstr(encoding) self._buffered = buffered self._target = _create_output_buffer( outfile, self._c_encoding, compresslevel, &self._c_out, close) @@ -1650,20 +1715,34 @@ cdef class _IncrementalFileWriter: else: tree.xmlOutputBufferWriteEscape(self._c_out, _xcstr(bstring), NULL) + elif isinstance(content, CDATA): + if self._status > WRITER_IN_ELEMENT: + raise LxmlSyntaxError("not in an element") + _write_cdata_string(self._c_out, (content)._utf8_data) + elif iselement(content): if self._status > WRITER_IN_ELEMENT: raise LxmlSyntaxError("cannot append trailing element to complete XML document") - _writeNodeToBuffer(self._c_out, (<_Element>content)._c_node, - self._c_encoding, NULL, c_method, - False, False, pretty_print, with_tail, False) + + doc = (<_Element>content)._doc + doc.lock_read() + try: + _writeNodeToBuffer(self._c_out, (<_Element>content)._c_node, + self._c_encoding, NULL, c_method, + False, False, pretty_print, with_tail, False) + finally: + doc.unlock_read() + if (<_Element>content)._c_node.type == tree.XML_ELEMENT_NODE: if not self._element_stack: self._status = WRITER_FINISHED elif content is not None: raise TypeError( - f"got invalid input value of type {type(content)}, expected string or Element") + f"got invalid input value of type {type(content)}, expected string, CDATA or Element") + self._handle_error(self._c_out.error) + if not self._buffered: tree.xmlOutputBufferFlush(self._c_out) self._handle_error(self._c_out.error) @@ -1684,12 +1763,9 @@ cdef class _IncrementalFileWriter: if self._element_stack: raise LxmlSyntaxError("pending open tags on close") error_result = self._c_out.error - if error_result == xmlerror.XML_ERR_OK: - error_result = tree.xmlOutputBufferClose(self._c_out) - if error_result != -1: - error_result = xmlerror.XML_ERR_OK - else: - tree.xmlOutputBufferClose(self._c_out) + close_error_result = tree.xmlOutputBufferClose(self._c_out) + if error_result == xmlerror.XML_ERR_OK and close_error_result == -1: + error_result = -1 self._status = WRITER_FINISHED self._c_out = NULL del self._element_stack[:] diff --git a/src/lxml/tests/_testlock.pyx b/src/lxml/tests/_testlock.pyx new file mode 100644 index 000000000..9a638aee9 --- /dev/null +++ b/src/lxml/tests/_testlock.pyx @@ -0,0 +1,123 @@ +# cython: freethreading_compatible=True +# distutils: define_macros=LXML_LOCK_PERFORMANCE=1 + +cimport cython + +cimport cpython.pythread as python + +from contextlib import contextmanager + +include "../rwlock.pxi" + + +cdef class _RWLock: + cdef RWLock _lock + + def __cinit__(self): + self._lock = RWLock() + + @property + def reader_count(self): + return self._lock._reader_count & (2**30 - 1) + + @property + def writer_blocking_readers(self) -> bool: + return self._lock._reader_count < 0 + + @property + def writer_reentry(self): + return self._lock._writer_reentry + + @property + def lock_thread_id(self): + return self._lock._write_locked_id - 1 + + def get_perf_counters(self): + return self._lock.get_perf_counters() + + @contextmanager + def read_lock(self): + self.lock_read() + try: + yield + finally: + self.unlock_read() + + @contextmanager + def write_lock(self): + self.lock_write() + try: + yield + finally: + self.unlock_write() + + def lock_read(self): + self._lock.lock_read() + + def unlock_read(self): + self._lock.unlock_read() + + def lock_write(self): + self._lock.lock_write() + + def unlock_write(self): + self._lock.unlock_write() + + def lock_write_with(self, _RWLock second_lock): + """Lock two locks for writing at the same time. + """ + if self._lock is second_lock._lock: + self._lock.lock_write() + else: + self._lock.lock_write_with(second_lock._lock) + + def unlock_write_with(self, _RWLock second_lock): + """Unlock two locks for writing after locking them at the same time. + """ + if self._lock is second_lock._lock: + self._lock.unlock_write() + else: + self._lock.unlock_write_with(second_lock._lock) + + +def trace(test_function): + """Compiled test trace helper. + """ + import sys + from functools import partial, wraps + from threading import get_ident as get_thread_id + + _print = sys.stdout.write + sysmon = sys.monitoring + tool_id = sysmon.DEBUGGER_ID + + print_lock: cython.pymutex + + test_function_startline = test_function.__code__.co_firstlineno + + def trace_event(event, code, instruction_offset, *arg): + filename: str = code.co_filename + if 'test_' in filename and code.co_firstlineno < test_function_startline: + return sysmon.DISABLE + if 'lock' not in filename and 'etree' not in filename: + return sysmon.DISABLE + + message = f"[{get_thread_id() & 0xffffffff:08x}] {event:6} {code.co_name}\n" + with print_lock: + _print(message) + + @wraps(test_function) + def method(*args): + sysmon.use_tool_id(tool_id, "tracer") + sysmon.set_events(tool_id, sysmon.events.PY_START | sysmon.events.PY_RETURN) + + sysmon.register_callback( + tool_id, sysmon.events.PY_START, partial(trace_event, 'call')) + sysmon.register_callback( + tool_id, sysmon.events.PY_RETURN, partial(trace_event, 'return')) + + test_function(*args) + + sysmon.clear_tool_id(tool_id) + + return method diff --git a/src/lxml/tests/common_imports.py b/src/lxml/tests/common_imports.py index 68db7c2b2..bb73c81de 100644 --- a/src/lxml/tests/common_imports.py +++ b/src/lxml/tests/common_imports.py @@ -13,17 +13,11 @@ import sys import tempfile import unittest -from contextlib import contextmanager -try: - import urlparse -except ImportError: - import urllib.parse as urlparse - -try: - from urllib import pathname2url -except: - from urllib.request import pathname2url +from contextlib import contextmanager +from io import StringIO, BytesIO +import urllib.parse as urlparse +from urllib.request import pathname2url from lxml import etree, html @@ -36,8 +30,7 @@ def make_version_tuple(version_string): IS_PYPY = (getattr(sys, 'implementation', None) == 'pypy' or getattr(sys, 'pypy_version_info', None) is not None) -IS_PYTHON3 = sys.version_info[0] >= 3 -IS_PYTHON2 = sys.version_info[0] < 3 +IS_FT_PYTHON = not getattr(sys, '_is_gil_enabled', lambda : True)() from xml.etree import ElementTree @@ -46,16 +39,7 @@ def make_version_tuple(version_string): else: ET_VERSION = (0,0,0) -if IS_PYTHON2: - from xml.etree import cElementTree - - if hasattr(cElementTree, 'VERSION'): - CET_VERSION = make_version_tuple(cElementTree.VERSION) - else: - CET_VERSION = (0,0,0) -else: - CET_VERSION = (0, 0, 0) - cElementTree = None +DOC_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), 'doc') def filter_by_version(test_class, version_dict, current_version): @@ -76,93 +60,53 @@ def needs_libxml(*version): "needs libxml2 >= %s.%s.%s" % (version + (0, 0, 0))[:3]) +def needs_feature(feature_name): + assert feature_name in [ + 'catalog', 'ftp', 'html', 'http', 'iconv', 'icu', + 'lzma', 'regexp', 'schematron', 'xmlschema', 'xpath', 'zlib', + ], feature_name + features = ', '.join(sorted(etree.LIBXML_FEATURES)) + return unittest.skipIf( + feature_name not in etree.LIBXML_FEATURES, + f"needs libxml2 with feature {feature_name}, found [{features}]" + ) + + import doctest try: import pytest except ImportError: - class skipif(object): + class skipif: "Using a class because a function would bind into a method when used in classes" def __init__(self, *args): pass def __call__(self, func, *args): return func else: skipif = pytest.mark.skipif -def _get_caller_relative_path(filename, frame_depth=2): - module = sys.modules[sys._getframe(frame_depth).f_globals['__name__']] - return os.path.normpath(os.path.join( - os.path.dirname(getattr(module, '__file__', '')), filename)) - -from io import StringIO unichr_escape = re.compile(r'\\u[0-9a-fA-F]{4}|\\U[0-9a-fA-F]{8}') -if sys.version_info[0] >= 3: - # Python 3 - from builtins import str as unicode - from codecs import unicode_escape_decode - _chr = chr - def _str(s, encoding="UTF-8"): - return unichr_escape.sub(lambda x: unicode_escape_decode(x.group(0))[0], s) - def _bytes(s, encoding="UTF-8"): - return s.encode(encoding) - from io import BytesIO as _BytesIO - def BytesIO(*args): - if args and isinstance(args[0], str): - args = (args[0].encode("UTF-8"),) - return _BytesIO(*args) - - doctest_parser = doctest.DocTestParser() - _fix_unicode = re.compile(r'(\s+)u(["\'])').sub - _fix_exceptions = re.compile(r'(.*except [^(]*),\s*(.*:)').sub - def make_doctest(filename): - filename = _get_caller_relative_path(filename) - doctests = read_file(filename) - doctests = _fix_unicode(r'\1\2', doctests) - doctests = _fix_exceptions(r'\1 as \2', doctests) - return doctest.DocTestCase( - doctest_parser.get_doctest( - doctests, {}, os.path.basename(filename), filename, 0)) -else: - # Python 2 - from __builtin__ import unicode - _chr = unichr - def _str(s, encoding="UTF-8"): - s = unicode(s, encoding=encoding) - return unichr_escape.sub(lambda x: - x.group(0).decode('unicode-escape'), - s) - def _bytes(s, encoding="UTF-8"): - return s - from io import BytesIO - - doctest_parser = doctest.DocTestParser() - _fix_traceback = re.compile(r'^(\s*)(?:\w+\.)+(\w*(?:Error|Exception|Invalid):)', re.M).sub - _fix_exceptions = re.compile(r'(.*except [^(]*)\s+as\s+(.*:)').sub - _fix_bytes = re.compile(r'(\s+)b(["\'])').sub - def make_doctest(filename): - filename = _get_caller_relative_path(filename) - doctests = read_file(filename) - doctests = _fix_traceback(r'\1\2', doctests) - doctests = _fix_exceptions(r'\1, \2', doctests) - doctests = _fix_bytes(r'\1\2', doctests) - return doctest.DocTestCase( - doctest_parser.get_doctest( - doctests, {}, os.path.basename(filename), filename, 0)) -try: - skipIf = unittest.skipIf -except AttributeError: - def skipIf(condition, why): - def _skip(thing): - import types - if isinstance(thing, (type, types.ClassType)): - return type(thing.__name__, (object,), {}) - else: - return None - if condition: - return _skip - return lambda thing: thing +# Python 3 +from codecs import unicode_escape_decode +def _str(s, encoding="UTF-8"): + return unichr_escape.sub(lambda x: unicode_escape_decode(x.group(0))[0], s) +def _bytes(s, encoding="UTF-8"): + return s.encode(encoding) + +from io import BytesIO as _BytesIO + +def BytesIO(*args): + if args and isinstance(args[0], str): + args = (args[0].encode("UTF-8"),) + return _BytesIO(*args) + +doctest_parser = doctest.DocTestParser() + +def make_doctest(filename): + file_path = os.path.join(DOC_DIR, filename) + return doctest.DocFileSuite(file_path, module_relative=False, encoding='utf-8', optionflags=doctest.ELLIPSIS) class HelperTestCase(unittest.TestCase): @@ -173,16 +117,22 @@ def tearDown(self): def parse(self, text, parser=None): f = BytesIO(text) if isinstance(text, bytes) else StringIO(text) return etree.parse(f, parser=parser) - + def _rootstring(self, tree): return etree.tostring(tree.getroot()).replace( - _bytes(' '), _bytes('')).replace(_bytes('\n'), _bytes('')) + b' ', b'').replace(b'\n', b'') + + try: + unittest.TestCase.assertRegex + except AttributeError: + def assertRegex(self, *args, **kwargs): + return self.assertRegex(*args, **kwargs) class SillyFileLike: - def __init__(self, xml_data=_bytes('')): + def __init__(self, xml_data=b''): self.xml_data = xml_data - + def read(self, amount=None): if self.xml_data: if amount: @@ -190,28 +140,28 @@ def read(self, amount=None): self.xml_data = self.xml_data[amount:] else: data = self.xml_data - self.xml_data = _bytes('') + self.xml_data = b'' return data - return _bytes('') + return b'' + class LargeFileLike: def __init__(self, charlen=100, depth=4, children=5): self.data = BytesIO() - self.chars = _bytes('a') * charlen + self.chars = b'a' * charlen self.children = range(children) self.more = self.iterelements(depth) def iterelements(self, depth): - yield _bytes('') + yield b'' depth -= 1 if depth > 0: for child in self.children: - for element in self.iterelements(depth): - yield element + yield from self.iterelements(depth) yield self.chars else: yield self.chars - yield _bytes('') + yield b'' def read(self, amount=None): data = self.data @@ -232,54 +182,62 @@ def read(self, amount=None): result = result[:amount] return result + class LargeFileLikeUnicode(LargeFileLike): def __init__(self, charlen=100, depth=4, children=5): LargeFileLike.__init__(self, charlen, depth, children) self.data = StringIO() - self.chars = _str('a') * charlen + self.chars = 'a' * charlen self.more = self.iterelements(depth) def iterelements(self, depth): - yield _str('') + yield '' depth -= 1 if depth > 0: for child in self.children: - for element in self.iterelements(depth): - yield element + yield from self.iterelements(depth) yield self.chars else: yield self.chars - yield _str('') + yield '' -class SimpleFSPath(object): + +class SimpleFSPath: def __init__(self, path): self.path = path def __fspath__(self): return self.path + def fileInTestDir(name): _testdir = os.path.dirname(__file__) return os.path.join(_testdir, name) + def path2url(path): return urlparse.urljoin( - 'file:', pathname2url(path)) + 'file://', pathname2url(path)) + def fileUrlInTestDir(name): return path2url(fileInTestDir(name)) + def read_file(name, mode='r'): with open(name, mode) as f: data = f.read() return data + def write_to_file(name, data, mode='w'): with open(name, mode) as f: f.write(data) + def readFileInTestDir(name, mode='r'): return read_file(fileInTestDir(name), mode) + def canonicalize(xml): tree = etree.parse(BytesIO(xml) if isinstance(xml, bytes) else StringIO(xml)) f = BytesIO() diff --git a/src/lxml/tests/dummy_http_server.py b/src/lxml/tests/dummy_http_server.py index 70ef8d6a6..64b59ec90 100644 --- a/src/lxml/tests/dummy_http_server.py +++ b/src/lxml/tests/dummy_http_server.py @@ -3,14 +3,9 @@ """ import sys +import urllib.parse as urlparse from contextlib import contextmanager -try: - import urlparse -except ImportError: - # Python 3 - import urllib.parse as urlparse - @contextmanager def webserver(app, port=0, host=None): @@ -28,8 +23,8 @@ def webserver(app, port=0, host=None): import threading thread = threading.Thread(target=server.serve_forever, - kwargs={'poll_interval': 0.5}) - thread.setDaemon(True) + kwargs={'poll_interval': 0.5}, + daemon=True) thread.start() try: yield 'http://%s:%s/' % (host, port) # yield control to 'with' body @@ -39,13 +34,9 @@ def webserver(app, port=0, host=None): thread.join(timeout=1) -try: - from SocketServer import ThreadingMixIn -except ImportError: - # Python 3 - from socketserver import ThreadingMixIn - +from socketserver import ThreadingMixIn import wsgiref.simple_server as wsgiserver + class WebServer(wsgiserver.WSGIServer, ThreadingMixIn): """A web server that starts a new thread for each request. """ @@ -69,7 +60,7 @@ def build_web_server(app, port, host=None): return server -class HTTPRequestCollector(object): +class HTTPRequestCollector: def __init__(self, response_data, response_code=200, headers=()): self.requests = [] self.response_code = response_code diff --git a/src/lxml/tests/selftest.py b/src/lxml/tests/selftest.py index 6ee0ff6d8..67053cf13 100644 --- a/src/lxml/tests/selftest.py +++ b/src/lxml/tests/selftest.py @@ -12,8 +12,6 @@ import re, sys def stdout(): - if sys.version_info[0] < 3: - return sys.stdout class bytes_stdout(object): def write(self, data): if isinstance(data, bytes): @@ -21,10 +19,7 @@ def write(self, data): sys.stdout.write(data) return bytes_stdout() -try: - from StringIO import StringIO as BytesIO -except ImportError: - from io import BytesIO +from io import BytesIO from lxml import etree as ElementTree from lxml import _elementpath as ElementPath @@ -48,16 +43,7 @@ def serialize(elem, **options): file = BytesIO() tree = ElementTree.ElementTree(elem) tree.write(file, **options) - if sys.version_info[0] < 3: - try: - encoding = options["encoding"] - except KeyError: - encoding = "utf-8" - else: - encoding = 'ISO8859-1' - result = fix_compatibility(file.getvalue().decode(encoding)) - if sys.version_info[0] < 3: - result = result.encode(encoding) + result = fix_compatibility(file.getvalue().decode('ISO8859-1')) return result def summarize(elem): @@ -574,7 +560,7 @@ def encoding(): >>> serialize(elem, encoding="iso-8859-1").lower() '\n' - >>> elem.text = u'\xe5\xf6\xf6<>' + >>> elem.text = '\xe5\xf6\xf6<>' >>> elem.attrib.clear() >>> serialize(elem) 'åöö<>' @@ -585,7 +571,7 @@ def encoding(): >>> serialize(elem, encoding="iso-8859-1").lower() "\n\xe5\xf6\xf6<>" - >>> elem.attrib["key"] = u'\xe5\xf6\xf6<>' + >>> elem.attrib["key"] = '\xe5\xf6\xf6<>' >>> elem.text = None >>> serialize(elem) '' @@ -597,8 +583,6 @@ def encoding(): '\n' """ -if sys.version_info[0] >= 3: - encoding.__doc__ = encoding.__doc__.replace("u'", "'") def methods(): r""" @@ -622,6 +606,7 @@ def methods(): # doesn't work with lxml.etree del methods + def iterators(): """ Test iterators. diff --git a/src/lxml/tests/selftest2.py b/src/lxml/tests/selftest2.py index 80477af58..64efa60f5 100644 --- a/src/lxml/tests/selftest2.py +++ b/src/lxml/tests/selftest2.py @@ -5,19 +5,12 @@ # *test script* works as expected. import sys - -try: - from StringIO import StringIO - BytesIO = StringIO -except ImportError: - from io import BytesIO, StringIO +from io import BytesIO, StringIO from lxml import etree as ElementTree def stdout(): - if sys.version_info[0] < 3: - return sys.stdout - class bytes_stdout(object): + class bytes_stdout: def write(self, data): if isinstance(data, bytes): data = data.decode('ISO8859-1') @@ -37,8 +30,7 @@ def serialize(elem, encoding=None): else: tree.write(file) result = file.getvalue() - if sys.version_info[0] >= 3: - result = result.decode('ISO8859-1') + result = result.decode('ISO8859-1') result = result.replace(' />', '/>') if result[-1:] == '\n': result = result[:-1] @@ -162,7 +154,7 @@ def encoding(): Test encoding issues. >>> elem = ElementTree.Element("tag") - >>> elem.text = u'abc' + >>> elem.text = 'abc' >>> serialize(elem) 'abc' >>> serialize(elem, "utf-8") @@ -193,7 +185,7 @@ def encoding(): >>> serialize(elem, "iso-8859-1").lower() '\n' - >>> elem.text = u'\xe5\xf6\xf6<>' + >>> elem.text = '\xe5\xf6\xf6<>' >>> elem.attrib.clear() >>> serialize(elem) 'åöö<>' @@ -204,7 +196,7 @@ def encoding(): >>> serialize(elem, "iso-8859-1").lower() "\n\xe5\xf6\xf6<>" - >>> elem.attrib["key"] = u'\xe5\xf6\xf6<>' + >>> elem.attrib["key"] = '\xe5\xf6\xf6<>' >>> elem.text = None >>> serialize(elem) '' @@ -217,8 +209,6 @@ def encoding(): """ -if sys.version_info[0] >= 3: - encoding.__doc__ = encoding.__doc__.replace("u'", "'") def qname(): """ diff --git a/src/lxml/tests/test_annotations.py b/src/lxml/tests/test_annotations.py new file mode 100644 index 000000000..6fbe53673 --- /dev/null +++ b/src/lxml/tests/test_annotations.py @@ -0,0 +1,88 @@ +""" +Test typing annotations. +""" + +import inspect +import typing +import sys +import unittest + +from .common_imports import etree +from .common_imports import HelperTestCase +from lxml import builder, sax + + +def container_function_with_subscripted_types(): + # The function definition is in a container so that any errors would trigger + # when calling the function instead of during import. + def function_with_subscripted_types( + element_tree: etree.ElementTree[etree.Element], + xml_parser: etree.XMLParser[etree.Element], + html_parser: etree.HTMLParser[etree.Element], + element_maker: builder.ElementMaker[etree.Element], + element_tree_content_handler: sax.ElementTreeContentHandler[etree.Element], + ): + pass + + return function_with_subscripted_types + + +def container_function_with_subscripted_private_element_tree(): + def function_with_subscripted_private_element_tree( + _element_tree: etree._ElementTree[etree.Element], + ): + pass + + return function_with_subscripted_private_element_tree + + +class TypingTestCase(HelperTestCase): + """Typing test cases + """ + + def test_subscripted_generic(self): + # Test that all generic types can be subscripted. + # Based on PEP 560. + func = container_function_with_subscripted_types() + if sys.version_info >= (3, 10): + # inspect.get_annotations was added in python 3.10. + ann = inspect.get_annotations(func, eval_str=True) + + et_ann = ann["element_tree"] + assert typing.get_origin(et_ann) == etree.ElementTree + assert typing.get_args(et_ann) == (etree.Element,) + + xml_ann = ann["xml_parser"] + assert typing.get_origin(xml_ann) == etree.XMLParser + assert typing.get_args(xml_ann) == (etree.Element,) + + html_ann = ann["html_parser"] + assert typing.get_origin(html_ann) == etree.HTMLParser + assert typing.get_args(html_ann) == (etree.Element,) + + maker_ann = ann["element_maker"] + assert typing.get_origin(maker_ann) == builder.ElementMaker + assert typing.get_args(maker_ann) == (etree.Element,) + + handler_ann = ann["element_tree_content_handler"] + assert typing.get_origin(handler_ann) == sax.ElementTreeContentHandler + assert typing.get_args(handler_ann) == (etree.Element,) + + # Subscripting etree.Element should fail with the error: + # TypeError: 'type' _ElementTree is not subscriptable + # Make sure that the test works and it is indeed failing. + with self.assertRaises(TypeError): + # TypeError should be raised here for python < 3.14: + func = container_function_with_subscripted_private_element_tree() + # TypeError should be raised here for python >= 3.14: + inspect.get_annotations(func, eval_str=True) + + +def test_suite(): + suite = unittest.TestSuite() + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(TypingTestCase)]) + return suite + + +if __name__ == '__main__': + print('to test use test.py %s' % __file__) diff --git a/src/lxml/tests/test_builder.py b/src/lxml/tests/test_builder.py index b1ad4ebf6..8fbfbe46f 100644 --- a/src/lxml/tests/test_builder.py +++ b/src/lxml/tests/test_builder.py @@ -1,10 +1,7 @@ -# -*- coding: utf-8 -*- - """ Tests that ElementMaker works properly. """ -from __future__ import absolute_import import unittest @@ -21,16 +18,16 @@ class BuilderTestCase(HelperTestCase): def test_build_from_xpath_result(self): class StringSubclass(str): pass wrapped = E.b(StringSubclass('Hello')) - self.assertEqual(_bytes('Hello'), etree.tostring(wrapped)) + self.assertEqual(b'Hello', etree.tostring(wrapped)) def test_unknown_type_raises(self): - class UnknownType(object): + class UnknownType: pass self.assertRaises(TypeError, E.b, UnknownType()) def test_cdata(self): wrapped = E.b(etree.CDATA('Hello')) - self.assertEqual(_bytes(''), etree.tostring(wrapped)) + self.assertEqual(b'', etree.tostring(wrapped)) def test_cdata_solo(self): self.assertRaises(ValueError, E.b, 'Hello', etree.CDATA('World')) @@ -62,7 +59,7 @@ def test_qname_tag_default_namespace(self): def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(BuilderTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(BuilderTestCase)]) return suite if __name__ == '__main__': diff --git a/src/lxml/tests/test_classlookup.py b/src/lxml/tests/test_classlookup.py index 7c871d511..d1c034b7d 100644 --- a/src/lxml/tests/test_classlookup.py +++ b/src/lxml/tests/test_classlookup.py @@ -1,24 +1,21 @@ -# -*- coding: utf-8 -*- - """ Tests for different Element class lookup mechanisms. """ -from __future__ import absolute_import import unittest, gc from .common_imports import etree, HelperTestCase, _bytes, BytesIO -xml_str = _bytes('''\ +xml_str = b'''\ 0 1 2 -''') +''' class ProxyTestCase(HelperTestCase): @@ -75,7 +72,7 @@ def test_element_base(self): self.assertEqual('ElementBase', root[0].tag) def test_element_base_children(self): - el = self.etree.ElementBase(etree.ElementBase()) + el = self.etree.ElementBase(self.etree.ElementBase()) self.assertEqual('ElementBase', el.tag) self.assertEqual(1, len(el)) self.assertEqual('ElementBase', el[0].tag) @@ -109,7 +106,7 @@ class ClassLookupTestCase(HelperTestCase): def tearDown(self): etree.set_element_class_lookup() - super(ClassLookupTestCase, self).tearDown() + super().tearDown() def test_namespace_lookup(self): class TestElement(etree.ElementBase): @@ -142,12 +139,12 @@ class TestPI(etree.PIBase): element=TestElement, comment=TestComment, pi=TestPI) parser.set_element_class_lookup(lookup) - root = etree.XML(_bytes(""" + root = etree.XML(b""" - """), parser) + """, parser) self.assertEqual("default element", root.FIND_ME) self.assertEqual("default pi", root[0].FIND_ME) @@ -209,14 +206,14 @@ def lookup(self, t, d, ns, name): parser = etree.XMLParser() parser.set_element_class_lookup(MyLookup()) - root = etree.XML(_bytes(''), parser) + root = etree.XML(b'', parser) self.assertEqual('none', root.tag) self.assertRaises( TypeError, - etree.XML, _bytes(""), parser) + etree.XML, b"", parser) - root = etree.XML(_bytes(''), parser) + root = etree.XML(b'', parser) self.assertEqual('root', root.tag) def test_class_lookup_type_mismatch(self): @@ -238,26 +235,26 @@ def lookup(self, t, d, ns, name): parser = etree.XMLParser(resolve_entities=False) parser.set_element_class_lookup(MyLookup()) - root = etree.XML(_bytes(''), parser) + root = etree.XML(b'', parser) self.assertEqual('root', root.tag) self.assertEqual(etree.ElementBase, type(root)) - root = etree.XML(_bytes(""), parser) + root = etree.XML(b"", parser) self.assertRaises(TypeError, root.__getitem__, 0) - root = etree.XML(_bytes(""), parser) + root = etree.XML(b"", parser) self.assertRaises(TypeError, root.__getitem__, 0) - root = etree.XML(_bytes(""), parser) + root = etree.XML(b"", parser) self.assertRaises(TypeError, root.__getitem__, 0) root = etree.XML( - _bytes(']>' - '&myent;'), + b']>' + b'&myent;', parser) self.assertRaises(TypeError, root.__getitem__, 0) - root = etree.XML(_bytes(''), parser) + root = etree.XML(b'', parser) self.assertEqual('root', root[0].tag) def test_attribute_based_lookup(self): @@ -362,7 +359,7 @@ def lookup(self, t, d, ns, name): parser = self.etree.XMLParser() parser.set_element_class_lookup(MyLookup()) - root = XML(_bytes('AB'), + root = XML(b'AB', parser) a = root[0] @@ -394,8 +391,8 @@ def custom(self): def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(ProxyTestCase)]) - suite.addTests([unittest.makeSuite(ClassLookupTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ProxyTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ClassLookupTestCase)]) return suite if __name__ == '__main__': diff --git a/src/lxml/tests/test_css.py b/src/lxml/tests/test_css.py index e2afa65c7..184c124f0 100644 --- a/src/lxml/tests/test_css.py +++ b/src/lxml/tests/test_css.py @@ -1,6 +1,3 @@ - -from __future__ import absolute_import - import unittest import lxml.html @@ -64,5 +61,5 @@ def test_suite(): import lxml.cssselect suite.addTests(doctest.DocTestSuite(lxml.cssselect)) - suite.addTests([unittest.makeSuite(CSSTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(CSSTestCase)]) return suite diff --git a/src/lxml/tests/test_doctestcompare.py b/src/lxml/tests/test_doctestcompare.py index 366328124..201765f7e 100644 --- a/src/lxml/tests/test_doctestcompare.py +++ b/src/lxml/tests/test_doctestcompare.py @@ -1,6 +1,3 @@ - -from __future__ import absolute_import - import unittest from lxml import etree @@ -125,7 +122,7 @@ def test_missing_attributes(self): def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(DoctestCompareTest)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(DoctestCompareTest)]) return suite diff --git a/src/lxml/tests/test_dtd.py b/src/lxml/tests/test_dtd.py index 5c9b1c024..3a8ecdc5a 100644 --- a/src/lxml/tests/test_dtd.py +++ b/src/lxml/tests/test_dtd.py @@ -1,14 +1,14 @@ -# -*- coding: utf-8 -*- - """ Test cases related to DTD parsing and validation """ import unittest, sys +from io import BytesIO +from unittest import skipIf from .common_imports import ( - etree, html, BytesIO, _bytes, _str, - HelperTestCase, make_doctest, skipIf, + etree, html, + HelperTestCase, make_doctest, fileInTestDir, fileUrlInTestDir, SimpleFSPath ) @@ -24,7 +24,7 @@ def test_dtd_file(self): dtd = etree.DTD(fileInTestDir("test.dtd")) self.assertTrue(dtd.validate(root)) - + def test_dtd_file_pathlike(self): parse = etree.parse tree = parse(fileInTestDir("test.xml")) @@ -33,16 +33,24 @@ def test_dtd_file_pathlike(self): dtd = etree.DTD(SimpleFSPath(fileInTestDir("test.dtd"))) self.assertTrue(dtd.validate(root)) + def test_dtd_external_id(self): + # Only test that the 'external_id' option passes. + # Don't fail if catalogues aren't available. + try: + etree.DTD(external_id="-//W3C//DTD HTML 4.01//EN") + except etree.DTDParseError: + pass + def test_dtd_stringio(self): - root = etree.XML(_bytes("")) - dtd = etree.DTD(BytesIO("")) + root = etree.XML(b"") + dtd = etree.DTD(BytesIO(b"")) self.assertTrue(dtd.validate(root)) def test_dtd_parse_invalid(self): fromstring = etree.fromstring parser = etree.XMLParser(dtd_validation=True) - xml = _bytes('' % - fileInTestDir("test.dtd")) + xml = ('' % + fileInTestDir("test.dtd")).encode('utf-8') self.assertRaises(etree.XMLSyntaxError, fromstring, xml, parser=parser) @@ -50,9 +58,8 @@ def test_dtd_parse_file_not_found(self): fromstring = etree.fromstring dtd_filename = fileUrlInTestDir("__nosuch.dtd") parser = etree.XMLParser(dtd_validation=True) - xml = _bytes('' % dtd_filename) - self.assertRaises(etree.XMLSyntaxError, - fromstring, xml, parser=parser) + xml = '' % dtd_filename + self.assertRaises(etree.XMLSyntaxError, fromstring, xml, parser=parser) errors = None try: fromstring(xml, parser=parser) @@ -90,57 +97,57 @@ def test_dtd_parse_valid_relative_file_url(self): def test_dtd_invalid(self): root = etree.XML("") - dtd = etree.DTD(BytesIO("")) + dtd = etree.DTD(BytesIO(b"")) self.assertRaises(etree.DocumentInvalid, dtd.assertValid, root) def test_dtd_assertValid(self): root = etree.XML("") - dtd = etree.DTD(BytesIO("")) + dtd = etree.DTD(BytesIO(b"")) dtd.assertValid(root) def test_dtd_internal(self): - root = etree.XML(_bytes(''' + root = etree.XML(b''' ]> - ''')) + ''') dtd = etree.ElementTree(root).docinfo.internalDTD self.assertTrue(dtd) dtd.assertValid(root) def test_dtd_internal_invalid(self): - root = etree.XML(_bytes(''' + root = etree.XML(b''' ]> - ''')) + ''') dtd = etree.ElementTree(root).docinfo.internalDTD self.assertTrue(dtd) self.assertFalse(dtd.validate(root)) def test_dtd_invalid_duplicate_id(self): - root = etree.XML(_bytes(''' + root = etree.XML(b''' - ''')) - dtd = etree.DTD(BytesIO(_bytes(""" + ''') + dtd = etree.DTD(BytesIO(b""" - """))) + """)) self.assertFalse(dtd.validate(root)) self.assertTrue(dtd.error_log) self.assertTrue([error for error in dtd.error_log if 'id1' in error.message]) def test_dtd_api_internal(self): - root = etree.XML(_bytes(''' + root = etree.XML(b''' ]> - ''')) + ''') dtd = etree.ElementTree(root).docinfo.internalDTD self.assertTrue(dtd) dtd.assertValid(root) @@ -183,7 +190,7 @@ def test_dtd_api_internal(self): def test_internal_dtds(self): for el_count in range(2, 5): for attr_count in range(4): - root = etree.XML(_bytes(''' + root = etree.XML(''' - ''' % ' '.join(['attr%d="x"' % a for a in range(attr_count)]))) + ''' % ' '.join(['attr%d="x"' % a for a in range(attr_count)])) dtd = etree.ElementTree(root).docinfo.internalDTD self.assertTrue(dtd) dtd.assertValid(root) @@ -219,7 +226,7 @@ def test_internal_dtds(self): def test_dtd_broken(self): self.assertRaises(etree.DTDParseError, etree.DTD, - BytesIO("")) + BytesIO(b"")) def test_parse_file_dtd(self): parser = etree.XMLParser(attribute_defaults=True) @@ -290,12 +297,12 @@ def test_dtd_attrs(self): self.assertEqual(c.content, "*") # Test DTD.name attribute - root = etree.XML(_bytes(''' + root = etree.XML(b''' ]> - ''')) + ''') dtd = etree.ElementTree(root).docinfo.internalDTD self.assertEqual(dtd.name, "a") @@ -318,21 +325,21 @@ def test_declaration_escape_quote_pid(self): self.assertEqual(doc.docinfo.doctype, '''''') self.assertEqual(etree.tostring(doc), - _bytes('''\n''')) + b'''\n''') def test_declaration_quote_withoutpid(self): root = etree.XML('''''') doc = root.getroottree() self.assertEqual(doc.docinfo.doctype, '''''') self.assertEqual(etree.tostring(doc), - _bytes('''\n''')) + b'''\n''') def test_declaration_apos(self): root = etree.XML('''''') doc = root.getroottree() self.assertEqual(doc.docinfo.doctype, '''''') self.assertEqual(etree.tostring(doc), - _bytes('''\n''')) + b'''\n''') def test_ietf_decl(self): html_data = ( @@ -342,7 +349,7 @@ def test_ietf_decl(self): doc = root.getroottree() self.assertEqual(doc.docinfo.doctype, '') - self.assertEqual(etree.tostring(doc, method='html'), _bytes(html_data)) + self.assertEqual(etree.tostring(doc, method='html'), html_data.encode('utf-8')) def test_set_decl_public(self): doc = etree.Element('test').getroottree() @@ -351,7 +358,7 @@ def test_set_decl_public(self): self.assertEqual(doc.docinfo.doctype, '') self.assertEqual(etree.tostring(doc), - _bytes('\n')) + b'\n') def test_html_decl(self): # Slightly different to one above: when we create an html element, @@ -362,7 +369,7 @@ def test_html_decl(self): self.assertEqual(doc.docinfo.doctype, '') self.assertEqual(etree.tostring(doc), - _bytes('\n')) + b'\n') def test_clean_doctype(self): doc = html.Element('html').getroottree() @@ -376,7 +383,7 @@ def test_set_decl_system(self): self.assertEqual(doc.docinfo.doctype, '') self.assertEqual(etree.tostring(doc), - _bytes('\n')) + b'\n') def test_empty_decl(self): doc = etree.Element('test').getroottree() @@ -386,15 +393,15 @@ def test_empty_decl(self): self.assertTrue(doc.docinfo.public_id is None) self.assertTrue(doc.docinfo.system_url is None) self.assertEqual(etree.tostring(doc), - _bytes('\n')) + b'\n') def test_invalid_decl_1(self): docinfo = etree.Element('test').getroottree().docinfo def set_public_id(value): docinfo.public_id = value - self.assertRaises(ValueError, set_public_id, _str('ä')) - self.assertRaises(ValueError, set_public_id, _str('qwerty ä asdf')) + self.assertRaises(ValueError, set_public_id, 'ä') + self.assertRaises(ValueError, set_public_id, 'qwerty ä asdf') def test_invalid_decl_2(self): docinfo = etree.Element('test').getroottree().docinfo @@ -409,22 +416,22 @@ def test_comment_before_dtd(self): data = '\n' doc = etree.fromstring(data).getroottree() self.assertEqual(etree.tostring(doc), - _bytes(data)) + data.encode('utf-8')) def test_entity_system_url(self): - xml = etree.parse(BytesIO(' ]>')) + xml = etree.parse(BytesIO(b' ]>')) self.assertEqual(xml.docinfo.internalDTD.entities()[0].system_url, "./foo.bar") def test_entity_system_url_none(self): - xml = etree.parse(BytesIO(' ]>')) + xml = etree.parse(BytesIO(b' ]>')) self.assertEqual(xml.docinfo.internalDTD.entities()[0].system_url, None) def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(ETreeDtdTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeDtdTestCase)]) suite.addTests( - [make_doctest('../../../doc/validation.txt')]) + [make_doctest('validation.txt')]) return suite if __name__ == '__main__': diff --git a/src/lxml/tests/test_elementpath.py b/src/lxml/tests/test_elementpath.py index 1793ff821..ffaf69654 100644 --- a/src/lxml/tests/test_elementpath.py +++ b/src/lxml/tests/test_elementpath.py @@ -1,15 +1,61 @@ -# -*- coding: utf-8 -*- - """ Tests for the ElementPath implementation. """ -from __future__ import absolute_import - +import sys import unittest from copy import deepcopy from .common_imports import etree, HelperTestCase +ET = etree # compatibility with CPython tests. + +SAMPLE_XML = """\ + + text + +
+ subtext +
+ +""" + +SAMPLE_SECTION = """\ +
+ subtext + + + + +
+""" + +SAMPLE_XML_NS = """ + + text + +
+ subtext +
+ +""" + +SAMPLE_XML_NS_ELEMS = """ + + + + Apples + Bananas + + + + + African Coffee Table + 80 + 120 + + +""" + def summarize(elem): return elem.tag @@ -27,6 +73,8 @@ class EtreeElementPathTestCase(HelperTestCase): etree = etree from lxml import _elementpath + _empty_namespaces = None + def test_cache(self): self._elementpath._cache.clear() el = self.etree.XML(b'
') @@ -44,6 +92,8 @@ def test_cache(self): self.assertEqual(2, len(self._elementpath._cache)) def _assert_tokens(self, tokens, path, namespaces=None): + if namespaces is None: + namespaces = self._empty_namespaces self.assertEqual(tokens, list(self._elementpath.xpath_tokenizer(path, namespaces))) def test_tokenizer(self): @@ -86,11 +136,33 @@ def test_tokenizer_predicates(self): 'a[. = "abc"]', ) + def test_tokenizer_index(self): + assert_tokens = self._assert_tokens + assert_tokens( + [('/', ''), ('', 'a'), ('/', ''), ('', 'b'), ('/', ''), ('', 'c'), ('[', ''), ('', '1'), (']', '')], + '/a/b/c[1]', + ) + assert_tokens( + [('/', ''), ('', '{nsnone}a'), ('/', ''), ('', '{nsnone}b'), ('/', ''), ('', '{nsnone}c'), ('[', ''), ('', '1'), (']', '')], + '/a/b/c[1]', + namespaces={None:'nsnone'}, + ) + assert_tokens( + [('/', ''), ('', '{nsnone}a'), ('/', ''), ('', '{nsnone}b'), ('[', ''), ('', '2'), (']', ''), ('/', ''), ('', '{nsnone}c'), ('[', ''), ('', '1'), (']', '')], + '/a/b[2]/c[1]', + namespaces={None:'nsnone'}, + ) + assert_tokens( + [('/', ''), ('', '{nsnone}a'), ('/', ''), ('', '{nsnone}b'), ('[', ''), ('', '100'), (']', '')], + '/a/b[100]', + namespaces={None:'nsnone'} + ) + def test_xpath_tokenizer(self): # Test the XPath tokenizer. Copied from CPython's "test_xml_etree.py" ElementPath = self._elementpath - def check(p, expected, namespaces=None): + def check(p, expected, namespaces=self._empty_namespaces): self.assertEqual([op or tag for op, tag in ElementPath.xpath_tokenizer(p, namespaces)], expected) @@ -145,6 +217,20 @@ def check(p, expected, namespaces=None): {'': 'http://www.w3.org/2001/XMLSchema', 'ns': 'http://www.w3.org/2001/XMLSchema'}) + if self.etree is etree: + check("/doc/section[2]", + ['/', '{http://www.w3.org/2001/XMLSchema}doc', '/', '{http://www.w3.org/2001/XMLSchema}section', '[', '2', ']'], + {"":"http://www.w3.org/2001/XMLSchema"} + ) + check("/doc/section[2]", + ['/', '{http://www.w3.org/2001/XMLSchema}doc', '/', '{http://www.w3.org/2001/XMLSchema}section', '[', '2', ']'], + {None:"http://www.w3.org/2001/XMLSchema"} + ) + check("/ns:doc/ns:section[2]", + ['/', '{http://www.w3.org/2001/XMLSchema}doc', '/', '{http://www.w3.org/2001/XMLSchema}section', '[', '2', ']'], + {"ns":"http://www.w3.org/2001/XMLSchema"} + ) + def test_find(self): """ Test find methods (including xpath syntax). @@ -271,10 +357,12 @@ def test_find(self): self.assertEqual(summarize_list(etree.ElementTree(elem).findall("./tag")), ['tag', 'tag']) - # FIXME: ET's Path module handles this case incorrectly; this gives - # a warning in 1.3, and the behaviour will be modified in 1.4. self.assertEqual(summarize_list(etree.ElementTree(elem).findall("/tag")), ['tag', 'tag']) + # This would be correct: + if False: + self.assertEqual(summarize_list(etree.ElementTree(elem).findall("/body")), + ['body']) # duplicate section => 2x tag matches elem[1] = deepcopy(elem[2]) @@ -285,16 +373,317 @@ def test_find(self): self.assertEqual(summarize_list(elem.findall(".//tag[@class][@id]")), ['tag', 'tag']) + def test_find_warning(self): + etree = self.etree + elem = etree.XML(""" + + text + +
+ subtext +
+ + """) + + # FIXME: ET's Path module handles this case incorrectly; this gives + # a warning in 1.3, and the behaviour will be modified in the future. + self.assertWarnsRegex( + FutureWarning, ".*If you rely on the current behaviour, change it to './tag'", + etree.ElementTree(elem).findall, "/tag") + self.assertWarnsRegex( + FutureWarning, ".*If you rely on the current behaviour, change it to './tag'", + etree.ElementTree(elem).findtext, "/tag") + self.assertWarnsRegex( + FutureWarning, ".*If you rely on the current behaviour, change it to './tag'", + etree.ElementTree(elem).find, "/tag") + self.assertWarnsRegex( + FutureWarning, ".*If you rely on the current behaviour, change it to './tag'", + etree.ElementTree(elem).iterfind, "/tag") + + +class ElementFindTest(unittest.TestCase): + # Copied from CPython's test_xml_etree.py. + + def test_find_simple(self): + e = ET.XML(SAMPLE_XML) + self.assertEqual(e.find('tag').tag, 'tag') + self.assertEqual(e.find('section/tag').tag, 'tag') + self.assertEqual(e.find('./tag').tag, 'tag') + + e[2] = ET.XML(SAMPLE_SECTION) + self.assertEqual(e.find('section/nexttag').tag, 'nexttag') + + self.assertEqual(e.findtext('./tag'), 'text') + self.assertEqual(e.findtext('section/tag'), 'subtext') + + # section/nexttag is found but has no text + self.assertEqual(e.findtext('section/nexttag'), '') + self.assertEqual(e.findtext('section/nexttag', 'default'), '') + + # tog doesn't exist and 'default' kicks in + self.assertIsNone(e.findtext('tog')) + self.assertEqual(e.findtext('tog', 'default'), 'default') + + # Issue #16922 + self.assertEqual(ET.XML('').findtext('empty'), '') + + def test_find_xpath(self): + LINEAR_XML = ''' + + + + + + ''' + e = ET.XML(LINEAR_XML) + + # Test for numeric indexing and last() + self.assertEqual(e.find('./tag[1]').attrib['class'], 'a') + self.assertEqual(e.find('./tag[2]').attrib['class'], 'b') + self.assertEqual(e.find('./tag[last()]').attrib['class'], 'd') + self.assertEqual(e.find('./tag[last()-1]').attrib['class'], 'c') + self.assertEqual(e.find('./tag[last()-2]').attrib['class'], 'b') + + """ # Error messages differ in lxml. + self.assertRaisesRegex(SyntaxError, 'XPath', e.find, './tag[0]') + self.assertRaisesRegex(SyntaxError, 'XPath', e.find, './tag[-1]') + self.assertRaisesRegex(SyntaxError, 'XPath', e.find, './tag[last()-0]') + self.assertRaisesRegex(SyntaxError, 'XPath', e.find, './tag[last()+1]') + """ + self.assertRaises(SyntaxError, e.find, './tag[0]') + self.assertRaises(SyntaxError, e.find, './tag[-1]') + self.assertRaises(SyntaxError, e.find, './tag[last()-0]') + self.assertRaises(SyntaxError, e.find, './tag[last()+1]') + + def test_findall(self): + e = ET.XML(SAMPLE_XML) + e[2] = ET.XML(SAMPLE_SECTION) + self.assertEqual(summarize_list(e.findall('.')), ['body']) + self.assertEqual(summarize_list(e.findall('tag')), ['tag', 'tag']) + self.assertEqual(summarize_list(e.findall('tog')), []) + self.assertEqual(summarize_list(e.findall('tog/foo')), []) + self.assertEqual(summarize_list(e.findall('*')), + ['tag', 'tag', 'section']) + self.assertEqual(summarize_list(e.findall('.//tag')), + ['tag'] * 4) + self.assertEqual(summarize_list(e.findall('section/tag')), ['tag']) + self.assertEqual(summarize_list(e.findall('section//tag')), ['tag'] * 2) + self.assertEqual(summarize_list(e.findall('section/*')), + ['tag', 'nexttag', 'nextsection']) + self.assertEqual(summarize_list(e.findall('section//*')), + ['tag', 'nexttag', 'nextsection', 'tag']) + self.assertEqual(summarize_list(e.findall('section/.//*')), + ['tag', 'nexttag', 'nextsection', 'tag']) + self.assertEqual(summarize_list(e.findall('*/*')), + ['tag', 'nexttag', 'nextsection']) + self.assertEqual(summarize_list(e.findall('*//*')), + ['tag', 'nexttag', 'nextsection', 'tag']) + self.assertEqual(summarize_list(e.findall('*/tag')), ['tag']) + self.assertEqual(summarize_list(e.findall('*/./tag')), ['tag']) + self.assertEqual(summarize_list(e.findall('./tag')), ['tag'] * 2) + self.assertEqual(summarize_list(e.findall('././tag')), ['tag'] * 2) + + self.assertEqual(summarize_list(e.findall('.//tag[@class]')), + ['tag'] * 3) + self.assertEqual(summarize_list(e.findall('.//tag[@class="a"]')), + ['tag']) + self.assertEqual(summarize_list(e.findall('.//tag[@class!="a"]')), + ['tag'] * 2) + self.assertEqual(summarize_list(e.findall('.//tag[@class="b"]')), + ['tag'] * 2) + self.assertEqual(summarize_list(e.findall('.//tag[@class!="b"]')), + ['tag']) + self.assertEqual(summarize_list(e.findall('.//tag[@id]')), + ['tag']) + self.assertEqual(summarize_list(e.findall('.//section[tag]')), + ['section']) + self.assertEqual(summarize_list(e.findall('.//section[element]')), []) + self.assertEqual(summarize_list(e.findall('../tag')), []) + self.assertEqual(summarize_list(e.findall('section/../tag')), + ['tag'] * 2) + self.assertEqual(e.findall('section//'), e.findall('section//*')) + + self.assertEqual(summarize_list(e.findall(".//section[tag='subtext']")), + ['section']) + self.assertEqual(summarize_list(e.findall(".//section[tag ='subtext']")), + ['section']) + self.assertEqual(summarize_list(e.findall(".//section[tag= 'subtext']")), + ['section']) + self.assertEqual(summarize_list(e.findall(".//section[tag = 'subtext']")), + ['section']) + self.assertEqual(summarize_list(e.findall(".//section[ tag = 'subtext' ]")), + ['section']) + + # Negations of above tests. They match nothing because the sole section + # tag has subtext. + self.assertEqual(summarize_list(e.findall(".//section[tag!='subtext']")), + []) + self.assertEqual(summarize_list(e.findall(".//section[tag !='subtext']")), + []) + self.assertEqual(summarize_list(e.findall(".//section[tag!= 'subtext']")), + []) + self.assertEqual(summarize_list(e.findall(".//section[tag != 'subtext']")), + []) + self.assertEqual(summarize_list(e.findall(".//section[ tag != 'subtext' ]")), + []) + + self.assertEqual(summarize_list(e.findall(".//tag[.='subtext']")), + ['tag']) + self.assertEqual(summarize_list(e.findall(".//tag[. ='subtext']")), + ['tag']) + self.assertEqual(summarize_list(e.findall('.//tag[.= "subtext"]')), + ['tag']) + self.assertEqual(summarize_list(e.findall('.//tag[ . = "subtext" ]')), + ['tag']) + self.assertEqual(summarize_list(e.findall(".//tag[. = 'subtext']")), + ['tag']) + self.assertEqual(summarize_list(e.findall(".//tag[. = 'subtext ']")), + []) + self.assertEqual(summarize_list(e.findall(".//tag[.= ' subtext']")), + []) + + # Negations of above tests. + # Matches everything but the tag containing subtext + self.assertEqual(summarize_list(e.findall(".//tag[.!='subtext']")), + ['tag'] * 3) + self.assertEqual(summarize_list(e.findall(".//tag[. !='subtext']")), + ['tag'] * 3) + self.assertEqual(summarize_list(e.findall('.//tag[.!= "subtext"]')), + ['tag'] * 3) + self.assertEqual(summarize_list(e.findall('.//tag[ . != "subtext" ]')), + ['tag'] * 3) + self.assertEqual(summarize_list(e.findall(".//tag[. != 'subtext']")), + ['tag'] * 3) + # Matches all tags. + self.assertEqual(summarize_list(e.findall(".//tag[. != 'subtext ']")), + ['tag'] * 4) + self.assertEqual(summarize_list(e.findall(".//tag[.!= ' subtext']")), + ['tag'] * 4) + + # duplicate section => 2x tag matches + e[1] = deepcopy(e[2]) # lxml requires deepcopy() + self.assertEqual(summarize_list(e.findall(".//section[tag = 'subtext']")), + ['section', 'section']) + self.assertEqual(summarize_list(e.findall(".//tag[. = 'subtext']")), + ['tag', 'tag']) -#class ElementTreeElementPathTestCase(EtreeElementPathTestCase): -# import xml.etree.ElementTree as etree -# import xml.etree.ElementPath as _elementpath + def test_test_find_with_ns(self): + e = ET.XML(SAMPLE_XML_NS) + self.assertEqual(summarize_list(e.findall('tag')), []) + self.assertEqual( + summarize_list(e.findall("{http://effbot.org/ns}tag")), + ['{http://effbot.org/ns}tag'] * 2) + self.assertEqual( + summarize_list(e.findall(".//{http://effbot.org/ns}tag")), + ['{http://effbot.org/ns}tag'] * 3) + + def test_findall_different_nsmaps(self): + root = ET.XML(''' + + + + + ''') + nsmap = {'xx': 'X'} + self.assertEqual(len(root.findall(".//xx:b", namespaces=nsmap)), 2) + self.assertEqual(len(root.findall(".//b", namespaces=nsmap)), 2) + nsmap = {'xx': 'Y'} + self.assertEqual(len(root.findall(".//xx:b", namespaces=nsmap)), 1) + self.assertEqual(len(root.findall(".//b", namespaces=nsmap)), 2) + nsmap = {'xx': 'X', '': 'Y'} + self.assertEqual(len(root.findall(".//xx:b", namespaces=nsmap)), 2) + self.assertEqual(len(root.findall(".//b", namespaces=nsmap)), 1) + + def test_findall_wildcard(self): + root = ET.XML(''' + + + + + ''') + root.append(ET.Comment('test')) + + self.assertEqual(summarize_list(root.findall("{*}b")), + ['{X}b', 'b', '{Y}b']) + self.assertEqual(summarize_list(root.findall("{*}c")), + ['c']) + self.assertEqual(summarize_list(root.findall("{X}*")), + ['{X}b']) + self.assertEqual(summarize_list(root.findall("{Y}*")), + ['{Y}b']) + self.assertEqual(summarize_list(root.findall("{}*")), + ['b', 'c']) + self.assertEqual(summarize_list(root.findall("{}b")), # only for consistency + ['b']) + self.assertEqual(summarize_list(root.findall("{}b")), + summarize_list(root.findall("b"))) + self.assertEqual(summarize_list(root.findall("{*}*")), + ['{X}b', 'b', 'c', '{Y}b']) + # This is an unfortunate difference, but that's how find('*') works. + # ... not in lxml + #self.assertEqual(summarize_list(root.findall("{*}*") + [root[-1]]), + # summarize_list(root.findall("*"))) + + self.assertEqual(summarize_list(root.findall(".//{*}b")), + ['{X}b', 'b', '{X}b', 'b', '{Y}b']) + self.assertEqual(summarize_list(root.findall(".//{*}c")), + ['c', 'c']) + self.assertEqual(summarize_list(root.findall(".//{X}*")), + ['{X}b', '{X}b']) + self.assertEqual(summarize_list(root.findall(".//{Y}*")), + ['{Y}b']) + self.assertEqual(summarize_list(root.findall(".//{}*")), + ['c', 'b', 'c', 'b']) + self.assertEqual(summarize_list(root.findall(".//{}b")), # only for consistency + ['b', 'b']) + self.assertEqual(summarize_list(root.findall(".//{}b")), + summarize_list(root.findall(".//b"))) + + def test_bad_find(self): + e = ET.XML(SAMPLE_XML) + with self.assertRaisesRegex(SyntaxError, 'cannot use absolute path'): + e.findall('/tag') + + def test_find_through_ElementTree(self): + e = ET.XML(SAMPLE_XML) + self.assertEqual(ET.ElementTree(e).find('tag').tag, 'tag') + self.assertEqual(ET.ElementTree(e).findtext('tag'), 'text') + self.assertEqual(summarize_list(ET.ElementTree(e).findall('tag')), + ['tag'] * 2) + # this produces a warning + msg = ("This search is broken in 1.3 and earlier, and will be fixed " + "in a future version. If you rely on the current behaviour, " + "change it to '.+'") + msg = ".*" # lxml gives a different warning + with self.assertWarnsRegex(FutureWarning, msg): + it = ET.ElementTree(e).findall('//tag') + self.assertEqual(summarize_list(it), ['tag'] * 3) + + +class ElementTreeElementPathTestCase(EtreeElementPathTestCase): + import xml.etree.ElementTree as etree + import xml.etree.ElementPath as _elementpath + + test_cache = unittest.skip("lxml-only")(EtreeElementPathTestCase.test_cache) + test_tokenizer = unittest.skip("lxml-only")(EtreeElementPathTestCase.test_tokenizer) + test_tokenizer_index = unittest.skip("lxml-only")(EtreeElementPathTestCase.test_tokenizer_index) + + +class EtreeElementPathEmptyNamespacesTestCase(EtreeElementPathTestCase): + _empty_namespaces = {} # empty dict as opposed to None + + +class EtreeElementPathNonEmptyNamespacesTestCase(EtreeElementPathTestCase): + _empty_namespaces = {'unrelated_prefix': 'unrelated_namespace'} # non-empty but unused dict def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(EtreeElementPathTestCase)]) - #suite.addTests([unittest.makeSuite(ElementTreeElementPathTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(EtreeElementPathTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ElementTreeElementPathTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(EtreeElementPathEmptyNamespacesTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(EtreeElementPathNonEmptyNamespacesTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ElementFindTest)]) return suite diff --git a/src/lxml/tests/test_elementtree.py b/src/lxml/tests/test_elementtree.py index 96426cba5..ed1c1b58b 100644 --- a/src/lxml/tests/test_elementtree.py +++ b/src/lxml/tests/test_elementtree.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ Tests for the ElementTree API @@ -8,36 +6,29 @@ for IO related test cases. """ -from __future__ import absolute_import - import copy import io import operator import os +import pyexpat import re import sys import textwrap +import types import unittest from contextlib import contextmanager from functools import wraps, partial from itertools import islice from .common_imports import ( - BytesIO, etree, HelperTestCase, - ElementTree, cElementTree, ET_VERSION, CET_VERSION, + BytesIO, etree, HelperTestCase as helper_base, + ElementTree, ET_VERSION, IS_PYPY, filter_by_version, fileInTestDir, canonicalize, tmpfile, - _str, _bytes, unicode, IS_PYTHON2 ) -if cElementTree is not None and (CET_VERSION <= (1,0,7) or sys.version_info[0] >= 3): - cElementTree = None - if ElementTree is not None: print("Comparing with ElementTree %s" % getattr(ElementTree, "VERSION", "?")) -if cElementTree is not None: - print("Comparing with cElementTree %s" % getattr(cElementTree, "VERSION", "?")) - def et_needs_pyversion(*version): def wrap(method): @@ -61,10 +52,10 @@ def testfunc(self, *args): return wrap -class _ETreeTestCaseBase(HelperTestCase): +class _ETreeTestCaseBase(helper_base): + __test__ = False etree = None required_versions_ET = {} - required_versions_cET = {} def XMLParser(self, **kwargs): try: @@ -74,12 +65,6 @@ def XMLParser(self, **kwargs): XMLParser = self.etree.TreeBuilder return XMLParser(**kwargs) - try: - HelperTestCase.assertRegex - except AttributeError: - def assertRegex(self, *args, **kwargs): - return self.assertRegexpMatches(*args, **kwargs) - @et_needs_pyversion(3, 6) def test_interface(self): # Test element tree interface. @@ -125,7 +110,7 @@ def check_element(element): check_element(element) tree = self.etree.ElementTree(element) check_element(tree.getroot()) - element = self.etree.Element(u"t\xe4g", key="value") + element = self.etree.Element("t\xe4g", key="value") tree = self.etree.ElementTree(element) # lxml and ET Py2: slightly different repr() #self.assertRegex(repr(element), r"^$") @@ -160,7 +145,7 @@ def check_method(method): # These methods return an iterable. See bug 6472. def check_iter(it): - check_method(it.next if IS_PYTHON2 else it.__next__) + check_method(it.__next__) check_iter(element.iterfind("tag")) check_iter(element.iterfind("*")) @@ -197,7 +182,7 @@ def test_simple(self): def test_weird_dict_interaction(self): root = self.etree.Element('root') self.assertEqual(root.tag, "root") - add = self.etree.ElementTree(file=BytesIO('Foo')) + add = self.etree.ElementTree(file=BytesIO(b'Foo')) self.assertEqual(add.getroot().tag, "foo") self.assertEqual(add.getroot().text, "Foo") root.append(self.etree.Element('baz')) @@ -238,7 +223,7 @@ def test_element_contains(self): def test_element_indexing_with_text(self): ElementTree = self.etree.ElementTree - f = BytesIO('TestOne') + f = BytesIO(b'TestOne') doc = ElementTree(file=f) root = doc.getroot() self.assertEqual(1, len(root)) @@ -248,7 +233,7 @@ def test_element_indexing_with_text(self): def test_element_indexing_with_text2(self): ElementTree = self.etree.ElementTree - f = BytesIO('OneTwohmThree') + f = BytesIO(b'OneTwohmThree') doc = ElementTree(file=f) root = doc.getroot() self.assertEqual(3, len(root)) @@ -259,7 +244,7 @@ def test_element_indexing_with_text2(self): def test_element_indexing_only_text(self): ElementTree = self.etree.ElementTree - f = BytesIO('Test') + f = BytesIO(b'Test') doc = ElementTree(file=f) root = doc.getroot() self.assertEqual(0, len(root)) @@ -283,7 +268,7 @@ def test_element_indexing_negative(self): def test_elementtree(self): ElementTree = self.etree.ElementTree - f = BytesIO('OneTwo') + f = BytesIO(b'OneTwo') doc = ElementTree(file=f) root = doc.getroot() self.assertEqual(2, len(root)) @@ -293,7 +278,7 @@ def test_elementtree(self): def test_text(self): ElementTree = self.etree.ElementTree - f = BytesIO('This is a text') + f = BytesIO(b'This is a text') doc = ElementTree(file=f) root = doc.getroot() self.assertEqual('This is a text', root.text) @@ -301,7 +286,7 @@ def test_text(self): def test_text_empty(self): ElementTree = self.etree.ElementTree - f = BytesIO('') + f = BytesIO(b'') doc = ElementTree(file=f) root = doc.getroot() self.assertEqual(None, root.text) @@ -309,7 +294,7 @@ def test_text_empty(self): def test_text_other(self): ElementTree = self.etree.ElementTree - f = BytesIO('One') + f = BytesIO(b'One') doc = ElementTree(file=f) root = doc.getroot() self.assertEqual(None, root.text) @@ -318,7 +303,7 @@ def test_text_other(self): def test_text_escape_in(self): ElementTree = self.etree.ElementTree - f = BytesIO('This is > than a text') + f = BytesIO(b'This is > than a text') doc = ElementTree(file=f) root = doc.getroot() self.assertEqual('This is > than a text', root.text) @@ -328,7 +313,7 @@ def test_text_escape_out(self): a = Element("a") a.text = "<>&" - self.assertXML(_bytes('<>&'), + self.assertXML(b'<>&', a) def test_text_escape_tostring(self): @@ -337,7 +322,7 @@ def test_text_escape_tostring(self): a = Element("a") a.text = "<>&" - self.assertEqual(_bytes('<>&'), + self.assertEqual(b'<>&', tostring(a)) def test_text_str_subclass(self): @@ -348,13 +333,13 @@ class strTest(str): a = Element("a") a.text = strTest("text") - self.assertXML(_bytes('text'), + self.assertXML(b'text', a) def test_tail(self): ElementTree = self.etree.ElementTree - f = BytesIO('This is mixed content.') + f = BytesIO(b'This is mixed content.') doc = ElementTree(file=f) root = doc.getroot() self.assertEqual(1, len(root)) @@ -372,14 +357,14 @@ class strTest(str): a = Element("a") SubElement(a, "t").tail = strTest("tail") - self.assertXML(_bytes('tail'), + self.assertXML(b'tail', a) def _test_del_tail(self): # this is discouraged for ET compat, should not be tested... XML = self.etree.XML - root = XML(_bytes('This is mixed content.')) + root = XML(b'This is mixed content.') self.assertEqual(1, len(root)) self.assertEqual('This is ', root.text) self.assertEqual(None, root.tail) @@ -415,7 +400,7 @@ def test_ElementTree(self): def test_attrib(self): ElementTree = self.etree.ElementTree - f = BytesIO('') + f = BytesIO(b'') doc = ElementTree(file=f) root = doc.getroot() self.assertEqual('One', root.attrib['one']) @@ -425,7 +410,7 @@ def test_attrib(self): def test_attrib_get(self): ElementTree = self.etree.ElementTree - f = BytesIO('') + f = BytesIO(b'') doc = ElementTree(file=f) root = doc.getroot() self.assertEqual('One', root.attrib.get('one')) @@ -436,7 +421,7 @@ def test_attrib_get(self): def test_attrib_dict(self): ElementTree = self.etree.ElementTree - f = BytesIO('') + f = BytesIO(b'') doc = ElementTree(file=f) root = doc.getroot() attrib = dict(root.attrib) @@ -447,7 +432,7 @@ def test_attrib_dict(self): def test_attrib_copy(self): ElementTree = self.etree.ElementTree - f = BytesIO('') + f = BytesIO(b'') doc = ElementTree(file=f) root = doc.getroot() attrib = copy.copy(root.attrib) @@ -458,7 +443,7 @@ def test_attrib_copy(self): def test_attrib_deepcopy(self): ElementTree = self.etree.ElementTree - f = BytesIO('') + f = BytesIO(b'') doc = ElementTree(file=f) root = doc.getroot() attrib = copy.deepcopy(root.attrib) @@ -469,7 +454,7 @@ def test_attrib_deepcopy(self): def test_attributes_get(self): ElementTree = self.etree.ElementTree - f = BytesIO('') + f = BytesIO(b'') doc = ElementTree(file=f) root = doc.getroot() self.assertEqual('One', root.get('one')) @@ -480,7 +465,7 @@ def test_attributes_get(self): def test_attrib_clear(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') self.assertEqual('One', root.get('one')) self.assertEqual('Two', root.get('two')) root.attrib.clear() @@ -519,7 +504,7 @@ def test_attrib_ns_clear(self): def test_attrib_pop(self): ElementTree = self.etree.ElementTree - f = BytesIO('') + f = BytesIO(b'') doc = ElementTree(file=f) root = doc.getroot() self.assertEqual('One', root.attrib['one']) @@ -531,28 +516,28 @@ def test_attrib_pop(self): self.assertEqual('Two', root.attrib['two']) def test_attrib_pop_unknown(self): - root = self.etree.XML(_bytes('')) + root = self.etree.XML(b'') self.assertRaises(KeyError, root.attrib.pop, 'NONE') self.assertEqual('One', root.attrib['one']) self.assertEqual('Two', root.attrib['two']) def test_attrib_pop_default(self): - root = self.etree.XML(_bytes('')) + root = self.etree.XML(b'') self.assertEqual('Three', root.attrib.pop('three', 'Three')) def test_attrib_pop_empty_default(self): - root = self.etree.XML(_bytes('')) + root = self.etree.XML(b'') self.assertEqual('Three', root.attrib.pop('three', 'Three')) def test_attrib_pop_invalid_args(self): - root = self.etree.XML(_bytes('')) + root = self.etree.XML(b'') self.assertRaises(TypeError, root.attrib.pop, 'One', None, None) def test_attribute_update_dict(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') items = list(root.attrib.items()) items.sort() self.assertEqual( @@ -570,7 +555,7 @@ def test_attribute_update_dict(self): def test_attribute_update_sequence(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') items = list(root.attrib.items()) items.sort() self.assertEqual( @@ -588,7 +573,7 @@ def test_attribute_update_sequence(self): def test_attribute_update_iter(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') items = list(root.attrib.items()) items.sort() self.assertEqual( @@ -606,14 +591,14 @@ def test_attribute_update_iter(self): def test_attribute_update_attrib(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') items = list(root.attrib.items()) items.sort() self.assertEqual( [('alpha', 'Alpha'), ('beta', 'Beta')], items) - other = XML(_bytes('')) + other = XML(b'') root.attrib.update(other.attrib) items = list(root.attrib.items()) @@ -625,7 +610,7 @@ def test_attribute_update_attrib(self): def test_attribute_keys(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') keys = list(root.attrib.keys()) keys.sort() self.assertEqual(['alpha', 'beta', 'gamma'], keys) @@ -633,7 +618,7 @@ def test_attribute_keys(self): def test_attribute_keys2(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') keys = list(root.keys()) keys.sort() self.assertEqual(['alpha', 'beta', 'gamma'], keys) @@ -641,7 +626,7 @@ def test_attribute_keys2(self): def test_attribute_items2(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') items = list(root.items()) items.sort() self.assertEqual( @@ -651,7 +636,7 @@ def test_attribute_items2(self): def test_attribute_keys_ns(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') keys = list(root.keys()) keys.sort() self.assertEqual(['bar', '{http://ns.codespeak.net/test}baz'], @@ -660,7 +645,7 @@ def test_attribute_keys_ns(self): def test_attribute_values(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') values = list(root.attrib.values()) values.sort() self.assertEqual(['Alpha', 'Beta', 'Gamma'], values) @@ -668,7 +653,7 @@ def test_attribute_values(self): def test_attribute_values_ns(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') values = list(root.attrib.values()) values.sort() self.assertEqual( @@ -677,20 +662,20 @@ def test_attribute_values_ns(self): def test_attribute_items(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') items = list(root.attrib.items()) items.sort() self.assertEqual([ ('alpha', 'Alpha'), ('beta', 'Beta'), ('gamma', 'Gamma'), - ], + ], items) def test_attribute_items_ns(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') items = list(root.attrib.items()) items.sort() self.assertEqual( @@ -703,7 +688,7 @@ def test_attribute_str(self): expected = "{'{http://ns.codespeak.net/test}baz': 'Baz', 'bar': 'Bar'}" alternative = "{'bar': 'Bar', '{http://ns.codespeak.net/test}baz': 'Baz'}" - root = XML(_bytes('')) + root = XML(b'') try: self.assertEqual(expected, str(root.attrib)) except AssertionError: @@ -712,7 +697,7 @@ def test_attribute_str(self): def test_attribute_contains(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') self.assertEqual( True, 'bar' in root.attrib) self.assertEqual( @@ -743,7 +728,7 @@ def test_attrib_as_attrib(self): def test_attribute_iterator(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') result = [] for key in root.attrib: result.append(key) @@ -779,7 +764,7 @@ def test_del_attribute_ns(self): def test_del_attribute_ns_parsed(self): XML = self.etree.XML - a = XML(_bytes('')) + a = XML(b'') self.assertEqual('Foo', a.attrib['foo']) self.assertEqual('FooNS', a.attrib['{http://a/}foo']) @@ -793,7 +778,7 @@ def test_del_attribute_ns_parsed(self): self.assertRaises(KeyError, operator.getitem, a.attrib, '{http://a/}foo') self.assertRaises(KeyError, operator.getitem, a.attrib, 'foo') - a = XML(_bytes('')) + a = XML(b'') self.assertEqual('Foo', a.attrib['foo']) self.assertEqual('FooNS', a.attrib['{http://a/}foo']) @@ -809,14 +794,14 @@ def test_del_attribute_ns_parsed(self): def test_XML(self): XML = self.etree.XML - root = XML(_bytes('This is a text.')) + root = XML(b'This is a text.') self.assertEqual(0, len(root)) self.assertEqual('This is a text.', root.text) def test_XMLID(self): XMLID = self.etree.XMLID XML = self.etree.XML - xml_text = _bytes(''' + xml_text = b'''

...

...

@@ -824,7 +809,7 @@ def test_XMLID(self):

XML:ID paragraph.

...

- ''') + ''' root, dic = XMLID(xml_text) root2 = XML(xml_text) @@ -844,6 +829,31 @@ def test_fromstring(self): self.assertEqual(0, len(root)) self.assertEqual('This is a text.', root.text) + def test_fromstring_memoryview(self): + fromstring = self.etree.fromstring + + root = fromstring(memoryview(b'This is a text.')) + self.assertEqual(0, len(root)) + self.assertEqual('This is a text.', root.text) + + def test_fromstring_char_array(self): + fromstring = self.etree.fromstring + + import array + + root = fromstring(array.array('B', b'This is a text.')) + self.assertEqual(0, len(root)) + self.assertEqual('This is a text.', root.text) + + def test_fromstring_uchar_array(self): + fromstring = self.etree.fromstring + + import array + + root = fromstring(array.array('b', b'This is a text.')) + self.assertEqual(0, len(root)) + self.assertEqual('This is a text.', root.text) + required_versions_ET['test_fromstringlist'] = (1,3) def test_fromstringlist(self): fromstringlist = self.etree.fromstringlist @@ -880,7 +890,7 @@ def test_iselement(self): el = Element('hoi') self.assertTrue(iselement(el)) - el2 = XML(_bytes('')) + el2 = XML(b'') self.assertTrue(iselement(el2)) tree = ElementTree(element=Element('dag')) @@ -896,7 +906,7 @@ def test_iselement(self): def test_iteration(self): XML = self.etree.XML - root = XML(_bytes('TwoHm')) + root = XML(b'TwoHm') result = [] for el in root: result.append(el.tag) @@ -905,7 +915,7 @@ def test_iteration(self): def test_iteration_empty(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') result = [] for el in root: result.append(el.tag) @@ -914,7 +924,7 @@ def test_iteration_empty(self): def test_iteration_text_only(self): XML = self.etree.XML - root = XML(_bytes('Text')) + root = XML(b'Text') result = [] for el in root: result.append(el.tag) @@ -936,7 +946,7 @@ def test_iteration_clear_tail(self): def test_iteration_reversed(self): XML = self.etree.XML - root = XML(_bytes('TwoHm')) + root = XML(b'TwoHm') result = [] for el in reversed(root): result.append(el.tag) @@ -945,7 +955,7 @@ def test_iteration_reversed(self): def test_iteration_subelement(self): XML = self.etree.XML - root = XML(_bytes('TwoHm')) + root = XML(b'TwoHm') result = [] add = True for el in root: @@ -958,7 +968,7 @@ def test_iteration_subelement(self): def test_iteration_del_child(self): XML = self.etree.XML - root = XML(_bytes('TwoHm')) + root = XML(b'TwoHm') result = [] for el in root: result.append(el.tag) @@ -968,7 +978,7 @@ def test_iteration_del_child(self): def test_iteration_double(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') result = [] for el0 in root: result.append(el0.tag) @@ -980,7 +990,7 @@ def test_iteration_double(self): def test_itertext(self): # ET 1.3+ XML = self.etree.XML - root = XML(_bytes("RTEXT
ATAILCTEXTCTAIL
")) + root = XML(b"RTEXTATAILCTEXTCTAIL") text = list(root.itertext()) self.assertEqual(["RTEXT", "ATAIL", "CTEXT", "CTAIL"], @@ -990,7 +1000,7 @@ def test_itertext(self): def test_itertext_child(self): # ET 1.3+ XML = self.etree.XML - root = XML(_bytes("RTEXTATAILCTEXTCTAIL")) + root = XML(b"RTEXTATAILCTEXTCTAIL") text = list(root[2].itertext()) self.assertEqual(["CTEXT"], @@ -998,7 +1008,7 @@ def test_itertext_child(self): def test_findall(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') self.assertEqual(len(list(root.findall("c"))), 1) self.assertEqual(len(list(root.findall(".//c"))), 2) self.assertEqual(len(list(root.findall(".//b"))), 3) @@ -1008,7 +1018,7 @@ def test_findall(self): def test_findall_ns(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') self.assertEqual(len(list(root.findall(".//{X}b"))), 2) self.assertEqual(len(list(root.findall(".//b"))), 3) self.assertEqual(len(list(root.findall("b"))), 2) @@ -1118,13 +1128,13 @@ def test_write(self): XML = self.etree.XML for i in range(10): - f = BytesIO() - root = XML(_bytes('This is a test.' % (i, i))) + f = BytesIO() + root = XML(b'This is a test.' % (i, i)) tree = ElementTree(element=root) tree.write(f) data = f.getvalue() self.assertEqual( - _bytes('This is a test.' % (i, i)), + b'This is a test.' % (i, i), canonicalize(data)) required_versions_ET['test_write_method_html'] = (1,3) @@ -1140,11 +1150,11 @@ def test_write_method_html(self): SubElement(p, 'br').tail = "test" tree = ElementTree(element=html) - f = BytesIO() + f = BytesIO() tree.write(f, method="html") - data = f.getvalue().replace(_bytes('\n'),_bytes('')) + data = f.getvalue().replace(b'\n',b'') - self.assertEqual(_bytes('

html
test

'), + self.assertEqual(b'

html
test

', data) required_versions_ET['test_write_method_text'] = (1,3) @@ -1163,18 +1173,18 @@ def test_write_method_text(self): c.text = "C" tree = ElementTree(element=a) - f = BytesIO() + f = BytesIO() tree.write(f, method="text") data = f.getvalue() - self.assertEqual(_bytes('ABTAILCtail'), + self.assertEqual(b'ABTAILCtail', data) def test_write_fail(self): ElementTree = self.etree.ElementTree XML = self.etree.XML - tree = ElementTree( XML(_bytes('This is a test.')) ) + tree = ElementTree( XML(b'This is a test.') ) self.assertRaises(IOError, tree.write, "definitely////\\-\\nonexisting\\-\\////FILE") @@ -1264,7 +1274,7 @@ def test_set_text_none(self): self.assertEqual( None, a.text) - self.assertXML(_bytes(''), a) + self.assertXML(b'', a) def test_set_text_empty(self): Element = self.etree.Element @@ -1274,7 +1284,7 @@ def test_set_text_empty(self): a.text = '' self.assertEqual('', a.text) - self.assertXML(_bytes(''), a) + self.assertXML(b'', a) def test_tail1(self): Element = self.etree.Element @@ -1311,7 +1321,7 @@ def test_tail_set_twice(self): b.tail = 'bar' self.assertEqual('bar', b.tail) - self.assertXML(_bytes('bar'), a) + self.assertXML(b'bar', a) def test_tail_set_none(self): Element = self.etree.Element @@ -1321,7 +1331,7 @@ def test_tail_set_none(self): self.assertEqual( None, a.tail) - self.assertXML(_bytes(''), a) + self.assertXML(b'', a) required_versions_ET['test_extend'] = (1,3) def test_extend(self): @@ -1373,14 +1383,14 @@ def test_comment_text(self): self.assertEqual(a[0].text, 'foo') self.assertEqual( - _bytes(''), + b'', tostring(a)) a[0].text = "TEST" self.assertEqual(a[0].text, 'TEST') self.assertEqual( - _bytes(''), + b'', tostring(a)) # ElementTree < 1.3 adds whitespace around comments @@ -1395,7 +1405,7 @@ def test_comment_whitespace(self): a.append(Comment(' foo ')) self.assertEqual(a[0].text, ' foo ') self.assertEqual( - _bytes(''), + b'', tostring(a)) def test_comment_nonsense(self): @@ -1419,7 +1429,7 @@ def test_pi(self): a = Element('a') a.append(ProcessingInstruction('foo', 'some more text')) self.assertEqual(a[0].tag, ProcessingInstruction) - self.assertXML(_bytes(""), + self.assertXML(b"", a) def test_processinginstruction(self): @@ -1431,7 +1441,7 @@ def test_processinginstruction(self): a = Element('a') a.append(ProcessingInstruction('foo', 'some more text')) self.assertEqual(a[0].tag, ProcessingInstruction) - self.assertXML(_bytes(""), + self.assertXML(b"", a) def test_pi_nonsense(self): @@ -1457,9 +1467,9 @@ def test_setitem(self): self.assertEqual( c, a[0]) - self.assertXML(_bytes(''), + self.assertXML(b'', a) - self.assertXML(_bytes(''), + self.assertXML(b'', b) def test_setitem2(self): @@ -1475,9 +1485,9 @@ def test_setitem2(self): e = SubElement(d, 'e') a[i] = d self.assertXML( - _bytes(''), + b'', a) - self.assertXML(_bytes(''), + self.assertXML(b'', c) def test_setitem_replace(self): @@ -1488,7 +1498,7 @@ def test_setitem_replace(self): SubElement(a, 'b') d = Element('d') a[0] = d - self.assertXML(_bytes(''), a) + self.assertXML(b'', a) def test_setitem_indexerror(self): Element = self.etree.Element @@ -1511,7 +1521,7 @@ def test_setitem_tail(self): a[0] = c self.assertXML( - _bytes('C2'), + b'C2', a) def test_tag_write(self): @@ -1528,7 +1538,7 @@ def test_tag_write(self): a.tag) self.assertXML( - _bytes(''), + b'', a) def test_tag_reset_ns(self): @@ -1546,8 +1556,8 @@ def test_tag_reset_ns(self): # can't use C14N here! self.assertEqual('c', b1.tag) - self.assertEqual(_bytes('
'), + self.assertXML(b'', a) def test_delitem(self): @@ -1590,23 +1600,23 @@ def test_delitem(self): del a[1] self.assertXML( - _bytes(''), + b'', a) del a[0] self.assertXML( - _bytes(''), + b'', a) del a[0] self.assertXML( - _bytes(''), + b'', a) # move deleted element into other tree afterwards other = Element('other') other.append(c) self.assertXML( - _bytes(''), + b'', other) def test_del_insert(self): @@ -1621,24 +1631,24 @@ def test_del_insert(self): el = a[0] self.assertXML( - _bytes(''), + b'', a) - self.assertXML(_bytes(''), b) - self.assertXML(_bytes(''), c) + self.assertXML(b'', b) + self.assertXML(b'', c) del a[0] self.assertXML( - _bytes(''), + b'', a) - self.assertXML(_bytes(''), b) - self.assertXML(_bytes(''), c) + self.assertXML(b'', b) + self.assertXML(b'', c) a.insert(0, el) self.assertXML( - _bytes(''), + b'', a) - self.assertXML(_bytes(''), b) - self.assertXML(_bytes(''), c) + self.assertXML(b'', b) + self.assertXML(b'', c) def test_del_setitem(self): Element = self.etree.Element @@ -1654,10 +1664,10 @@ def test_del_setitem(self): del a[0] a[0] = el self.assertXML( - _bytes(''), + b'', a) - self.assertXML(_bytes(''), b) - self.assertXML(_bytes(''), c) + self.assertXML(b'', b) + self.assertXML(b'', c) def test_del_setslice(self): Element = self.etree.Element @@ -1673,14 +1683,14 @@ def test_del_setslice(self): del a[0] a[0:0] = [el] self.assertXML( - _bytes(''), + b'', a) - self.assertXML(_bytes(''), b) - self.assertXML(_bytes(''), c) + self.assertXML(b'', b) + self.assertXML(b'', c) def test_replace_slice_tail(self): XML = self.etree.XML - a = XML(_bytes('B2C2')) + a = XML(b'B2C2') b, c = a a[:] = [] @@ -1690,8 +1700,8 @@ def test_replace_slice_tail(self): def test_merge_namespaced_subtree_as_slice(self): XML = self.etree.XML - root = XML(_bytes( - '')) + root = XML( + b'') root[:] = root.findall('.//puh') # delete bar from hierarchy # previously, this lost a namespace declaration on bump2 @@ -1704,23 +1714,23 @@ def test_merge_namespaced_subtree_as_slice(self): def test_delitem_tail_dealloc(self): ElementTree = self.etree.ElementTree - f = BytesIO('B2C2') + f = BytesIO(b'B2C2') doc = ElementTree(file=f) a = doc.getroot() del a[0] self.assertXML( - _bytes('C2'), + b'C2', a) def test_delitem_tail(self): ElementTree = self.etree.ElementTree - f = BytesIO('B2C2') + f = BytesIO(b'B2C2') doc = ElementTree(file=f) a = doc.getroot() b, c = a del a[0] self.assertXML( - _bytes('C2'), + b'C2', a) self.assertEqual("B2", b.tail) self.assertEqual("C2", c.tail) @@ -1754,19 +1764,19 @@ def test_clear_sub(self): self.assertEqual(None, a.get('hoi')) self.assertEqual('a', a.tag) self.assertEqual(0, len(a)) - self.assertXML(_bytes(''), + self.assertXML(b'', a) - self.assertXML(_bytes(''), + self.assertXML(b'', b) def test_clear_tail(self): ElementTree = self.etree.ElementTree - f = BytesIO('B2C2') + f = BytesIO(b'B2C2') doc = ElementTree(file=f) a = doc.getroot() a.clear() self.assertXML( - _bytes(''), + b'', a) def test_insert(self): @@ -1784,7 +1794,7 @@ def test_insert(self): a[0]) self.assertXML( - _bytes(''), + b'', a) e = Element('e') @@ -1793,7 +1803,7 @@ def test_insert(self): e, a[2]) self.assertXML( - _bytes(''), + b'', a) def test_insert_name_interning(self): @@ -1803,7 +1813,7 @@ def test_insert_name_interning(self): # Use unique names to make sure they are new in the tag name dict. import uuid - names = dict((k, 'tag-' + str(uuid.uuid4())) for k in 'abcde') + names = {k: f'tag-{uuid.uuid4()}' for k in 'abcde'} a = Element(names['a']) b = SubElement(a, names['b']) @@ -1816,7 +1826,7 @@ def test_insert_name_interning(self): a[0]) self.assertXML( - _bytes('<%(a)s><%(d)s><%(b)s><%(c)s>' % names), + ('<%(a)s><%(d)s><%(b)s><%(c)s>' % names).encode('utf-8'), a) e = Element(names['e']) @@ -1825,7 +1835,7 @@ def test_insert_name_interning(self): e, a[2]) self.assertXML( - _bytes('<%(a)s><%(d)s><%(b)s><%(e)s><%(c)s>' % names), + ('<%(a)s><%(d)s><%(b)s><%(e)s><%(c)s>' % names).encode('utf-8'), a) def test_insert_beyond_index(self): @@ -1841,7 +1851,7 @@ def test_insert_beyond_index(self): c, a[1]) self.assertXML( - _bytes(''), + b'', a) def test_insert_negative(self): @@ -1858,7 +1868,7 @@ def test_insert_negative(self): d, a[-2]) self.assertXML( - _bytes(''), + b'', a) def test_insert_tail(self): @@ -1873,7 +1883,7 @@ def test_insert_tail(self): a.insert(0, c) self.assertXML( - _bytes('C2'), + b'C2', a) def test_remove(self): @@ -1889,7 +1899,7 @@ def test_remove(self): c, a[0]) self.assertXML( - _bytes(''), + b'', a) def test_remove_ns(self): @@ -1902,10 +1912,10 @@ def test_remove_ns(self): a.remove(b) self.assertXML( - _bytes(''), + b'', a) self.assertXML( - _bytes(''), + b'', b) def test_remove_nonexisting(self): @@ -1928,7 +1938,7 @@ def test_remove_tail(self): b.tail = 'b2' a.remove(b) self.assertXML( - _bytes(''), + b'', a) self.assertEqual('b2', b.tail) @@ -1952,7 +1962,7 @@ def test_makeelement(self): a = Element('a') b = a.makeelement('c', {'hoi':'dag'}) self.assertXML( - _bytes(''), + b'', b) required_versions_ET['test_iter'] = (1,3) @@ -2080,7 +2090,7 @@ def test_getslice_step(self): def test_getslice_text(self): ElementTree = self.etree.ElementTree - f = BytesIO('BB1CC1') + f = BytesIO(b'BB1CC1') doc = ElementTree(file=f) a = doc.getroot() b = a[0] @@ -2116,7 +2126,7 @@ def test_comment_getitem_getslice(self): new, a[1]) self.assertXML( - _bytes(''), + b'', a) def test_delslice(self): @@ -2211,23 +2221,23 @@ def test_delslice_step_negative2(self): def test_delslice_child_tail_dealloc(self): ElementTree = self.etree.ElementTree - f = BytesIO('B2C2D2E2') + f = BytesIO(b'B2C2D2E2') doc = ElementTree(file=f) a = doc.getroot() del a[1:3] self.assertXML( - _bytes('B2E2'), + b'B2E2', a) def test_delslice_child_tail(self): ElementTree = self.etree.ElementTree - f = BytesIO('B2C2D2E2') + f = BytesIO(b'B2C2D2E2') doc = ElementTree(file=f) a = doc.getroot() b, c, d, e = a del a[1:3] self.assertXML( - _bytes('B2E2'), + b'B2E2', a) self.assertEqual("B2", b.tail) self.assertEqual("C2", c.tail) @@ -2236,7 +2246,7 @@ def test_delslice_child_tail(self): def test_delslice_tail(self): XML = self.etree.XML - a = XML(_bytes('B2C2')) + a = XML(b'B2C2') b, c = a del a[:] @@ -2452,7 +2462,7 @@ def test_setslice_single(self): def test_setslice_tail(self): ElementTree = self.etree.ElementTree Element = self.etree.Element - f = BytesIO('B2C2D2E2') + f = BytesIO(b'B2C2D2E2') doc = ElementTree(file=f) a = doc.getroot() x = Element('x') @@ -2463,7 +2473,7 @@ def test_setslice_tail(self): z.tail = 'Z2' a[1:3] = [x, y, z] self.assertXML( - _bytes('B2X2Y2Z2E2'), + b'B2X2Y2Z2E2', a) def test_setslice_negative(self): @@ -2526,7 +2536,7 @@ def test_tail_elementtree_root(self): def test_ns_access(self): ElementTree = self.etree.ElementTree ns = 'http://xml.infrae.com/1' - f = BytesIO('' % ns) + f = BytesIO(('' % ns).encode('utf-8')) t = ElementTree(file=f) a = t.getroot() self.assertEqual('{%s}a' % ns, @@ -2538,7 +2548,7 @@ def test_ns_access2(self): ElementTree = self.etree.ElementTree ns = 'http://xml.infrae.com/1' ns2 = 'http://xml.infrae.com/2' - f = BytesIO('' % (ns, ns2)) + f = BytesIO(('' % (ns, ns2)).encode('utf-8')) t = ElementTree(file=f) a = t.getroot() self.assertEqual('{%s}a' % ns, @@ -2570,13 +2580,11 @@ def test_ns_setting(self): c.tag) def test_ns_tag_parse(self): - Element = self.etree.Element - SubElement = self.etree.SubElement ElementTree = self.etree.ElementTree ns = 'http://xml.infrae.com/1' ns2 = 'http://xml.infrae.com/2' - f = BytesIO('' % (ns, ns2)) + f = BytesIO(('' % (ns, ns2)).encode('utf-8')) t = ElementTree(file=f) a = t.getroot() @@ -2602,17 +2610,17 @@ def test_ns_attr(self): a.get('{%s}bar' % ns2)) try: self.assertXML( - _bytes('' % (ns, ns2)), + ('' % (ns, ns2)).encode('utf-8'), a) except AssertionError: self.assertXML( - _bytes('' % (ns2, ns)), + ('' % (ns2, ns)).encode('utf-8'), a) def test_ns_move(self): Element = self.etree.Element one = self.etree.fromstring( - _bytes('')) + b'') baz = one[0][0] two = Element('root') @@ -2625,33 +2633,33 @@ def test_ns_move(self): def test_ns_decl_tostring(self): tostring = self.etree.tostring root = self.etree.XML( - _bytes('')) + b'') baz = root[0][0] - nsdecl = re.findall(_bytes("xmlns(?::[a-z0-9]+)?=[\"']([^\"']+)[\"']"), + nsdecl = re.findall(b"xmlns(?::[a-z0-9]+)?=[\"']([^\"']+)[\"']", tostring(baz)) - self.assertEqual([_bytes("http://a.b.c")], nsdecl) + self.assertEqual([b"http://a.b.c"], nsdecl) def test_ns_decl_tostring_default(self): tostring = self.etree.tostring root = self.etree.XML( - _bytes('')) + b'') baz = root[0][0] - nsdecl = re.findall(_bytes("xmlns(?::[a-z0-9]+)?=[\"']([^\"']+)[\"']"), + nsdecl = re.findall(b"xmlns(?::[a-z0-9]+)?=[\"']([^\"']+)[\"']", tostring(baz)) - self.assertEqual([_bytes("http://a.b.c")], nsdecl) + self.assertEqual([b"http://a.b.c"], nsdecl) def test_ns_decl_tostring_root(self): tostring = self.etree.tostring root = self.etree.XML( - _bytes('')) + b'') baz = root[0][0] - nsdecl = re.findall(_bytes("xmlns(?::[a-z0-9]+)?=[\"']([^\"']+)[\"']"), + nsdecl = re.findall(b"xmlns(?::[a-z0-9]+)?=[\"']([^\"']+)[\"']", tostring(baz)) - self.assertEqual([_bytes("http://a.b.c")], nsdecl) + self.assertEqual([b"http://a.b.c"], nsdecl) def test_ns_decl_tostring_element(self): Element = self.etree.Element @@ -2661,10 +2669,10 @@ def test_ns_decl_tostring_element(self): bar = SubElement(root, "{http://a.b.c}bar") baz = SubElement(bar, "{http://a.b.c}baz") - nsdecl = re.findall(_bytes("xmlns(?::[a-z0-9]+)?=[\"']([^\"']+)[\"']"), + nsdecl = re.findall(b"xmlns(?::[a-z0-9]+)?=[\"']([^\"']+)[\"']", self.etree.tostring(baz)) - self.assertEqual([_bytes("http://a.b.c")], nsdecl) + self.assertEqual([b"http://a.b.c"], nsdecl) def test_attribute_xmlns_move(self): Element = self.etree.Element @@ -2693,7 +2701,7 @@ def test_namespaces_after_serialize(self): ns_href = "http://a.b.c" one = parse( - BytesIO('' % ns_href)) + BytesIO(('' % ns_href).encode('utf-8'))) baz = one.getroot()[0][0] parsed = parse(BytesIO( tostring(baz) )).getroot() @@ -2704,13 +2712,13 @@ def test_attribute_namespace_roundtrip(self): tostring = self.etree.tostring ns_href = "http://a.b.c" - xml = _bytes('' % ( - ns_href,ns_href)) + xml = '' % ( + ns_href, ns_href) root = fromstring(xml) self.assertEqual('test', root[0].get('{%s}a' % ns_href)) xml2 = tostring(root) - self.assertTrue(_bytes(':a=') in xml2, xml2) + self.assertTrue(b':a=' in xml2, xml2) root2 = fromstring(xml2) self.assertEqual('test', root2[0].get('{%s}a' % ns_href)) @@ -2720,15 +2728,15 @@ def test_attribute_namespace_roundtrip_replaced(self): tostring = self.etree.tostring ns_href = "http://a.b.c" - xml = _bytes('' % ( - ns_href,ns_href)) + xml = '' % ( + ns_href, ns_href) root = fromstring(xml) self.assertEqual('test', root[0].get('{%s}a' % ns_href)) root[0].set('{%s}a' % ns_href, 'TEST') xml2 = tostring(root) - self.assertTrue(_bytes(':a=') in xml2, xml2) + self.assertTrue(b':a=' in xml2, xml2) root2 = fromstring(xml2) self.assertEqual('TEST', root2[0].get('{%s}a' % ns_href)) @@ -2741,14 +2749,15 @@ def test_register_namespace(self): namespace = 'http://seriously.unknown/namespace/URI' el = Element('{%s}test' % namespace) - self.assertEqual(_bytes('' % namespace), - self._writeElement(el)) + self.assertEqual( + '' % namespace, + self._writeElement(el).decode()) self.etree.register_namespace(prefix, namespace) el = Element('{%s}test' % namespace) - self.assertEqual(_bytes('<%s:test xmlns:%s="%s">' % ( - prefix, prefix, namespace, prefix)), - self._writeElement(el)) + self.assertEqual('<%s:test xmlns:%s="%s">' % ( + prefix, prefix, namespace, prefix), + self._writeElement(el).decode()) self.assertRaises(ValueError, self.etree.register_namespace, 'ns25', namespace) @@ -2761,7 +2770,7 @@ def test_tostring(self): b = SubElement(a, 'b') c = SubElement(a, 'c') - self.assertEqual(_bytes(''), + self.assertEqual(b'', canonicalize(tostring(a))) def test_tostring_element(self): @@ -2773,9 +2782,9 @@ def test_tostring_element(self): b = SubElement(a, 'b') c = SubElement(a, 'c') d = SubElement(c, 'd') - self.assertEqual(_bytes(''), + self.assertEqual(b'', canonicalize(tostring(b))) - self.assertEqual(_bytes(''), + self.assertEqual(b'', canonicalize(tostring(c))) def test_tostring_element_tail(self): @@ -2789,8 +2798,8 @@ def test_tostring_element_tail(self): d = SubElement(c, 'd') b.tail = 'Foo' - self.assertTrue(tostring(b) == _bytes('Foo') or - tostring(b) == _bytes('Foo')) + self.assertTrue(tostring(b) == b'Foo' or + tostring(b) == b'Foo') required_versions_ET['test_tostring_method_html'] = (1,3) def test_tostring_method_html(self): @@ -2804,7 +2813,7 @@ def test_tostring_method_html(self): p.text = "html" SubElement(p, 'br').tail = "test" - self.assertEqual(_bytes('

html
test

'), + self.assertEqual(b'

html
test

', tostring(html, method="html")) required_versions_ET['test_tostring_method_text'] = (1,3) @@ -2822,12 +2831,12 @@ def test_tostring_method_text(self): c = SubElement(a, 'c') c.text = "C" - self.assertEqual(_bytes('ABTAILCtail'), + self.assertEqual(b'ABTAILCtail', tostring(a, method="text")) def test_iterparse(self): iterparse = self.etree.iterparse - f = BytesIO('') + f = BytesIO(b'') iterator = iterparse(f) self.assertEqual(None, @@ -2840,7 +2849,7 @@ def test_iterparse(self): def test_iterparse_incomplete(self): iterparse = self.etree.iterparse - f = BytesIO('') + f = BytesIO(b'') iterator = iterparse(f) self.assertEqual(None, @@ -2862,7 +2871,7 @@ def test_iterparse_file(self): def test_iterparse_start(self): iterparse = self.etree.iterparse - f = BytesIO('') + f = BytesIO(b'') iterator = iterparse(f, events=('start',)) events = list(iterator) @@ -2873,7 +2882,7 @@ def test_iterparse_start(self): def test_iterparse_start_end(self): iterparse = self.etree.iterparse - f = BytesIO('') + f = BytesIO(b'') iterator = iterparse(f, events=('start','end')) events = list(iterator) @@ -2885,7 +2894,7 @@ def test_iterparse_start_end(self): def test_iterparse_clear(self): iterparse = self.etree.iterparse - f = BytesIO('') + f = BytesIO(b'') iterator = iterparse(f) for event, elem in iterator: @@ -2898,7 +2907,7 @@ def test_iterparse_clear(self): def test_iterparse_large(self): iterparse = self.etree.iterparse CHILD_COUNT = 12345 - f = BytesIO('%s' % ('test'*CHILD_COUNT)) + f = BytesIO(b'%s' % (b'test' * CHILD_COUNT)) i = 0 for key in iterparse(f): @@ -2908,7 +2917,7 @@ def test_iterparse_large(self): def test_iterparse_set_ns_attribute(self): iterparse = self.etree.iterparse - f = BytesIO('') + f = BytesIO(b'') attr_name = '{http://testns/}bla' events = [] @@ -2934,7 +2943,7 @@ def test_iterparse_set_ns_attribute(self): def test_iterparse_only_end_ns(self): iterparse = self.etree.iterparse - f = BytesIO('') + f = BytesIO(b'') attr_name = '{http://testns/}bla' events = [] @@ -2960,7 +2969,7 @@ def test_iterparse_only_end_ns(self): def test_iterparse_move_elements(self): iterparse = self.etree.iterparse - f = BytesIO('') + f = BytesIO(b'') for event, node in etree.iterparse(f): pass @@ -2973,12 +2982,12 @@ def test_iterparse_move_elements(self): def test_iterparse_cdata(self): tostring = self.etree.tostring - f = BytesIO('') + f = BytesIO(b'') context = self.etree.iterparse(f) content = [ el.text for event,el in context ] self.assertEqual(['test'], content) - self.assertEqual(_bytes('test'), + self.assertEqual(b'test', tostring(context.root)) def test_parse_file(self): @@ -2986,12 +2995,12 @@ def test_parse_file(self): # from file tree = parse(fileInTestDir('test.xml')) self.assertXML( - _bytes(''), + b'', tree.getroot()) def test_parse_file_nonexistent(self): parse = self.etree.parse - self.assertRaises(IOError, parse, fileInTestDir('notthere.xml')) + self.assertRaises(IOError, parse, fileInTestDir('notthere.xml')) def test_parse_error_none(self): parse = self.etree.parse @@ -3001,7 +3010,7 @@ def test_parse_error_none(self): def test_parse_error(self): # ET < 1.3 raises ExpatError parse = self.etree.parse - f = BytesIO('') + f = BytesIO(b'') self.assertRaises(SyntaxError, parse, f) f.close() @@ -3020,41 +3029,41 @@ def test_parse_file_object(self): tree = parse(f) f.close() self.assertXML( - _bytes(''), + b'', tree.getroot()) def test_parse_stringio(self): parse = self.etree.parse - f = BytesIO('') + f = BytesIO(b'') tree = parse(f) f.close() self.assertXML( - _bytes(''), + b'', tree.getroot() ) def test_parse_cdata(self): tostring = self.etree.tostring - root = self.etree.XML(_bytes('')) + root = self.etree.XML(b'') self.assertEqual('test', root.text) - self.assertEqual(_bytes('test'), + self.assertEqual(b'test', tostring(root)) def test_parse_with_encoding(self): # this can fail in libxml2 <= 2.6.22 parse = self.etree.parse - tree = parse(BytesIO('')) - self.assertXML(_bytes(''), + tree = parse(BytesIO(b'')) + self.assertXML(b'', tree.getroot()) def test_encoding(self): Element = self.etree.Element a = Element('a') - a.text = _str('Søk på nettet') + a.text = 'Søk på nettet' self.assertXML( - _str('Søk på nettet').encode('UTF-8'), + 'Søk på nettet'.encode(), a, 'utf-8') def test_encoding_exact(self): @@ -3062,20 +3071,20 @@ def test_encoding_exact(self): Element = self.etree.Element a = Element('a') - a.text = _str('Søk på nettet') + a.text = 'Søk på nettet' f = BytesIO() tree = ElementTree(element=a) tree.write(f, encoding='utf-8') - self.assertEqual(_str('Søk på nettet').encode('UTF-8'), - f.getvalue().replace(_bytes('\n'),_bytes(''))) + self.assertEqual('Søk på nettet'.encode(), + f.getvalue().replace(b'\n',b'')) def test_parse_file_encoding(self): parse = self.etree.parse # from file tree = parse(fileInTestDir('test-string.xml')) self.assertXML( - _str('Søk på nettet').encode('UTF-8'), + 'Søk på nettet'.encode(), tree.getroot(), 'UTF-8') def test_parse_file_object_encoding(self): @@ -3085,7 +3094,7 @@ def test_parse_file_object_encoding(self): tree = parse(f) f.close() self.assertXML( - _str('Søk på nettet').encode('UTF-8'), + 'Søk på nettet'.encode(), tree.getroot(), 'UTF-8') def test_encoding_8bit_latin1(self): @@ -3093,24 +3102,24 @@ def test_encoding_8bit_latin1(self): Element = self.etree.Element a = Element('a') - a.text = _str('Søk på nettet') + a.text = 'Søk på nettet' f = BytesIO() tree = ElementTree(element=a) tree.write(f, encoding='iso-8859-1') result = f.getvalue() - declaration = _bytes("") - self.assertEncodingDeclaration(result, _bytes('iso-8859-1')) - result = result.split(_bytes('?>'), 1)[-1].replace(_bytes('\n'),_bytes('')) - self.assertEqual(_str('Søk på nettet').encode('iso-8859-1'), + declaration = b"" + self.assertEncodingDeclaration(result, b'iso-8859-1') + result = result.split(b'?>', 1)[-1].replace(b'\n',b'') + self.assertEqual('Søk på nettet'.encode('iso-8859-1'), result) required_versions_ET['test_parse_encoding_8bit_explicit'] = (1,3) def test_parse_encoding_8bit_explicit(self): XMLParser = self.XMLParser - text = _str('Søk på nettet') - xml_latin1 = (_str('%s') % text).encode('iso-8859-1') + text = 'Søk på nettet' + xml_latin1 = ('%s' % text).encode('iso-8859-1') self.assertRaises(self.etree.ParseError, self.etree.parse, @@ -3125,9 +3134,9 @@ def test_parse_encoding_8bit_explicit(self): def test_parse_encoding_8bit_override(self): XMLParser = self.XMLParser - text = _str('Søk på nettet') - wrong_declaration = _str("") - xml_latin1 = (_str('%s%s') % (wrong_declaration, text) + text = 'Søk på nettet' + wrong_declaration = "" + xml_latin1 = ('%s%s' % (wrong_declaration, text) ).encode('iso-8859-1') self.assertRaises(self.etree.ParseError, @@ -3142,8 +3151,8 @@ def test_parse_encoding_8bit_override(self): def _test_wrong_unicode_encoding(self): # raise error on wrong encoding declaration in unicode strings XML = self.etree.XML - test_utf = (_str('') + - _str('Søk på nettet')) + test_utf = ('' + + 'Søk på nettet') self.assertRaises(SyntaxError, XML, test_utf) def test_encoding_write_default_encoding(self): @@ -3151,14 +3160,14 @@ def test_encoding_write_default_encoding(self): Element = self.etree.Element a = Element('a') - a.text = _str('Søk på nettet') + a.text = 'Søk på nettet' f = BytesIO() tree = ElementTree(element=a) tree.write(f) - data = f.getvalue().replace(_bytes('\n'),_bytes('')) + data = f.getvalue().replace(b'\n',b'') self.assertEqual( - _str('Søk på nettet').encode('ASCII', 'xmlcharrefreplace'), + 'Søk på nettet'.encode('ASCII', 'xmlcharrefreplace'), data) def test_encoding_tostring(self): @@ -3166,8 +3175,8 @@ def test_encoding_tostring(self): tostring = self.etree.tostring a = Element('a') - a.text = _str('Søk på nettet') - self.assertEqual(_str('Søk på nettet').encode('UTF-8'), + a.text = 'Søk på nettet' + self.assertEqual('Søk på nettet'.encode(), tostring(a, encoding='utf-8')) def test_encoding_tostring_unknown(self): @@ -3175,7 +3184,7 @@ def test_encoding_tostring_unknown(self): tostring = self.etree.tostring a = Element('a') - a.text = _str('Søk på nettet') + a.text = 'Søk på nettet' self.assertRaises(LookupError, tostring, a, encoding='Invalid Encoding') @@ -3186,8 +3195,8 @@ def test_encoding_tostring_sub(self): a = Element('a') b = SubElement(a, 'b') - b.text = _str('Søk på nettet') - self.assertEqual(_str('Søk på nettet').encode('UTF-8'), + b.text = 'Søk på nettet' + self.assertEqual('Søk på nettet'.encode(), tostring(b, encoding='utf-8')) def test_encoding_tostring_sub_tail(self): @@ -3197,9 +3206,9 @@ def test_encoding_tostring_sub_tail(self): a = Element('a') b = SubElement(a, 'b') - b.text = _str('Søk på nettet') - b.tail = _str('Søk') - self.assertEqual(_str('Søk på nettetSøk').encode('UTF-8'), + b.text = 'Søk på nettet' + b.tail = 'Søk' + self.assertEqual('Søk på nettetSøk'.encode(), tostring(b, encoding='utf-8')) def test_encoding_tostring_default_encoding(self): @@ -3208,9 +3217,9 @@ def test_encoding_tostring_default_encoding(self): tostring = self.etree.tostring a = Element('a') - a.text = _str('Søk på nettet') + a.text = 'Søk på nettet' - expected = _bytes('Søk på nettet') + expected = b'Søk på nettet' self.assertEqual( expected, tostring(a)) @@ -3222,34 +3231,34 @@ def test_encoding_sub_tostring_default_encoding(self): a = Element('a') b = SubElement(a, 'b') - b.text = _str('Søk på nettet') + b.text = 'Søk på nettet' - expected = _bytes('Søk på nettet') + expected = b'Søk på nettet' self.assertEqual( expected, tostring(b)) def test_encoding_8bit_xml(self): - utext = _str('Søk på nettet') - uxml = _str('

%s

') % utext - prologue = _bytes('') + utext = 'Søk på nettet' + uxml = '

%s

' % utext + prologue = b'' isoxml = prologue + uxml.encode('iso-8859-1') tree = self.etree.XML(isoxml) self.assertEqual(utext, tree.text) def test_encoding_utf8_bom(self): - utext = _str('Søk på nettet') - uxml = (_str('') + - _str('

%s

') % utext) - bom = _bytes('\\xEF\\xBB\\xBF').decode("unicode_escape").encode("latin1") + utext = 'Søk på nettet' + uxml = ('' + + '

%s

' % utext) + bom = b'\\xEF\\xBB\\xBF'.decode("unicode_escape").encode("latin1") xml = bom + uxml.encode("utf-8") tree = etree.XML(xml) self.assertEqual(utext, tree.text) def test_encoding_8bit_parse_stringio(self): - utext = _str('Søk på nettet') - uxml = _str('

%s

') % utext - prologue = _bytes('') + utext = 'Søk på nettet' + uxml = '

%s

' % utext + prologue = b'' isoxml = prologue + uxml.encode('iso-8859-1') el = self.etree.parse(BytesIO(isoxml)).getroot() self.assertEqual(utext, el.text) @@ -3325,9 +3334,9 @@ def test_deepcopy_subelement(self): self.assertEqual('BarText', b.text) def test_deepcopy_namespaces(self): - root = self.etree.XML(_bytes(''' + root = self.etree.XML(b''' - ''')) + ''') self.assertEqual( root[0][0].get('{tns}foo'), copy.deepcopy(root[0])[0].get('{tns}foo') ) @@ -3345,10 +3354,10 @@ def test_deepcopy_append(self): a.append( Element('C') ) b.append( Element('X') ) - self.assertEqual(_bytes(''), - tostring(a).replace(_bytes(' '), _bytes(''))) - self.assertEqual(_bytes(''), - tostring(b).replace(_bytes(' '), _bytes(''))) + self.assertEqual(b'', + tostring(a).replace(b' ', b'')) + self.assertEqual(b'', + tostring(b).replace(b' ', b'')) def test_deepcopy_comment(self): # previously caused a crash @@ -3413,16 +3422,16 @@ def test_multiple_elementrees(self): b = etree.SubElement(a, 'b') t = etree.ElementTree(a) - self.assertEqual(self._rootstring(t), _bytes('')) + self.assertEqual(self._rootstring(t), b'') t1 = etree.ElementTree(a) - self.assertEqual(self._rootstring(t1), _bytes('')) - self.assertEqual(self._rootstring(t), _bytes('')) + self.assertEqual(self._rootstring(t1), b'') + self.assertEqual(self._rootstring(t), b'') t2 = etree.ElementTree(b) - self.assertEqual(self._rootstring(t2), _bytes('')) - self.assertEqual(self._rootstring(t1), _bytes('')) - self.assertEqual(self._rootstring(t), _bytes('')) + self.assertEqual(self._rootstring(t2), b'') + self.assertEqual(self._rootstring(t1), b'') + self.assertEqual(self._rootstring(t), b'') def test_qname(self): etree = self.etree @@ -3471,7 +3480,7 @@ def test_qname_attribute_resolve(self): a.set(qname, qname) self.assertXML( - _bytes(''), + b'', a) def test_qname_attribute_resolve_new(self): @@ -3481,7 +3490,7 @@ def test_qname_attribute_resolve_new(self): a.set('a', qname) self.assertXML( - _bytes(''), + b'', a) def test_qname_attrib_resolve(self): @@ -3491,7 +3500,7 @@ def test_qname_attrib_resolve(self): a.attrib[qname] = qname self.assertXML( - _bytes(''), + b'', a) def test_parser_version(self): @@ -3506,12 +3515,12 @@ def test_parser_version(self): def test_feed_parser_bytes(self): parser = self.XMLParser() - parser.feed(_bytes('<')) - parser.feed(_bytes('a test="works"/')) - parser.feed(_bytes('>')) + parser.feed(b'<') + parser.feed(b'a test="works"/') + parser.feed(b'>') root = parser.close() @@ -3522,12 +3531,12 @@ def test_feed_parser_bytes(self): def test_feed_parser_unicode_ascii(self): parser = self.XMLParser() - parser.feed(_bytes(u'<')) - parser.feed(_bytes(u'a test="works"/')) - parser.feed(_bytes(u'>')) + parser.feed('<') + parser.feed('a test="works"/') + parser.feed('>') root = parser.close() @@ -3539,44 +3548,43 @@ def test_feed_parser_unicode_ascii(self): def test_feed_parser_unicode_astral(self): parser = self.XMLParser() - astral_chunk = u'-- \U00010143 --' # astral (4 bytes/chr) - latin1_chunk = u'-- \xf8 --' # Latin1 (1 byte/chr) + astral_chunk = '-- \U00010143 --' # astral (4 bytes/chr) + latin1_chunk = '-- \xf8 --' # Latin1 (1 byte/chr) - parser.feed(u'<') - parser.feed(u'a test="w\N{DIAMETER SIGN}rks">') # BMP (2 bytes/chr) + parser.feed('<') + parser.feed('a test="w\N{DIAMETER SIGN}rks">') # BMP (2 bytes/chr) parser.feed(astral_chunk) parser.feed(latin1_chunk) - parser.feed(u'') + parser.feed('') root = parser.close() self.assertEqual(root.tag, "root") self.assertEqual(root[0].tag, "a") - self.assertEqual(root[0].get("test"), u"w\N{DIAMETER SIGN}rks") + self.assertEqual(root[0].get("test"), "w\N{DIAMETER SIGN}rks") self.assertEqual(root[0].text, astral_chunk + latin1_chunk) - @et_needs_pyversion(3) def test_feed_parser_unicode_astral_large(self): parser = self.XMLParser() - astral_chunk = u'-- \U00010143 --' * (2 ** 16) # astral (4 bytes/chr) - latin1_chunk = u'-- \xf8 --' # Latin1 (1 byte/chr) + astral_chunk = '-- \U00010143 --' * (2 ** 16) # astral (4 bytes/chr) + latin1_chunk = '-- \xf8 --' # Latin1 (1 byte/chr) - parser.feed(u'<') # ASCII (1 byte/chr) - parser.feed(u'a test="w\N{DIAMETER SIGN}rks">') # BMP (2 bytes/chr) + parser.feed('<') # ASCII (1 byte/chr) + parser.feed('a test="w\N{DIAMETER SIGN}rks">') # BMP (2 bytes/chr) parser.feed(astral_chunk) - parser.feed((astral_chunk + u" " + astral_chunk) * 16) + parser.feed((astral_chunk + " " + astral_chunk) * 16) parser.feed(latin1_chunk) - parser.feed(u'') + parser.feed('') root = parser.close() self.assertEqual(root.tag, "root") - self.assertEqual(root[0].get("test"), u"w\N{DIAMETER SIGN}rks") + self.assertEqual(root[0].get("test"), "w\N{DIAMETER SIGN}rks") for child in root[:-1]: self.assertEqual(child.tag, "a") self.assertEqual(child.text, astral_chunk * 2) @@ -3631,7 +3639,7 @@ def test_feed_parser_error_position(self): required_versions_ET['test_parser_target_property'] = (1,3) def test_parser_target_property(self): - class Target(object): + class Target: pass target = Target() @@ -3644,7 +3652,7 @@ def test_parser_target_tag(self): assertFalse = self.assertFalse events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append("start") assertFalse(attrib) @@ -3667,7 +3675,7 @@ def test_parser_target_error_in_start(self): assertEqual = self.assertEqual events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append("start") assertEqual("TAG", tag) @@ -3696,7 +3704,7 @@ def test_parser_target_error_in_end(self): assertEqual = self.assertEqual events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append("start") assertEqual("TAG", tag) @@ -3721,7 +3729,7 @@ def test_parser_target_error_in_close(self): assertEqual = self.assertEqual events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append("start") assertEqual("TAG", tag) @@ -3746,7 +3754,7 @@ def test_parser_target_error_in_start_and_close(self): assertEqual = self.assertEqual events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append("start") assertEqual("TAG", tag) @@ -3786,7 +3794,7 @@ def test_elementtree_parser_target(self): Element = self.etree.Element events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append("start") assertFalse(attrib) @@ -3799,7 +3807,7 @@ def close(self): parser = self.XMLParser(target=Target()) tree = self.etree.ElementTree() - tree.parse(BytesIO(""), parser=parser) + tree.parse(BytesIO(b""), parser=parser) self.assertEqual("DONE", tree.getroot().tag) self.assertEqual(["start", "end"], events) @@ -3808,7 +3816,7 @@ def test_parser_target_attrib(self): assertEqual = self.assertEqual events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append("start-" + tag) for name, value in attrib.items(): @@ -3829,7 +3837,7 @@ def close(self): def test_parser_target_data(self): events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append("start-" + tag) def end(self, tag): @@ -3851,7 +3859,7 @@ def close(self): def test_parser_target_entity(self): events = [] - class Target(object): + class Target: def __init__(self): self._data = [] def _flush_data(self): @@ -3891,7 +3899,7 @@ def close(self): required_versions_ET['test_parser_target_entity_unknown'] = (1,3) def test_parser_target_entity_unknown(self): events = [] - class Target(object): + class Target: def __init__(self): self._data = [] def _flush_data(self): @@ -4156,7 +4164,7 @@ def assertXML(self, expected, element, encoding='us-ascii'): Does this two ways; once using BytesIO, once using a real file. """ - if isinstance(expected, unicode): + if isinstance(expected, str): expected = expected.encode(encoding) self.assertEqual(expected, self._writeElement(element, encoding)) self.assertEqual(expected, self._writeElementFile(element, encoding)) @@ -4167,14 +4175,14 @@ def assertEncodingDeclaration(self, result, encoding): if isinstance(result, str): has_encoding = re.compile(enc_re).match else: - has_encoding = re.compile(_bytes(enc_re)).match + has_encoding = re.compile(enc_re.encode('ascii')).match self.assertTrue(has_encoding(result)) result_encoding = has_encoding(result).group(1) self.assertEqual(result_encoding.upper(), encoding.upper()) def _rootstring(self, tree): return self.etree.tostring(tree.getroot()).replace( - _bytes(' '), _bytes('')).replace(_bytes('\n'), _bytes('')) + b' ', b'').replace(b'\n', b'') def _check_element_tree(self, tree): self._check_element(tree.getroot()) @@ -4211,6 +4219,7 @@ def _check_mapping(self, mapping): class _ElementSlicingTest(unittest.TestCase): + __test__ = False etree = None def _elem_tags(self, elemlist): @@ -4363,6 +4372,7 @@ def test_setslice_negative_steps(self): class _XMLPullParserTest(unittest.TestCase): + __test__ = False etree = None def _close_and_return_root(self, parser): @@ -4396,29 +4406,44 @@ def assert_event_tags(self, parser, expected, max_events=None): self.assertEqual([(action, elem.tag) for action, elem in events], expected) - def test_simple_xml(self): - for chunk_size in (None, 1, 5): - #with self.subTest(chunk_size=chunk_size): - parser = self.etree.XMLPullParser() - self.assert_event_tags(parser, []) - self._feed(parser, "\n", chunk_size) - self.assert_event_tags(parser, []) - self._feed(parser, - "\n text\n", chunk_size) - self.assert_event_tags(parser, [('end', 'element')]) - self._feed(parser, "texttail\n", chunk_size) - self._feed(parser, "\n", chunk_size) - self.assert_event_tags(parser, [ - ('end', 'element'), - ('end', 'empty-element'), - ]) - self._feed(parser, "\n", chunk_size) - self.assert_event_tags(parser, [('end', 'root')]) - root = self._close_and_return_root(parser) - self.assertEqual(root.tag, 'root') + def test_simple_xml(self, chunk_size=None): + parser = self.etree.XMLPullParser() + self.assert_event_tags(parser, []) + self._feed(parser, "\n", chunk_size) + self.assert_event_tags(parser, []) + self._feed(parser, + "\n text\n", chunk_size) + self._feed(parser, "texttail\n", chunk_size) + self._feed(parser, "\n", chunk_size) + self._feed(parser, "\n", chunk_size) + self.assert_event_tags(parser, [ + ('end', 'element'), + ('end', 'element'), + ('end', 'empty-element'), + ('end', 'root'), + ]) + root = self._close_and_return_root(parser) + self.assertEqual(root.tag, 'root') + + def test_simple_xml_chunk_1(self): + if self.etree is not etree and pyexpat.version_info >= (2, 6, 0): + raise unittest.SkipTest( + "Feeding the parser by too small chunks defers parsing" + ) + self.test_simple_xml(chunk_size=1) + + def test_simple_xml_chunk_5(self): + if self.etree is not etree and pyexpat.version_info >= (2, 6, 0): + raise unittest.SkipTest( + "Feeding the parser by too small chunks defers parsing" + ) + self.test_simple_xml(chunk_size=5) + + def test_simple_xml_chunk_22(self): + self.test_simple_xml(chunk_size=22) def test_feed_while_iterating(self): parser = self.etree.XMLPullParser() @@ -4624,7 +4649,7 @@ def test_events_sequence(self): self._feed(parser, "bar") self.assert_event_tags(parser, [('start', 'foo'), ('end', 'foo')]) - class DummyIter(object): + class DummyIter: def __init__(self): self.events = iter(['start', 'end', 'start-ns']) def __iter__(self): @@ -4644,6 +4669,7 @@ def test_unknown_event(self): class _C14NTest(unittest.TestCase): + __test__ = False etree = None maxDiff = None @@ -4655,7 +4681,7 @@ def subTest(self, name, **kwargs): except unittest.SkipTest: raise except Exception as e: - print("Subtest {} failed: {}".format(name, e)) + print(f"Subtest {name} failed: {e}") raise def _canonicalize(self, input_file, **options): @@ -4860,7 +4886,7 @@ def get_option(config, option_name, default=None): for name, (value, children) in sorted(config.items()) ) - with self.subTest("{}({})".format(output_file, config_descr)): + with self.subTest(f"{output_file}({config_descr})"): if input_file == 'inNsRedecl' and not rewrite_prefixes: self.skipTest( "Redeclared namespace handling is not supported in {}".format( @@ -4889,7 +4915,7 @@ def get_option(config, option_name, default=None): rewrite_prefixes=rewrite_prefixes, qname_aware_tags=qtags, qname_aware_attrs=qattrs) - with io.open(full_path(output_file + ".xml"), 'r', encoding='utf8') as f: + with open(full_path(output_file + ".xml"), encoding='utf8') as f: expected = f.read() if input_file == 'inC14N3' and self.etree is not etree: # FIXME: cET resolves default attributes but ET does not! @@ -4900,15 +4926,19 @@ def get_option(config, option_name, default=None): if etree: class ETreeTestCase(_ETreeTestCaseBase): + __test__ = True etree = etree class ETreePullTestCase(_XMLPullParserTest): + __test__ = True etree = etree class ETreeElementSlicingTest(_ElementSlicingTest): + __test__ = True etree = etree class ETreeC14NTest(_C14NTest): + __test__ = True etree = etree class ETreeC14N2WriteTest(ETreeC14NTest): @@ -4925,7 +4955,7 @@ def _canonicalize(self, input_file, with_comments=True, strip_text=False, out, method='c14n2', with_comments=with_comments, strip_text=strip_text, **options) - return out.getvalue().decode('utf8') + return out.getvalue().decode('utf-8') class ETreeC14N2TostringTest(ETreeC14NTest): def _canonicalize(self, input_file, with_comments=True, strip_text=False, @@ -4939,11 +4969,12 @@ def _canonicalize(self, input_file, with_comments=True, strip_text=False, return self.etree.tostring( tree, method='c14n2', with_comments=with_comments, strip_text=strip_text, - **options).decode('utf8') + **options).decode('utf-8') if ElementTree: class ElementTreeTestCase(_ETreeTestCaseBase): + __test__ = True etree = ElementTree @classmethod @@ -4957,58 +4988,80 @@ def setUpClass(cls): r'This method will be removed.*\.iter\(\).*instead', PendingDeprecationWarning) + def test_elementtree_serialises_lxml_tree(self): + # Parse tree with lxml.etree. + root = etree.XML(""" + + A + + + + """) + + # Sanity checks. + self.assertNotIsInstance(etree.tostring, types.FunctionType) + self.assertIsInstance(self.etree.tostring, types.FunctionType) + + # Serialised with xml.etree.ElementTree.tostring() + xml_tostring = self.etree.tostring(root, encoding='utf8') + self.assertIn(b'', xml_tostring) + self.assertIn(b'', xml_tostring) + + # ET.write() + out = io.BytesIO() + self.etree.ElementTree(root).write(out, encoding='utf8') + xml_write = out.getvalue() + self.assertIn(b'', xml_write) + self.assertIn(b'', xml_write) + + # Both should be identical because they used the same serialiser. + self.assertEqual(xml_tostring, xml_write) + filter_by_version( ElementTreeTestCase, ElementTreeTestCase.required_versions_ET, ET_VERSION) if hasattr(ElementTree, 'XMLPullParser'): class ElementTreePullTestCase(_XMLPullParserTest): + __test__ = True etree = ElementTree else: ElementTreePullTestCase = None if hasattr(ElementTree, 'canonicalize'): class ElementTreeC14NTest(_C14NTest): + __test__ = True etree = ElementTree else: ElementTreeC14NTest = None class ElementTreeElementSlicingTest(_ElementSlicingTest): + __test__ = True etree = ElementTree -if cElementTree: - class CElementTreeTestCase(_ETreeTestCaseBase): - etree = cElementTree - - filter_by_version( - CElementTreeTestCase, - CElementTreeTestCase.required_versions_cET, CET_VERSION) - - class CElementTreeElementSlicingTest(_ElementSlicingTest): - etree = cElementTree - - def test_suite(): suite = unittest.TestSuite() if etree: - suite.addTests([unittest.makeSuite(ETreeTestCase)]) - suite.addTests([unittest.makeSuite(ETreePullTestCase)]) - suite.addTests([unittest.makeSuite(ETreeElementSlicingTest)]) - suite.addTests([unittest.makeSuite(ETreeC14NTest)]) - suite.addTests([unittest.makeSuite(ETreeC14N2WriteTest)]) - suite.addTests([unittest.makeSuite(ETreeC14N2TostringTest)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreePullTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeElementSlicingTest)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeC14NTest)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeC14N2WriteTest)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeC14N2TostringTest)]) if ElementTree: - suite.addTests([unittest.makeSuite(ElementTreeTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ElementTreeTestCase)]) if ElementTreePullTestCase: - suite.addTests([unittest.makeSuite(ElementTreePullTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ElementTreePullTestCase)]) if ElementTreeC14NTest: - suite.addTests([unittest.makeSuite(ElementTreeC14NTest)]) - suite.addTests([unittest.makeSuite(ElementTreeElementSlicingTest)]) - if cElementTree: - suite.addTests([unittest.makeSuite(CElementTreeTestCase)]) - suite.addTests([unittest.makeSuite(CElementTreeElementSlicingTest)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ElementTreeC14NTest)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ElementTreeElementSlicingTest)]) return suite + +# Hide test base classes from test discovery. +del _ETreeTestCaseBase, _ElementSlicingTest, _XMLPullParserTest, _C14NTest + + if __name__ == '__main__': print('to test use test.py %s' % __file__) diff --git a/src/lxml/tests/test_errors.py b/src/lxml/tests/test_errors.py index c0aee7449..edb4b9c54 100644 --- a/src/lxml/tests/test_errors.py +++ b/src/lxml/tests/test_errors.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - import unittest # These tests check that error handling in the Pyrex code is @@ -8,10 +5,13 @@ # It is likely that if there are errors, instead of failing the code # will simply crash. -import sys, gc, os.path +import gc +import os.path +import sys +import unittest from lxml import etree -from .common_imports import HelperTestCase +from .common_imports import HelperTestCase, IS_PYPY class ErrorTestCase(HelperTestCase): @@ -25,6 +25,7 @@ def test_bad_element(self): def test_empty_parse(self): self.assertRaises(etree.XMLSyntaxError, etree.fromstring, '') + @unittest.skipIf(IS_PYPY, "needs sys.getrefcount()") def test_element_cyclic_gc_none(self): # test if cyclic reference can crash etree Element = self.etree.Element @@ -45,7 +46,11 @@ def test_element_cyclic_gc_none(self): gc.collect() count = getrefcount(None) - count - self.assertEqual(count, 0) + if sys.version_info[:2] == (3, 11) and count == -1: + # FIXME: it's currently unclear why this happens, but it's reproducible on Py3.11. + self.assertEqual(count, -1) + else: + self.assertEqual(count, 0) finally: sys.settrace(trace_func) @@ -63,14 +68,14 @@ def test_xmlsyntaxerror_has_info(self): self.assertEqual(e.lineno, 1) self.assertEqual(e.offset, 10) except Exception as e: - self.fail('{0}, not {1}'.format(fail_msg, type(e))) + self.fail(f'{fail_msg}, not {type(e)}') else: self.fail('test_broken.xml should raise an etree.XMLSyntaxError') def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(ErrorTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ErrorTestCase)]) return suite if __name__ == '__main__': diff --git a/src/lxml/tests/test_etree.py b/src/lxml/tests/test_etree.py index e5f084692..8a35cd43a 100644 --- a/src/lxml/tests/test_etree.py +++ b/src/lxml/tests/test_etree.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ Tests specific to the extended etree API @@ -7,52 +5,55 @@ test_elementtree """ -from __future__ import absolute_import from collections import OrderedDict +from io import StringIO, BytesIO import os.path import unittest +import contextlib import copy +import itertools import sys import re import gc import operator +import shutil +import tempfile import textwrap import zlib import gzip -from .common_imports import etree, StringIO, BytesIO, HelperTestCase +from .common_imports import etree, HelperTestCase, needs_feature, IS_PYPY from .common_imports import fileInTestDir, fileUrlInTestDir, read_file, path2url, tmpfile from .common_imports import SillyFileLike, LargeFileLikeUnicode, doctest, make_doctest from .common_imports import canonicalize, _str, _bytes from .common_imports import SimpleFSPath -print(""" -TESTED VERSION: %s""" % etree.__version__ + """ - Python: %r""" % (sys.version_info,) + """ - lxml.etree: %r""" % (etree.LXML_VERSION,) + """ - libxml used: %r""" % (etree.LIBXML_VERSION,) + """ - libxml compiled: %r""" % (etree.LIBXML_COMPILED_VERSION,) + """ - libxslt used: %r""" % (etree.LIBXSLT_VERSION,) + """ - libxslt compiled: %r""" % (etree.LIBXSLT_COMPILED_VERSION,) + """ - FS encoding: %s""" % (sys.getfilesystemencoding(),) + """ - Default encoding: %s""" % (sys.getdefaultencoding(),) + """ - Max Unicode: %s""" % (sys.maxunicode,) + """ +print(f""" +TESTED VERSION: {etree.__version__} + Python: {tuple(sys.version_info)!r} + lxml.etree: {etree.LXML_VERSION!r} + libxml used: {etree.LIBXML_VERSION!r} + features: {' '.join(sorted(etree.LIBXML_FEATURES))} + libxml compiled: {etree.LIBXML_COMPILED_VERSION!r} + features: {' '.join(sorted(etree.LIBXML_COMPILED_FEATURES))} + libxslt used: {etree.LIBXSLT_VERSION!r} + libxslt compiled: {etree.LIBXSLT_COMPILED_VERSION!r} + iconv compiled: {etree.ICONV_COMPILED_VERSION!r} + FS encoding: {sys.getfilesystemencoding()} + Default encoding: {sys.getdefaultencoding()} + Max Unicode: {sys.maxunicode} + PyUCS4 encoding: {getattr(etree, '_pyucs4_encoding_name', '')} + freethreading: {getattr(etree, '_freethreading_enabled', '')} """) -try: - _unicode = unicode -except NameError: - # Python 3 - _unicode = str - class ETreeOnlyTestCase(HelperTestCase): """Tests only for etree, not ElementTree""" etree = etree def test_version(self): - self.assertTrue(isinstance(etree.__version__, _unicode)) + self.assertTrue(isinstance(etree.__version__, str)) self.assertTrue(isinstance(etree.LXML_VERSION, tuple)) self.assertEqual(len(etree.LXML_VERSION), 4) self.assertTrue(isinstance(etree.LXML_VERSION[0], int)) @@ -62,6 +63,16 @@ def test_version(self): self.assertTrue(etree.__version__.startswith( str(etree.LXML_VERSION[0]))) + def test_libxml_features(self): + self.assertIsInstance(etree.LIBXML_FEATURES, set) + self.assertTrue(etree.LIBXML_FEATURES) + self.assertIn("xpath", etree.LIBXML_FEATURES) + + def test_libxml_compiled_features(self): + self.assertIsInstance(etree.LIBXML_COMPILED_FEATURES, set) + self.assertTrue(etree.LIBXML_COMPILED_FEATURES) + self.assertIn("xpath", etree.LIBXML_COMPILED_FEATURES) + def test_c_api(self): if hasattr(self.etree, '__pyx_capi__'): # newer Pyrex compatible C-API @@ -239,13 +250,21 @@ def test_clear_keep_tail(self): tostring = self.etree.tostring a = XML('B1B2C1C2') a[0].clear(keep_tail=True) - self.assertEqual(_bytes('B2C1C2'), tostring(a)) + self.assertEqual(b'B2C1C2', tostring(a)) + + def test_attrib_is_Mapping(self): + from collections.abc import Mapping, MutableMapping + Element = self.etree.Element + root = Element("root") + + self.assertTrue(isinstance(root.attrib, Mapping)) + self.assertTrue(isinstance(root.attrib, MutableMapping)) def test_attribute_has_key(self): # ET in Py 3.x has no "attrib.has_key()" method XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') self.assertEqual( True, root.attrib.has_key('bar')) self.assertEqual( @@ -301,12 +320,7 @@ def test_attrib_order(self): ('attr_99', 'TOAST-1'), ('attr_98', 'TOAST-2'), ] - ordered_dict_types = [OrderedDict, lambda x:x] - if sys.version_info >= (3, 6): - ordered_dict_types.append(dict) - else: - # Keyword arguments are not ordered in Py<3.6, and thus get sorted. - attr_order.sort() + ordered_dict_types = [dict, OrderedDict, lambda x:x] attr_order += items expected_keys = [attr[0] for attr in attr_order] expected_values = [attr[1] for attr in attr_order] @@ -339,126 +353,126 @@ def test_attribute_set_invalid(self): def test_strip_attributes(self): XML = self.etree.XML - xml = _bytes('') + xml = b'' root = XML(xml) self.etree.strip_attributes(root, 'a') - self.assertEqual(_bytes(''), + self.assertEqual(b'', self._writeElement(root)) root = XML(xml) self.etree.strip_attributes(root, 'b', 'c') - self.assertEqual(_bytes(''), + self.assertEqual(b'', self._writeElement(root)) def test_strip_attributes_ns(self): XML = self.etree.XML - xml = _bytes('') + xml = b'' root = XML(xml) self.etree.strip_attributes(root, 'a') self.assertEqual( - _bytes(''), + b'', self._writeElement(root)) root = XML(xml) self.etree.strip_attributes(root, '{http://test/ns}a', 'c') self.assertEqual( - _bytes(''), + b'', self._writeElement(root)) root = XML(xml) self.etree.strip_attributes(root, '{http://test/ns}*') self.assertEqual( - _bytes(''), + b'', self._writeElement(root)) def test_strip_elements(self): XML = self.etree.XML - xml = _bytes('') + xml = b'' root = XML(xml) self.etree.strip_elements(root, 'a') - self.assertEqual(_bytes(''), + self.assertEqual(b'', self._writeElement(root)) root = XML(xml) self.etree.strip_elements(root, 'b', 'c', 'X', 'Y', 'Z') - self.assertEqual(_bytes(''), + self.assertEqual(b'', self._writeElement(root)) root = XML(xml) self.etree.strip_elements(root, 'c') - self.assertEqual(_bytes(''), + self.assertEqual(b'', self._writeElement(root)) def test_strip_elements_ns(self): XML = self.etree.XML - xml = _bytes('TESTABCBTATXABTCTATXT') + xml = b'TESTABCBTATXABTCTATXT' root = XML(xml) self.etree.strip_elements(root, 'a') - self.assertEqual(_bytes('TESTABCBTATXXT'), + self.assertEqual(b'TESTABCBTATXXT', self._writeElement(root)) root = XML(xml) self.etree.strip_elements(root, '{urn:a}b', 'c') - self.assertEqual(_bytes('TESTABCBTATXACTATXT'), + self.assertEqual(b'TESTABCBTATXACTATXT', self._writeElement(root)) root = XML(xml) self.etree.strip_elements(root, '{urn:a}*', 'c') - self.assertEqual(_bytes('TESTXACTATXT'), + self.assertEqual(b'TESTXACTATXT', self._writeElement(root)) root = XML(xml) self.etree.strip_elements(root, '{urn:a}*', 'c', with_tail=False) - self.assertEqual(_bytes('TESTATXABTCTATXT'), + self.assertEqual(b'TESTATXABTCTATXT', self._writeElement(root)) def test_strip_tags(self): XML = self.etree.XML - xml = _bytes('TESTABCTBTATXABTCTATXT') + xml = b'TESTABCTBTATXABTCTATXT' root = XML(xml) self.etree.strip_tags(root, 'a') - self.assertEqual(_bytes('TESTABCTBTATXABTCTATXT'), + self.assertEqual(b'TESTABCTBTATXABTCTATXT', self._writeElement(root)) root = XML(xml) self.etree.strip_tags(root, 'b', 'c', 'X', 'Y', 'Z') - self.assertEqual(_bytes('TESTABCTBTATXABTCTATXT'), + self.assertEqual(b'TESTABCTBTATXABTCTATXT', self._writeElement(root)) root = XML(xml) self.etree.strip_tags(root, 'c') - self.assertEqual(_bytes('TESTABCTBTATXABTCTATXT'), + self.assertEqual(b'TESTABCTBTATXABTCTATXT', self._writeElement(root)) def test_strip_tags_pi_comment(self): XML = self.etree.XML PI = self.etree.ProcessingInstruction Comment = self.etree.Comment - xml = _bytes('\n\nTESTXT\n\n') + xml = b'\n\nTESTXT\n\n' root = XML(xml) self.etree.strip_tags(root, PI) - self.assertEqual(_bytes('\n\nTESTXT\n\n'), + self.assertEqual(b'\n\nTESTXT\n\n', self._writeElement(root)) root = XML(xml) self.etree.strip_tags(root, Comment) - self.assertEqual(_bytes('\n\nTESTXT\n\n'), + self.assertEqual(b'\n\nTESTXT\n\n', self._writeElement(root)) root = XML(xml) self.etree.strip_tags(root, PI, Comment) - self.assertEqual(_bytes('\n\nTESTXT\n\n'), + self.assertEqual(b'\n\nTESTXT\n\n', self._writeElement(root)) root = XML(xml) self.etree.strip_tags(root, Comment, PI) - self.assertEqual(_bytes('\n\nTESTXT\n\n'), + self.assertEqual(b'\n\nTESTXT\n\n', self._writeElement(root)) def test_strip_tags_pi_comment_all(self): @@ -466,31 +480,31 @@ def test_strip_tags_pi_comment_all(self): ElementTree = self.etree.ElementTree PI = self.etree.ProcessingInstruction Comment = self.etree.Comment - xml = _bytes('\n\nTESTXT\n\n') + xml = b'\n\nTESTXT\n\n' root = XML(xml) self.etree.strip_tags(ElementTree(root), PI) - self.assertEqual(_bytes('\nTESTXT\n'), + self.assertEqual(b'\nTESTXT\n', self._writeElement(root)) root = XML(xml) self.etree.strip_tags(ElementTree(root), Comment) - self.assertEqual(_bytes('\nTESTXT\n'), + self.assertEqual(b'\nTESTXT\n', self._writeElement(root)) root = XML(xml) self.etree.strip_tags(ElementTree(root), PI, Comment) - self.assertEqual(_bytes('TESTXT'), + self.assertEqual(b'TESTXT', self._writeElement(root)) root = XML(xml) self.etree.strip_tags(ElementTree(root), Comment, PI) - self.assertEqual(_bytes('TESTXT'), + self.assertEqual(b'TESTXT', self._writeElement(root)) def test_strip_tags_doc_style(self): XML = self.etree.XML - xml = _bytes(''' + xml = b'''
I like sheep. @@ -502,51 +516,86 @@ def test_strip_tags_doc_style(self):
- '''.strip()) + '''.strip() root = XML(xml) self.etree.strip_tags(root, 'a') - self.assertEqual(re.sub(_bytes(']*>'), _bytes(''), xml).replace(_bytes('
'), _bytes('

')), + self.assertEqual(re.sub(b']*>', b'', xml).replace(b'
', b'

'), self._writeElement(root)) root = XML(xml) self.etree.strip_tags(root, 'a', 'br') - self.assertEqual(re.sub(_bytes(']*>'), _bytes(''), - re.sub(_bytes(']*>'), _bytes(''), xml)), + self.assertEqual(re.sub(b']*>', b'', + re.sub(b']*>', b'', xml)), self._writeElement(root)) def test_strip_tags_ns(self): XML = self.etree.XML - xml = _bytes('TESTABCTBTATXABTCTATXT') + xml = b'TESTABCTBTATXABTCTATXT' root = XML(xml) self.etree.strip_tags(root, 'a') - self.assertEqual(_bytes('TESTABCTBTATXABTCTATXT'), + self.assertEqual(b'TESTABCTBTATXABTCTATXT', self._writeElement(root)) root = XML(xml) self.etree.strip_tags(root, '{urn:a}b', 'c') - self.assertEqual(_bytes('TESTABCTBTATXABTCTATXT'), + self.assertEqual(b'TESTABCTBTATXABTCTATXT', self._writeElement(root)) root = XML(xml) self.etree.strip_tags(root, '{urn:a}*', 'c') - self.assertEqual(_bytes('TESTABCTBTATXABTCTATXT'), + self.assertEqual(b'TESTABCTBTATXABTCTATXT', self._writeElement(root)) def test_strip_tags_and_remove(self): # previously crashed HTML = self.etree.HTML - root = HTML(_bytes('

title

foo

boo

'))[0][0] - self.assertEqual(_bytes('

title

foo

boo

'), + root = HTML(b'

title

foo

boo

')[0][0] + self.assertEqual(b'

title

foo

boo

', self.etree.tostring(root)) self.etree.strip_tags(root, 'b') - self.assertEqual(_bytes('

title

foo

boo

'), + self.assertEqual(b'

title

foo

boo

', self.etree.tostring(root)) root.remove(root[0]) - self.assertEqual(_bytes('

boo

'), + self.assertEqual(b'

boo

', self.etree.tostring(root)) + def test_append_rejects_ancestor(self): + XML = self.etree.XML + root = XML("") + a = root[0] + self.assertRaises(ValueError, a.append, root) + self.assertRaises(ValueError, a[0].append, root) + self.assertRaises(ValueError, a[0].append, a) + self.assertRaises(ValueError, a[0][0].append, root) + self.assertRaises(ValueError, a[0][0].append, a) + self.assertRaises(ValueError, a[0][0].append, a[0]) + + def test_insert_rejects_ancestor(self): + XML = self.etree.XML + root = XML("") + a = root[0] + self.assertRaises(ValueError, a.insert, 0, root) + self.assertRaises(ValueError, a[0].insert, 0, root) + self.assertRaises(ValueError, a[0].insert, 0, a) + self.assertRaises(ValueError, a[0][0].insert, 0, root) + self.assertRaises(ValueError, a[0][0].insert, 0, a) + self.assertRaises(ValueError, a[0][0].insert, 0, a[0]) + + def test_replace_rejects_ancestor(self): + XML = self.etree.XML + root = XML("") + a = root[0] + root.replace(a, a) + self.assertRaises(ValueError, root.replace, a, root) + a.replace(a[0], a[0]) + self.assertRaises(ValueError, a.replace, a[0], root) + a[0].replace(a[0][0], a[0][0]) + self.assertRaises(ValueError, a[0].replace, a[0][0], root) + self.assertRaises(ValueError, a[0].replace, a[0][0], a) + self.assertRaises(ValueError, a[0].replace, a[0][0], a[0]) + def test_pi(self): # lxml.etree separates target and text Element = self.etree.Element @@ -560,13 +609,13 @@ def test_pi(self): def test_pi_parse(self): XML = self.etree.XML - root = XML(_bytes("")) + root = XML(b"") self.assertEqual(root[0].target, "mypi") self.assertEqual(root[0].text, "my test ") def test_pi_pseudo_attributes_get(self): XML = self.etree.XML - root = XML(_bytes("")) + root = XML(b"") self.assertEqual(root[0].target, "mypi") self.assertEqual(root[0].get('my'), "1") self.assertEqual(root[0].get('test'), " abc ") @@ -577,7 +626,7 @@ def test_pi_pseudo_attributes_get(self): def test_pi_pseudo_attributes_attrib(self): XML = self.etree.XML - root = XML(_bytes("")) + root = XML(b"") self.assertEqual(root[0].target, "mypi") self.assertEqual(root[0].attrib['my'], "1") self.assertEqual(root[0].attrib['test'], " abc ") @@ -589,7 +638,7 @@ def test_pi_pseudo_attributes_attrib(self): def test_deepcopy_pi(self): # previously caused a crash ProcessingInstruction = self.etree.ProcessingInstruction - + a = ProcessingInstruction("PI", "ONE") b = copy.deepcopy(a) b.text = "ANOTHER" @@ -600,23 +649,23 @@ def test_deepcopy_pi(self): def test_deepcopy_elementtree_pi(self): XML = self.etree.XML tostring = self.etree.tostring - root = XML(_bytes("")) + root = XML(b"") tree1 = self.etree.ElementTree(root) - self.assertEqual(_bytes(""), + self.assertEqual(b"", tostring(tree1)) tree2 = copy.deepcopy(tree1) - self.assertEqual(_bytes(""), + self.assertEqual(b"", tostring(tree2)) root2 = copy.deepcopy(tree1.getroot()) - self.assertEqual(_bytes(""), + self.assertEqual(b"", tostring(root2)) def test_deepcopy_elementtree_dtd(self): XML = self.etree.XML tostring = self.etree.tostring - xml = _bytes('\n]>\n') + xml = b'\n]>\n' root = XML(xml) tree1 = self.etree.ElementTree(root) self.assertEqual(xml, tostring(tree1)) @@ -625,13 +674,13 @@ def test_deepcopy_elementtree_dtd(self): self.assertEqual(xml, tostring(tree2)) root2 = copy.deepcopy(tree1.getroot()) - self.assertEqual(_bytes(""), + self.assertEqual(b"", tostring(root2)) def test_deepcopy_pi_dtd(self): XML = self.etree.XML tostring = self.etree.tostring - xml = _bytes('\n]>\n') + xml = b'\n]>\n' root = XML(xml) tree1 = self.etree.ElementTree(root) self.assertEqual(xml, tostring(tree1)) @@ -644,11 +693,11 @@ def test_parse_remove_comments(self): tostring = self.etree.tostring XMLParser = self.etree.XMLParser - xml = _bytes('') + xml = b'' parser = XMLParser(remove_comments=True) root = fromstring(xml, parser) self.assertEqual( - _bytes(''), + b'', tostring(root)) def test_parse_remove_pis(self): @@ -656,7 +705,7 @@ def test_parse_remove_pis(self): tostring = self.etree.tostring XMLParser = self.etree.XMLParser - xml = _bytes('') + xml = b'' f = BytesIO(xml) tree = parse(f) @@ -667,7 +716,7 @@ def test_parse_remove_pis(self): parser = XMLParser(remove_pis=True) tree = parse(f, parser) self.assertEqual( - _bytes(''), + b'', tostring(tree)) def test_parse_parser_type_error(self): @@ -675,9 +724,49 @@ def test_parse_parser_type_error(self): parse = self.etree.parse self.assertRaises(TypeError, parse, 'notthere.xml', object()) + def test_parse_huge_tree(self): + fromstring = self.etree.fromstring + XMLParser = self.etree.XMLParser + + xml = b'' + parser = XMLParser(huge_tree=True) + self.assertEqual(2, len(fromstring(xml, parser=parser))) + + def test_parse_premature_end(self): + fromstring = self.etree.fromstring + XMLParser = self.etree.XMLParser + + xml = b'' + parser = XMLParser() + self.assertRaises(self.etree.XMLSyntaxError, fromstring, xml, parser) + + def test_parse_premature_end_with_target(self): + # tests issue https://bugs.launchpad.net/lxml/+bug/1980767 is fixed + fromstring = self.etree.fromstring + XMLParser = self.etree.XMLParser + + xml = b'' + parser = XMLParser(target=etree.TreeBuilder()) + self.assertRaises(self.etree.XMLSyntaxError, fromstring, xml, parser) + + @unittest.skipIf(IS_PYPY, "currently crashes PyPy") + def test_parser_reentry_from_target(self): + fromstring = self.etree.fromstring + XMLParser = self.etree.XMLParser + + class Target: + def start(self, tag, attrib): + etree.fromstring(b"", parser=parser) + def close(self): + pass + + xml = b'' + parser = XMLParser(target=Target()) + self.assertRaises(RuntimeError, fromstring, xml, parser) + def test_iterparse_getiterator(self): iterparse = self.etree.iterparse - f = BytesIO('') + f = BytesIO(b'') counts = [] for event, elem in iterparse(f): @@ -686,17 +775,28 @@ def test_iterparse_getiterator(self): [1,2,1,4], counts) + def test_iterparse_huge_tree(self): + iterparse = self.etree.iterparse + f = BytesIO(b'') + + counts = [] + for _, elem in iterparse(f, huge_tree=True): + counts.append(len(elem)) + self.assertEqual( + [0,1,0,2], + counts) + def test_iterparse_tree_comments(self): # ET removes comments iterparse = self.etree.iterparse tostring = self.etree.tostring - f = BytesIO('') + f = BytesIO(b'') events = list(iterparse(f)) root = events[-1][1] self.assertEqual(3, len(events)) self.assertEqual( - _bytes(''), + b'', tostring(root)) def test_iterparse_comments(self): @@ -710,14 +810,14 @@ def name(event, el): else: return el.tag - f = BytesIO('') + f = BytesIO(b'') events = list(iterparse(f, events=('end', 'comment'))) root = events[-1][1] self.assertEqual(6, len(events)) self.assertEqual(['A', ' B ', 'c', 'b', 'C', 'a'], [ name(*item) for item in events ]) self.assertEqual( - _bytes(''), + b'', tostring(root)) def test_iterparse_pis(self): @@ -732,7 +832,7 @@ def name(event, el): else: return el.tag - f = BytesIO('') + f = BytesIO(b'') events = list(iterparse(f, events=('end', 'pi'))) root = events[-2][1] self.assertEqual(8, len(events)) @@ -740,14 +840,14 @@ def name(event, el): ('pid','d'), 'a', ('pie','e')], [ name(*item) for item in events ]) self.assertEqual( - _bytes(''), + b'', tostring(ElementTree(root))) def test_iterparse_remove_comments(self): iterparse = self.etree.iterparse tostring = self.etree.tostring - f = BytesIO('') + f = BytesIO(b'') events = list(iterparse(f, remove_comments=True, events=('end', 'comment'))) root = events[-1][1] @@ -755,18 +855,18 @@ def test_iterparse_remove_comments(self): self.assertEqual(['c', 'b', 'a'], [ el.tag for (event, el) in events ]) self.assertEqual( - _bytes(''), + b'', tostring(root)) def test_iterparse_broken(self): iterparse = self.etree.iterparse - f = BytesIO('') + f = BytesIO(b'') # ET raises ExpatError, lxml raises XMLSyntaxError self.assertRaises(self.etree.XMLSyntaxError, list, iterparse(f)) def test_iterparse_broken_recover(self): iterparse = self.etree.iterparse - f = BytesIO('') + f = BytesIO(b'') it = iterparse(f, events=('start', 'end'), recover=True) events = [(ev, el.tag) for ev, el in it] root = it.root @@ -783,7 +883,7 @@ def test_iterparse_broken_recover(self): def test_iterparse_broken_multi_recover(self): iterparse = self.etree.iterparse - f = BytesIO('') + f = BytesIO(b'') it = iterparse(f, events=('start', 'end'), recover=True) events = [(ev, el.tag) for ev, el in it] root = it.root @@ -800,7 +900,7 @@ def test_iterparse_broken_multi_recover(self): def test_iterparse_strip(self): iterparse = self.etree.iterparse - f = BytesIO(""" + f = BytesIO(b""" \n \n b test \n \n\t \n \n """) @@ -813,7 +913,7 @@ def test_iterparse_strip(self): def test_iterparse_tag(self): iterparse = self.etree.iterparse - f = BytesIO('') + f = BytesIO(b'') iterator = iterparse(f, tag="b", events=('start', 'end')) events = list(iterator) @@ -824,7 +924,7 @@ def test_iterparse_tag(self): def test_iterparse_tag_all(self): iterparse = self.etree.iterparse - f = BytesIO('') + f = BytesIO(b'') iterator = iterparse(f, tag="*", events=('start', 'end')) events = list(iterator) @@ -834,7 +934,7 @@ def test_iterparse_tag_all(self): def test_iterparse_tag_ns(self): iterparse = self.etree.iterparse - f = BytesIO('') + f = BytesIO(b'') iterator = iterparse(f, tag="{urn:test:1}b", events=('start', 'end')) events = list(iterator) @@ -845,7 +945,7 @@ def test_iterparse_tag_ns(self): def test_iterparse_tag_ns_empty(self): iterparse = self.etree.iterparse - f = BytesIO('') + f = BytesIO(b'') iterator = iterparse(f, tag="{}b", events=('start', 'end')) events = list(iterator) root = iterator.root @@ -853,7 +953,7 @@ def test_iterparse_tag_ns_empty(self): [('start', root[0]), ('end', root[0])], events) - f = BytesIO('') + f = BytesIO(b'') iterator = iterparse(f, tag="{}b", events=('start', 'end')) events = list(iterator) root = iterator.root @@ -861,27 +961,27 @@ def test_iterparse_tag_ns_empty(self): def test_iterparse_tag_ns_all(self): iterparse = self.etree.iterparse - f = BytesIO('') + f = BytesIO(b'') iterator = iterparse(f, tag="{urn:test:1}*", events=('start', 'end')) events = list(iterator) self.assertEqual(8, len(events)) def test_iterparse_tag_ns_empty_all(self): iterparse = self.etree.iterparse - f = BytesIO('') + f = BytesIO(b'') iterator = iterparse(f, tag="{}*", events=('start', 'end')) events = list(iterator) self.assertEqual([], events) - f = BytesIO('') + f = BytesIO(b'') iterator = iterparse(f, tag="{}*", events=('start', 'end')) events = list(iterator) self.assertEqual(8, len(events)) def test_iterparse_encoding_error(self): - text = _str('Søk på nettet') + text = 'Søk på nettet' wrong_declaration = "" - xml_latin1 = (_str('%s%s') % (wrong_declaration, text) + xml_latin1 = ('%s%s' % (wrong_declaration, text) ).encode('iso-8859-1') self.assertRaises(self.etree.ParseError, @@ -890,7 +990,7 @@ def test_iterparse_encoding_error(self): def test_iterparse_encoding_8bit_override(self): text = _str('Søk på nettet', encoding="UTF-8") wrong_declaration = "" - xml_latin1 = (_str('%s%s') % (wrong_declaration, text) + xml_latin1 = ('%s%s' % (wrong_declaration, text) ).encode('iso-8859-1') iterator = self.etree.iterparse(BytesIO(xml_latin1), @@ -902,12 +1002,12 @@ def test_iterparse_encoding_8bit_override(self): def test_iterparse_keep_cdata(self): tostring = self.etree.tostring - f = BytesIO('') + f = BytesIO(b'') context = self.etree.iterparse(f, strip_cdata=False) content = [ el.text for event,el in context ] self.assertEqual(['test'], content) - self.assertEqual(_bytes(''), + self.assertEqual(b'', tostring(context.root)) def test_parser_encoding_unknown(self): @@ -970,7 +1070,7 @@ def test_elementtree_parser_target_type_error(self): assertFalse = self.assertFalse events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append("start") assertFalse(attrib) @@ -985,13 +1085,13 @@ def close(self): tree = self.etree.ElementTree() self.assertRaises(TypeError, - tree.parse, BytesIO(""), parser=parser) + tree.parse, BytesIO(b""), parser=parser) self.assertEqual(["start", "end"], events) def test_parser_target_feed_exception(self): # ET doesn't call .close() on errors events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append("start-" + tag) def end(self, tag): @@ -1007,7 +1107,7 @@ def close(self): parser = self.etree.XMLParser(target=Target()) try: - parser.feed(_bytes('AcaB')) + parser.feed(b'AcaB') done = parser.close() self.fail("error expected, but parsing succeeded") except ValueError: @@ -1020,7 +1120,7 @@ def close(self): def test_parser_target_fromstring_exception(self): # ET doesn't call .close() on errors events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append("start-" + tag) def end(self, tag): @@ -1036,7 +1136,7 @@ def close(self): parser = self.etree.XMLParser(target=Target()) try: - done = self.etree.fromstring(_bytes('AcaB'), + done = self.etree.fromstring(b'AcaB', parser=parser) self.fail("error expected, but parsing succeeded") except ValueError: @@ -1049,7 +1149,7 @@ def close(self): def test_parser_target_feed_no_id_dict(self): # test that target parsing works nicely with the no-id-hash setup events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append("start-" + tag) def end(self, tag): @@ -1063,8 +1163,8 @@ def close(self): parser = self.etree.XMLParser(target=Target(), collect_ids=False) - parser.feed(_bytes('A')) - parser.feed(_bytes('B')) + parser.feed(b'A') + parser.feed(b'B') done = parser.close() self.assertEqual("DONE", done) @@ -1074,7 +1174,7 @@ def close(self): def test_parser_target_comment(self): events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append("start-" + tag) def end(self, tag): @@ -1088,7 +1188,7 @@ def close(self): parser = self.etree.XMLParser(target=Target()) - parser.feed(_bytes('AB')) + parser.feed(b'AB') done = parser.close() self.assertEqual("DONE", done) @@ -1099,7 +1199,7 @@ def close(self): def test_parser_target_pi(self): events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append("start-" + tag) def end(self, tag): @@ -1113,7 +1213,7 @@ def close(self): parser = self.etree.XMLParser(target=Target()) - parser.feed(_bytes('AB')) + parser.feed(b'AB') done = parser.close() self.assertEqual("DONE", done) @@ -1123,7 +1223,7 @@ def close(self): def test_parser_target_cdata(self): events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append("start-" + tag) def end(self, tag): @@ -1136,7 +1236,7 @@ def close(self): parser = self.etree.XMLParser(target=Target(), strip_cdata=False) - parser.feed(_bytes('AB')) + parser.feed(b'AB') done = parser.close() self.assertEqual("DONE", done) @@ -1146,7 +1246,7 @@ def close(self): def test_parser_target_recover(self): events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append("start-" + tag) def end(self, tag): @@ -1160,7 +1260,7 @@ def close(self): parser = self.etree.XMLParser(target=Target(), recover=True) - parser.feed(_bytes('AcaB')) + parser.feed(b'AcaB') done = parser.close() self.assertEqual("DONE", done) @@ -1171,7 +1271,7 @@ def close(self): def test_iterwalk_tag(self): iterwalk = self.etree.iterwalk - root = self.etree.XML(_bytes('')) + root = self.etree.XML(b'') iterator = iterwalk(root, tag="b", events=('start', 'end')) events = list(iterator) @@ -1181,7 +1281,7 @@ def test_iterwalk_tag(self): def test_iterwalk_tag_all(self): iterwalk = self.etree.iterwalk - root = self.etree.XML(_bytes('')) + root = self.etree.XML(b'') iterator = iterwalk(root, tag="*", events=('start', 'end')) events = list(iterator) @@ -1191,7 +1291,7 @@ def test_iterwalk_tag_all(self): def test_iterwalk(self): iterwalk = self.etree.iterwalk - root = self.etree.XML(_bytes('')) + root = self.etree.XML(b'') events = list(iterwalk(root)) self.assertEqual( @@ -1295,7 +1395,7 @@ def test_iterwalk_pis_comments_tree_no_events(self): def test_iterwalk_start(self): iterwalk = self.etree.iterwalk - root = self.etree.XML(_bytes('')) + root = self.etree.XML(b'') iterator = iterwalk(root, events=('start',)) events = list(iterator) @@ -1305,7 +1405,7 @@ def test_iterwalk_start(self): def test_iterwalk_start_end(self): iterwalk = self.etree.iterwalk - root = self.etree.XML(_bytes('')) + root = self.etree.XML(b'') iterator = iterwalk(root, events=('start','end')) events = list(iterator) @@ -1316,7 +1416,7 @@ def test_iterwalk_start_end(self): def test_iterwalk_start_tags(self): iterwalk = self.etree.iterwalk - root = self.etree.XML(_bytes('')) + root = self.etree.XML(b'') iterator = iterwalk(root, events=('start',), tag='b') events = list(iterator) @@ -1326,7 +1426,7 @@ def test_iterwalk_start_tags(self): def test_iterwalk_start_end_tags(self): iterwalk = self.etree.iterwalk - root = self.etree.XML(_bytes('')) + root = self.etree.XML(b'') iterator = iterwalk(root, events=('start', 'end'), tag='b') events = list(iterator) @@ -1336,7 +1436,7 @@ def test_iterwalk_start_end_tags(self): def test_iterwalk_start_end_tags_with_root(self): iterwalk = self.etree.iterwalk - root = self.etree.XML(_bytes('')) + root = self.etree.XML(b'') iterator = iterwalk(root, events=('start', 'end'), tag=('b', 'a')) events = list(iterator) @@ -1350,7 +1450,7 @@ def test_iterwalk_start_end_tags_with_root(self): def test_iterwalk_clear(self): iterwalk = self.etree.iterwalk - root = self.etree.XML(_bytes('')) + root = self.etree.XML(b'') iterator = iterwalk(root) for event, elem in iterator: @@ -1361,7 +1461,7 @@ def test_iterwalk_clear(self): def test_iterwalk_attrib_ns(self): iterwalk = self.etree.iterwalk - root = self.etree.XML(_bytes('')) + root = self.etree.XML(b'') attr_name = '{testns}bla' events = [] @@ -1386,7 +1486,7 @@ def test_iterwalk_attrib_ns(self): def test_iterwalk_end_skip(self): iterwalk = self.etree.iterwalk - root = self.etree.XML(_bytes('')) + root = self.etree.XML(b'') iterator = iterwalk(root) tags = [] @@ -1399,7 +1499,7 @@ def test_iterwalk_end_skip(self): def test_iterwalk_start_end_skip(self): iterwalk = self.etree.iterwalk - root = self.etree.XML(_bytes('')) + root = self.etree.XML(b'') iterator = iterwalk(root, events=('start', 'end')) tags = [] @@ -1451,7 +1551,7 @@ def test_iterwalk_ns_skip(self): def test_iterwalk_getiterator(self): iterwalk = self.etree.iterwalk - root = self.etree.XML(_bytes('')) + root = self.etree.XML(b'') counts = [] for event, elem in iterwalk(root): @@ -1460,6 +1560,27 @@ def test_iterwalk_getiterator(self): [1,2,1,4], counts) + def test_walk_after_parse_failure(self): + # This used to be an issue because libxml2 can leak empty namespaces + # between failed parser runs. iterwalk() failed to handle such a tree. + parser = etree.XMLParser() + + try: + etree.XML('''''', parser=parser) + except etree.XMLSyntaxError: + pass + else: + assert False, "invalid input did not fail to parse" + + et = etree.XML(''' ''', parser=parser) + try: + ns = next(etree.iterwalk(et, events=('start-ns',))) + except StopIteration: + # This would be the expected result, because there was no namespace + pass + else: + assert False, "Found unexpected namespace '%s'" % ns + def test_itertext_comment_pi(self): # https://bugs.launchpad.net/lxml/+bug/1844674 XML = self.etree.XML @@ -1471,6 +1592,16 @@ def test_itertext_comment_pi(self): self.assertEqual(["RTEXT", "ATAIL", "CTAIL", " PITAIL "], text) + def test_itertext_no_tails(self): + XML = self.etree.XML + root = XML(_bytes( + "RTEXTATEXTATAILCTAIL PITAIL " + )) + + text = list(root.itertext(with_tail=False)) + self.assertEqual(["RTEXT", "ATEXT"], + text) + def test_resolve_string_dtd(self): parse = self.etree.parse parser = self.etree.XMLParser(dtd_validation=True) @@ -1486,7 +1617,7 @@ def resolve(self, url, id, context): parser.resolvers.add(MyResolver()) - xml = _str('&myentity;') % test_url + xml = '&myentity;' % test_url tree = parse(StringIO(xml), parser) root = tree.getroot() self.assertEqual(root.text, test_url) @@ -1507,7 +1638,7 @@ def resolve(self, url, id, context): parser.resolvers.add(MyResolver()) - xml = _str('&myentity;') % test_url + xml = '&myentity;' % test_url tree = parse(StringIO(xml), parser) root = tree.getroot() self.assertEqual(root.text, test_url) @@ -1528,7 +1659,7 @@ def resolve(self, url, id, context): parser.resolvers.add(MyResolver()) - xml = _str('&myentity;') % test_url + xml = '&myentity;' % test_url tree = parse(StringIO(xml), parser) root = tree.getroot() self.assertEqual(root.text, test_url) @@ -1547,7 +1678,7 @@ def resolve(self, url, id, context): parser.resolvers.add(MyResolver()) - xml = _str('') % test_url + xml = '' % test_url tree = parse(StringIO(xml), parser) root = tree.getroot() self.assertEqual( @@ -1572,7 +1703,7 @@ def resolve(self, url, id, context): parser.resolvers.add(MyResolver()) - xml = _str('') % test_url + xml = '' % test_url tree = parse(StringIO(xml), parser, base_url=fileUrlInTestDir('__test.xml')) root = tree.getroot() @@ -1595,7 +1726,7 @@ def resolve(self, url, id, context): parser.resolvers.add(MyResolver()) - xml = _str('') % test_url + xml = '' % test_url tree = parse(StringIO(xml), parser) root = tree.getroot() self.assertEqual( @@ -1609,7 +1740,7 @@ def test_resolve_empty(self): assertEqual = self.assertEqual test_url = _str("__nosuch.dtd") - class check(object): + class check: resolved = False class MyResolver(self.etree.Resolver): @@ -1620,7 +1751,7 @@ def resolve(self, url, id, context): parser.resolvers.add(MyResolver()) - xml = _str('&myentity;') % test_url + xml = '&myentity;' % test_url self.assertRaises(etree.XMLSyntaxError, parse, StringIO(xml), parser) self.assertTrue(check.resolved) @@ -1637,7 +1768,7 @@ def resolve(self, url, id, context): parser.resolvers.add(MyResolver()) - xml = '&myentity;' + xml = b'&myentity;' self.assertRaises(_LocalException, parse, BytesIO(xml), parser) def test_entity_parse(self): @@ -1646,7 +1777,7 @@ def test_entity_parse(self): parser = self.etree.XMLParser(resolve_entities=False) Entity = self.etree.Entity - xml = _bytes('&myentity;') + xml = b'&myentity;' tree = parse(BytesIO(xml), parser) root = tree.getroot() self.assertEqual(root[0].tag, Entity) @@ -1654,16 +1785,118 @@ def test_entity_parse(self): self.assertEqual(root[0].tail, None) self.assertEqual(root[0].name, "myentity") - self.assertEqual(_bytes('&myentity;'), + self.assertEqual(b'&myentity;', tostring(root)) + @contextlib.contextmanager + def _xml_test_file(self, name, content=b'XML'): + temp_dir = tempfile.mkdtemp() + try: + xml_file = os.path.join(temp_dir, name) + with open(xml_file, 'wb') as tmpfile: + tmpfile.write(content) + yield xml_file + finally: + shutil.rmtree(temp_dir) + + def test_entity_parse_external(self): + fromstring = self.etree.fromstring + tostring = self.etree.tostring + parser = self.etree.XMLParser(resolve_entities=True) + + with self._xml_test_file("entity.xml") as entity_file: + xml = ''' + + ]> + &my_external_entity; + ''' % path2url(entity_file) + root = fromstring(xml, parser) + + self.assertEqual(b'XML', + tostring(root)) + self.assertEqual(root.tag, 'doc') + self.assertEqual(root[0].tag, 'evil') + self.assertEqual(root[0].text, 'XML') + self.assertEqual(root[0].tail, None) + + def test_entity_parse_external_no_resolve(self): + fromstring = self.etree.fromstring + parser = self.etree.XMLParser(resolve_entities=False) + Entity = self.etree.Entity + + with self._xml_test_file("entity.xml") as entity_file: + xml = ''' + + ]> + &my_external_entity; + ''' % path2url(entity_file) + root = fromstring(xml, parser) + + self.assertEqual(root[0].tag, Entity) + self.assertEqual(root[0].text, "&my_external_entity;") + + def test_entity_parse_no_external_default(self): + fromstring = self.etree.fromstring + + with self._xml_test_file("entity.xml") as entity_file: + xml = ''' + + ]> + &my_failing_external_entity; + ''' % path2url(entity_file) + + try: + fromstring(xml) + except self.etree.XMLSyntaxError as exc: + exception = exc + else: + self.assertTrue(False, "XMLSyntaxError was not raised") + + self.assertIn("my_failing_external_entity", str(exception)) + self.assertTrue(exception.error_log) + # Depending on the libxml2 version, we get different errors here, + # not necessarily the one that lxml produced. But it should fail either way. + for error in exception.error_log: + if "my_failing_external_entity" in error.message: + self.assertEqual(5, error.line) + break + else: + self.assertFalse("entity error not found in parser error log") + + def test_entity_parse_xxe(self): + fromstring = self.etree.fromstring + tostring = self.etree.tostring + xml = textwrap.dedent("""\ + + + "> + '> + %a; + %b; + ]> + &c; + """).format(FILE=fileUrlInTestDir("test-string.xml")).encode('UTF-8') + + try: + root = fromstring(xml) + except self.etree.XMLSyntaxError: + # This is the normal outcome - we should never access the external file. + pass + else: + self.assertNotIn("Søk på nettet", tostring(root, encoding="unicode")) + def test_entity_restructure(self): - xml = _bytes(''' ]> + xml = b''' ]>   - ''') + ''' parser = self.etree.XMLParser(resolve_entities=False) root = etree.fromstring(xml, parser) @@ -1689,7 +1922,7 @@ def test_entity_append(self): self.assertEqual(root[0].tail, None) self.assertEqual(root[0].name, "test") - self.assertEqual(_bytes('&test;'), + self.assertEqual(b'&test;', tostring(root)) def test_entity_append_parsed(self): @@ -1735,7 +1968,7 @@ def test_cdata(self): self.assertEqual('test', root.text) - self.assertEqual(_bytes(''), + self.assertEqual(b'', tostring(root)) def test_cdata_tail(self): @@ -1749,14 +1982,14 @@ def test_cdata_tail(self): child.tail = CDATA('test') self.assertEqual('test', child.tail) - self.assertEqual(_bytes(''), + self.assertEqual(b'', tostring(root)) root = Element("root") root.tail = CDATA('test') self.assertEqual('test', root.tail) - self.assertEqual(_bytes(''), + self.assertEqual(b'', tostring(root)) def test_cdata_type(self): @@ -1787,21 +2020,35 @@ def test_cdata_errors(self): def test_cdata_parser(self): tostring = self.etree.tostring parser = self.etree.XMLParser(strip_cdata=False) - root = self.etree.XML(_bytes(''), parser) + root = self.etree.XML(b'', parser) self.assertEqual('test', root.text) - self.assertEqual(_bytes(''), + self.assertEqual(b'', tostring(root)) def test_cdata_xpath(self): tostring = self.etree.tostring parser = self.etree.XMLParser(strip_cdata=False) - root = self.etree.XML(_bytes(''), parser) - self.assertEqual(_bytes(''), + root = self.etree.XML(b'', parser) + self.assertEqual(b'', tostring(root)) self.assertEqual(['test'], root.xpath('//text()')) + def test_cdata_split_cdend(self): + # Tests that existing ']]>' in CDATA is split to 'escape' it + CDATA = self.etree.CDATA + Element = self.etree.Element + tostring = self.etree.tostring + + root = Element("root") + root.text = CDATA('test]]>') + + self.assertEqual('test]]>', + root.text) + self.assertEqual(b']]>', + tostring(root)) + # TypeError in etree, AssertionError in ElementTree; def test_setitem_assert(self): Element = self.etree.Element @@ -1809,7 +2056,7 @@ def test_setitem_assert(self): a = Element('a') b = SubElement(a, 'b') - + self.assertRaises(TypeError, a.__setitem__, 0, 'foo') @@ -1847,6 +2094,42 @@ def test_addnext(self): self.assertEqual(['b', 'a'], [c.tag for c in root]) + def test_addnext_tails(self): + Element = self.etree.Element + SubElement = self.etree.SubElement + root = Element('root') + SubElement(root, 'a').tail = "A" + SubElement(root, 'b').tail = "B" + SubElement(root, 'c').tail = "C" + SubElement(root, 'd').tail = "D" + + self.assertEqual(['a', 'b', 'c', 'd'], + [c.tag for c in root]) + self.assertEqual(['A', 'B', 'C', 'D'], [c.tail for c in root]) + + root[2].addnext(root[1]) + self.assertEqual(['a', 'c', 'b', 'd'], + [c.tag for c in root]) + self.assertEqual(['A', 'C', 'B', 'D'], [c.tail for c in root]) + + def test_addnext_with_tail(self): + Element = self.etree.Element + SubElement = self.etree.SubElement + root = Element('root') + SubElement(root, 'a') + SubElement(root, 'b').tail = "B" + SubElement(root, 'c') + SubElement(root, 'd') + + self.assertEqual(['a', 'b', 'c', 'd'], + [c.tag for c in root]) + self.assertEqual([None, 'B', None, None], [c.tail for c in root]) + + root[2].addnext(root[1]) + self.assertEqual(['a', 'c', 'b', 'd'], + [c.tag for c in root]) + self.assertEqual([None, None, 'B', None], [c.tail for c in root]) + def test_addprevious(self): Element = self.etree.Element SubElement = self.etree.SubElement @@ -1860,6 +2143,42 @@ def test_addprevious(self): self.assertEqual(['b', 'a'], [c.tag for c in root]) + def test_addprevious_tails(self): + Element = self.etree.Element + SubElement = self.etree.SubElement + root = Element('root') + SubElement(root, 'a').tail = "A" + SubElement(root, 'b').tail = "B" + SubElement(root, 'c').tail = "C" + SubElement(root, 'd').tail = "D" + + self.assertEqual(['a', 'b', 'c', 'd'], + [c.tag for c in root]) + self.assertEqual(['A', 'B', 'C', 'D'], [c.tail for c in root]) + + root[1].addprevious(root[2]) + self.assertEqual(['a', 'c', 'b', 'd'], + [c.tag for c in root]) + self.assertEqual(['A', 'C', 'B', 'D'], [c.tail for c in root]) + + def test_addprevious_with_tail(self): + Element = self.etree.Element + SubElement = self.etree.SubElement + root = Element('root') + SubElement(root, 'a') + SubElement(root, 'b') + SubElement(root, 'c').tail = "C" + SubElement(root, 'd') + + self.assertEqual(['a', 'b', 'c', 'd'], + [c.tag for c in root]) + self.assertEqual([None, None, 'C', None], [c.tail for c in root]) + + root[1].addprevious(root[2]) + self.assertEqual(['a', 'c', 'b', 'd'], + [c.tag for c in root]) + self.assertEqual([None, 'C', None, None], [c.tail for c in root]) + def test_addnext_cycle(self): Element = self.etree.Element SubElement = self.etree.SubElement @@ -1949,10 +2268,10 @@ def test_addprevious_pi(self): pi = PI('TARGET', 'TEXT') pi.tail = "TAIL" - self.assertEqual(_bytes(''), + self.assertEqual(b'', self._writeElement(root)) root[0].addprevious(pi) - self.assertEqual(_bytes('TAIL'), + self.assertEqual(b'TAIL', self._writeElement(root)) def test_addprevious_root_pi(self): @@ -1962,10 +2281,10 @@ def test_addprevious_root_pi(self): pi = PI('TARGET', 'TEXT') pi.tail = "TAIL" - self.assertEqual(_bytes(''), + self.assertEqual(b'', self._writeElement(root)) root.addprevious(pi) - self.assertEqual(_bytes('\n'), + self.assertEqual(b'\n', self._writeElement(root)) def test_addnext_pi(self): @@ -1977,10 +2296,10 @@ def test_addnext_pi(self): pi = PI('TARGET', 'TEXT') pi.tail = "TAIL" - self.assertEqual(_bytes(''), + self.assertEqual(b'', self._writeElement(root)) root[0].addnext(pi) - self.assertEqual(_bytes('TAIL'), + self.assertEqual(b'TAIL', self._writeElement(root)) def test_addnext_root_pi(self): @@ -1990,10 +2309,10 @@ def test_addnext_root_pi(self): pi = PI('TARGET', 'TEXT') pi.tail = "TAIL" - self.assertEqual(_bytes(''), + self.assertEqual(b'', self._writeElement(root)) root.addnext(pi) - self.assertEqual(_bytes('\n'), + self.assertEqual(b'\n', self._writeElement(root)) def test_addnext_comment(self): @@ -2005,10 +2324,10 @@ def test_addnext_comment(self): comment = Comment('TEXT ') comment.tail = "TAIL" - self.assertEqual(_bytes(''), + self.assertEqual(b'', self._writeElement(root)) root[0].addnext(comment) - self.assertEqual(_bytes('TAIL'), + self.assertEqual(b'TAIL', self._writeElement(root)) def test_addnext_root_comment(self): @@ -2018,10 +2337,10 @@ def test_addnext_root_comment(self): comment = Comment('TEXT ') comment.tail = "TAIL" - self.assertEqual(_bytes(''), + self.assertEqual(b'', self._writeElement(root)) root.addnext(comment) - self.assertEqual(_bytes('\n'), + self.assertEqual(b'\n', self._writeElement(root)) def test_addprevious_comment(self): @@ -2033,10 +2352,10 @@ def test_addprevious_comment(self): comment = Comment('TEXT ') comment.tail = "TAIL" - self.assertEqual(_bytes(''), + self.assertEqual(b'', self._writeElement(root)) root[0].addprevious(comment) - self.assertEqual(_bytes('TAIL'), + self.assertEqual(b'TAIL', self._writeElement(root)) def test_addprevious_root_comment(self): @@ -2046,17 +2365,17 @@ def test_addprevious_root_comment(self): comment = Comment('TEXT ') comment.tail = "TAIL" - self.assertEqual(_bytes(''), + self.assertEqual(b'', self._writeElement(root)) root.addprevious(comment) - self.assertEqual(_bytes('\n'), + self.assertEqual(b'\n', self._writeElement(root)) # ET's Elements have items() and key(), but not values() def test_attribute_values(self): XML = self.etree.XML - - root = XML(_bytes('')) + + root = XML(b'') values = root.values() values.sort() self.assertEqual(['Alpha', 'Beta', 'Gamma'], values) @@ -2069,7 +2388,7 @@ def test_comment_empty(self): a = Element('a') a.append(Comment()) self.assertEqual( - _bytes(''), + b'', self._writeElement(a)) # ElementTree ignores comments @@ -2077,7 +2396,7 @@ def test_comment_parse_empty(self): ElementTree = self.etree.ElementTree tostring = self.etree.tostring - xml = _bytes('') + xml = b'' f = BytesIO(xml) doc = ElementTree(file=f) a = doc.getroot() @@ -2091,8 +2410,8 @@ def test_comment_parse_empty(self): # ElementTree ignores comments def test_comment_no_proxy_yet(self): ElementTree = self.etree.ElementTree - - f = BytesIO('') + + f = BytesIO(b'') doc = ElementTree(file=f) a = doc.getroot() self.assertEqual( @@ -2144,8 +2463,8 @@ def test_dump_none(self): def test_prefix(self): ElementTree = self.etree.ElementTree - - f = BytesIO('') + + f = BytesIO(b'') doc = ElementTree(file=f) a = doc.getroot() self.assertEqual( @@ -2157,8 +2476,8 @@ def test_prefix(self): def test_prefix_default_ns(self): ElementTree = self.etree.ElementTree - - f = BytesIO('') + + f = BytesIO(b'') doc = ElementTree(file=f) a = doc.getroot() self.assertEqual( @@ -2191,8 +2510,8 @@ def test_getparent(self): def test_iterchildren(self): XML = self.etree.XML - - root = XML(_bytes('TwoHm')) + + root = XML(b'TwoHm') result = [] for el in root.iterchildren(): result.append(el.tag) @@ -2200,8 +2519,8 @@ def test_iterchildren(self): def test_iterchildren_reversed(self): XML = self.etree.XML - - root = XML(_bytes('TwoHm')) + + root = XML(b'TwoHm') result = [] for el in root.iterchildren(reversed=True): result.append(el.tag) @@ -2209,8 +2528,8 @@ def test_iterchildren_reversed(self): def test_iterchildren_tag(self): XML = self.etree.XML - - root = XML(_bytes('TwoHmBla')) + + root = XML(b'TwoHmBla') result = [] for el in root.iterchildren(tag='two'): result.append(el.text) @@ -2219,7 +2538,7 @@ def test_iterchildren_tag(self): def test_iterchildren_tag_posarg(self): XML = self.etree.XML - root = XML(_bytes('TwoHmBla')) + root = XML(b'TwoHmBla') result = [] for el in root.iterchildren('two'): result.append(el.text) @@ -2227,8 +2546,8 @@ def test_iterchildren_tag_posarg(self): def test_iterchildren_tag_reversed(self): XML = self.etree.XML - - root = XML(_bytes('TwoHmBla')) + + root = XML(b'TwoHmBla') result = [] for el in root.iterchildren(reversed=True, tag='two'): result.append(el.text) @@ -2237,7 +2556,7 @@ def test_iterchildren_tag_reversed(self): def test_iterchildren_tag_multiple(self): XML = self.etree.XML - root = XML(_bytes('TwoHmBla')) + root = XML(b'TwoHmBla') result = [] for el in root.iterchildren(tag=['two', 'three']): result.append(el.text) @@ -2246,7 +2565,7 @@ def test_iterchildren_tag_multiple(self): def test_iterchildren_tag_multiple_posarg(self): XML = self.etree.XML - root = XML(_bytes('TwoHmBla')) + root = XML(b'TwoHmBla') result = [] for el in root.iterchildren('two', 'three'): result.append(el.text) @@ -2255,7 +2574,7 @@ def test_iterchildren_tag_multiple_posarg(self): def test_iterchildren_tag_multiple_reversed(self): XML = self.etree.XML - root = XML(_bytes('TwoHmBla')) + root = XML(b'TwoHmBla') result = [] for el in root.iterchildren(reversed=True, tag=['two', 'three']): result.append(el.text) @@ -2558,7 +2877,7 @@ def test_itersiblings_tag_multiple(self): def test_parseid(self): parseid = self.etree.parseid XML = self.etree.XML - xml_text = _bytes(''' + xml_text = b''' @@ -2573,7 +2892,7 @@ def test_parseid(self):

XML:ID paragraph.

...

- ''') + ''' tree, dic = parseid(BytesIO(xml_text)) root = tree.getroot() @@ -2593,7 +2912,7 @@ def test_parseid(self): def test_XMLDTDID(self): XMLDTDID = self.etree.XMLDTDID XML = self.etree.XML - xml_text = _bytes(''' + xml_text = b''' @@ -2608,7 +2927,7 @@ def test_XMLDTDID(self):

XML:ID paragraph.

...

- ''') + ''' root, dic = XMLDTDID(xml_text) root2 = XML(xml_text) @@ -2627,14 +2946,14 @@ def test_XMLDTDID(self): def test_XMLDTDID_empty(self): XMLDTDID = self.etree.XMLDTDID XML = self.etree.XML - xml_text = _bytes(''' + xml_text = b'''

...

...

Regular paragraph.

...

- ''') + ''' root, dic = XMLDTDID(xml_text) root2 = XML(xml_text) @@ -2646,7 +2965,7 @@ def test_XMLDTDID_empty(self): def test_XMLDTDID_no_id_dict(self): XMLDTDID = self.etree.XMLDTDID XML = self.etree.XML - xml_text = _bytes(''' + xml_text = b''' @@ -2661,7 +2980,7 @@ def test_XMLDTDID_no_id_dict(self):

XML:ID paragraph.

...

- ''') + ''' parser = etree.XMLParser(collect_ids=False) root, dic = XMLDTDID(xml_text, parser=parser) @@ -2676,19 +2995,8 @@ def _checkIDDict(self, dic, expected): len(expected)) self.assertEqual(sorted(dic.items()), sorted(expected.items())) - if sys.version_info < (3,): - self.assertEqual(sorted(dic.iteritems()), - sorted(expected.iteritems())) self.assertEqual(sorted(dic.keys()), sorted(expected.keys())) - if sys.version_info < (3,): - self.assertEqual(sorted(dic.iterkeys()), - sorted(expected.iterkeys())) - if sys.version_info < (3,): - self.assertEqual(sorted(dic.values()), - sorted(expected.values())) - self.assertEqual(sorted(dic.itervalues()), - sorted(expected.itervalues())) def test_register_namespace_xml(self): self.assertRaises(ValueError, self.etree.register_namespace, @@ -2706,9 +3014,9 @@ def test_namespaces(self): 'foo', e.prefix) self.assertEqual( - _bytes(''), + b'', self._writeElement(e)) - + def test_namespaces_default(self): etree = self.etree @@ -2721,7 +3029,7 @@ def test_namespaces_default(self): '{http://ns.infrae.com/foo}bar', e.tag) self.assertEqual( - _bytes(''), + b'', self._writeElement(e)) def test_namespaces_default_and_other(self): @@ -2732,7 +3040,7 @@ def test_namespaces_default_and_other(self): self.assertEqual(None, e.prefix) self.assertEqual('{http://ns.infrae.com/foo}bar', e.tag) self.assertEqual( - _bytes(''), + b'', self._writeElement(e)) def test_namespaces_default_and_attr(self): @@ -2743,7 +3051,7 @@ def test_namespaces_default_and_attr(self): e = etree.Element('{http://ns.infrae.com/foo}bar', nsmap=r) e.set('{http://ns.infrae.com/hoi}test', 'value') self.assertEqual( - _bytes(''), + b'', self._writeElement(e)) def test_attribute_keeps_namespace_prefix_on_merge(self): @@ -2757,14 +3065,14 @@ def test_attribute_keeps_namespace_prefix_on_merge(self): sub.attrib['{http://test/ns}attr'] = 'value' self.assertEqual(sub.attrib['{http://test/ns}attr'], 'value') self.assertEqual( - _bytes(''), + b'', etree.tostring(sub)) root.append(sub) self.assertEqual( - _bytes('' - '' - ''), + b'' + b'' + b'', etree.tostring(root)) def test_attribute_keeps_namespace_prefix_on_merge_with_nons(self): @@ -2777,14 +3085,14 @@ def test_attribute_keeps_namespace_prefix_on_merge_with_nons(self): sub.attrib['{http://test/ns}attr'] = 'value' self.assertEqual(sub.attrib['{http://test/ns}attr'], 'value') self.assertEqual( - _bytes(''), + b'', etree.tostring(sub)) root.append(sub) self.assertEqual( - _bytes('' - '' - ''), + b'' + b'' + b'', etree.tostring(root)) def test_attribute_gets_namespace_prefix_on_merge_with_nons(self): @@ -2797,16 +3105,16 @@ def test_attribute_gets_namespace_prefix_on_merge_with_nons(self): sub.attrib['{http://test/ns}attr'] = 'value' self.assertEqual(sub.attrib['{http://test/ns}attr'], 'value') self.assertEqual( - _bytes(''), + b'', etree.tostring(sub)) root.append(sub) self.assertEqual( - _bytes('' - '' - ''), + b'' + b'' + b'', etree.tostring(root)) def test_attribute_gets_namespace_prefix_on_merge(self): @@ -2821,26 +3129,26 @@ def test_attribute_gets_namespace_prefix_on_merge(self): sub.attrib['{http://test/ns}attr'] = 'value' self.assertEqual(sub.attrib['{http://test/ns}attr'], 'value') self.assertEqual( - _bytes(''), + b'', etree.tostring(sub)) root.append(sub) self.assertEqual( - _bytes('' - '' - ''), + b'' + b'' + b'', etree.tostring(root)) def test_namespaces_elementtree(self): etree = self.etree r = {None: 'http://ns.infrae.com/foo', - 'hoi': 'http://ns.infrae.com/hoi'} + 'hoi': 'http://ns.infrae.com/hoi'} e = etree.Element('{http://ns.infrae.com/foo}z', nsmap=r) tree = etree.ElementTree(element=e) etree.SubElement(e, '{http://ns.infrae.com/hoi}x') self.assertEqual( - _bytes(''), + b'', self._writeElement(e)) def test_namespaces_default_copy_element(self): @@ -2890,48 +3198,48 @@ def test_namespaces_copy_element(self): def test_namespaces_reuse_after_move(self): ns_href = "http://a.b.c" one = self.etree.fromstring( - _bytes('' % ns_href)) + '' % ns_href) baz = one[0][0] two = self.etree.fromstring( - _bytes('' % ns_href)) + '' % ns_href) two.append(baz) del one # make sure the source document is deallocated self.assertEqual('{%s}baz' % ns_href, baz.tag) self.assertEqual( - _bytes('' % ns_href), + ('' % ns_href).encode('utf-8'), self.etree.tostring(two)) def test_namespace_cleanup(self): - xml = _bytes( - '' - '' - '' - '' + xml = ( + b'' + b'' + b'' + b'' ) root = self.etree.fromstring(xml) self.assertEqual(xml, self.etree.tostring(root)) self.etree.cleanup_namespaces(root) self.assertEqual( - _bytes(''), + b'', self.etree.tostring(root)) def test_namespace_cleanup_attributes(self): - xml = _bytes( - '' - '' - '' - '' + xml = ( + b'' + b'' + b'' + b'' ) root = self.etree.fromstring(xml) self.assertEqual(xml, self.etree.tostring(root)) self.etree.cleanup_namespaces(root) self.assertEqual( - _bytes('' - '' - '' - ''), + b'' + b'' + b'' + b'', self.etree.tostring(root)) def test_namespace_cleanup_many(self): @@ -2972,10 +3280,10 @@ def test_namespace_cleanup_deep_to_top(self): self.etree.tostring(root)) def test_namespace_cleanup_keep_prefixes(self): - xml = ('' - '' - 'foo:bar' - '').encode('utf8') + xml = (b'' + b'' + b'foo:bar' + b'') root = self.etree.fromstring(xml) self.assertEqual(xml, self.etree.tostring(root)) self.etree.cleanup_namespaces(root, keep_ns_prefixes=['foo']) @@ -2987,12 +3295,12 @@ def test_namespace_cleanup_keep_prefixes(self): self.etree.tostring(root)) def test_namespace_cleanup_keep_prefixes_top(self): - xml = ('' - '' - '' - 'foo:bar' - '' - '').encode('utf8') + xml = (b'' + b'' + b'' + b'foo:bar' + b'' + b'') root = self.etree.fromstring(xml) self.assertEqual(xml, self.etree.tostring(root)) self.etree.cleanup_namespaces( @@ -3008,6 +3316,27 @@ def test_namespace_cleanup_keep_prefixes_top(self): b'', self.etree.tostring(root)) + def test_namespace_cleanup_replace_default_namespace(self): + # LP#2148019, made xmlNewNs() return NULL in _setNodeNamespaces() due to prefix reuse. + xml = b'childtext' + root = self.etree.fromstring(xml) + self.etree.SubElement(root, '{nsb}newchild').text = 'newtext' + self.etree.cleanup_namespaces(root, top_nsmap={None: "nsb"}) + + def test_namespace_cleanup_replace_namespaces(self): + # LP#2148019, made xmlNewNs() return NULL in _setNodeNamespaces() due to prefix reuse. + xml = b'childtext' + root = self.etree.fromstring(xml) + self.etree.SubElement(root, '{nsb}newchild').text = 'newtext' + self.etree.cleanup_namespaces(root, top_nsmap={None: "nsb", 'pa': "nsa"}) + + def test_namespace_cleanup_swap_prefixes(self): + # LP#2148019, made xmlNewNs() return NULL in _setNodeNamespaces() due to prefix reuse. + xml = b'achildtextbchildtext' + root = self.etree.fromstring(xml) + self.etree.SubElement(root, '{nsb}newchild').text = 'newtext' + self.etree.cleanup_namespaces(root, top_nsmap={'a': "nsb", 'b': "nsa"}) + def test_element_nsmap(self): etree = self.etree @@ -3036,11 +3365,29 @@ def test_subelement_nsmap(self): def test_html_prefix_nsmap(self): etree = self.etree - el = etree.HTML('aa').find('.//page-description') - if etree.LIBXML_VERSION < (2, 9, 11): - self.assertEqual({'hha': None}, el.nsmap) + el = etree.HTML('aa') + pd = el[-1] + while len(pd): + pd = pd[-1] + + if etree.LIBXML_VERSION >= (2, 10, 4): + # "Prefix" is kept as part of the tag name. + self.assertEqual("hha:page-description", pd.tag) + self.assertIsNone(el.find('.//page-description')) + self.assertIsNotNone(el.find('.//hha:page-description')) # no namespaces! + for e in el.iter(): + self.assertEqual({}, e.nsmap) + elif etree.LIBXML_VERSION >= (2, 9, 11): + # "Prefix" is stripped. + self.assertEqual("page-description", pd.tag) + self.assertIsNotNone(el.find('.//page-description')) + for e in el.iter(): + self.assertEqual({}, e.nsmap) else: - self.assertEqual({}, el.nsmap) + # "Prefix" is parsed as XML prefix. + self.assertEqual("page-description", pd.tag) + pd = el.find('.//page-description') + self.assertEqual({'hha': None}, pd.nsmap) def test_getchildren(self): Element = self.etree.Element @@ -3052,7 +3399,7 @@ def test_getchildren(self): d = SubElement(b, 'd') e = SubElement(c, 'e') self.assertEqual( - _bytes(''), + b'', self.etree.tostring(a, method="c14n")) self.assertEqual( [b, c], @@ -3521,7 +3868,7 @@ def test_elementtree_iter_qname(self): ElementTree = self.etree.ElementTree QName = self.etree.QName tree = ElementTree(XML( - _bytes(''))) + b'')) self.assertEqual( list(tree.iter(QName("b"))), list(tree.iter("b")), @@ -3544,14 +3891,14 @@ def test_elementtree_find_qname(self): XML = self.etree.XML ElementTree = self.etree.ElementTree QName = self.etree.QName - tree = ElementTree(XML(_bytes(''))) + tree = ElementTree(XML(b'')) self.assertEqual(tree.find(QName("c")), tree.getroot()[2]) def test_elementtree_findall_qname(self): XML = self.etree.XML ElementTree = self.etree.ElementTree QName = self.etree.QName - tree = ElementTree(XML(_bytes(''))) + tree = ElementTree(XML(b'')) self.assertEqual(len(list(tree.findall(QName("c")))), 1) def test_elementtree_findall_ns_qname(self): @@ -3559,20 +3906,20 @@ def test_elementtree_findall_ns_qname(self): ElementTree = self.etree.ElementTree QName = self.etree.QName tree = ElementTree(XML( - _bytes(''))) + b'')) self.assertEqual(len(list(tree.findall(QName("b")))), 2) self.assertEqual(len(list(tree.findall(QName("X", "b")))), 1) def test_findall_ns(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') self.assertEqual(len(root.findall(".//{X}b")), 2) self.assertEqual(len(root.findall(".//{X}*")), 2) self.assertEqual(len(root.findall(".//b")), 3) def test_findall_different_nsmaps(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') nsmap = {'xx': 'X'} self.assertEqual(len(root.findall(".//xx:b", namespaces=nsmap)), 2) self.assertEqual(len(root.findall(".//xx:*", namespaces=nsmap)), 2) @@ -3584,7 +3931,7 @@ def test_findall_different_nsmaps(self): def test_findall_empty_prefix(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') nsmap = {'xx': 'X'} self.assertEqual(len(root.findall(".//xx:b", namespaces=nsmap)), 2) nsmap = {'xx': 'X', None: 'Y'} @@ -3594,11 +3941,29 @@ def test_findall_empty_prefix(self): def test_findall_syntax_error(self): XML = self.etree.XML - root = XML(_bytes('')) + root = XML(b'') self.assertRaises(SyntaxError, root.findall, '') self.assertRaises(SyntaxError, root.findall, '//') # absolute path on Element self.assertRaises(SyntaxError, root.findall, './//') + def test_iterfind_tree_modifications(self): + def insert_prev(it, tag='tagname'): + for element in it: + yield element + element.addprevious(element.makeelement(tag)) + + tostring = self.etree.tostring + root = self.etree.XML('ABC') + it = root.iterfind('./*') + tags = [element.tag for element in itertools.islice(insert_prev(it, tag='x'), 4)] + self.assertEqual(tostring(root), b'ABC') + self.assertListEqual(tags, ['a', 'b', 'c']) + + root = self.etree.XML('ABC') + it = root.iterfind('./b[1]') + tags = [element.tag for element in insert_prev(it, tag='b')] + self.assertListEqual(tags, ['b']) + def test_index(self): etree = self.etree e = etree.Element('foo') @@ -3786,12 +4151,12 @@ def test_setslice_step_overrun(self): def test_sourceline_XML(self): XML = self.etree.XML - root = XML(_bytes(''' + root = XML(b''' - ''')) + ''') self.assertEqual( [2, 2, 4], @@ -3799,12 +4164,12 @@ def test_sourceline_XML(self): def test_large_sourceline_XML(self): XML = self.etree.XML - root = XML(_bytes( - '\n' - '' + '\n' * 65536 + - '

' + '\n' * 65536 + '

\n' + - '
\n' - '
')) + root = XML( + b'\n' + b'' + b'\n' * 65536 + + b'

' + b'\n' * 65536 + b'

\n' + + b'
\n' + b'
') if self.etree.LIBXML_VERSION >= (2, 9): expected = [2, 131074, 131076] @@ -3823,7 +4188,7 @@ def test_sourceline_parse(self): def test_sourceline_iterparse_end(self): iterparse = self.etree.iterparse - lines = [ el.sourceline for (event, el) in + lines = [ el.sourceline for (event, el) in iterparse(fileInTestDir('include/test_xinclude.xml')) ] self.assertEqual( @@ -3832,7 +4197,7 @@ def test_sourceline_iterparse_end(self): def test_sourceline_iterparse_start(self): iterparse = self.etree.iterparse - lines = [ el.sourceline for (event, el) in + lines = [ el.sourceline for (event, el) in iterparse(fileInTestDir('include/test_xinclude.xml'), events=("start",)) ] @@ -3840,6 +4205,50 @@ def test_sourceline_iterparse_start(self): [1, 2, 3], lines) + def test_very_large_sourceline_iterparse(self): + if self.etree.LIBXML_VERSION < (2, 11): + return + # libxml2 has a default limit of 10M for text content, so we use 125*3*6M text chunks, 2.2G total. + lines = b'\n' * (1024 * 1024 * 6) + data = [b'\n', b'\n'] + data += [lines + b'
', lines + b'

', lines + b'

'] * 125 + data.append(b'\n

xxx

') + data.append(b'\n
') + expected_last_line = 3 + (len(data) - 4) * len(lines) + assert expected_last_line > 2**31 + + chunks = iter(data) + + class Source(object): + def read(self, _): + try: + return next(chunks) + except StopIteration: + return b'' + + events = self.etree.iterparse(Source(), events=['end']) + + root = last_el = None + for _, el in events: + root = last_el = el.getparent() + break + + max_line = 0 + for _, el in events: + if len(root) > 20: + del root[:18] + line = last_el.sourceline + if line is not None: + if max_line > line: + # This is the main thing that we currently test: + self.assertLessEqual(max_line, line) + max_line = line + last_el = el + + # The final line does not seem very accurate, so we stop here. + #self.assertGreater(max_line, 2**31) + #self.assertEqual(expected_last_line, max_line) + def test_sourceline_element(self): Element = self.etree.Element SubElement = self.etree.SubElement @@ -3852,13 +4261,13 @@ def test_sourceline_element(self): def test_XML_base_url_docinfo(self): etree = self.etree - root = etree.XML(_bytes(""), base_url="http://no/such/url") + root = etree.XML(b"", base_url="http://no/such/url") docinfo = root.getroottree().docinfo self.assertEqual(docinfo.URL, "http://no/such/url") def test_XML_set_base_url_docinfo(self): etree = self.etree - root = etree.XML(_bytes(""), base_url="http://no/such/url") + root = etree.XML(b"", base_url="http://no/such/url") docinfo = root.getroottree().docinfo self.assertEqual(docinfo.URL, "http://no/such/url") docinfo.URL = "https://secret/url" @@ -3866,7 +4275,7 @@ def test_XML_set_base_url_docinfo(self): def test_parse_stringio_base_url(self): etree = self.etree - tree = etree.parse(BytesIO(""), base_url="http://no/such/url") + tree = etree.parse(BytesIO(b""), base_url="http://no/such/url") docinfo = tree.docinfo self.assertEqual(docinfo.URL, "http://no/such/url") @@ -3879,7 +4288,7 @@ def test_parse_base_url_docinfo(self): def test_HTML_base_url_docinfo(self): etree = self.etree - root = etree.HTML(_bytes(""), base_url="http://no/such/url") + root = etree.HTML(b"", base_url="http://no/such/url") docinfo = root.getroottree().docinfo self.assertEqual(docinfo.URL, "http://no/such/url") @@ -3919,7 +4328,7 @@ def test_docinfo_system(self): def test_docinfo_empty(self): etree = self.etree - xml = _bytes('') + xml = b'' tree = etree.parse(BytesIO(xml)) docinfo = tree.docinfo self.assertEqual(docinfo.encoding, "UTF-8") @@ -3931,7 +4340,7 @@ def test_docinfo_empty(self): def test_docinfo_name_only(self): etree = self.etree - xml = _bytes('') + xml = b'' tree = etree.parse(BytesIO(xml)) docinfo = tree.docinfo self.assertEqual(docinfo.encoding, "UTF-8") @@ -3943,24 +4352,24 @@ def test_docinfo_name_only(self): def test_doctype_name_only_roundtrip(self): etree = self.etree - xml = _bytes('\n') + xml = b'\n' tree = etree.parse(BytesIO(xml)) self.assertEqual(xml, etree.tostring(tree)) def test_doctype_output_override(self): etree = self.etree - pub_id = "-//W3C//DTD XHTML 1.0 Transitional//EN" - sys_id = "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" - doctype_string = _bytes('' % (pub_id, sys_id)) + pub_id = b"-//W3C//DTD XHTML 1.0 Transitional//EN" + sys_id = b"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" + doctype_string = b'' % (pub_id, sys_id) - xml = _bytes('\n') + xml = b'\n' tree = etree.parse(BytesIO(xml)) - self.assertEqual(xml.replace(_bytes(''), doctype_string), + self.assertEqual(xml.replace(b'', doctype_string), etree.tostring(tree, doctype=doctype_string)) def test_xml_base(self): etree = self.etree - root = etree.XML(_bytes(""), base_url="http://no/such/url") + root = etree.XML(b"", base_url="http://no/such/url") self.assertEqual(root.base, "http://no/such/url") self.assertEqual( root.get('{http://www.w3.org/XML/1998/namespace}base'), None) @@ -3972,7 +4381,7 @@ def test_xml_base(self): def test_xml_base_attribute(self): etree = self.etree - root = etree.XML(_bytes(""), base_url="http://no/such/url") + root = etree.XML(b"", base_url="http://no/such/url") self.assertEqual(root.base, "http://no/such/url") self.assertEqual( root.get('{http://www.w3.org/XML/1998/namespace}base'), None) @@ -3985,13 +4394,13 @@ def test_xml_base_attribute(self): def test_html_base(self): etree = self.etree - root = etree.HTML(_bytes(""), + root = etree.HTML(b"", base_url="http://no/such/url") self.assertEqual(root.base, "http://no/such/url") def test_html_base_tag(self): etree = self.etree - root = etree.HTML(_bytes('')) + root = etree.HTML(b'') self.assertEqual(root.base, "http://no/such/url") def test_indent(self): @@ -4133,17 +4542,17 @@ def test_parse_fileobject_unicode(self): def test_dtd_io(self): # check that DTDs that go in also go back out - xml = _bytes('''\ + xml = b'''\ ]> test-test\ - ''') + ''' tree = self.etree.parse(BytesIO(xml)) - self.assertEqual(self.etree.tostring(tree).replace(_bytes(" "), _bytes("")), - xml.replace(_bytes(" "), _bytes(""))) + self.assertEqual(self.etree.tostring(tree).replace(b" ", b""), + xml.replace(b" ", b"")) def test_byte_zero(self): Element = self.etree.Element @@ -4159,12 +4568,12 @@ def test_unicode_byte_zero(self): a = Element('a') self.assertRaises(ValueError, setattr, a, "text", - _str('ha\0ho')) + 'ha\0ho') self.assertRaises(ValueError, setattr, a, "tail", - _str('ha\0ho')) + 'ha\0ho') self.assertRaises(ValueError, Element, - _str('ha\0ho')) + 'ha\0ho') def test_byte_invalid(self): Element = self.etree.Element @@ -4184,51 +4593,51 @@ def test_unicode_byte_invalid(self): a = Element('a') self.assertRaises(ValueError, setattr, a, "text", - _str('ha\x07ho')) + 'ha\x07ho') self.assertRaises(ValueError, setattr, a, "text", - _str('ha\x02ho')) + 'ha\x02ho') self.assertRaises(ValueError, setattr, a, "tail", - _str('ha\x07ho')) + 'ha\x07ho') self.assertRaises(ValueError, setattr, a, "tail", - _str('ha\x02ho')) + 'ha\x02ho') self.assertRaises(ValueError, Element, - _str('ha\x07ho')) + 'ha\x07ho') self.assertRaises(ValueError, Element, - _str('ha\x02ho')) + 'ha\x02ho') def test_unicode_byte_invalid_sequence(self): Element = self.etree.Element a = Element('a') self.assertRaises(ValueError, setattr, a, "text", - _str('ha\u1234\x07ho')) + 'ha\u1234\x07ho') self.assertRaises(ValueError, setattr, a, "text", - _str('ha\u1234\x02ho')) + 'ha\u1234\x02ho') self.assertRaises(ValueError, setattr, a, "tail", - _str('ha\u1234\x07ho')) + 'ha\u1234\x07ho') self.assertRaises(ValueError, setattr, a, "tail", - _str('ha\u1234\x02ho')) + 'ha\u1234\x02ho') self.assertRaises(ValueError, Element, - _str('ha\u1234\x07ho')) + 'ha\u1234\x07ho') self.assertRaises(ValueError, Element, - _str('ha\u1234\x02ho')) + 'ha\u1234\x02ho') def test_encoding_tostring_utf16(self): # ElementTree fails to serialize this tostring = self.etree.tostring Element = self.etree.Element SubElement = self.etree.SubElement - + a = Element('a') b = SubElement(a, 'b') c = SubElement(a, 'c') result = tostring(a, encoding='UTF-16') - self.assertEqual(_bytes(''), + self.assertEqual(b'', canonicalize(result)) def test_tostring_none(self): @@ -4246,13 +4655,13 @@ def test_tostring_pretty(self): c = SubElement(a, 'c') result = tostring(a) - self.assertEqual(result, _bytes("")) + self.assertEqual(result, b"") result = tostring(a, pretty_print=False) - self.assertEqual(result, _bytes("")) + self.assertEqual(result, b"") result = tostring(a, pretty_print=True) - self.assertEqual(result, _bytes("\n \n \n\n")) + self.assertEqual(result, b"\n \n \n\n") def test_tostring_with_tail(self): tostring = self.etree.tostring @@ -4266,36 +4675,36 @@ def test_tostring_with_tail(self): c = SubElement(a, 'c') result = tostring(a) - self.assertEqual(result, _bytes("bTAILaTAIL")) + self.assertEqual(result, b"bTAILaTAIL") result = tostring(a, with_tail=False) - self.assertEqual(result, _bytes("bTAIL")) + self.assertEqual(result, b"bTAIL") result = tostring(a, with_tail=True) - self.assertEqual(result, _bytes("bTAILaTAIL")) + self.assertEqual(result, b"bTAILaTAIL") def test_tostring_method_html_with_tail(self): tostring = self.etree.tostring html = self.etree.fromstring( '' - '

Some text\r\n

\r\n' + '

Some text\n

\n' '', parser=self.etree.HTMLParser()) self.assertEqual(html.tag, 'html') div = html.find('.//div') - self.assertEqual(div.tail, '\r\n') + self.assertEqual(div.tail, '\n') result = tostring(div, method='html') self.assertEqual( result, - _bytes("

Some text\r\n

\r\n")) + b"

Some text\n

\n") result = tostring(div, method='html', with_tail=True) self.assertEqual( result, - _bytes("

Some text\r\n

\r\n")) + b"

Some text\n

\n") result = tostring(div, method='html', with_tail=False) self.assertEqual( result, - _bytes("

Some text\r\n

")) + b"

Some text\n

") def test_standalone(self): tostring = self.etree.tostring @@ -4306,7 +4715,7 @@ def test_standalone(self): tree = Element("root").getroottree() self.assertEqual(None, tree.docinfo.standalone) - tree = XML(_bytes("")).getroottree() + tree = XML(b"").getroottree() self.assertEqual(None, tree.docinfo.standalone) tree = XML(_bytes( @@ -4324,7 +4733,7 @@ def test_tostring_standalone(self): XML = self.etree.XML ElementTree = self.etree.ElementTree - root = XML(_bytes("")) + root = XML(b"") tree = ElementTree(root) self.assertEqual(None, tree.docinfo.standalone) @@ -4373,7 +4782,7 @@ def test_tostring_method_text_encoding(self): tostring = self.etree.tostring Element = self.etree.Element SubElement = self.etree.SubElement - + a = Element('a') a.text = "A" a.tail = "tail" @@ -4385,57 +4794,57 @@ def test_tostring_method_text_encoding(self): result = tostring(a, method="text", encoding="UTF-16") - self.assertEqual(_str('ABSøk på nettetCtail').encode("UTF-16"), + self.assertEqual('ABSøk på nettetCtail'.encode("UTF-16"), result) def test_tostring_method_text_unicode(self): tostring = self.etree.tostring Element = self.etree.Element SubElement = self.etree.SubElement - + a = Element('a') - a.text = _str('Søk på nettetA') + a.text = 'Søk på nettetA' a.tail = "tail" b = SubElement(a, 'b') b.text = "B" - b.tail = _str('Søk på nettetB') + b.tail = 'Søk på nettetB' c = SubElement(a, 'c') c.text = "C" - + self.assertRaises(UnicodeEncodeError, tostring, a, method="text") - + self.assertEqual( - _str('Søk på nettetABSøk på nettetBCtail').encode('utf-8'), + 'Søk på nettetABSøk på nettetBCtail'.encode(), tostring(a, encoding="UTF-8", method="text")) def test_tounicode(self): tounicode = self.etree.tounicode Element = self.etree.Element SubElement = self.etree.SubElement - + a = Element('a') b = SubElement(a, 'b') c = SubElement(a, 'c') - - self.assertTrue(isinstance(tounicode(a), _unicode)) - self.assertEqual(_bytes(''), + + self.assertTrue(isinstance(tounicode(a), str)) + self.assertEqual(b'', canonicalize(tounicode(a))) def test_tounicode_element(self): tounicode = self.etree.tounicode Element = self.etree.Element SubElement = self.etree.SubElement - + a = Element('a') b = SubElement(a, 'b') c = SubElement(a, 'c') d = SubElement(c, 'd') - self.assertTrue(isinstance(tounicode(b), _unicode)) - self.assertTrue(isinstance(tounicode(c), _unicode)) - self.assertEqual(_bytes(''), + self.assertTrue(isinstance(tounicode(b), str)) + self.assertTrue(isinstance(tounicode(c), str)) + self.assertEqual(b'', canonicalize(tounicode(b))) - self.assertEqual(_bytes(''), + self.assertEqual(b'', canonicalize(tounicode(c))) def test_tounicode_none(self): @@ -4446,14 +4855,14 @@ def test_tounicode_element_tail(self): tounicode = self.etree.tounicode Element = self.etree.Element SubElement = self.etree.SubElement - + a = Element('a') b = SubElement(a, 'b') c = SubElement(a, 'c') d = SubElement(c, 'd') b.tail = 'Foo' - self.assertTrue(isinstance(tounicode(b), _unicode)) + self.assertTrue(isinstance(tounicode(b), str)) self.assertTrue(tounicode(b) == 'Foo' or tounicode(b) == 'Foo') @@ -4479,50 +4888,50 @@ def test_tostring_unicode(self): tostring = self.etree.tostring Element = self.etree.Element SubElement = self.etree.SubElement - + a = Element('a') b = SubElement(a, 'b') c = SubElement(a, 'c') - - self.assertTrue(isinstance(tostring(a, encoding=_unicode), _unicode)) - self.assertEqual(_bytes(''), - canonicalize(tostring(a, encoding=_unicode))) + + self.assertTrue(isinstance(tostring(a, encoding='unicode'), str)) + self.assertEqual(b'', + canonicalize(tostring(a, encoding='unicode'))) def test_tostring_unicode_element(self): tostring = self.etree.tostring Element = self.etree.Element SubElement = self.etree.SubElement - + a = Element('a') b = SubElement(a, 'b') c = SubElement(a, 'c') d = SubElement(c, 'd') - self.assertTrue(isinstance(tostring(b, encoding=_unicode), _unicode)) - self.assertTrue(isinstance(tostring(c, encoding=_unicode), _unicode)) - self.assertEqual(_bytes(''), - canonicalize(tostring(b, encoding=_unicode))) - self.assertEqual(_bytes(''), - canonicalize(tostring(c, encoding=_unicode))) + self.assertTrue(isinstance(tostring(b, encoding='unicode'), str)) + self.assertTrue(isinstance(tostring(c, encoding='unicode'), str)) + self.assertEqual(b'', + canonicalize(tostring(b, encoding=str))) + self.assertEqual(b'', + canonicalize(tostring(c, encoding=str))) def test_tostring_unicode_none(self): tostring = self.etree.tostring self.assertRaises(TypeError, self.etree.tostring, - None, encoding=_unicode) + None, encoding='unicode') def test_tostring_unicode_element_tail(self): tostring = self.etree.tostring Element = self.etree.Element SubElement = self.etree.SubElement - + a = Element('a') b = SubElement(a, 'b') c = SubElement(a, 'c') d = SubElement(c, 'd') b.tail = 'Foo' - self.assertTrue(isinstance(tostring(b, encoding=_unicode), _unicode)) - self.assertTrue(tostring(b, encoding=_unicode) == 'Foo' or - tostring(b, encoding=_unicode) == 'Foo') + self.assertTrue(isinstance(tostring(b, encoding='unicode'), str)) + self.assertTrue(tostring(b, encoding='unicode') == 'Foo' or + tostring(b, encoding='unicode') == 'Foo') def test_tostring_unicode_pretty(self): tostring = self.etree.tostring @@ -4533,15 +4942,16 @@ def test_tostring_unicode_pretty(self): b = SubElement(a, 'b') c = SubElement(a, 'c') - result = tostring(a, encoding=_unicode) + result = tostring(a, encoding='unicode') self.assertEqual(result, "") - result = tostring(a, encoding=_unicode, pretty_print=False) + result = tostring(a, encoding='unicode', pretty_print=False) self.assertEqual(result, "") - result = tostring(a, encoding=_unicode, pretty_print=True) + result = tostring(a, encoding='unicode', pretty_print=True) self.assertEqual(result, "\n \n \n\n") + @unittest.skipIf(IS_PYPY, "currently crashes PyPy") def test_pypy_proxy_collect(self): root = etree.Element('parent') etree.SubElement(root, 'child') @@ -4605,15 +5015,61 @@ def test_parse_source_pathlike(self): tounicode = self.etree.tounicode tree = etree.parse(SimpleFSPath(fileInTestDir('test.xml'))) - self.assertEqual(_bytes(''), + self.assertEqual(b'', canonicalize(tounicode(tree))) - + def test_iterparse_source_pathlike(self): iterparse = self.etree.iterparse events = list(iterparse(SimpleFSPath(fileInTestDir('test.xml')))) self.assertEqual(2, len(events)) + def test_class_hierarchy(self): + element = etree.Element("test") + # The Element class constructs an _Element instance + self.assertIs(type(element), etree._Element) + # _Element is a subclass implementation of Element + self.assertTrue(issubclass(etree._Element, etree.Element)) + # Therefore, element is an instance of Element + self.assertIsInstance(element, etree.Element) + + comment = etree.Comment("text") + self.assertIs(type(comment), etree._Comment) + self.assertIsInstance(comment, etree._Element) + self.assertIsInstance(comment, etree.Element) + + pi = etree.ProcessingInstruction("target", "text") + self.assertIs(type(pi), etree._ProcessingInstruction) + self.assertIsInstance(pi, etree._Element) + self.assertIsInstance(pi, etree.Element) + + entity = etree.Entity("text") + self.assertIs(type(entity), etree._Entity) + self.assertIsInstance(entity, etree._Element) + self.assertIsInstance(entity, etree.Element) + + sub_element = etree.SubElement(element, "child") + self.assertIs(type(sub_element), etree._Element) + self.assertIsInstance(sub_element, etree.Element) + + tree = etree.ElementTree(element) + self.assertIs(type(tree), etree._ElementTree) + self.assertIsInstance(tree, etree.ElementTree) + self.assertNotIsInstance(tree, etree._Element) + + # XML is a factory function and not a class. + xml = etree.XML("") + self.assertIs(type(xml), etree._Element) + self.assertIsInstance(xml, etree._Element) + self.assertIsInstance(xml, etree.Element) + + self.assertNotIsInstance(element, etree.ElementBase) + self.assertIs(type(element), etree._Element) + self.assertTrue(issubclass(etree.ElementBase, etree._Element)) + + self.assertTrue(callable(etree.Element)) + self.assertTrue(callable(etree.ElementTree)) + # helper methods def _writeElement(self, element, encoding='us-ascii', compression=0): @@ -4630,13 +5086,15 @@ def _writeElement(self, element, encoding='us-ascii', compression=0): class _XIncludeTestCase(HelperTestCase): + __test__ = False + def test_xinclude_text(self): filename = fileInTestDir('test_broken.xml') - root = etree.XML(_bytes('''\ + root = etree.XML('''\ - ''' % path2url(filename))) + ''' % path2url(filename)) old_text = root.text content = read_file(filename) old_tail = root[0].tail @@ -4734,11 +5192,13 @@ def resolve(self, url, id, context): class ETreeXIncludeTestCase(_XIncludeTestCase): + __test__ = True def include(self, tree): tree.xinclude() class ElementIncludeTestCase(_XIncludeTestCase): + __test__ = True from lxml import ElementInclude def include(self, tree, loader=None, max_depth=None): @@ -4875,63 +5335,63 @@ def test_multiple_include_of_same_file(self): class ETreeC14NTestCase(HelperTestCase): def test_c14n(self): - tree = self.parse(_bytes('')) + tree = self.parse(b'') f = BytesIO() tree.write_c14n(f) s = f.getvalue() - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) def test_c14n_gzip(self): - tree = self.parse(_bytes(''+''*200+'')) + tree = self.parse(b''+b''*200+b'') f = BytesIO() tree.write_c14n(f, compression=9) with gzip.GzipFile(fileobj=BytesIO(f.getvalue())) as gzfile: s = gzfile.read() - self.assertEqual(_bytes(''+''*200+''), + self.assertEqual(b''+b''*200+b'', s) def test_c14n_file(self): - tree = self.parse(_bytes('')) + tree = self.parse(b'') with tmpfile() as filename: tree.write_c14n(filename) data = read_file(filename, 'rb') - self.assertEqual(_bytes(''), + self.assertEqual(b'', data) - + def test_c14n_file_pathlike(self): - tree = self.parse(_bytes('')) + tree = self.parse(b'') with tmpfile() as filename: tree.write_c14n(SimpleFSPath(filename)) data = read_file(filename, 'rb') - self.assertEqual(_bytes(''), + self.assertEqual(b'', data) def test_c14n_file_gzip(self): - tree = self.parse(_bytes(''+''*200+'')) + tree = self.parse(b''+b''*200+b'') with tmpfile() as filename: tree.write_c14n(filename, compression=9) with gzip.open(filename, 'rb') as f: data = f.read() - self.assertEqual(_bytes(''+''*200+''), + self.assertEqual(b''+b''*200+b'', data) - + def test_c14n_file_gzip_pathlike(self): - tree = self.parse(_bytes(''+''*200+'')) + tree = self.parse(b''+b''*200+b'') with tmpfile() as filename: tree.write_c14n(SimpleFSPath(filename), compression=9) with gzip.open(filename, 'rb') as f: data = f.read() - self.assertEqual(_bytes(''+''*200+''), + self.assertEqual(b''+b''*200+b'', data) def test_c14n2_file_gzip(self): - tree = self.parse(_bytes(''+''*200+'')) + tree = self.parse(b''+b''*200+b'') with tmpfile() as filename: tree.write(filename, method='c14n2', compression=9) with gzip.open(filename, 'rb') as f: data = f.read() - self.assertEqual(_bytes(''+''*200+''), + self.assertEqual(b''+b''*200+b'', data) def test_c14n2_with_text(self): @@ -4950,21 +5410,21 @@ def test_c14n2_with_text(self): s) def test_c14n_with_comments(self): - tree = self.parse(_bytes('')) + tree = self.parse(b'') f = BytesIO() tree.write_c14n(f) s = f.getvalue() - self.assertEqual(_bytes('\n\n'), + self.assertEqual(b'\n\n', s) f = BytesIO() tree.write_c14n(f, with_comments=True) s = f.getvalue() - self.assertEqual(_bytes('\n\n'), + self.assertEqual(b'\n\n', s) f = BytesIO() tree.write_c14n(f, with_comments=False) s = f.getvalue() - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) def test_c14n2_with_comments(self): @@ -4991,15 +5451,15 @@ def test_c14n2_with_comments_strip_text(self): etree.tostring(tree, method='c14n2', with_comments=False, strip_text=True)) def test_c14n_tostring_with_comments(self): - tree = self.parse(_bytes('')) + tree = self.parse(b'') s = etree.tostring(tree, method='c14n') - self.assertEqual(_bytes('\n\n'), + self.assertEqual(b'\n\n', s) s = etree.tostring(tree, method='c14n', with_comments=True) - self.assertEqual(_bytes('\n\n'), + self.assertEqual(b'\n\n', s) s = etree.tostring(tree, method='c14n', with_comments=False) - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) def test_c14n2_tostring_with_comments(self): @@ -5015,15 +5475,15 @@ def test_c14n2_tostring_with_comments(self): s) def test_c14n_element_tostring_with_comments(self): - tree = self.parse(_bytes('')) + tree = self.parse(b'') s = etree.tostring(tree.getroot(), method='c14n') - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) s = etree.tostring(tree.getroot(), method='c14n', with_comments=True) - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) s = etree.tostring(tree.getroot(), method='c14n', with_comments=False) - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) def test_c14n_exclusive(self): @@ -5032,64 +5492,64 @@ def test_c14n_exclusive(self): f = BytesIO() tree.write_c14n(f) s = f.getvalue() - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) f = BytesIO() tree.write_c14n(f, exclusive=False) s = f.getvalue() - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) f = BytesIO() tree.write_c14n(f, exclusive=True) s = f.getvalue() - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) f = BytesIO() tree.write_c14n(f, exclusive=True, inclusive_ns_prefixes=['z']) s = f.getvalue() - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) def test_c14n_tostring_exclusive(self): tree = self.parse(_bytes( '')) s = etree.tostring(tree, method='c14n') - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) s = etree.tostring(tree, method='c14n', exclusive=False) - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) s = etree.tostring(tree, method='c14n', exclusive=True) - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) s = etree.tostring(tree, method='c14n', exclusive=True, inclusive_ns_prefixes=['y']) - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) def test_c14n_element_tostring_exclusive(self): tree = self.parse(_bytes( '')) s = etree.tostring(tree.getroot(), method='c14n') - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) s = etree.tostring(tree.getroot(), method='c14n', exclusive=False) - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) s = etree.tostring(tree.getroot(), method='c14n', exclusive=True) - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) s = etree.tostring(tree.getroot()[0], method='c14n', exclusive=False) - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) s = etree.tostring(tree.getroot()[0], method='c14n', exclusive=True) - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) s = etree.tostring(tree.getroot()[0], method='c14n', exclusive=True, inclusive_ns_prefixes=['y']) - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) def test_c14n_tostring_inclusive_ns_prefixes(self): @@ -5098,11 +5558,11 @@ def test_c14n_tostring_inclusive_ns_prefixes(self): '')) s = etree.tostring(tree, method='c14n', exclusive=True, inclusive_ns_prefixes=['x', 'y', 'z']) - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) - + def test_python3_problem_bytesio_iterparse(self): - content = BytesIO(''' '''.encode('utf-8')) + content = BytesIO(b''' ''') def handle_div_end(event, element): if event == 'end' and element.tag.lower().startswith("{http://www.w3.org/1999/xhtml}div"): # for ns_id, ns_uri in element.nsmap.items(): @@ -5113,10 +5573,10 @@ def handle_div_end(event, element): events=('start', 'end') ): handle_div_end(event, element) - + def test_python3_problem_filebased_iterparse(self): with open('test.xml', 'w+b') as f: - f.write(''' '''.encode('utf-8')) + f.write(b''' ''') def handle_div_end(event, element): if event == 'end' and element.tag.lower() == "{http://www.w3.org/1999/xhtml}div": # for ns_id, ns_uri in element.nsmap.items(): @@ -5127,11 +5587,11 @@ def handle_div_end(event, element): events=('start', 'end') ): handle_div_end(event, element) - + def test_python3_problem_filebased_parse(self): with open('test.xml', 'w+b') as f: - f.write(''' '''.encode('utf-8')) - def serialize_div_element(element): + f.write(b''' ''') + def serialize_div_element(element): # for ns_id, ns_uri in element.nsmap.items(): # print(type(ns_id), type(ns_uri), ns_id, '=', ns_uri) etree.tostring(element, method="c14n2") @@ -5143,41 +5603,41 @@ def serialize_div_element(element): class ETreeWriteTestCase(HelperTestCase): def test_write(self): - tree = self.parse(_bytes('')) + tree = self.parse(b'') f = BytesIO() tree.write(f) s = f.getvalue() - self.assertEqual(_bytes(''), + self.assertEqual(b'', s) def test_write_doctype(self): - tree = self.parse(_bytes('')) + tree = self.parse(b'') f = BytesIO() tree.write(f, doctype='HUHU') s = f.getvalue() - self.assertEqual(_bytes('HUHU\n'), - s) + self.assertEqual(b'HUHU\n', + s) def test_write_gzip(self): - tree = self.parse(_bytes(''+''*200+'')) + tree = self.parse(b''+b''*200+b'') f = BytesIO() tree.write(f, compression=9) with gzip.GzipFile(fileobj=BytesIO(f.getvalue())) as gzfile: s = gzfile.read() - self.assertEqual(_bytes(''+''*200+''), - s) + self.assertEqual(b''+b''*200+b'', + s) def test_write_gzip_doctype(self): - tree = self.parse(_bytes(''+''*200+'')) + tree = self.parse(b''+b''*200+b'') f = BytesIO() tree.write(f, compression=9, doctype='') with gzip.GzipFile(fileobj=BytesIO(f.getvalue())) as gzfile: s = gzfile.read() - self.assertEqual(_bytes('\n'+''*200+''), - s) + self.assertEqual(b'\n'+b''*200+b'', + s) def test_write_gzip_level(self): - tree = self.parse(_bytes(''+''*200+'')) + tree = self.parse(b''+b''*200+b'') f = BytesIO() tree.write(f, compression=0) s0 = f.getvalue() @@ -5200,72 +5660,75 @@ def test_write_gzip_level(self): with gzip.GzipFile(fileobj=BytesIO(s)) as gzfile: s9 = gzfile.read() - self.assertEqual(_bytes(''+''*200+''), + self.assertEqual(b''+b''*200+b'', s0) - self.assertEqual(_bytes(''+''*200+''), + self.assertEqual(b''+b''*200+b'', s1) - self.assertEqual(_bytes(''+''*200+''), + self.assertEqual(b''+b''*200+b'', s9) def test_write_file(self): - tree = self.parse(_bytes('')) + tree = self.parse(b'') with tmpfile() as filename: tree.write(filename) data = read_file(filename, 'rb') - self.assertEqual(_bytes(''), + self.assertEqual(b'', data) - + def test_write_file_pathlike(self): - tree = self.parse(_bytes('')) + tree = self.parse(b'') with tmpfile() as filename: tree.write(SimpleFSPath(filename)) data = read_file(filename, 'rb') - self.assertEqual(_bytes(''), + self.assertEqual(b'', data) def test_write_file_gzip(self): - tree = self.parse(_bytes(''+''*200+'')) + tree = self.parse(b''+b''*200+b'') with tmpfile() as filename: tree.write(filename, compression=9) with gzip.open(filename, 'rb') as f: data = f.read() - self.assertEqual(_bytes(''+''*200+''), + self.assertEqual(b''+b''*200+b'', data) def test_write_file_gzip_pathlike(self): - tree = self.parse(_bytes(''+''*200+'')) + tree = self.parse(b''+b''*200+b'') with tmpfile() as filename: tree.write(SimpleFSPath(filename), compression=9) with gzip.open(filename, 'rb') as f: data = f.read() - self.assertEqual(_bytes(''+''*200+''), + self.assertEqual(b''+b''*200+b'', data) + @needs_feature("zlib") def test_write_file_gzip_parse(self): - tree = self.parse(_bytes(''+''*200+'')) + tree = self.parse(b''+b''*200+b'') + parser = etree.XMLParser(decompress=True) with tmpfile() as filename: tree.write(filename, compression=9) - data = etree.tostring(etree.parse(filename)) - self.assertEqual(_bytes(''+''*200+''), + data = etree.tostring(etree.parse(filename, parser)) + self.assertEqual(b''+b''*200+b'', data) + @needs_feature("zlib") def test_write_file_gzipfile_parse(self): - tree = self.parse(_bytes(''+''*200+'')) + tree = self.parse(b''+b''*200+b'') with tmpfile() as filename: tree.write(filename, compression=9) with gzip.GzipFile(filename) as f: data = etree.tostring(etree.parse(f)) - self.assertEqual(_bytes(''+''*200+''), + self.assertEqual(b''+b''*200+b'', data) def test_write_file_url(self): - xml = _bytes(''+''*200+'') + xml = b''+b''*200+b'' tree = self.parse(xml) with tmpfile(prefix="p+%20", suffix=".xml") as filename: url = 'file://' + (filename if sys.platform != 'win32' else '/' + filename.replace('\\', '/')) tree.write(url) - data = read_file(filename, 'rb').replace(_bytes('\n'), _bytes('')) + data = read_file(filename, 'rb').replace(b'\n', b'') self.assertEqual(data, xml) @@ -5274,7 +5737,7 @@ class ETreeErrorLogTest(HelperTestCase): def test_parse_error_logging(self): parse = self.etree.parse - f = BytesIO('') + f = BytesIO(b'') self.etree.clear_error_log() try: parse(f) @@ -5306,7 +5769,7 @@ def log(self, entry, message, *args): messages.append(message) self.etree.use_global_python_log(Logger()) - f = BytesIO('') + f = BytesIO(b'') try: parse(f) except SyntaxError: @@ -5331,7 +5794,7 @@ def assert_event_tags(self, events, expected): expected) def test_pull_from_simple_target(self): - class Target(object): + class Target: def start(self, tag, attrib): return 'start(%s)' % tag def end(self, tag): @@ -5355,7 +5818,7 @@ def close(self): self.assertEqual('close()', parser.close()) def test_pull_from_simple_target_start_end(self): - class Target(object): + class Target: def start(self, tag, attrib): return 'start(%s)' % tag def end(self, tag): @@ -5412,7 +5875,7 @@ def test_pull_from_tree_builder(self): def test_pull_from_tree_builder_subclass(self): class Target(etree.TreeBuilder): def end(self, tag): - el = super(Target, self).end(tag) + el = super().end(tag) el.tag += '-huhu' return el @@ -5440,13 +5903,13 @@ def end(self, tag): def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(ETreeOnlyTestCase)]) - suite.addTests([unittest.makeSuite(ETreeXIncludeTestCase)]) - suite.addTests([unittest.makeSuite(ElementIncludeTestCase)]) - suite.addTests([unittest.makeSuite(ETreeC14NTestCase)]) - suite.addTests([unittest.makeSuite(ETreeWriteTestCase)]) - suite.addTests([unittest.makeSuite(ETreeErrorLogTest)]) - suite.addTests([unittest.makeSuite(XMLPullParserTest)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeOnlyTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeXIncludeTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ElementIncludeTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeC14NTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeWriteTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeErrorLogTest)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(XMLPullParserTest)]) # add original doctests from ElementTree selftest modules from . import selftest, selftest2 @@ -5454,19 +5917,34 @@ def test_suite(): suite.addTests(doctest.DocTestSuite(selftest2)) # add doctests - suite.addTests(doctest.DocTestSuite(etree)) + doctest_stubs = {} + if 'schematron' not in etree.LIBXML_COMPILED_FEATURES: + # See doctest of class "lxml.etree.Schematron". + class FakeSchematron: + def __init__(self, schema): + self._results = iter([0, 1]) + def validate(self, xml): + return next(self._results) + + doctest_stubs['Schematron'] = FakeSchematron + + suite.addTests(doctest.DocTestSuite(etree, extraglobs=doctest_stubs)) suite.addTests( - [make_doctest('../../../doc/tutorial.txt')]) + [make_doctest('tutorial.txt')]) suite.addTests( - [make_doctest('../../../doc/api.txt')]) + [make_doctest('api.txt')]) suite.addTests( - [make_doctest('../../../doc/FAQ.txt')]) + [make_doctest('FAQ.txt')]) suite.addTests( - [make_doctest('../../../doc/parsing.txt')]) + [make_doctest('parsing.txt')]) suite.addTests( - [make_doctest('../../../doc/resolvers.txt')]) + [make_doctest('resolvers.txt')]) return suite +# Hide test base classes from test discovery. +del _XIncludeTestCase + + if __name__ == '__main__': print('to test use test.py %s' % __file__) diff --git a/src/lxml/tests/test_external_document.py b/src/lxml/tests/test_external_document.py index 0d1d0639b..ba615404e 100644 --- a/src/lxml/tests/test_external_document.py +++ b/src/lxml/tests/test_external_document.py @@ -1,9 +1,7 @@ -# -*- coding: utf-8 -*- """ Test cases related to direct loading of external libxml2 documents """ -from __future__ import absolute_import import sys import unittest @@ -98,7 +96,7 @@ def test_external_document_adoption(self): def test_suite(): suite = unittest.TestSuite() if sys.platform != 'win32': - suite.addTests([unittest.makeSuite(ExternalDocumentTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ExternalDocumentTestCase)]) return suite diff --git a/src/lxml/tests/test_htmlparser.py b/src/lxml/tests/test_htmlparser.py index 2f3186ff1..610cb3748 100644 --- a/src/lxml/tests/test_htmlparser.py +++ b/src/lxml/tests/test_htmlparser.py @@ -1,43 +1,38 @@ -# -*- coding: utf-8 -*- - """ HTML parser test cases for etree """ -from __future__ import absolute_import import unittest import tempfile, os, os.path, sys +from io import BytesIO -from .common_imports import etree, html, BytesIO, fileInTestDir, _bytes, _str +from .common_imports import etree, html, fileInTestDir from .common_imports import SillyFileLike, HelperTestCase, write_to_file, needs_libxml -try: - unicode -except NameError: - unicode = str - class HtmlParserTestCase(HelperTestCase): """HTML parser test cases """ etree = etree - html_str = _bytes("test

page title

") - html_str_pretty = _bytes("""\ + html_str = b"test

page title

" + html_str_pretty = b"""\ test

page title

-""") - broken_html_str = _bytes("test" - "<body><h1>page title</h3></p></html>") - uhtml_str = _bytes( +""" + broken_html_str = ( + b"<html><head><title>test" + b"

page title

") + uhtml_str = ( "test á" - "

page á title

").decode('utf8') + "

page á title

" + ) def tearDown(self): - super(HtmlParserTestCase, self).tearDown() + super().tearDown() self.etree.set_default_parser() def test_module_HTML(self): @@ -51,18 +46,16 @@ def test_module_HTML_unicode(self): self.etree.tostring(element, method="html", encoding='unicode'), self.uhtml_str) self.assertEqual(element.findtext('.//h1'), - _bytes("page á title").decode('utf8')) + "page á title") @needs_libxml(2, 9, 5) # not sure, at least 2.9.4 fails def test_wide_unicode_html(self): if sys.maxunicode < 1114111: return # skip test - element = self.etree.HTML(_bytes( - '

\\U00026007

' - ).decode('unicode_escape')) + element = self.etree.HTML('

\U00026007

') p_text = element.findtext('.//p') self.assertEqual(1, len(p_text)) - self.assertEqual(_bytes('\\U00026007').decode('unicode_escape'), + self.assertEqual('\U00026007', p_text) def test_html_ids(self): @@ -91,7 +84,7 @@ def test_module_HTML_pretty_print(self): def test_module_parse_html_error(self): parser = self.etree.HTMLParser(recover=False) parse = self.etree.parse - f = BytesIO("") + f = BytesIO(b"") self.assertRaises(self.etree.XMLSyntaxError, parse, f, parser) @@ -205,23 +198,23 @@ def test_module_parse_html_default_doctype(self): self.assertEqual(d.getroottree().docinfo.doctype, '') def test_parse_encoding_8bit_explicit(self): - text = _str('Søk på nettet') - html_latin1 = (_str('

%s

') % text).encode('iso-8859-1') + text = 'Søk på nettet' + html_latin1 = ('

%s

' % text).encode('iso-8859-1') tree = self.etree.parse( BytesIO(html_latin1), self.etree.HTMLParser(encoding="iso-8859-1")) - p = tree.find("//p") + p = tree.find(".//p") self.assertEqual(p.text, text) def test_parse_encoding_8bit_override(self): - text = _str('Søk på nettet') - wrong_head = _str(''' + text = 'Søk på nettet' + wrong_head = ''' - ''') - html_latin1 = (_str('%s

%s

') % (wrong_head, + ''' + html_latin1 = ('%s

%s

' % (wrong_head, text) ).encode('iso-8859-1') @@ -232,7 +225,7 @@ def test_parse_encoding_8bit_override(self): tree = self.etree.parse( BytesIO(html_latin1), self.etree.HTMLParser(encoding="iso-8859-1")) - p = tree.find("//p") + p = tree.find(".//p") self.assertEqual(p.text, text) def test_module_HTML_broken(self): @@ -240,12 +233,43 @@ def test_module_HTML_broken(self): self.assertEqual(self.etree.tostring(element, method="html"), self.html_str) - def test_module_HTML_cdata(self): + def test_module_HTML_script(self): # by default, libxml2 generates CDATA nodes for ' element = self.etree.HTML(html) + self.assertEqual(element[0][0].tag, "style") self.assertEqual(element[0][0].text, "foo") + self.assertEqual(element[0][1].tag, "script") + self.assertEqual(element[0][1].text, "too") + + @needs_libxml(2, 10, 0) + def test_module_HTML_cdata_ignored(self): + # libxml2 discards CDATA "content" since HTML does not know them. + import warnings + html = b'' + element = self.etree.HTML(html) + self.assertEqual(element[0].tag, "body") + self.assertFalse(element[0].text) + + with warnings.catch_warnings(record=True) as warnings_seen: + warnings.simplefilter("always") + parser = self.etree.HTMLParser(strip_cdata=True) + self.assertTrue(warnings_seen) + + element = self.etree.HTML(html, parser) + self.assertEqual(element[0].tag, "body") + self.assertFalse(element[0].text) + + with warnings.catch_warnings(record=True) as warnings_seen: + warnings.simplefilter("always") + parser = self.etree.HTMLParser(strip_cdata=False) + self.assertTrue(warnings_seen) + + element = self.etree.HTML(html, parser) + self.assertEqual(element[0].tag, "body") + self.assertFalse(element[0].text) + def test_module_HTML_access(self): element = self.etree.HTML(self.html_str) self.assertEqual(element[0][0].tag, 'title') @@ -301,8 +325,7 @@ def test_default_parser_HTML_broken(self): def test_html_iterparse(self): iterparse = self.etree.iterparse - f = BytesIO( - 'TITLE

P

') + f = BytesIO(b'TITLE

P

') iterator = iterparse(f, html=True) self.assertEqual(None, iterator.root) @@ -317,8 +340,7 @@ def test_html_iterparse(self): def test_html_iterparse_tag(self): iterparse = self.etree.iterparse - f = BytesIO( - 'TITLE

P

') + f = BytesIO(b'TITLE

P

') iterator = iterparse(f, html=True, tag=["p", "title"]) self.assertEqual(None, iterator.root) @@ -332,8 +354,7 @@ def test_html_iterparse_tag(self): def test_html_iterparse_stop_short(self): iterparse = self.etree.iterparse - f = BytesIO( - 'TITLE

P

') + f = BytesIO(b'TITLE

P

') iterator = iterparse(f, html=True) self.assertEqual(None, iterator.root) @@ -353,7 +374,7 @@ def test_html_iterparse_stop_short(self): def test_html_iterparse_broken(self): iterparse = self.etree.iterparse - f = BytesIO('TEST></head><p>P<br></div>') + f = BytesIO(b'<head><title>TEST>

P
') iterator = iterparse(f, html=True) self.assertEqual(None, iterator.root) @@ -371,9 +392,25 @@ def test_html_iterparse_broken(self): ('end', root[1][0]), ('end', root[1]), ('end', root)], events) + def test_html_iterparse_broken_meta(self): + # Broken HTML with a misplaced tag before the real html tag. + body = ''' + + + + + + + ''' + PARSE_TAGS = {'meta', 'html', 'body'} + + iterator = etree.iterparse( + BytesIO(body.encode()), events=('start', 'end'), html=True, recover=True, tag=PARSE_TAGS) + parse_events = list(iterator) + def test_html_iterparse_broken_no_recover(self): iterparse = self.etree.iterparse - f = BytesIO('

P
') + f = BytesIO(b'

P
') iterator = iterparse(f, html=True, recover=False) self.assertRaises(self.etree.XMLSyntaxError, list, iterator) @@ -392,8 +429,7 @@ def test_html_iterparse_file(self): def test_html_iterparse_start(self): iterparse = self.etree.iterparse - f = BytesIO( - 'TITLE

P

') + f = BytesIO(b'TITLE

P

') iterator = iterparse(f, html=True, events=('start',)) self.assertEqual(None, iterator.root) @@ -406,6 +442,46 @@ def test_html_iterparse_start(self): ('start', root[1]), ('start', root[1][0])], events) + def test_html_iterparse_cdata(self): + import warnings + + iterparse = self.etree.iterparse + f = BytesIO(b'') + + with warnings.catch_warnings(record=True) as warned_novalue: + warnings.simplefilter("always") + iterator = iterparse(f, html=True, events=('start', )) + self.assertFalse(warned_novalue) + + events = list(iterator) + root = iterator.root + self.assertNotEqual(None, root) + self.assertEqual(('start', root), events[0]) + + f.seek(0) + with warnings.catch_warnings(record=True) as warned_true: + warnings.simplefilter("always") + iterator = iterparse( + f, html=True, events=('start', ), strip_cdata=True) + self.assertFalse(warned_true) + + events = list(iterator) + root = iterator.root + self.assertNotEqual(None, root) + self.assertEqual(('start', root), events[0]) + + f.seek(0) + with warnings.catch_warnings(record=True) as warned_false: + warnings.simplefilter("always") + iterator = iterparse( + f, html=True, events=('start', ), strip_cdata=False) + self.assertFalse(warned_false) + + events = list(iterator) + root = iterator.root + self.assertNotEqual(None, root) + self.assertEqual(('start', root), events[0]) + def test_html_feed_parser(self): parser = self.etree.HTMLParser() parser.feed("2011-03-13_135411/') + + events = parser.read_events() + self.assertEqual( + ['a', 'body', 'html'], + [el.tag for _, el in events]) + root = parser.close() + + self.assertEqual('html', root.tag) + self.assertEqual('body', root[0].tag) + self.assertEqual('a', root[0][0].tag) + self.assertEqual('2011-03-13_135411/', root[0][0].get("href")) + def test_html_parser_target_tag(self): assertFalse = self.assertFalse events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append(("start", tag)) assertFalse(attrib) @@ -476,7 +571,7 @@ def close(self): def test_html_parser_target_doctype_empty(self): assertFalse = self.assertFalse events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append(("start", tag)) assertFalse(attrib) @@ -500,7 +595,7 @@ def close(self): def test_html_parser_target_doctype_html(self): assertFalse = self.assertFalse events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append(("start", tag)) assertFalse(attrib) @@ -524,7 +619,7 @@ def close(self): def test_html_parser_target_doctype_html_full(self): assertFalse = self.assertFalse events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append(("start", tag)) assertFalse(attrib) @@ -548,7 +643,7 @@ def close(self): def test_html_parser_target_exceptions(self): events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append(("start", tag)) raise ValueError("START") @@ -575,7 +670,7 @@ def close(self): def test_html_fromstring_target_exceptions(self): events = [] - class Target(object): + class Target: def start(self, tag, attrib): events.append(("start", tag)) raise ValueError("START") @@ -607,8 +702,8 @@ def test_set_decl_html(self): self.assertEqual(doc.docinfo.doctype, '') self.assertEqual(self.etree.tostring(doc), - _bytes(''' -''')) + b''' +''') def test_html5_doctype(self): # document type declaration with neither public if nor system url @@ -619,7 +714,7 @@ def test_html5_doctype(self): '') self.assertTrue(doc.docinfo.public_id is None) self.assertEqual(self.etree.tostring(doc), - _bytes('\n')) + b'\n') def test_ietf_decl(self): # legacy declaration with public id, no system url @@ -629,29 +724,29 @@ def test_ietf_decl(self): self.assertEqual(doc.docinfo.doctype, '') self.assertEqual(self.etree.tostring(doc), - _bytes('\n')) + b'\n') def test_boolean_attribute(self): # ability to serialize boolean attribute by setting value to None form = html.Element('form') form.set('novalidate', None) self.assertEqual(html.tostring(form), - _bytes('
')) + b'
') form.set('custom') self.assertEqual(html.tostring(form), - _bytes('
')) + b'
') def test_boolean_attribute_round_trip(self): # ability to pass boolean attributes unmodified fragment = '' self.assertEqual(html.tostring(html.fragment_fromstring(fragment)), - _bytes(fragment)) + fragment.encode('utf-8')) def test_boolean_attribute_xml_adds_empty_string(self): # html serialized as xml converts boolean attributes to empty strings fragment = '' self.assertEqual(self.etree.tostring(html.fragment_fromstring(fragment)), - _bytes('')) + b'') def test_xhtml_as_html_as_xml(self): # parse XHTML as HTML, serialise as XML @@ -679,7 +774,7 @@ def test_xhtml_as_html_as_xml(self): def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(HtmlParserTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(HtmlParserTestCase)]) return suite diff --git a/src/lxml/tests/test_http_io.py b/src/lxml/tests/test_http_io.py index 07f274231..93b1d2f0a 100644 --- a/src/lxml/tests/test_http_io.py +++ b/src/lxml/tests/test_http_io.py @@ -1,47 +1,54 @@ -# -*- coding: utf-8 -*- - """ Web IO test cases (wsgiref) """ -from __future__ import absolute_import import unittest import textwrap import sys import gzip -from .common_imports import etree, HelperTestCase, BytesIO, _bytes +from .common_imports import etree, HelperTestCase, BytesIO, _bytes, IS_PYPY from .dummy_http_server import webserver, HTTPRequestCollector +def needs_http(test_method, _skip_when_called=unittest.skip("needs HTTP support in libxml2")): + if "http" in etree.LIBXML_FEATURES: + return test_method + return _skip_when_called(test_method) + + class HttpIOTestCase(HelperTestCase): etree = etree - def _parse_from_http(self, data, code=200, headers=None, parser=None): + def _parse_from_http(self, data, code=200, headers=None): + parser = self.etree.XMLParser(no_network=False) handler = HTTPRequestCollector(data, code, headers) with webserver(handler) as host_url: tree = self.etree.parse(host_url + 'TEST', parser=parser) self.assertEqual([('/TEST', [])], handler.requests) return tree + @needs_http def test_http_client(self): - tree = self._parse_from_http(_bytes('')) + tree = self._parse_from_http(b'
') self.assertEqual('root', tree.getroot().tag) self.assertEqual('a', tree.getroot()[0].tag) + @needs_http def test_http_client_404(self): try: - self._parse_from_http(_bytes(''), code=404) - except IOError: + self._parse_from_http(b'', code=404) + except OSError: self.assertTrue(True) else: self.assertTrue(False, "expected IOError") + @needs_http def test_http_client_gzip(self): f = BytesIO() gz = gzip.GzipFile(fileobj=f, mode='w', filename='test.xml') - gz.write(_bytes('
')) + gz.write(b'
') gz.close() data = f.getvalue() del f, gz @@ -51,19 +58,21 @@ def test_http_client_gzip(self): self.assertEqual('root', tree.getroot().tag) self.assertEqual('a', tree.getroot()[0].tag) + @needs_http def test_parser_input_mix(self): - data = _bytes('
') + data = b'
' handler = HTTPRequestCollector(data) + parser = self.etree.XMLParser(no_network=False) with webserver(handler) as host_url: - tree = self.etree.parse(host_url) + tree = self.etree.parse(host_url, parser=parser) root = tree.getroot() self.assertEqual('a', root[0].tag) root = self.etree.fromstring(data) self.assertEqual('a', root[0].tag) - tree = self.etree.parse(host_url) + tree = self.etree.parse(host_url, parser=parser) root = tree.getroot() self.assertEqual('a', root[0].tag) @@ -73,6 +82,7 @@ def test_parser_input_mix(self): root = self.etree.fromstring(data) self.assertEqual('a', root[0].tag) + @needs_http def test_network_dtd(self): data = [_bytes(textwrap.dedent(s)) for s in [ # XML file @@ -110,14 +120,18 @@ def handler(environ, start_response): load_dtd=True, no_network=True)) except self.etree.XMLSyntaxError: self.assertTrue("myentity" in str(sys.exc_info()[1])) + self.assertEqual(1, len(responses)) # DTD not read + except OSError: + self.assertTrue("failed to load" in str(sys.exc_info()[1])) + self.assertEqual(2, len(responses)) # nothing read else: self.assertTrue(False) - self.assertEqual(1, len(responses)) # DTD not read def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(HttpIOTestCase)]) + if not IS_PYPY: + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(HttpIOTestCase)]) return suite diff --git a/src/lxml/tests/test_incremental_xmlfile.py b/src/lxml/tests/test_incremental_xmlfile.py index ddf81652a..be650b003 100644 --- a/src/lxml/tests/test_incremental_xmlfile.py +++ b/src/lxml/tests/test_incremental_xmlfile.py @@ -1,24 +1,25 @@ -# -*- coding: utf-8 -*- - +# coding: utf-8 """ Tests for the incremental XML serialisation API. """ -from __future__ import absolute_import - import io import os import sys import unittest import textwrap import tempfile +from io import BytesIO + +from unittest import skipIf -from lxml.etree import LxmlSyntaxError +from lxml.etree import CDATA, LxmlSyntaxError -from .common_imports import etree, BytesIO, HelperTestCase, skipIf, _str +from .common_imports import etree, HelperTestCase class _XmlFileTestCaseBase(HelperTestCase): + __test__ = False _file = None # to be set by specific subtypes below def test_element(self): @@ -33,6 +34,12 @@ def test_element_write_text(self): xf.write('toast') self.assertXml('toast') + def test_element_write_cdata(self): + with etree.xmlfile(self._file) as xf: + with xf.element('test'): + xf.write(CDATA('toast & jam')) + self.assertXml('') + def test_element_write_empty(self): with etree.xmlfile(self._file) as xf: with xf.element('test'): @@ -63,6 +70,20 @@ def test_element_nested_with_text(self): self.assertXml('contentinside' 'tnetnoc') + def test_element_nested_with_cdata(self): + with etree.xmlfile(self._file) as xf: + with xf.element('test'): + xf.write(CDATA('con')) + with xf.element('toast'): + xf.write(CDATA('tent')) + with xf.element('taste'): + xf.write(CDATA('inside')) + xf.write(CDATA('tnet')) + xf.write(CDATA('noc')) + self.assertXml( + '' + '') + def test_write_Element(self): with etree.xmlfile(self._file) as xf: xf.write(etree.Element('test')) @@ -161,6 +182,13 @@ def test_attribute_extra_duplicate(self): pass self.assertXml('') + def test_attribute_unicode(self): + with etree.xmlfile(self._file, encoding="utf-8") as xf: + with xf.element('älämänt', attrib={"Тест": "Атрибут"}): + el = etree.Element("älämänt", attrib={"Тест": "Атрибут"}) + xf.write(el) + self.assertXml('<älämänt Тест="Атрибут"><älämänt Тест="Атрибут"/>') + def test_escaping(self): with etree.xmlfile(self._file) as xf: with xf.element('test'): @@ -169,40 +197,47 @@ def test_escaping(self): self.assertXml( 'Comments: <!-- text -->\nEntities: &amp;') + def test_cdata_escaping(self): + with etree.xmlfile(self._file) as xf: + with xf.element('test'): + xf.write(CDATA('Ensure ]]> is escaped using separate CDATA nodes')) + self.assertXml( + ' is escaped using separate CDATA nodes]]>') + def test_encoding(self): - with etree.xmlfile(self._file, encoding='utf16') as xf: + with etree.xmlfile(self._file, encoding='utf-16') as xf: with xf.element('test'): xf.write('toast') - self.assertXml('toast', encoding='utf16') + self.assertXml('toast', encoding='utf-16') def test_buffering(self): with etree.xmlfile(self._file, buffered=False) as xf: with xf.element('test'): - self.assertXml("") + self.assertXml("", reparse=False) xf.write('toast') - self.assertXml("toast") + self.assertXml("toast", reparse=False) with xf.element('taste'): - self.assertXml("toast") + self.assertXml("toast", reparse=False) xf.write('some', etree.Element("more"), "toast") - self.assertXml("toastsometoast") - self.assertXml("toastsometoast") + self.assertXml("toastsometoast", reparse=False) + self.assertXml("toastsometoast", reparse=False) xf.write('end') - self.assertXml("toastsometoastend") - self.assertXml("toastsometoastend") + self.assertXml("toastsometoastend", reparse=False) + self.assertXml("toastsometoastend", reparse=False) self.assertXml("toastsometoastend") def test_flush(self): with etree.xmlfile(self._file, buffered=True) as xf: with xf.element('test'): - self.assertXml("") + self.assertXml("", reparse=False) xf.write('toast') - self.assertXml("") + self.assertXml("", reparse=False) with xf.element('taste'): - self.assertXml("") + self.assertXml("", reparse=False) xf.flush() - self.assertXml("toast") - self.assertXml("toast") - self.assertXml("toast") + self.assertXml("toast", reparse=False) + self.assertXml("toast", reparse=False) + self.assertXml("toast", reparse=False) self.assertXml("toast") def test_non_io_exception_continues_closing(self): @@ -245,6 +280,15 @@ def test_failure_preceding_text(self): else: self.assertTrue(False) + def test_failure_preceding_cdata(self): + try: + with etree.xmlfile(self._file) as xf: + xf.write(CDATA('toast & jam')) + except etree.LxmlSyntaxError: + self.assertTrue(True) + else: + self.assertTrue(False) + def test_failure_trailing_text(self): with etree.xmlfile(self._file) as xf: with xf.element('test'): @@ -256,6 +300,17 @@ def test_failure_trailing_text(self): else: self.assertTrue(False) + def test_failure_trailing_cdata(self): + with etree.xmlfile(self._file) as xf: + with xf.element('test'): + pass + try: + xf.write(CDATA('toast & jam')) + except etree.LxmlSyntaxError: + self.assertTrue(True) + else: + self.assertTrue(False) + def test_failure_trailing_Element(self): with etree.xmlfile(self._file) as xf: with xf.element('test'): @@ -306,11 +361,31 @@ def tearDown(self): if self._file is not None: self._file.close() - def assertXml(self, expected, encoding='utf8'): - self.assertEqual(self._read_file().decode(encoding), expected) + def assertXml(self, expected, encoding='utf8', reparse=True): + output = self._read_file() + self.assertEqual(output.decode(encoding), expected) + + if not reparse: + return + + def compare(el1, el2): + self.assertEqual(el1.tag, el2.tag) + self.assertEqual(el1.text, el2.text) + self.assertEqual(el1.tail, el2.tail) + self.assertEqual(el1.attrib, el2.attrib) + + self.assertEqual(len(el1), len(el2)) + for child1, child2 in zip(el1, el2): + compare(child1, child2) + + root_out = etree.fromstring(output) + root_expected = etree.fromstring(expected) + compare(root_out, root_expected) class BytesIOXmlFileTestCase(_XmlFileTestCaseBase): + __test__ = True + def setUp(self): self._file = BytesIO() @@ -322,12 +397,16 @@ def test_filelike_close(self): class TempXmlFileTestCase(_XmlFileTestCaseBase): + __test__ = True + def setUp(self): self._file = tempfile.TemporaryFile() @skipIf(sys.platform.startswith("win"), "Can't reopen temporary files on Windows") class TempPathXmlFileTestCase(_XmlFileTestCaseBase): + __test__ = True + def setUp(self): self._tmpfile = tempfile.NamedTemporaryFile() self._file = self._tmpfile.name @@ -357,7 +436,9 @@ def test_flush(self): class SimpleFileLikeXmlFileTestCase(_XmlFileTestCaseBase): - class SimpleFileLike(object): + __test__ = True + + class SimpleFileLike: def __init__(self, target): self._target = target self.write = target.write @@ -402,7 +483,7 @@ def test_write_fails(self): class WriteError(Exception): pass - class Writer(object): + class Writer: def __init__(self, trigger): self._trigger = trigger self._failed = False @@ -431,9 +512,14 @@ def write(self, data): class HtmlFileTestCase(_XmlFileTestCaseBase): + __test__ = True + def setUp(self): self._file = BytesIO() + def assertXml(self, expected, encoding='utf8', reparse=False): + super(HtmlFileTestCase, self).assertXml(expected, encoding, reparse=reparse) + def test_void_elements(self): # http://www.w3.org/TR/html5/syntax.html#elements-0 void_elements = { @@ -530,10 +616,10 @@ def test_attribute_quoting(self): def test_attribute_quoting_unicode(self): with etree.htmlfile(self._file) as xf: - with xf.element("tagname", attrib={"attr": _str('"misquöted\\u3344\\U00013344"')}): + with xf.element("tagname", attrib={"attr": '"misquöted\u3344\U00013344"'}): xf.write("foo") - self.assertXml('foo') + self.assertXml('foo') def test_unescaped_script(self): with etree.htmlfile(self._file) as xf: @@ -597,7 +683,6 @@ def _run_async(self, coro): except StopIteration as ex: return ex.value - @skipIf(sys.version_info < (3, 5), "requires support for async-def (Py3.5+)") def test_async(self): code = textwrap.dedent("""\ async def test_async_xmlfile(close=True, buffered=True): @@ -660,15 +745,19 @@ async def generate(out, close=True, buffered=True): def test_suite(): suite = unittest.TestSuite() suite.addTests([ - unittest.makeSuite(BytesIOXmlFileTestCase), - unittest.makeSuite(TempXmlFileTestCase), - unittest.makeSuite(TempPathXmlFileTestCase), - unittest.makeSuite(SimpleFileLikeXmlFileTestCase), - unittest.makeSuite(HtmlFileTestCase), - unittest.makeSuite(AsyncXmlFileTestCase), + unittest.defaultTestLoader.loadTestsFromTestCase(BytesIOXmlFileTestCase), + unittest.defaultTestLoader.loadTestsFromTestCase(TempXmlFileTestCase), + unittest.defaultTestLoader.loadTestsFromTestCase(TempPathXmlFileTestCase), + unittest.defaultTestLoader.loadTestsFromTestCase(SimpleFileLikeXmlFileTestCase), + unittest.defaultTestLoader.loadTestsFromTestCase(HtmlFileTestCase), + unittest.defaultTestLoader.loadTestsFromTestCase(AsyncXmlFileTestCase), ]) return suite +# Hide test base classes from test discovery. +del _XmlFileTestCaseBase + + if __name__ == '__main__': print('to test use test.py %s' % __file__) diff --git a/src/lxml/tests/test_io.py b/src/lxml/tests/test_io.py index cbdbcef06..eee5b9789 100644 --- a/src/lxml/tests/test_io.py +++ b/src/lxml/tests/test_io.py @@ -1,26 +1,26 @@ -# -*- coding: utf-8 -*- - """ IO test cases that apply to both etree and ElementTree """ -from __future__ import absolute_import +import pathlib import unittest import tempfile, gzip, os, os.path, gc, shutil from .common_imports import ( etree, ElementTree, _str, _bytes, SillyFileLike, LargeFileLike, HelperTestCase, - read_file, write_to_file, BytesIO, tmpfile + read_file, write_to_file, BytesIO, tmpfile, + needs_feature, ) class _IOTestCaseBase(HelperTestCase): """(c)ElementTree compatibility for IO functions/methods """ + __test__ = False etree = None - + def setUp(self): """Setting up a minimal tree """ @@ -110,7 +110,7 @@ def difference(filenames): after_write = os.listdir(tempfile.gettempdir()) self.assertEqual(read_file(filename, 'rb').replace(b'\n', b''), self.root_str) - except (AssertionError, IOError, OSError): + except (AssertionError, OSError): print("Before write: %s, after write: %s" % ( difference(before_write), difference(after_write)) ) @@ -129,7 +129,7 @@ def test_write_invalid_filename(self): 'invalid_file.xml') try: self.tree.write(filename) - except IOError: + except OSError: pass else: self.assertTrue( @@ -252,24 +252,28 @@ def read(*args): def test_etree_parse_io_error(self): # this is a directory name that contains characters beyond latin-1 - dirnameEN = _str('Directory') - dirnameRU = _str('Каталог') - filename = _str('nosuchfile.xml') + dirnameEN = 'Directory' + dirnameRU = 'Каталог' + filename = 'nosuchfile.xml' dn = tempfile.mkdtemp(prefix=dirnameEN) try: self.assertRaises(IOError, self.etree.parse, os.path.join(dn, filename)) finally: os.rmdir(dn) - dn = tempfile.mkdtemp(prefix=dirnameRU) + try: + dn = tempfile.mkdtemp(prefix=dirnameRU) + except (OSError, UnicodeEncodeError, UnicodeDecodeError): + # Creating the directory might fail on some platforms depending on encodings. + raise unittest.SkipTest("file system cannot create slavic file names") try: self.assertRaises(IOError, self.etree.parse, os.path.join(dn, filename)) finally: os.rmdir(dn) def test_parse_utf8_bom(self): - utext = _str('Søk på nettet') + utext = 'Søk på nettet' uxml = '

%s

' % utext - bom = _bytes('\\xEF\\xBB\\xBF').decode( + bom = b'\\xEF\\xBB\\xBF'.decode( "unicode_escape").encode("latin1") self.assertEqual(3, len(bom)) f = tempfile.NamedTemporaryFile(delete=False) @@ -285,9 +289,9 @@ def test_parse_utf8_bom(self): self.assertEqual(utext, tree.getroot().text) def test_iterparse_utf8_bom(self): - utext = _str('Søk på nettet') + utext = 'Søk på nettet' uxml = '

%s

' % utext - bom = _bytes('\\xEF\\xBB\\xBF').decode( + bom = b'\\xEF\\xBB\\xBF'.decode( "unicode_escape").encode("latin1") self.assertEqual(3, len(bom)) f = tempfile.NamedTemporaryFile(delete=False) @@ -305,9 +309,9 @@ def test_iterparse_utf8_bom(self): self.assertEqual(utext, root.text) def test_iterparse_utf16_bom(self): - utext = _str('Søk på nettet') + utext = 'Søk på nettet' uxml = '

%s

' % utext - boms = _bytes('\\xFE\\xFF \\xFF\\xFE').decode( + boms = b'\\xFE\\xFF \\xFF\\xFE'.decode( "unicode_escape").encode("latin1") self.assertEqual(5, len(boms)) xml = uxml.encode("utf-16") @@ -328,8 +332,48 @@ def test_iterparse_utf16_bom(self): class ETreeIOTestCase(_IOTestCaseBase): + __test__ = True etree = etree + @needs_feature('zlib') + def test_parse_gzip_file_decompress(self): + XMLParser = self.etree.XMLParser + parse = self.etree.parse + tostring = self.etree.tostring + + data = b'
' + b'' * 200 + b'' + parser = XMLParser(decompress=True) + + with tempfile.TemporaryDirectory() as temp_dir: + gzfile = pathlib.Path(temp_dir) / "input.xml.gz" + with gzip.GzipFile(gzfile, mode='wb') as outfile: + outfile.write(data) + + root = parse(str(gzfile), parser=parser) + + self.assertEqual(tostring(root), data) + + @needs_feature('zlib') + def test_parse_gzip_file_default_no_unzip(self): + parse = self.etree.parse + tostring = self.etree.tostring + + data = b'' + b'' * 200 + b'' + + with tempfile.TemporaryDirectory() as temp_dir: + gzfile = pathlib.Path(temp_dir) / "input.xml.gz" + with gzip.GzipFile(gzfile, mode='wb') as outfile: + outfile.write(data) + + try: + root = parse(str(gzfile)) + except self.etree.XMLSyntaxError: + pass # self.assertGreaterEqual(self.etree.LIBXML_VERSION, (2, 15)) + else: + pass # self.assertLess(self.etree.LIBXML_VERSION, (2, 15)) + output = tostring(root) + self.assertEqual(output, data) + def test_write_compressed_text(self): Element = self.etree.Element SubElement = self.etree.SubElement @@ -358,16 +402,21 @@ def test_write_compressed_text(self): if ElementTree: class ElementTreeIOTestCase(_IOTestCaseBase): + __test__ = True etree = ElementTree def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(ETreeIOTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeIOTestCase)]) if ElementTree: - suite.addTests([unittest.makeSuite(ElementTreeIOTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ElementTreeIOTestCase)]) return suite +# Hide test base classes from test discovery. +del _IOTestCaseBase + + if __name__ == '__main__': print('to test use test.py %s' % __file__) diff --git a/src/lxml/tests/test_isoschematron.py b/src/lxml/tests/test_isoschematron.py index 6d2aa3fb6..b2bc313a5 100644 --- a/src/lxml/tests/test_isoschematron.py +++ b/src/lxml/tests/test_isoschematron.py @@ -1,10 +1,7 @@ -# -*- coding: utf-8 -*- - """ Test cases related to ISO-Schematron parsing and validation """ -from __future__ import absolute_import import unittest from lxml import isoschematron @@ -55,6 +52,8 @@ def test_schematron_empty_pattern(self): schema = isoschematron.Schematron(schema) self.assertTrue(schema) + @unittest.skipIf(not isoschematron.schematron_schema_valid_supported, + 'SchematronParseError is only raised when validate_schema is true') def test_schematron_invalid_schema_empty(self): schema = self.parse('''\ @@ -65,6 +64,20 @@ def test_schematron_invalid_schema_empty(self): def test_schematron_invalid_schema_namespace(self): schema = self.parse('''\ +''') + self.assertRaises(etree.SchematronParseError, + isoschematron.Schematron, schema) + + def test_schematron_invalid_namespace_prefix(self): + schema = self.parse('''\ + +''') + self.assertRaises(etree.SchematronParseError, + isoschematron.Schematron, schema) + + def test_schematron_missing_namespace_prefix(self): + schema = self.parse('''\ + ''') self.assertRaises(etree.SchematronParseError, isoschematron.Schematron, schema) @@ -860,10 +873,10 @@ def test_schematron_fail_on_report(self): def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(ETreeISOSchematronTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeISOSchematronTestCase)]) suite.addTests(doctest.DocTestSuite(isoschematron)) suite.addTests( - [make_doctest('../../../doc/validation.txt')]) + [make_doctest('validation.txt')]) return suite if __name__ == '__main__': diff --git a/src/lxml/tests/test_nsclasses.py b/src/lxml/tests/test_nsclasses.py index a0aa608d7..b6d19337c 100644 --- a/src/lxml/tests/test_nsclasses.py +++ b/src/lxml/tests/test_nsclasses.py @@ -1,18 +1,15 @@ -# -*- coding: utf-8 -*- - """ Test cases related to namespace implementation classes and the namespace registry mechanism """ -from __future__ import absolute_import - +import gc import unittest -from .common_imports import etree, HelperTestCase, _bytes, make_doctest +from .common_imports import etree, HelperTestCase, make_doctest, IS_PYPY class ETreeNamespaceClassesTestCase(HelperTestCase): - + class default_class(etree.ElementBase): pass class maeh_class(etree.ElementBase): @@ -23,7 +20,7 @@ def bluff(self): return 'bluff' def setUp(self): - super(ETreeNamespaceClassesTestCase, self).setUp() + super().setUp() lookup = etree.ElementNamespaceClassLookup() self.Namespace = lookup.get_namespace parser = etree.XMLParser() @@ -33,7 +30,7 @@ def setUp(self): def tearDown(self): etree.set_default_parser() del self.Namespace - super(ETreeNamespaceClassesTestCase, self).tearDown() + super().tearDown() def test_registry(self): ns = self.Namespace('ns01') @@ -52,7 +49,7 @@ def test_ns_classes(self): self.Namespace('ns10').update(bluff_dict) - tree = self.parse(_bytes('')) + tree = self.parse(b'') el = tree.getroot() self.assertTrue(isinstance(el, etree.ElementBase)) @@ -61,6 +58,12 @@ def test_ns_classes(self): self.assertFalse(hasattr(el[0], 'bluff')) self.assertEqual(el.bluff(), 'bluff') del el + gc.collect() + + if IS_PYPY: + # PyPy doesn't necessarily clean up the tree immediately. + # Relax the test and use a new tree. + tree = self.parse(b'') self.Namespace('ns11').update(maeh_dict) el = tree.getroot() @@ -69,10 +72,11 @@ def test_ns_classes(self): self.assertEqual(el.bluff(), 'bluff') self.assertEqual(el[0].maeh(), 'maeh') del el + gc.collect() self.Namespace('ns10').clear() - tree = self.parse(_bytes('')) + tree = self.parse(b'') el = tree.getroot() self.assertFalse(hasattr(el, 'bluff')) self.assertFalse(hasattr(el, 'maeh')) @@ -90,11 +94,11 @@ def test_default_tagname(self): ns = self.Namespace("uri:nsDefClass") ns.update(bluff_dict) - tree = self.parse(_bytes(''' + tree = self.parse(b''' - ''')) + ''') el = tree.getroot() self.assertFalse(isinstance(el, etree.ElementBase)) @@ -199,13 +203,13 @@ class honk(etree.ElementBase): self.assertEqual('TAiL', el[1][1].tail) self.assertEqual('bluff_class', el[1][0].tag) self.assertEqual('{http://a.b/c}HONK', el[1][1].tag) - + def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(ETreeNamespaceClassesTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeNamespaceClassesTestCase)]) suite.addTests( - [make_doctest('../../../doc/element_classes.txt')]) + [make_doctest('element_classes.txt')]) return suite if __name__ == '__main__': diff --git a/src/lxml/tests/test_objectify.py b/src/lxml/tests/test_objectify.py index f50a34474..f482d9830 100644 --- a/src/lxml/tests/test_objectify.py +++ b/src/lxml/tests/test_objectify.py @@ -1,21 +1,23 @@ -# -*- coding: utf-8 -*- - """ Tests specific to the lxml.objectify API """ -from __future__ import absolute_import - +import functools +import itertools import operator import random import unittest from .common_imports import ( - etree, HelperTestCase, fileInTestDir, doctest, make_doctest, _bytes, _str, BytesIO + etree, HelperTestCase, fileInTestDir, doctest, make_doctest, IS_PYPY, _str, BytesIO ) from lxml import objectify +def no_pypy(cls): + return None if IS_PYPY else cls + + PYTYPE_NAMESPACE = "http://codespeak.net/lxml/objectify/pytype" XML_SCHEMA_NS = "http://www.w3.org/2001/XMLSchema" XML_SCHEMA_INSTANCE_NS = "http://www.w3.org/2001/XMLSchema-instance" @@ -40,8 +42,9 @@ # None: xsi:nil="true" } -xsitype2objclass = dict([ (v, k) for k in objectclass2xsitype - for v in objectclass2xsitype[k] ]) +xsitype2objclass = { v: k + for k in objectclass2xsitype + for v in objectclass2xsitype[k] } objectclass2pytype = { # objectify built-in @@ -52,8 +55,8 @@ # None: xsi:nil="true" } -pytype2objclass = dict([ (objectclass2pytype[k], k) - for k in objectclass2pytype]) +pytype2objclass = { objectclass2pytype[k]: k + for k in objectclass2pytype} xml_str = '''\ @@ -66,16 +69,17 @@ ''' +@no_pypy class ObjectifyTestCase(HelperTestCase): """Test cases for lxml.objectify """ etree = etree - + def XML(self, xml): return self.etree.XML(xml, self.parser) def setUp(self): - super(ObjectifyTestCase, self).setUp() + super().setUp() self.parser = self.etree.XMLParser(remove_blank_text=True) self.lookup = etree.ElementNamespaceClassLookup( objectify.ObjectifyElementClassLookup() ) @@ -100,7 +104,7 @@ def tearDown(self): pytype.register() del self._orig_types - super(ObjectifyTestCase, self).tearDown() + super().tearDown() def test_element_nsmap_default(self): @@ -118,7 +122,7 @@ def test_element_nsmap_custom_prefixes(self): "myxsd": XML_SCHEMA_NS} elt = objectify.Element("test", nsmap=nsmap) self.assertEqual(elt.nsmap, nsmap) - + def test_element_nsmap_custom(self): nsmap = {"my": "someNS", "myother": "someOtherNS", @@ -127,8 +131,8 @@ def test_element_nsmap_custom(self): self.assertTrue(PYTYPE_NAMESPACE in elt.nsmap.values()) for prefix, ns in nsmap.items(): self.assertTrue(prefix in elt.nsmap) - self.assertEqual(nsmap[prefix], elt.nsmap[prefix]) - + self.assertEqual(nsmap[prefix], elt.nsmap[prefix]) + def test_sub_element_nsmap_default(self): root = objectify.Element("root") root.sub = objectify.Element("test") @@ -147,7 +151,7 @@ def test_sub_element_nsmap_custom_prefixes(self): "myxsd": XML_SCHEMA_NS} root.sub = objectify.Element("test", nsmap=nsmap) self.assertEqual(root.sub.nsmap, DEFAULT_NSMAP) - + def test_sub_element_nsmap_custom(self): root = objectify.Element("root") nsmap = {"my": "someNS", @@ -157,8 +161,8 @@ def test_sub_element_nsmap_custom(self): expected = nsmap.copy() del expected["myxsd"] expected.update(DEFAULT_NSMAP) - self.assertEqual(root.sub.nsmap, expected) - + self.assertEqual(root.sub.nsmap, expected) + def test_data_element_nsmap_default(self): value = objectify.DataElement("test this") self.assertEqual(value.nsmap, DEFAULT_NSMAP) @@ -174,7 +178,7 @@ def test_data_element_nsmap_custom_prefixes(self): "myxsd": XML_SCHEMA_NS} value = objectify.DataElement("test this", nsmap=nsmap) self.assertEqual(value.nsmap, nsmap) - + def test_data_element_nsmap_custom(self): nsmap = {"my": "someNS", "myother": "someOtherNS", @@ -183,8 +187,8 @@ def test_data_element_nsmap_custom(self): self.assertTrue(PYTYPE_NAMESPACE in value.nsmap.values()) for prefix, ns in nsmap.items(): self.assertTrue(prefix in value.nsmap) - self.assertEqual(nsmap[prefix], value.nsmap[prefix]) - + self.assertEqual(nsmap[prefix], value.nsmap[prefix]) + def test_sub_data_element_nsmap_default(self): root = objectify.Element("root") root.value = objectify.DataElement("test this") @@ -203,7 +207,7 @@ def test_sub_data_element_nsmap_custom_prefixes(self): "myxsd": XML_SCHEMA_NS} root.value = objectify.DataElement("test this", nsmap=nsmap) self.assertEqual(root.value.nsmap, DEFAULT_NSMAP) - + def test_sub_data_element_nsmap_custom(self): root = objectify.Element("root") nsmap = {"my": "someNS", @@ -235,7 +239,7 @@ def test_data_element_attrib_attributes_precedence(self): self.assertEqual(value.get("cat"), "meeow") self.assertEqual(value.get("dog"), "grrr") self.assertEqual(value.get("bird"), "tchilp") - + def test_data_element_data_element_arg(self): # Check that DataElement preserves all attributes ObjectifiedDataElement # arguments @@ -317,7 +321,7 @@ def test_data_element_invalid_pytype(self): def test_data_element_invalid_xsi(self): self.assertRaises(ValueError, objectify.DataElement, 3.1415, _xsi="xsd:int") - + def test_data_element_data_element_arg_invalid_pytype(self): arg = objectify.DataElement(3.1415) self.assertRaises(ValueError, objectify.DataElement, arg, @@ -334,7 +338,7 @@ def test_data_element_element_arg(self): self.assertTrue(isinstance(value, objectify.ObjectifiedElement)) for attr in arg.attrib: self.assertEqual(value.get(attr), arg.get(attr)) - + def test_root(self): root = self.Element("test") self.assertTrue(isinstance(root, objectify.ObjectifiedElement)) @@ -371,6 +375,13 @@ def test_child_nonexistant(self): self.assertRaises(AttributeError, getattr, root.c1, "NOT_THERE") self.assertRaises(AttributeError, getattr, root.c1, "{unknownNS}c2") + def test_child_special(self): + root = self.XML(xml_str) + self.assertEqual(objectify.ObjectifiedElement, root.c1.__class__) + self.assertTrue(callable(root.c1.__str__)) + self.assertTrue(callable(root.c1.__len__)) + self.assertTrue(callable(root.c1.__getattr__)) + def test_child_getattr_empty_ns(self): root = self.XML(xml_str) self.assertEqual("4", getattr(root.c1, "{}c2").text) @@ -378,23 +389,23 @@ def test_child_getattr_empty_ns(self): def test_setattr(self): for val in [ - 2, 2**32, 1.2, "Won't get fooled again", + 2, 2**32, 1.2, "Won't get fooled again", _str("W\xf6n't get f\xf6\xf6led \xe4g\xe4in", 'ISO-8859-1'), True, - False, None]: + False, None]: root = self.Element('root') attrname = 'val' setattr(root, attrname, val) result = getattr(root, attrname) self.assertEqual(val, result) self.assertEqual(type(val), type(result.pyval)) - + def test_setattr_nonunicode(self): root = self.Element('root') attrname = 'val' - val = _bytes("W\xf6n't get f\xf6\xf6led \xe4g\xe4in", 'ISO-8859-1') + val = bytes("W\xf6n't get f\xf6\xf6led \xe4g\xe4in", 'ISO-8859-1') self.assertRaises(ValueError, setattr, root, attrname, val) - self.assertRaises(AttributeError, getattr, root, attrname) - + self.assertRaises(AttributeError, getattr, root, attrname) + def test_addattr(self): root = self.XML(xml_str) self.assertEqual(1, len(root.c1)) @@ -919,7 +930,7 @@ def test_type_str_add(self): s = "toast" self.assertEqual("test" + s, root.s + s) self.assertEqual(s + "test", s + root.s) - + def test_type_str_mod(self): s = "%d %f %s %r" el = objectify.DataElement(s) @@ -950,7 +961,7 @@ def test_type_str_as_int(self): v = "1" el = objectify.DataElement(v) self.assertEqual(int(el), 1) - + def test_type_str_as_float(self): v = "1" el = objectify.DataElement(v) @@ -960,7 +971,7 @@ def test_type_str_as_complex(self): v = "1" el = objectify.DataElement(v) self.assertEqual(complex(el), 1) - + def test_type_str_mod_data_elements(self): s = "%d %f %s %r" el = objectify.DataElement(s) @@ -1093,7 +1104,7 @@ def test_type_float_instantiation_precision(self): # test precision preservation for FloatElement instantiation s = "2.305064300557" self.assertEqual(objectify.FloatElement(s), float(s)) - + def test_type_float_precision_consistency(self): # test consistent FloatElement values for the different instantiation # possibilities @@ -1132,7 +1143,7 @@ def test_data_element_xsitypes(self): self.assertTrue(isinstance(value, objclass), "DataElement(%s, _xsi='%s') returns %s, expected %s" % (pyval, xsi, type(value), objclass)) - + def test_data_element_xsitypes_xsdprefixed(self): for xsi, objclass in xsitype2objclass.items(): # 1 is a valid value for all ObjectifiedDataElement classes @@ -1141,7 +1152,7 @@ def test_data_element_xsitypes_xsdprefixed(self): self.assertTrue(isinstance(value, objclass), "DataElement(%s, _xsi='%s') returns %s, expected %s" % (pyval, xsi, type(value), objclass)) - + def test_data_element_xsitypes_prefixed(self): for xsi, objclass in xsitype2objclass.items(): # 1 is a valid value for all ObjectifiedDataElement classes @@ -1167,7 +1178,7 @@ def test_data_element_pytype_none(self): % (pyval, pytype, type(value), objclass)) self.assertEqual(value.text, None) self.assertEqual(value.pyval, None) - + def test_data_element_pytype_none_compat(self): # pre-2.0 lxml called NoneElement "none" pyval = 1 @@ -1209,7 +1220,7 @@ def test_schema_types(self): 5 5 - + 5 5 5 @@ -1229,7 +1240,7 @@ def test_schema_types(self): 5 5 5 - + 5 5 5 @@ -1250,7 +1261,7 @@ def test_schema_types(self): for f in root.f: self.assertTrue(isinstance(f, objectify.FloatElement)) self.assertEqual(5, f) - + for s in root.s: self.assertTrue(isinstance(s, objectify.StringElement)) self.assertEqual("5", s) @@ -1262,7 +1273,7 @@ def test_schema_types(self): for l in root.l: self.assertTrue(isinstance(l, objectify.IntElement)) self.assertEqual(5, i) - + self.assertTrue(isinstance(root.n, objectify.NoneElement)) self.assertEqual(None, root.n) @@ -1278,7 +1289,7 @@ def test_schema_types_prefixed(self): 5 5 - + 5 5 5 @@ -1298,7 +1309,7 @@ def test_schema_types_prefixed(self): 5 5 5 - + 5 5 5 @@ -1319,7 +1330,7 @@ def test_schema_types_prefixed(self): for f in root.f: self.assertTrue(isinstance(f, objectify.FloatElement)) self.assertEqual(5, f) - + for s in root.s: self.assertTrue(isinstance(s, objectify.StringElement)) self.assertEqual("5", s) @@ -1331,20 +1342,20 @@ def test_schema_types_prefixed(self): for l in root.l: self.assertTrue(isinstance(l, objectify.IntElement)) self.assertEqual(5, l) - + self.assertTrue(isinstance(root.n, objectify.NoneElement)) self.assertEqual(None, root.n) - + def test_type_str_sequence(self): XML = self.XML - root = XML(_bytes('whytry')) + root = XML(b'whytry') strs = [ str(s) for s in root.b ] self.assertEqual(["why", "try"], strs) def test_type_str_cmp(self): XML = self.XML - root = XML(_bytes('testtaste')) + root = XML(b'testtaste') self.assertFalse(root.b[0] < root.b[1]) self.assertFalse(root.b[0] <= root.b[1]) self.assertFalse(root.b[0] == root.b[1]) @@ -1361,7 +1372,7 @@ def test_type_str_cmp(self): self.assertEqual("", root.b[3]) self.assertEqual(root.b[3], "") self.assertEqual(root.b[2], root.b[3]) - + root.b = "test" self.assertTrue(root.b) root.b = "" @@ -1371,7 +1382,7 @@ def test_type_str_cmp(self): def test_type_int_cmp(self): XML = self.XML - root = XML(_bytes('56')) + root = XML(b'56') self.assertTrue(root.b[0] < root.b[1]) self.assertTrue(root.b[0] <= root.b[1]) self.assertTrue(root.b[0] != root.b[1]) @@ -1388,12 +1399,12 @@ def test_type_int_cmp(self): self.assertTrue(root.b) root.b = 0 self.assertFalse(root.b) - + # float + long share the NumberElement implementation with int def test_type_bool_cmp(self): XML = self.XML - root = XML(_bytes('falsetrue')) + root = XML(b'falsetrue') self.assertTrue(root.b[0] < root.b[1]) self.assertTrue(root.b[0] <= root.b[1]) self.assertTrue(root.b[0] != root.b[1]) @@ -1417,10 +1428,10 @@ def test_type_bool_cmp(self): def test_type_none_cmp(self): XML = self.XML - root = XML(_bytes(""" + root = XML(b""" - """)) +
""") self.assertTrue(root.b[0] == root.b[1]) self.assertFalse(root.b[0]) self.assertEqual(root.b[0], None) @@ -1442,7 +1453,7 @@ def test_dataelement_xsi(self): 'xsd:string') def test_dataelement_xsi_nsmap(self): - el = objectify.DataElement(1, _xsi="string", + el = objectify.DataElement(1, _xsi="string", nsmap={'schema': XML_SCHEMA_NS}) self.assertEqual( el.get(XML_SCHEMA_INSTANCE_TYPE_ATTR), @@ -1454,7 +1465,7 @@ def test_dataelement_xsi_prefix_error(self): def test_pytype_annotation(self): XML = self.XML - root = XML(_bytes('''\ + root = XML('''\ 5 @@ -1472,7 +1483,7 @@ def test_pytype_annotation(self): 2 - ''')) + ''') objectify.annotate(root) child_types = [ c.get(objectify.PYTYPE_ATTRIBUTE) @@ -1491,17 +1502,17 @@ def test_pytype_annotation(self): self.assertEqual("int", child_types[11]) self.assertEqual("int", child_types[12]) self.assertEqual(None, child_types[13]) - + self.assertEqual("true", root.n.get(XML_SCHEMA_NIL_ATTR)) def test_pytype_annotation_empty(self): XML = self.XML - root = XML(_bytes('''\ + root = XML(b'''\ - ''')) + ''') objectify.annotate(root) child_types = [ c.get(objectify.PYTYPE_ATTRIBUTE) @@ -1516,7 +1527,7 @@ def test_pytype_annotation_empty(self): def test_pytype_annotation_use_old(self): XML = self.XML - root = XML(_bytes('''\ + root = XML('''\ 5 @@ -1534,7 +1545,7 @@ def test_pytype_annotation_use_old(self): 2 - ''')) + ''') objectify.annotate(root, ignore_old=False) child_types = [ c.get(objectify.PYTYPE_ATTRIBUTE) @@ -1553,12 +1564,12 @@ def test_pytype_annotation_use_old(self): self.assertEqual("float", child_types[11]) self.assertEqual("int", child_types[12]) self.assertEqual(TREE_PYTYPE, child_types[13]) - + self.assertEqual("true", root.n.get(XML_SCHEMA_NIL_ATTR)) def test_pytype_xsitype_annotation(self): XML = self.XML - root = XML(_bytes('''\ + root = XML('''\ 5 @@ -1576,10 +1587,10 @@ def test_pytype_xsitype_annotation(self): 2 - ''')) + ''') objectify.annotate(root, ignore_old=False, ignore_xsi=False, annotate_xsi=1, annotate_pytype=1) - + # check py annotations child_types = [ c.get(objectify.PYTYPE_ATTRIBUTE) for c in root.iterchildren() ] @@ -1597,7 +1608,7 @@ def test_pytype_xsitype_annotation(self): self.assertEqual("float", child_types[11]) self.assertEqual("int", child_types[12]) self.assertEqual(TREE_PYTYPE, child_types[13]) - + self.assertEqual("true", root.n.get(XML_SCHEMA_NIL_ATTR)) child_xsitypes = [ c.get(XML_SCHEMA_INSTANCE_TYPE_ATTR) @@ -1625,7 +1636,7 @@ def test_pytype_xsitype_annotation(self): def test_xsiannotate_use_old(self): XML = self.XML - root = XML(_bytes('''\ + root = XML('''\ 5 @@ -1643,7 +1654,7 @@ def test_xsiannotate_use_old(self): 2 - ''')) + ''') objectify.xsiannotate(root, ignore_old=False) child_types = [ c.get(XML_SCHEMA_INSTANCE_TYPE_ATTR) @@ -1665,7 +1676,7 @@ def test_xsiannotate_use_old(self): def test_pyannotate_ignore_old(self): XML = self.XML - root = XML(_bytes('''\ + root = XML('''\ 5 @@ -1683,7 +1694,7 @@ def test_pyannotate_ignore_old(self): 2 - ''')) + ''') objectify.pyannotate(root, ignore_old=True) child_types = [ c.get(objectify.PYTYPE_ATTRIBUTE) @@ -1702,7 +1713,7 @@ def test_pyannotate_ignore_old(self): self.assertEqual("int", child_types[11]) self.assertEqual("int", child_types[12]) self.assertEqual(None, child_types[13]) - + self.assertEqual("true", root.n.get(XML_SCHEMA_NIL_ATTR)) def test_pyannotate_empty(self): @@ -1764,12 +1775,12 @@ def test_pyannotate_use_old(self): self.assertEqual("float", child_types[11]) self.assertEqual("int", child_types[12]) self.assertEqual(TREE_PYTYPE, child_types[13]) - + self.assertEqual("true", root.n.get(XML_SCHEMA_NIL_ATTR)) - + def test_xsiannotate_ignore_old(self): XML = self.XML - root = XML(_bytes('''\ + root = XML('''\ 5 @@ -1787,7 +1798,7 @@ def test_xsiannotate_ignore_old(self): 2 - ''')) + ''') objectify.xsiannotate(root, ignore_old=True) child_types = [ c.get(XML_SCHEMA_INSTANCE_TYPE_ATTR) @@ -1811,7 +1822,7 @@ def test_xsiannotate_ignore_old(self): def test_deannotate(self): XML = self.XML - root = XML(_bytes('''\ + root = XML('''\ 5 @@ -1829,7 +1840,7 @@ def test_deannotate(self): 2 - ''')) + ''') objectify.deannotate(root) for c in root.getiterator(): @@ -1840,7 +1851,7 @@ def test_deannotate(self): def test_xsinil_deannotate(self): XML = self.XML - root = XML(_bytes('''\ + root = XML('''\ 5 @@ -1858,7 +1869,7 @@ def test_xsinil_deannotate(self): 2 - ''')) + ''') objectify.annotate( root, ignore_old=False, ignore_xsi=False, annotate_xsi=True, empty_pytype='str', empty_type='string') @@ -1886,14 +1897,14 @@ def test_xsinil_deannotate(self): for c in root.iterchildren(): self.assertNotEqual(None, c.get(objectify.PYTYPE_ATTRIBUTE)) # these have no equivalent in xsi:type - if (c.get(objectify.PYTYPE_ATTRIBUTE) not in [TREE_PYTYPE, + if (c.get(objectify.PYTYPE_ATTRIBUTE) not in [TREE_PYTYPE, "NoneType"]): self.assertNotEqual( None, c.get(XML_SCHEMA_INSTANCE_TYPE_ATTR)) def test_xsitype_deannotate(self): XML = self.XML - root = XML(_bytes('''\ + root = XML('''\ @@ -1912,7 +1923,7 @@ def test_xsitype_deannotate(self): 2 - ''')) + ''') objectify.annotate(root) objectify.deannotate(root, pytype=False) @@ -1932,7 +1943,7 @@ def test_xsitype_deannotate(self): self.assertEqual("int", child_types[11]) self.assertEqual("int", child_types[12]) self.assertEqual(None, child_types[13]) - + self.assertEqual("true", root.n.get(XML_SCHEMA_NIL_ATTR)) for c in root.getiterator(): @@ -1940,7 +1951,7 @@ def test_xsitype_deannotate(self): def test_pytype_deannotate(self): XML = self.XML - root = XML(_bytes('''\ + root = XML('''\ @@ -1959,7 +1970,7 @@ def test_pytype_deannotate(self): 2 - ''')) + ''') objectify.annotate(root) objectify.deannotate(root, xsi=False) @@ -1988,7 +1999,7 @@ def test_pytype_deannotate(self): def test_change_pytype_attribute(self): XML = self.XML - xml = _bytes('''\ + xml = '''\ 5 test @@ -1999,7 +2010,7 @@ def test_change_pytype_attribute(self): 5 - ''') + ''' pytype_ns, pytype_name = objectify.PYTYPE_ATTRIBUTE[1:].split('}') objectify.set_pytype_attribute_tag("{TEST}test") @@ -2597,22 +2608,22 @@ def __init__(self): self.assertEqual(attr.get("range"), "0.,1.") def test_XML_base_url_docinfo(self): - root = objectify.XML(_bytes(""), base_url="http://no/such/url") + root = objectify.XML(b"", base_url="http://no/such/url") docinfo = root.getroottree().docinfo self.assertEqual(docinfo.URL, "http://no/such/url") - + def test_XML_set_base_url_docinfo(self): - root = objectify.XML(_bytes(""), base_url="http://no/such/url") + root = objectify.XML(b"", base_url="http://no/such/url") docinfo = root.getroottree().docinfo self.assertEqual(docinfo.URL, "http://no/such/url") docinfo.URL = "https://secret/url" self.assertEqual(docinfo.URL, "https://secret/url") - + def test_parse_stringio_base_url(self): - tree = objectify.parse(BytesIO(""), base_url="http://no/such/url") + tree = objectify.parse(BytesIO(b""), base_url="http://no/such/url") docinfo = tree.docinfo self.assertEqual(docinfo.URL, "http://no/such/url") - + def test_parse_base_url_docinfo(self): tree = objectify.parse(fileInTestDir('include/test_xinclude.xml'), base_url="http://no/such/url") @@ -2620,7 +2631,7 @@ def test_parse_base_url_docinfo(self): self.assertEqual(docinfo.URL, "http://no/such/url") def test_xml_base(self): - root = objectify.XML(_bytes(""), base_url="http://no/such/url") + root = objectify.XML(b"", base_url="http://no/such/url") self.assertEqual(root.base, "http://no/such/url") self.assertEqual( root.get('{http://www.w3.org/XML/1998/namespace}base'), None) @@ -2629,9 +2640,9 @@ def test_xml_base(self): self.assertEqual( root.get('{http://www.w3.org/XML/1998/namespace}base'), "https://secret/url") - + def test_xml_base_attribute(self): - root = objectify.XML(_bytes(""), base_url="http://no/such/url") + root = objectify.XML(b"", base_url="http://no/such/url") self.assertEqual(root.base, "http://no/such/url") self.assertEqual( root.get('{http://www.w3.org/XML/1998/namespace}base'), None) @@ -2645,7 +2656,7 @@ def test_xml_base_attribute(self): def test_standard_lookup(self): XML = self.XML - xml = _bytes('''\ + root = XML('''\ 5 -5 @@ -2663,6 +2674,7 @@ def test_standard_lookup(self): t f + ²²²² 12_34 1.2_34 34E @@ -2672,7 +2684,6 @@ def test_standard_lookup(self): ''') - root = XML(xml) for i in root.i: self.assertTrue(isinstance(i, objectify.IntElement), (i.text, type(i))) @@ -2690,14 +2701,16 @@ def test_standard_lookup(self): self.assertEqual(None, root.n) def test_standard_lookup_fuzz(self): - SPACES = ('',) * 10 + ('\t', 'x', '\n', '\r\n', u'\xA0', u'\x0A', u'\u200A', u'\u200B') DIGITS = ('', '0', '1', '11', '21', '345678', '9'*20) + SPECIAL_STRINGS = ('', 'INF', 'inf', 'NaN', 'nan', 'an', 'na', 'ana', 'nf') + SPACES = [''] * 9 + ['\t', 'x', '\n', '\r\n', '\xA0', '\x0A', '\u200A', '\u200B'] # 9+8 = 17 items - def space(_choice=random.choice): - return _choice(SPACES) + # Spaces should generally not matter, so we just shuffle them in randomly. + random.shuffle(SPACES) + space = functools.partial(next, itertools.cycle(SPACES)) fuzz = [ - '%s\n' % (space() + sign + digits + point + fraction + exp + exp_sign + exp_digits + special + space()) + f'{space()}{sign}{digits}{point}{fraction}{exp}{exp_sign}{exp_digits}{special}{space()}\n' for sign in ('', '+', '-') for digits in DIGITS for point in ('', '.') @@ -2705,29 +2718,29 @@ def space(_choice=random.choice): for exp in ('', 'E') for exp_sign in ('', '+', '-') for exp_digits in DIGITS - for special in ('', 'INF', 'inf', 'NaN', 'nan', 'an', 'na', 'ana', 'nf') + for special in SPECIAL_STRINGS ] - root = self.XML(_bytes('''\ + root = self.XML('''\ ''' + ''.join(fuzz) + ''' - ''')) + ''') test_count = 0 for el in root.iterchildren(): text = el.text - expected_type = objectify.ObjectifiedElement if text: + expected_type = objectify.StringElement try: + float(text) + expected_type = objectify.FloatElement int(text) expected_type = objectify.IntElement except ValueError: - try: - float(text) - expected_type = objectify.FloatElement - except ValueError: - expected_type = objectify.StringElement + pass + else: + expected_type = objectify.ObjectifiedElement self.assertTrue(isinstance(el, expected_type), (text, expected_type, type(el))) test_count += 1 @@ -2736,10 +2749,12 @@ def space(_choice=random.choice): def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(ObjectifyTestCase)]) - suite.addTests(doctest.DocTestSuite(objectify)) - suite.addTests([make_doctest('../../../doc/objectify.txt')]) + if not IS_PYPY: + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ObjectifyTestCase)]) + suite.addTests(doctest.DocTestSuite(objectify)) + suite.addTests([make_doctest('objectify.txt')]) return suite + if __name__ == '__main__': print('to test use test.py %s' % __file__) diff --git a/src/lxml/tests/test_pyclasslookup.py b/src/lxml/tests/test_pyclasslookup.py index d650870a5..a709e5c21 100644 --- a/src/lxml/tests/test_pyclasslookup.py +++ b/src/lxml/tests/test_pyclasslookup.py @@ -1,10 +1,7 @@ -# -*- coding: utf-8 -*- - """ Tests specific to the Python based class lookup. """ -from __future__ import absolute_import import unittest @@ -13,7 +10,7 @@ from lxml.etree import PythonElementClassLookup -xml_str = _bytes('''\ +xml_str = b'''\ 0 @@ -22,7 +19,7 @@ 3 3 -''') +''' class PyClassLookupTestCase(HelperTestCase): @@ -34,7 +31,7 @@ class PyClassLookupTestCase(HelperTestCase): def tearDown(self): self.parser.set_element_class_lookup(None) - super(PyClassLookupTestCase, self).tearDown() + super().tearDown() def _setClassLookup(self, lookup_function): class Lookup(PythonElementClassLookup): @@ -344,7 +341,7 @@ def return_none(*args): def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(PyClassLookupTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(PyClassLookupTestCase)]) return suite if __name__ == '__main__': diff --git a/src/lxml/tests/test_relaxng.py b/src/lxml/tests/test_relaxng.py index 3c589c18a..537bfc79f 100644 --- a/src/lxml/tests/test_relaxng.py +++ b/src/lxml/tests/test_relaxng.py @@ -1,10 +1,7 @@ -# -*- coding: utf-8 -*- - """ Test cases related to RelaxNG parsing and validation """ -from __future__ import absolute_import import unittest @@ -44,7 +41,7 @@ def test_relaxng(self): def test_relaxng_stringio(self): tree_valid = self.parse('') tree_invalid = self.parse('') - schema_file = BytesIO('''\ + schema_file = BytesIO(b'''\ @@ -208,12 +205,12 @@ def test_multiple_elementrees(self): ''') ) c_tree = etree.ElementTree(tree.getroot()[1]) - self.assertEqual(self._rootstring(c_tree), _bytes('C')) + self.assertEqual(self._rootstring(c_tree), b'C') self.assertFalse(schema.validate(c_tree)) self.assertTrue(schema.error_log.filter_from_errors()) b_tree = etree.ElementTree(tree.getroot()[0]) - self.assertEqual(self._rootstring(b_tree), _bytes('B')) + self.assertEqual(self._rootstring(b_tree), b'B') self.assertTrue(schema.validate(b_tree)) self.assertFalse(schema.error_log.filter_from_errors()) @@ -230,7 +227,7 @@ def test_relaxng_compact(self): self.assertFalse(schema.validate(tree_invalid)) def test_relaxng_compact_file_obj(self): - with open(fileInTestDir('test.rnc'), 'r') as f: + with open(fileInTestDir('test.rnc')) as f: schema = etree.RelaxNG(file=f) tree_valid = self.parse('BC') @@ -249,11 +246,11 @@ def test_relaxng_compact_str(self): def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(ETreeRelaxNGTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeRelaxNGTestCase)]) suite.addTests( - [make_doctest('../../../doc/validation.txt')]) + [make_doctest('validation.txt')]) if rnc2rng is not None: - suite.addTests([unittest.makeSuite(RelaxNGCompactTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(RelaxNGCompactTestCase)]) return suite if __name__ == '__main__': diff --git a/src/lxml/tests/test_rwlock.py b/src/lxml/tests/test_rwlock.py new file mode 100644 index 000000000..bc6212523 --- /dev/null +++ b/src/lxml/tests/test_rwlock.py @@ -0,0 +1,488 @@ +import threading +import time +import unittest +from contextlib import contextmanager +from functools import partial, wraps + +from lxml.tests._testlock import _RWLock as RWLock, trace + + +def diff_perf_counters(start_counters, end_counters): + return { + name: end_value - start_counters[name] + for name, end_value in end_counters.items() + } + + +def assert_perf_counters(**counter_diffs): + def decorator(f): + @wraps(f) + def check_counters(self): + lock = RWLock() + start_counters = lock.get_perf_counters() + + f(self, lock) + + end_counters = lock.get_perf_counters() + diff = diff_perf_counters(start_counters, end_counters) + + for name in counter_diffs: + assert name in diff, f"Test bug: unknown counter name '{name}'" + + differences = [] + for name, diff_value in diff.items(): + if name in counter_diffs: + expected_diff_value = counter_diffs[name] + if expected_diff_value is None: + continue + else: + expected_diff_value = 0 + + if diff_value != expected_diff_value: + differences.append(f"{name} == {diff_value} != {expected_diff_value}") + + if differences: + self.fail(', '.join(sorted(differences))) + + return check_counters + return decorator + + +class RWLockTest(unittest.TestCase): + @contextmanager + def run_threads(self, *functions): + + def name(target, _counter={}): + if isinstance(target, partial): + target = target.func + try: + count = _counter[target] + except KeyError: + count = 1 + _counter[target] = count + 1 + return f"{target.__name__}-{count:02d}" + + threads = [threading.Thread(target=function, name=name(function)) for function in functions] + for thread in threads: + thread.start() + yield + for thread in threads: + thread.join() + + @assert_perf_counters(read_acquired=2) + def test_lock_read(self, lock): + assert lock.reader_count == 0, lock.reader_count + lock.lock_read() + assert lock.reader_count == 1, lock.reader_count + lock.unlock_read() + assert lock.reader_count == 0, lock.reader_count + lock.lock_read() + assert lock.reader_count == 1, lock.reader_count + lock.unlock_read() + assert lock.reader_count == 0, lock.reader_count + + @assert_perf_counters(read_acquired=4) + def test_lock_read_reentry(self, lock): + lock.lock_read() + assert lock.reader_count == 1, lock.reader_count + + lock.lock_read() + assert lock.reader_count == 2, lock.reader_count + lock.unlock_read() + assert lock.reader_count == 1, lock.reader_count + lock.lock_read() + assert lock.reader_count == 2, lock.reader_count + lock.unlock_read() + assert lock.reader_count == 1, lock.reader_count + + lock.unlock_read() + assert lock.reader_count == 0, lock.reader_count + + lock.lock_read() + assert lock.reader_count == 1, lock.reader_count + lock.unlock_read() + assert lock.reader_count == 0, lock.reader_count + + @assert_perf_counters(write_acquired=3) + def test_lock_write(self, lock): + assert not lock.writer_blocking_readers + assert lock.reader_count == 0, lock.reader_count + lock.lock_write() + assert lock.writer_blocking_readers + assert lock.reader_count == 0, lock.reader_count + lock.unlock_write() + assert not lock.writer_blocking_readers + assert lock.reader_count == 0, lock.reader_count + + lock.lock_write() + assert lock.reader_count == 0, lock.reader_count + lock.unlock_write() + assert lock.reader_count == 0, lock.reader_count + + lock.lock_write() + assert lock.reader_count == 0, lock.reader_count + lock.unlock_write() + assert lock.reader_count == 0, lock.reader_count + + @assert_perf_counters(write_acquired=2, write_reentry=3) + def test_lock_write_reentry(self, lock): + assert not lock.writer_blocking_readers + assert lock.writer_reentry == 0, lock.writer_reentry + lock.lock_write() + assert lock.writer_blocking_readers + assert lock.reader_count == 0, lock.reader_count + assert lock.writer_reentry == 0, lock.writer_reentry + + lock.lock_write() + assert lock.writer_blocking_readers + assert lock.writer_reentry == 1, lock.writer_reentry + lock.unlock_write() + assert lock.writer_blocking_readers + assert lock.writer_reentry == 0, lock.writer_reentry + + lock.lock_write() + assert lock.writer_reentry == 1, lock.writer_reentry + lock.unlock_write() + assert lock.writer_reentry == 0, lock.writer_reentry + + lock.lock_write() + assert lock.writer_reentry == 1, lock.writer_reentry + lock.unlock_write() + assert lock.writer_blocking_readers + assert lock.writer_reentry == 0, lock.writer_reentry + + lock.unlock_write() + assert not lock.writer_blocking_readers + assert lock.writer_reentry == 0, lock.writer_reentry + + lock.lock_write() + assert lock.writer_reentry == 0, lock.writer_reentry + lock.unlock_write() + assert lock.writer_reentry == 0, lock.writer_reentry + + @assert_perf_counters(write_acquired=2, write_wait_on_writer=1) + def test_lock_write_wait_writers(self, lock): + order = [] + + start = threading.Barrier(2) + waiting = threading.Event() + release = threading.Event() + + def thread_lock(): + order.append("locking (1)") + lock.lock_write() + assert lock.writer_blocking_readers + order.append("locked (1)") + + start.wait() + order.append("waiting") + + release.wait() + + order.append("releasing (1)") + lock.unlock_write() + order.append("released (1)") + + def thread_wait(): + start.wait() + order.append("waiting") + + waiting.set() + + lock.lock_write() + order.append("locked (2)") + lock.unlock_write() + order.append("released (2)") + + with self.run_threads(thread_lock, thread_wait): + waiting.wait() + release.set() + + self.assertListEqual(['locking (1)', 'locked (1)', 'waiting', 'waiting', 'releasing (1)'], order[:5]) + self.assertListEqual(['locked (2)', 'released (1)', 'released (2)'], sorted(order[5:])) + + def test_lock_write_wait_readers(self): + lock = RWLock() + + order = [] + reader_ids = range(1, 10) + + start = threading.Barrier(len(reader_ids) + 1) + waiting = threading.Barrier(len(reader_ids) + 1 + 1) + release = threading.Event() + + def thread_lock_read(n): + order.append(f"locking ({n})") + lock.lock_read() + order.append(f"locked ({n})") + assert not lock.writer_blocking_readers + + start.wait() + order.append(f"waiting ({n})") + + waiting.wait() + + assert "locked (write)" not in order + + order.append(f"releasing ({n})") + lock.unlock_read() + order.append(f"released ({n})") + + def thread_wait_write(): + start.wait() + order.append(f"waiting (write)") + + waiting.wait() + + lock.lock_write() + assert lock.writer_blocking_readers + release.wait() + order.append(f"locked (write)") + assert lock.writer_blocking_readers + lock.unlock_write() + order.append(f"released (write)") + + readers = [partial(thread_lock_read, n) for n in reader_ids] + + with self.run_threads(thread_wait_write, *readers): + waiting.wait() + release.set() + + self.assertListEqual( + [f'locked ({n})' for n in reader_ids] + [f'locking ({n})' for n in reader_ids] + [f'waiting ({n})' for n in reader_ids] + ["waiting (write)"], + sorted(order[:3*len(reader_ids) + 1])) + + #self.assertListEqual(['locking (1)', 'locked (1)', 'waiting (2)', 'waiting (1)', 'releasing (1)'], order[:5]) + #self.assertListEqual(['locked (2)', 'released (1)', 'released (2)'], sorted(order[5:])) + + def test_concurrent_read_write_processing(self): + lock = RWLock() + + memory = bytearray(range(50)) + failures = [] + + def read(): + for i, ch in enumerate(memory, memory[0]): + if ch != i: + failures.append(f"{ch} != {i}") + + def write(): + for i, ch in enumerate(memory): + memory[i] = ch + 1 + + def reader(): + start.wait() + with lock.read_lock(): + read() + + def writer(): + start.wait() + with lock.write_lock(): + write() + + def writereader(): + start.wait() + with lock.write_lock(): + write() + with lock.read_lock(): + read() + + """ + # Deadlocks! - would require remembering what threads own the read lock to allow upgrading to a write lock. + def reader_writer(): + start.wait() + with lock.read_lock(): + read() + with lock.write_lock(): + write() + """ + + threads = [reader, writer, writereader] * 33 + start = threading.Barrier(len(threads)) + + with self.run_threads(*threads): + pass + + n_writers = len(threads) - threads.count(reader) + self.assertEqual(memory, bytearray(range(n_writers, n_writers + len(memory)))) + + self.assertFalse(failures) + + def test_concurrent_read_write_locking(self): + lock = RWLock() + + # Thread monitoring: + + local = threading.local() + + reader_count = 0 + writer_count = 0 + + def lock_read(): + nonlocal reader_count + lock.lock_read() + reader_count += 1 + + def unlock_read(): + nonlocal reader_count + reader_count -= 1 + lock.unlock_read() + + def lock_write(): + nonlocal writer_count + lock.lock_write() + writer_count += 1 + + def unlock_write(): + nonlocal writer_count + writer_count -= 1 + lock.unlock_write() + + def expect(readers, writers): + rw = (reader_count, writer_count) + if readers >= 0: + ok = rw == (readers, writers) + else: + ok = writer_count == writers and rw[0] >= - readers + + try: + counts = local.counts + except AttributeError: + counts = local.counts = [] + counts.append("(ok)" if ok else {'expected': (readers, writers), 'actual': rw}) + + def check(): + counts = local.counts + self.assertListEqual(counts, ['(ok)'] * len(counts)) + + # The test threads: + + def wait(t=0.1): + time.sleep(t) + + def read(): + start.wait() + + lock_read() + expect(-1,0) + wait() + expect(-1,0) + unlock_read() + + def write(): + start.wait() + + lock_write() + expect(0, 1) + wait() + expect(0, 1) + unlock_write() + + def rw_once(): + start.wait() + + lock_write() + expect(0, 1) + wait() + expect(0, 1) + + lock_read() + expect(1, 1) + wait() + expect(1, 1) + unlock_read() + + expect(0, 1) + unlock_write() + + def rw_many(): + start.wait() + + lock_write() + expect(0, 1) + wait() + expect(0, 1) + + lock_read() + expect(1, 1) + wait() + expect(1, 1) + + lock_read() + expect(2, 1) + unlock_read() + + expect(1, 1) + + lock_read() + expect(2, 1) + wait() + expect(2, 1) + unlock_read() + + expect(1, 1) + unlock_read() + + expect(0, 1) + + lock_read() + expect(1, 1) + wait() + expect(1, 1) + unlock_read() + + expect(0, 1) + unlock_write() + + failures = [] + + def guard(func): + @wraps(func) + def wrapped(): + try: + func() + except Exception as exc: + import traceback + traceback.print_exc() + if not failures: + failures.append(str(exc)) + + # Try to clean up + while reader_count: + unlock_read() + while writer_count: + unlock_write() + + try: + check() + except AssertionError as exc: + failures.append(str(exc)) + + return wrapped + + threads = [ + guard(func) + for func in ( + read, + write, + #rw_once, + #rw_many, + ) + for _ in range(2) + ] + + start = threading.Barrier(len(threads)) + + with self.run_threads(*threads): + pass + + self.assertFalse(failures) + + +def test_suite(): + suite = unittest.TestSuite() + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(RWLockTest)]) + return suite + +if __name__ == '__main__': + print('to test use test.py %s' % __file__) diff --git a/src/lxml/tests/test_sax.py b/src/lxml/tests/test_sax.py index 2ed1e5135..2c8379497 100644 --- a/src/lxml/tests/test_sax.py +++ b/src/lxml/tests/test_sax.py @@ -1,17 +1,14 @@ -# -*- coding: utf-8 -*- - """ Test cases related to SAX I/O """ -from __future__ import absolute_import import unittest from xml.dom import pulldom from xml.sax.handler import ContentHandler -from .common_imports import HelperTestCase, make_doctest, BytesIO, _bytes -from lxml import sax +from .common_imports import HelperTestCase, make_doctest, BytesIO +from lxml import etree, sax class ETreeSaxTestCase(HelperTestCase): @@ -19,43 +16,43 @@ class ETreeSaxTestCase(HelperTestCase): def test_etree_sax_simple(self): tree = self.parse('abba') xml_out = self._saxify_serialize(tree) - self.assertEqual(_bytes('abba'), + self.assertEqual(b'abba', xml_out) def test_etree_sax_double(self): tree = self.parse('abbbba') xml_out = self._saxify_serialize(tree) - self.assertEqual(_bytes('abbbba'), + self.assertEqual(b'abbbba', xml_out) def test_etree_sax_comment(self): tree = self.parse('abba') xml_out = self._saxify_serialize(tree) - self.assertEqual(_bytes('abba'), + self.assertEqual(b'abba', xml_out) def test_etree_sax_pi(self): tree = self.parse('abba') xml_out = self._saxify_serialize(tree) - self.assertEqual(_bytes('abba'), + self.assertEqual(b'abba', xml_out) def test_etree_sax_comment_root(self): tree = self.parse('ab') xml_out = self._saxify_serialize(tree) - self.assertEqual(_bytes('ab'), + self.assertEqual(b'ab', xml_out) def test_etree_sax_pi_root(self): tree = self.parse('ab') xml_out = self._saxify_serialize(tree) - self.assertEqual(_bytes('ab'), + self.assertEqual(b'ab', xml_out) def test_etree_sax_attributes(self): tree = self.parse('abba') xml_out = self._saxify_serialize(tree) - self.assertEqual(_bytes('abba'), + self.assertEqual(b'abba', xml_out) def test_etree_sax_ns1(self): @@ -124,17 +121,43 @@ def test_sax_to_pulldom_multiple_namespaces(self): self.assertEqual('a', dom.firstChild.prefix) + def test_sax_non_html(self): + # https://bugs.launchpad.net/lxml/+bug/2116333 + events = [] + + from xml.sax.handler import ContentHandler + class MyContentHandler(ContentHandler): + def startElementNS(self, name, qname, attributes): + events.append(("START", name, qname, attributes.items())) + + def characters(self, data): + events.append(("DATA", data)) + + markup = ( + '' + '' + '' + ) + + parser = etree.HTMLParser(recover=True) + tree = etree.fromstring(markup, parser) + + self.assertFalse(events) + sax.saxify(tree, MyContentHandler()) + # The exact list of parsed attributes depends on the libxml2 parser version. + self.assertTrue(events) + def test_element_sax(self): tree = self.parse('') a = tree.getroot() b = a[0] xml_out = self._saxify_serialize(a) - self.assertEqual(_bytes(''), + self.assertEqual(b'', xml_out) xml_out = self._saxify_serialize(b) - self.assertEqual(_bytes(''), + self.assertEqual(b'', xml_out) def test_element_sax_ns(self): @@ -292,15 +315,15 @@ def _saxify_serialize(self, tree): new_tree = self._saxify_unsaxify(tree) f = BytesIO() new_tree.write(f) - return f.getvalue().replace(_bytes('\n'), _bytes('')) + return f.getvalue().replace(b'\n', b'') -class SimpleContentHandler(ContentHandler, object): +class SimpleContentHandler(ContentHandler): """A SAX content handler that just stores the events""" def __init__(self): self.sax_events = [] - super(SimpleContentHandler, self).__init__() + super().__init__() def startDocument(self): self.sax_events.append(('startDocument',)) @@ -405,10 +428,10 @@ def test_element_sax_twin_ns_prefix(self): def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(ETreeSaxTestCase)]) - suite.addTests([unittest.makeSuite(NSPrefixSaxTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeSaxTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(NSPrefixSaxTestCase)]) suite.addTests( - [make_doctest('../../../doc/sax.txt')]) + [make_doctest('sax.txt')]) return suite diff --git a/src/lxml/tests/test_schematron.py b/src/lxml/tests/test_schematron.py index 2096346e3..2e7544b7b 100644 --- a/src/lxml/tests/test_schematron.py +++ b/src/lxml/tests/test_schematron.py @@ -1,17 +1,16 @@ -# -*- coding: utf-8 -*- - """ Test cases related to Schematron parsing and validation """ -from __future__ import absolute_import import unittest +import warnings -from .common_imports import etree, HelperTestCase, make_doctest +from .common_imports import etree, HelperTestCase, make_doctest, needs_feature class ETreeSchematronTestCase(HelperTestCase): + @needs_feature("schematron") def test_schematron(self): tree_valid = self.parse('') tree_invalid = self.parse('') @@ -32,7 +31,12 @@ def test_schematron(self): ''') - schema = etree.Schematron(schema) + with warnings.catch_warnings(record=True) as depwarn: + warnings.resetwarnings() + schema = etree.Schematron(schema) + self.assertTrue(depwarn) + self.assertTrue([w for w in depwarn if w.category is DeprecationWarning]) + self.assertTrue(schema.validate(tree_valid)) self.assertFalse(schema.error_log.filter_from_errors()) @@ -42,9 +46,14 @@ def test_schematron(self): self.assertTrue(schema.validate(tree_valid)) # repeat valid self.assertFalse(schema.error_log.filter_from_errors()) # repeat valid + @needs_feature("schematron") def test_schematron_elementtree_error(self): - self.assertRaises(ValueError, etree.Schematron, etree.ElementTree()) + with warnings.catch_warnings(record=True) as depwarn: + warnings.resetwarnings() + self.assertRaises(ValueError, etree.Schematron, etree.ElementTree()) + self.assertTrue(depwarn) + @needs_feature("schematron") def test_schematron_invalid_schema(self): schema = self.parse('''\ @@ -52,30 +61,41 @@ def test_schematron_invalid_schema(self): ''') - self.assertRaises(etree.SchematronParseError, - etree.Schematron, schema) + with warnings.catch_warnings(record=True) as depwarn: + warnings.resetwarnings() + self.assertRaises(etree.SchematronParseError, + etree.Schematron, schema) + self.assertTrue(depwarn) + @needs_feature("schematron") def test_schematron_invalid_schema_empty(self): schema = self.parse('''\ ''') - self.assertRaises(etree.SchematronParseError, - etree.Schematron, schema) + with warnings.catch_warnings(record=True) as depwarn: + warnings.resetwarnings() + self.assertRaises(etree.SchematronParseError, + etree.Schematron, schema) + self.assertTrue(depwarn) + @needs_feature("schematron") def test_schematron_invalid_schema_namespace(self): # segfault schema = self.parse('''\ ''') - self.assertRaises(etree.SchematronParseError, - etree.Schematron, schema) + with warnings.catch_warnings(record=True) as depwarn: + warnings.resetwarnings() + self.assertRaises(etree.SchematronParseError, + etree.Schematron, schema) + self.assertTrue(depwarn) def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(ETreeSchematronTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeSchematronTestCase)]) suite.addTests( - [make_doctest('../../../doc/validation.txt')]) + [make_doctest('validation.txt')]) return suite if __name__ == '__main__': diff --git a/src/lxml/tests/test_threading.py b/src/lxml/tests/test_threading.py index 2a16858b1..b5f9098b4 100644 --- a/src/lxml/tests/test_threading.py +++ b/src/lxml/tests/test_threading.py @@ -1,22 +1,15 @@ -# -*- coding: utf-8 -*- - """ Tests for thread usage in lxml.etree. """ -from __future__ import absolute_import import re import sys import unittest import threading +from queue import Queue, Empty -from .common_imports import etree, HelperTestCase, BytesIO, _bytes - -try: - from Queue import Queue -except ImportError: - from queue import Queue # Py3 +from .common_imports import etree, HelperTestCase, BytesIO, IS_FT_PYTHON, IS_PYPY class ThreadingTestCase(HelperTestCase): @@ -62,11 +55,19 @@ def sync_start(func): self.assertEqual(0, counter['failed']) self.assertEqual(counter['finished'], counter['started']) + @unittest.skipIf(IS_FT_PYTHON, "in freethreading Python") + def test_freethreading_not_enabled(self): + self.assertFalse(getattr(etree, '_freethreading_enabled', False)) + + @unittest.skipIf(not IS_FT_PYTHON, "not in freethreading Python") + def test_freethreading_enabled(self): + self.assertTrue(getattr(etree, '_freethreading_enabled', False)) + def test_subtree_copy_thread(self): tostring = self.etree.tostring XML = self.etree.XML - xml = _bytes("") - main_root = XML(_bytes("")) + xml = b"" + main_root = XML(b"") def run_thread(): thread_root = XML(xml) @@ -76,21 +77,36 @@ def run_thread(): self._run_thread(run_thread) self.assertEqual(xml, tostring(main_root)) + def test_concurrent_parser_feed(self): + parser = self.etree.XMLParser() + parser.feed("") + + def feed_chunk(): + parser.feed("text") + + self._run_threads(15, feed_chunk) + + parser.feed("") + + root = parser.close() + self.assertEqual(len(root), 15) + self.assertListEqual(['node'] * 15, [child.tag for child in root]) + def test_main_xslt_in_thread(self): XML = self.etree.XML - style = XML(_bytes('''\ + style = XML(b'''\ -''')) +''') st = etree.XSLT(style) result = [] def run_thread(): - root = XML(_bytes('BC')) + root = XML(b'BC') result.append( st(root) ) self._run_thread(run_thread) @@ -103,30 +119,63 @@ def run_thread(): def test_thread_xslt(self): XML = self.etree.XML tostring = self.etree.tostring - root = XML(_bytes('BC')) + root = XML(b'BC') def run_thread(): - style = XML(_bytes('''\ + style = XML(b'''\ - ''')) + ''') st = etree.XSLT(style) root.append( st(root).getroot() ) self._run_thread(run_thread) - self.assertEqual(_bytes('BCB
'), + self.assertEqual(b'BCB
', tostring(root)) + def test_thread_xslt_serialisation(self): + XML = self.etree.XML + tostring = self.etree.tostring + root = XML(b'BC') + + style = XML(b'''\ + + + + + + ''') + st = etree.XSLT(style) + + result = st(root) + self.assertEqual(type(result).__name__, "_XSLTResultTree") + + def serialise(): + for i in range(10): + s = str(result) + self.assertEqual('\nBC\n', s) + b = bytes(result) + self.assertEqual(b'\nBC\n', b) + m = memoryview(result) + ba = bytearray(m) + self.assertEqual(bytearray(b'\nBC\n'), ba) + mb = bytes(m) + m = None + self.assertEqual(b'\nBC\n', mb) + + self._run_threads(15, serialise) + def test_thread_xslt_parsing_error_log(self): style = self.parse('''\ -''' + '\n'.join('' % i for i in range(200)) + ''' +''' + '\n'.join(f'' for i in range(200)) + ''' ''') self.assertRaises(etree.XSLTParseError, @@ -144,7 +193,6 @@ def run_thread(): self._run_threads(16, run_thread) - self.assertEqual(16, len(error_logs)) last_log = None for log in error_logs: self.assertTrue(len(log)) @@ -152,10 +200,12 @@ def run_thread(): self.assertEqual(len(last_log), len(log)) self.assertTrue(len(log) >= 2, len(log)) for error in log: - self.assertTrue(':ERROR:XSLT:' in str(error), str(error)) + self.assertIn(':ERROR:XSLT:', str(error), str(error)) self.assertTrue(any('UnExpectedElement' in str(error) for error in log), log) last_log = log + self.assertEqual(16, len(error_logs)) + def test_thread_xslt_apply_error_log(self): tree = self.parse('') style = self.parse('''\ @@ -165,7 +215,7 @@ def test_thread_xslt_apply_error_log(self): FAIL -''' + '\n'.join(' ' % (i, i, i-1) +''' + '\n'.join(f' ' for i in range(1, 256)) + ''' ''') self.assertRaises(etree.XSLTApplyError, @@ -192,7 +242,7 @@ def run_thread(): self.assertEqual(len(last_log), len(log)) self.assertEqual(1, len(log)) for error in log: - self.assertTrue(':ERROR:XSLT:' in str(error)) + self.assertIn(':ERROR:XSLT:', str(error)) last_log = log def test_thread_xslt_attr_replace(self): @@ -200,35 +250,35 @@ def test_thread_xslt_attr_replace(self): # modified in-place XML = self.etree.XML tostring = self.etree.tostring - style = self.etree.XSLT(XML(_bytes('''\ + style = self.etree.XSLT(XML(b'''\ - xyz + xyz - '''))) + ''')) result = [] def run_thread(): - root = XML(_bytes('')) + root = XML(b'') result.append( style(root).getroot() ) self._run_thread(run_thread) - self.assertEqual(_bytes(''), + self.assertEqual(b'', tostring(result[0])) def test_thread_create_xslt(self): XML = self.etree.XML tostring = self.etree.tostring - root = XML(_bytes('BC')) + root = XML(b'BC') stylesheets = [] def run_thread(): - style = XML(_bytes('''\ + style = XML(b'''\ @@ -238,7 +288,7 @@ def run_thread(): - ''')) + ''') stylesheets.append( etree.XSLT(style) ) self._run_thread(run_thread) @@ -246,7 +296,7 @@ def run_thread(): st = stylesheets[0] result = tostring( st(root) ) - self.assertEqual(_bytes('
BC
'), + self.assertEqual(b'
BC
', result) def test_thread_error_log(self): @@ -290,9 +340,9 @@ def test_thread_mix(self): Element = self.etree.Element SubElement = self.etree.SubElement tostring = self.etree.tostring - xml = _bytes('BC') + xml = b'BC' root = XML(xml) - fragment = XML(_bytes("")) + fragment = XML(b"") result = self.etree.Element("{myns}root", att = "someval") @@ -315,13 +365,13 @@ def run_build(): SubElement(result, "{otherns}tasty") def run_xslt(): - style = XML(_bytes('''\ + style = XML(b'''\ - ''')) + ''') st = etree.XSLT(style) result.append( st(root).getroot() ) @@ -330,11 +380,11 @@ def run_xslt(): self._run_thread(test) self.assertEqual( - _bytes('B' - 'CBC' - 'B' - '' - ''), + b'B' + b'CBC' + b'B' + b'' + b'', tostring(result)) def strip_first(): @@ -345,13 +395,13 @@ def strip_first(): self._run_thread(strip_first) self.assertEqual( - _bytes(''), + b'', tostring(result)) def test_concurrent_attribute_names_in_dicts(self): SubElement = self.etree.SubElement names = list('abcdefghijklmnop') - runs_per_name = range(50) + runs_per_name = range(20) result_matches = re.compile( br'' br'(?:<[a-p]{5} thread_attr_[a-p]="value" thread_attr2_[a-p]="value2"\s?/>)+' @@ -362,10 +412,10 @@ def testrun(): root = self.etree.Element('thread_root') for name in names: tag_name = name * 5 - new = [] - for _ in runs_per_name: - el = SubElement(root, tag_name, {'thread_attr_' + name: 'value'}) - new.append(el) + new = [ + SubElement(root, tag_name, {'thread_attr_' + name: 'value'}) + for _ in runs_per_name + ] for el in new: el.set('thread_attr2_' + name, 'value2') s = etree.tostring(root) @@ -377,13 +427,14 @@ def testrun(): # then, additionally include the main thread (and its parent dict) self._run_threads(10, testrun, main_func=testrun) + @unittest.skipIf(IS_PYPY, "currently crashes PyPy") def test_concurrent_proxies(self): XML = self.etree.XML - root = XML(_bytes('AB')) + root = XML(b'AB') child_count = len(root) def testrun(): for i in range(10000): - el = root[i%child_count] + el = root[i % child_count] del el self._run_threads(10, testrun) @@ -395,23 +446,25 @@ class TestElement(etree.ElementBase): class MyLookup(etree.CustomElementClassLookup): repeat = range(100) + _TestElement = TestElement + def lookup(self, t, d, ns, name): count = 0 for i in self.repeat: # allow other threads to run - count += 1 - return TestElement + count += i + return self._TestElement if count > 1 else self._TestElement parser = self.etree.XMLParser() parser.set_element_class_lookup(MyLookup()) - root = XML(_bytes('AB'), + root = XML(b'AB', parser) child_count = len(root) def testrun(): for i in range(1000): - el = root[i%child_count] + el = root[i % child_count] del el self._run_threads(10, testrun) @@ -423,28 +476,48 @@ class ThreadPipelineTestCase(HelperTestCase): item_count = 40 class Worker(threading.Thread): + _print_lock = threading.Lock() + _DEBUG = False + def __init__(self, in_queue, in_count, **kwargs): threading.Thread.__init__(self) self.in_queue = in_queue self.in_count = in_count self.out_queue = Queue(in_count) + self._print_counter = 0 self.__dict__.update(kwargs) + def _debug_print(self, s): + if not self._DEBUG: + return + with self._print_lock: + self._print_counter += 1 + print(f"{s}[{self._print_counter}]") + def run(self): get, put = self.in_queue.get, self.out_queue.put handle = self.handle - for _ in range(self.in_count): - put(handle(get())) + i = 0 + try: + for i in range(self.in_count): + put(handle(get(timeout=10))) + except Empty: + self._debug_print(f"failed({type(self).__name__})") + raise RuntimeError(f"timeout after {i} items in thread {type(self).__name__}") + + self._debug_print(f"done({type(self).__name__})") def handle(self, data): raise NotImplementedError() class ParseWorker(Worker): def handle(self, xml, _fromstring=etree.fromstring): + self._debug_print("parse") return _fromstring(xml) class RotateWorker(Worker): def handle(self, element): + self._debug_print("rotate") first = element[0] element[:] = element[1:] element.append(first) @@ -452,27 +525,32 @@ def handle(self, element): class ReverseWorker(Worker): def handle(self, element): + self._debug_print("reverse") element[:] = element[::-1] return element class ParseAndExtendWorker(Worker): def handle(self, element, _fromstring=etree.fromstring): + self._debug_print("parseandextend") element.extend(_fromstring(self.xml)) return element class ParseAndInjectWorker(Worker): def handle(self, element, _fromstring=etree.fromstring): + self._debug_print("parseandinject") root = _fromstring(self.xml) root.extend(element) return root class Validate(Worker): def handle(self, element): + self._debug_print("validate") element.getroottree().docinfo.internalDTD.assertValid(element) return element class SerialiseWorker(Worker): def handle(self, element): + self._debug_print("serialise") return etree.tostring(element) xml = (b'''\ @@ -506,13 +584,14 @@ def handle(self, element): def _build_pipeline(self, item_count, *classes, **kwargs): in_queue = Queue(item_count) start = last = classes[0](in_queue, item_count, **kwargs) - start.setDaemon(True) + start.daemon = True for worker_class in classes[1:]: last = worker_class(last.out_queue, item_count, **kwargs) - last.setDaemon(True) + last.daemon = True last.start() return in_queue, start, last + @unittest.skipIf(IS_PYPY, "currently crashes PyPy") def test_thread_pipeline_thread_parse(self): item_count = self.item_count xml = self.xml.replace(b'thread', b'THREAD') # use fresh tag names @@ -530,23 +609,24 @@ def test_thread_pipeline_thread_parse(self): xml=xml) # fill the queue - put = start.in_queue.put + put = in_queue.put for _ in range(item_count): put(xml) # start the first thread and thus everything start.start() # make sure the last thread has terminated - last.join(60) # time out after 60 seconds + last.join(60) # time out after x seconds self.assertEqual(item_count, last.out_queue.qsize()) # read the results get = last.out_queue.get - results = [get() for _ in range(item_count)] + results = [get(timeout=10) for _ in range(item_count)] comparison = results[0] for i, result in enumerate(results[1:]): self.assertEqual(comparison, result) + @unittest.skipIf(IS_PYPY, "currently crashes PyPy") def test_thread_pipeline_global_parse(self): item_count = self.item_count xml = self.xml.replace(b'thread', b'GLOBAL') # use fresh tag names @@ -562,18 +642,18 @@ def test_thread_pipeline_global_parse(self): xml=xml) # fill the queue - put = start.in_queue.put + put = in_queue.put for _ in range(item_count): put(XML(xml)) # start the first thread and thus everything start.start() # make sure the last thread has terminated - last.join(60) # time out after 90 seconds + last.join(60) # time out after x seconds self.assertEqual(item_count, last.out_queue.qsize()) # read the results get = last.out_queue.get - results = [get() for _ in range(item_count)] + results = [get(timeout=10) for _ in range(item_count)] comparison = results[0] for i, result in enumerate(results[1:]): @@ -582,8 +662,8 @@ def test_thread_pipeline_global_parse(self): def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(ThreadingTestCase)]) - suite.addTests([unittest.makeSuite(ThreadPipelineTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ThreadingTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ThreadPipelineTestCase)]) return suite if __name__ == '__main__': diff --git a/src/lxml/tests/test_unicode.py b/src/lxml/tests/test_unicode.py index 287a0f0f7..fa56f92a9 100644 --- a/src/lxml/tests/test_unicode.py +++ b/src/lxml/tests/test_unicode.py @@ -1,34 +1,26 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - import unittest import sys +from io import StringIO -from .common_imports import StringIO, etree, HelperTestCase, _str, _bytes, _chr, needs_libxml - -try: - unicode -except NameError: - unicode = str +from .common_imports import etree, HelperTestCase, needs_libxml -ascii_uni = _bytes('a').decode('utf8') +ascii_uni = 'a' -klingon = _bytes("\\uF8D2").decode("unicode_escape") # not valid for XML names +klingon = "\uF8D2" # not valid for XML names -invalid_tag = _bytes("test").decode('utf8') + klingon +invalid_tag = "test" + klingon -uni = _bytes('\\xc3\\u0680\\u3120').decode("unicode_escape") # some non-ASCII characters +uni = '\xc3\u0680\u3120' # some non-ASCII characters -uxml = _bytes("test \\xc3\\xa1\\u3120

page \\xc3\\xa1\\u3120 title

" - ).decode("unicode_escape") +uxml = "test \xc3\xa1\u3120

page \xc3\xa1\u3120 title

" class UnicodeTestCase(HelperTestCase): def test__str(self): # test the testing framework, namely _str from common_imports - self.assertEqual(_str('\x10'), _str('\u0010')) - self.assertEqual(_str('\x10'), _str('\U00000010')) - self.assertEqual(_str('\u1234'), _str('\U00001234')) + self.assertEqual('\x10', '\u0010') + self.assertEqual('\x10', '\U00000010') + self.assertEqual('\u1234', '\U00001234') def test_unicode_xml(self): tree = etree.XML('

%s

' % uni) @@ -38,11 +30,23 @@ def test_unicode_xml(self): def test_wide_unicode_xml(self): if sys.maxunicode < 1114111: return # skip test - tree = etree.XML(_bytes('

\\U00026007

').decode('unicode_escape')) + tree = etree.XML('

\U00026007

') self.assertEqual(1, len(tree.text)) - self.assertEqual(_bytes('\\U00026007').decode('unicode_escape'), + self.assertEqual('\U00026007', tree.text) + def test_emoji_xml(self): + p = etree.XML('

😄

') + self.assertEqual('😄', p.text) + self.assertEqual(1, len(p.text)) + + def test_emoji_html(self): + html = etree.HTML('

😄

') + p = html[0][0] + self.assertEqual('p', p.tag) + self.assertEqual('😄', p.text) + self.assertEqual(1, len(p.text)) + def test_unicode_xml_broken(self): uxml = ('' + '

%s

' % uni) @@ -75,7 +79,7 @@ def test_unicode_qname(self): qname = etree.QName(uni, uni) tag = "{%s}%s" % (uni, uni) self.assertEqual(qname.text, tag) - self.assertEqual(unicode(qname), tag) + self.assertEqual(str(qname), tag) def test_unicode_qname_invalid(self): self.assertRaises(ValueError, etree.QName, invalid_tag) @@ -89,20 +93,20 @@ def test_unicode_comment(self): self.assertEqual(uni, el.text) def test_unicode_repr1(self): - x = etree.Element(_str('å')) + x = etree.Element('å') # must not raise UnicodeEncodeError repr(x) def test_unicode_repr2(self): - x = etree.Comment(_str('ö')) + x = etree.Comment('ö') repr(x) def test_unicode_repr3(self): - x = etree.ProcessingInstruction(_str('Å'), _str('\u0131')) + x = etree.ProcessingInstruction('Å', '\u0131') repr(x) def test_unicode_repr4(self): - x = etree.Entity(_str('ä')) + x = etree.Entity('ä') repr(x) def test_unicode_text(self): @@ -111,28 +115,28 @@ def test_unicode_text(self): def settext(text): e.text = text - self.assertRaises(ValueError, settext, _str('ab\ufffe')) - self.assertRaises(ValueError, settext, _str('ö\ffff')) - self.assertRaises(ValueError, settext, _str('\u0123\ud800')) - self.assertRaises(ValueError, settext, _str('x\ud8ff')) - self.assertRaises(ValueError, settext, _str('\U00010000\udfff')) - self.assertRaises(ValueError, settext, _str('abd\x00def')) + self.assertRaises(ValueError, settext, 'ab\ufffe') + self.assertRaises(ValueError, settext, 'ö\ffff') + self.assertRaises(ValueError, settext, '\u0123\ud800') + self.assertRaises(ValueError, settext, 'x\ud8ff') + self.assertRaises(ValueError, settext, '\U00010000\udfff') + self.assertRaises(ValueError, settext, 'abd\x00def') # should not Raise - settext(_str('\ud7ff\ue000\U00010000\U0010FFFFäöas')) + settext('\ud7ff\ue000\U00010000\U0010FFFFäöas') for char_val in range(0xD800, 0xDFFF+1): - self.assertRaises(ValueError, settext, 'abc' + _chr(char_val)) - self.assertRaises(ValueError, settext, _chr(char_val)) - self.assertRaises(ValueError, settext, _chr(char_val) + 'abc') - - self.assertRaises(ValueError, settext, _bytes('\xe4')) - self.assertRaises(ValueError, settext, _bytes('\x80')) - self.assertRaises(ValueError, settext, _bytes('\xff')) - self.assertRaises(ValueError, settext, _bytes('\x08')) - self.assertRaises(ValueError, settext, _bytes('\x19')) - self.assertRaises(ValueError, settext, _bytes('\x20\x00')) + self.assertRaises(ValueError, settext, 'abc' + chr(char_val)) + self.assertRaises(ValueError, settext, chr(char_val)) + self.assertRaises(ValueError, settext, chr(char_val) + 'abc') + + self.assertRaises(ValueError, settext, b'\xe4') + self.assertRaises(ValueError, settext, b'\x80') + self.assertRaises(ValueError, settext, b'\xff') + self.assertRaises(ValueError, settext, b'\x08') + self.assertRaises(ValueError, settext, b'\x19') + self.assertRaises(ValueError, settext, b'\x20\x00') # should not Raise - settext(_bytes('\x09\x0A\x0D\x20\x60\x7f')) + settext(b'\x09\x0A\x0D\x20\x60\x7f') def test_uniname(self): Element = etree.Element @@ -141,11 +145,9 @@ def el(name): self.assertRaises(ValueError, el, ':') self.assertRaises(ValueError, el, '0a') - self.assertRaises(ValueError, el, _str('\u203f')) + self.assertRaises(ValueError, el, '\u203f') # should not Raise - el(_str('\u0132')) - - + el('\u0132') def test_unicode_parse_stringio(self): el = etree.parse(StringIO('

%s

' % uni)).getroot() @@ -155,31 +157,43 @@ def test_unicode_parse_stringio(self): ## # parse unicode from unnamed file object (not supported by ElementTree) ## f = SillyFileLike(uxml) ## root = etree.parse(f).getroot() -## self.assertEqual(unicode(etree.tostring(root, 'UTF-8'), 'UTF-8'), +## self.assertEqual(etree.tostring(root, 'UTF-8').decode('utf-8'), ## uxml) class EncodingsTestCase(HelperTestCase): def test_illegal_utf8(self): - data = _bytes('\x80\x80\x80', encoding='iso8859-1') + data = b'\x80\x80\x80' self.assertRaises(etree.XMLSyntaxError, etree.fromstring, data) def test_illegal_utf8_recover(self): - data = _bytes('\x80\x80\x80', encoding='iso8859-1') + data = b'\x80\x80\x80' parser = etree.XMLParser(recover=True) - self.assertRaises(etree.XMLSyntaxError, etree.fromstring, data, parser) + if etree.LIBXML_VERSION >= (2, 12, 0): + tree = etree.fromstring(data, parser) + self.assertEqual('\ufffd\ufffd\ufffd', tree.text) + else: + self.assertRaises(etree.XMLSyntaxError, etree.fromstring, data, parser) def _test_encoding(self, encoding, xml_encoding_name=None): - foo = """\n""" % ( - xml_encoding_name or encoding) + self._test_encoded_input("", 'tag', encoding, xml_encoding_name) + self._test_encoded_input("<älämänt öttrib='Атрибут'>", 'älämänt', encoding, xml_encoding_name) + + def _test_encoded_input(self, xml_input, tag_name, encoding, xml_encoding_name=None): + foo = """\n""" % ( + xml_encoding_name or encoding) + xml_input root = etree.fromstring(foo.encode(encoding)) - self.assertEqual('tag', root.tag) + self.assertEqual(tag_name, root.tag) doc_encoding = root.getroottree().docinfo.encoding - self.assertTrue( + self.assertEqual( doc_encoding.lower().rstrip('lbe'), (xml_encoding_name or encoding).lower().rstrip('lbe')) + if 'sig' not in encoding: + xml = etree.tostring(root, encoding=encoding) + etree.fromstring(xml) # encoding + def test_utf8_fromstring(self): self._test_encoding('utf-8') @@ -207,6 +221,6 @@ def test_utf32BE_fromstring(self): def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(UnicodeTestCase)]) - suite.addTests([unittest.makeSuite(EncodingsTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(UnicodeTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(EncodingsTestCase)]) return suite diff --git a/src/lxml/tests/test_xmlschema.py b/src/lxml/tests/test_xmlschema.py index dbfc251a5..9341d2a0a 100644 --- a/src/lxml/tests/test_xmlschema.py +++ b/src/lxml/tests/test_xmlschema.py @@ -1,10 +1,7 @@ -# -*- coding: utf-8 -*- - """ Test cases related to XML Schema parsing and validation """ -from __future__ import absolute_import import unittest @@ -220,7 +217,7 @@ def test_xmlschema_parse_fixed_attributes(self): self.assertEqual('hey', root[2].get('hardy')) def test_xmlschema_stringio(self): - schema_file = BytesIO(''' + schema_file = BytesIO(b''' @@ -251,7 +248,7 @@ def test_xmlschema_iterparse(self): ''') schema = etree.XMLSchema(schema) - xml = BytesIO('') + xml = BytesIO(b'') events = [ (event, el.tag) for (event, el) in etree.iterparse(xml, schema=schema) ] @@ -270,7 +267,7 @@ def test_xmlschema_iterparse_incomplete(self): ''') schema = etree.XMLSchema(schema) - xml = BytesIO('') + xml = BytesIO(b'') event, element = next(iter(etree.iterparse(xml, schema=schema))) self.assertEqual('end', event) self.assertEqual('b', element.tag) @@ -289,7 +286,7 @@ def test_xmlschema_iterparse_fail(self): schema = etree.XMLSchema(schema) self.assertRaises( etree.XMLSyntaxError, - list, etree.iterparse(BytesIO(''), schema=schema)) + list, etree.iterparse(BytesIO(b''), schema=schema)) def test_xmlschema_elementtree_error(self): self.assertRaises(ValueError, etree.XMLSchema, etree.ElementTree()) @@ -394,7 +391,7 @@ def test_xmlschema_pathlike(self): class ETreeXMLSchemaResolversTestCase(HelperTestCase): - resolver_schema_int = BytesIO("""\ + resolver_schema_int = BytesIO(b"""\ @@ -402,7 +399,7 @@ class ETreeXMLSchemaResolversTestCase(HelperTestCase): """) - resolver_schema_int2 = BytesIO("""\ + resolver_schema_int2 = BytesIO(b"""\ @@ -439,10 +436,13 @@ def test_xmlschema_resolvers_root(self): # test that the default resolver will get called if there's no # specific parser resolver. root_resolver = self.simple_resolver(self.resolver_schema_ext) - etree.get_default_parser().resolvers.add(root_resolver) - schema_doc = etree.parse(self.resolver_schema_int) - schema = etree.XMLSchema(schema_doc) - etree.get_default_parser().resolvers.remove(root_resolver) + default_resolvers = etree.get_default_parser().resolvers + default_resolvers.add(root_resolver) + try: + schema_doc = etree.parse(self.resolver_schema_int) + schema = etree.XMLSchema(schema_doc) + finally: + default_resolvers.remove(root_resolver) def test_xmlschema_resolvers_noroot(self): # test that the default resolver will not get called when a @@ -454,14 +454,16 @@ def resolve(self, url, id, context): return None root_resolver = res_root() - etree.get_default_parser().resolvers.add(root_resolver) - - parser = etree.XMLParser() - parser.resolvers.add(self.simple_resolver(self.resolver_schema_ext)) + default_resolvers = etree.get_default_parser().resolvers + default_resolvers.add(root_resolver) + try: + parser = etree.XMLParser() + parser.resolvers.add(self.simple_resolver(self.resolver_schema_ext)) - schema_doc = etree.parse(self.resolver_schema_int, parser = parser) - schema = etree.XMLSchema(schema_doc) - etree.get_default_parser().resolvers.remove(root_resolver) + schema_doc = etree.parse(self.resolver_schema_int, parser = parser) + schema = etree.XMLSchema(schema_doc) + finally: + default_resolvers.remove(root_resolver) def test_xmlschema_nested_resolvers(self): # test that resolvers work in a nested fashion. @@ -499,10 +501,10 @@ def resolve(self, url, id, context): def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(ETreeXMLSchemaTestCase)]) - suite.addTests([unittest.makeSuite(ETreeXMLSchemaResolversTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeXMLSchemaTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeXMLSchemaResolversTestCase)]) suite.addTests( - [make_doctest('../../../doc/validation.txt')]) + [make_doctest('validation.txt')]) return suite diff --git a/src/lxml/tests/test_xpathevaluator.py b/src/lxml/tests/test_xpathevaluator.py index 13ee97ece..65bc5a420 100644 --- a/src/lxml/tests/test_xpathevaluator.py +++ b/src/lxml/tests/test_xpathevaluator.py @@ -1,14 +1,11 @@ -# -*- coding: utf-8 -*- - """ Test cases related to XPath evaluation and the XPath class """ -from __future__ import absolute_import -import unittest, sys +import unittest -from .common_imports import etree, HelperTestCase, _bytes, BytesIO, doctest, make_doctest +from .common_imports import etree, HelperTestCase, BytesIO, doctest, make_doctest class ETreeXPathTestCase(HelperTestCase): @@ -28,7 +25,7 @@ def test_xpath_number(self): expected = ['nan', '1.#qnan', 'nanq'] if not actual.lower() in expected: self.fail('Expected a NAN value, got %s' % actual) - + def test_xpath_string(self): tree = self.parse('Foo') self.assertEqual('Foo', @@ -62,7 +59,7 @@ def test_xpath_list_nothing(self): # this seems to pass a different code path, also should return nothing self.assertEqual([], tree.xpath('/a/c/text()')) - + def test_xpath_list_text(self): tree = self.parse('FooBar') root = tree.getroot() @@ -99,11 +96,11 @@ def test_xpath_list_text_parent_no_smart_strings(self): tree.xpath('/a/b/text()', smart_strings=True)]) def test_xpath_list_unicode_text_parent(self): - xml = _bytes('FooBar\\u0680\\u3120BarFoo\\u0680\\u3120').decode("unicode_escape") + xml = b'FooBar\\u0680\\u3120BarFoo\\u0680\\u3120'.decode("unicode_escape") tree = self.parse(xml.encode('utf-8')) root = tree.getroot() - self.assertEqual([_bytes('FooBar\\u0680\\u3120').decode("unicode_escape"), - _bytes('BarFoo\\u0680\\u3120').decode("unicode_escape")], + self.assertEqual([b'FooBar\\u0680\\u3120'.decode("unicode_escape"), + b'BarFoo\\u0680\\u3120'.decode("unicode_escape")], tree.xpath('/a/b/text()')) self.assertEqual([root[0], root[1]], [r.getparent() for r in tree.xpath('/a/b/text()')]) @@ -285,7 +282,7 @@ def test_xpath_evaluator_element(self): self.assertEqual( [root[0][0]], e('c')) - + def test_xpath_extensions(self): def foo(evaluator, a): return 'hello %s' % a @@ -612,20 +609,19 @@ def test_xpath_compile_ns(self): # disabled this test as non-ASCII characters in namespace URIs are # not acceptable def _test_xpath_compile_unicode(self): - x = self.parse(_bytes('' - ).decode("unicode_escape")) + x = self.parse('') - expr = etree.ETXPath(_bytes("/a/{http://nsa/\\uf8d2}b").decode("unicode_escape")) + expr = etree.ETXPath("/a/{http://nsa/\uf8d2}b") r = expr(x) self.assertEqual(1, len(r)) - self.assertEqual(_bytes('{http://nsa/\\uf8d2}b').decode("unicode_escape"), r[0].tag) + self.assertEqual('{http://nsa/\uf8d2}b', r[0].tag) - expr = etree.ETXPath(_bytes("/a/{http://nsb/\\uf8d1}b").decode("unicode_escape")) + expr = etree.ETXPath("/a/{http://nsb/\\uf8d1}b") r = expr(x) self.assertEqual(1, len(r)) - self.assertEqual(_bytes('{http://nsb/\\uf8d1}b').decode("unicode_escape"), r[0].tag) + self.assertEqual('{http://nsb/\uf8d1}b', r[0].tag) -SAMPLE_XML = etree.parse(BytesIO(""" +SAMPLE_XML = etree.parse(BytesIO(b""" text
@@ -647,16 +643,16 @@ def stringTest(ctxt, s1): def stringListTest(ctxt, s1): return ["Hello "] + list(s1) + ["!"] - + def floatTest(ctxt, f1): return f1+4 def booleanTest(ctxt, b1): return not b1 - + def setTest(ctxt, st1): return st1[0] - + def setTest2(ctxt, st1): return st1[0:2] @@ -672,7 +668,7 @@ def resultTypesTest(ctxt): def resultTypesTest2(ctxt): return resultTypesTest - + uri = "http://www.example.com/" extension = {(None, 'stringTest'): stringTest, @@ -689,13 +685,13 @@ def resultTypesTest2(ctxt): def xpath(): """ Test xpath extension functions. - + >>> root = SAMPLE_XML >>> e = etree.XPathEvaluator(root, extensions=[extension]) >>> e("stringTest('you')") 'Hello you' - >>> e(_bytes("stringTest('\\\\xe9lan')").decode("unicode_escape")) - u'Hello \\xe9lan' + >>> print(e(b"stringTest('\\\\xe9lan')".decode("unicode_escape"))) + Hello \xe9lan >>> e("stringTest('you','there')") #doctest: +ELLIPSIS Traceback (most recent call last): ... @@ -714,10 +710,10 @@ def xpath(): "a, 1.5, True, ['tag', 'tag', 'tag']" >>> list(map(tag, e("argsTest2(/body/tag, /body/section)"))) ['tag', 'section', 'tag', 'tag'] - >>> e("resultTypesTest()") - Traceback (most recent call last): - ... - XPathResultError: This is not a supported node-set result: None + >>> try: e("resultTypesTest()") + ... except etree.XPathResultError as exc: print(exc) + ... else: print("SHOULD HAVE FAILED!") + This is not a supported node-set result: None >>> try: ... e("resultTypesTest2()") ... except etree.XPathResultError: @@ -725,23 +721,17 @@ def xpath(): Got error """ -if sys.version_info[0] >= 3: - xpath.__doc__ = xpath.__doc__.replace(" u'", " '") - xpath.__doc__ = xpath.__doc__.replace(" XPathResultError", - " lxml.etree.XPathResultError") - xpath.__doc__ = xpath.__doc__.replace(" exactly 2 arguments", - " exactly 2 positional arguments") def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(ETreeXPathTestCase)]) - suite.addTests([unittest.makeSuite(ETreeXPathClassTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeXPathTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeXPathClassTestCase)]) if etree.LIBXSLT_COMPILED_VERSION >= (1,1,25): - suite.addTests([unittest.makeSuite(ETreeXPathExsltTestCase)]) - suite.addTests([unittest.makeSuite(ETreeETXPathClassTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeXPathExsltTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeETXPathClassTestCase)]) suite.addTests([doctest.DocTestSuite()]) suite.addTests( - [make_doctest('../../../doc/xpathxslt.txt')]) + [make_doctest('xpathxslt.txt')]) return suite if __name__ == '__main__': diff --git a/src/lxml/tests/test_xslt.py b/src/lxml/tests/test_xslt.py index 0ef076694..244a46f78 100644 --- a/src/lxml/tests/test_xslt.py +++ b/src/lxml/tests/test_xslt.py @@ -1,41 +1,26 @@ -# -*- coding: utf-8 -*- - """ Test cases related to XSLT processing """ -from __future__ import absolute_import -import io -import sys import copy import gzip import os.path import unittest import contextlib + +from io import BytesIO from textwrap import dedent from tempfile import NamedTemporaryFile, mkdtemp -is_python3 = sys.version_info[0] >= 3 - -try: - unicode -except NameError: # Python 3 - unicode = str - -try: - basestring -except NameError: # Python 3 - basestring = str - from .common_imports import ( - etree, BytesIO, HelperTestCase, fileInTestDir, _bytes, make_doctest, skipif, SimpleFSPath + etree, HelperTestCase, fileInTestDir, make_doctest, SimpleFSPath ) class ETreeXSLTTestCase(HelperTestCase): """XSLT tests etree""" - + def test_xslt(self): tree = self.parse('BC') style = self.parse('''\ @@ -109,9 +94,8 @@ def test_xslt_copy(self): @contextlib.contextmanager def _xslt_setup( self, encoding='UTF-16', expected_encoding=None, - expected='\\uF8D2'): - tree = self.parse(_bytes('\\uF8D2\\uF8D2' - ).decode("unicode_escape")) + expected='\uF8D2'): + tree = self.parse('\uF8D2\uF8D2') style = self.parse('''\ @@ -123,7 +107,7 @@ def _xslt_setup( st = etree.XSLT(style) res = st(tree) - expected = _bytes(dedent(expected).strip()).decode("unicode_escape").replace('\n', '') % { + expected = dedent(expected).strip().replace('\n', '') % { 'ENCODING': expected_encoding or encoding, } @@ -133,22 +117,19 @@ def _xslt_setup( def test_xslt_utf8(self): with self._xslt_setup(encoding='UTF-8') as res: - res[0] = unicode(bytes(res[0]), 'UTF-8') + res[0] = bytes(res[0]).decode('UTF-8') assert 'UTF-8' in res[0] def test_xslt_encoding(self): with self._xslt_setup() as res: - res[0] = unicode(bytes(res[0]), 'UTF-16') + res[0] = bytes(res[0]).decode('UTF-16') assert 'UTF-16' in res[0] def test_xslt_encoding_override(self): with self._xslt_setup(encoding='UTF-8', expected_encoding='UTF-16') as res: f = BytesIO() res[0].write(f, encoding='UTF-16') - if is_python3: - output = str(f.getvalue(), 'UTF-16') - else: - output = unicode(str(f.getvalue()), 'UTF-16') + output = str(f.getvalue(), 'UTF-16') res[0] = output.replace("'", '"') def test_xslt_write_output_bytesio(self): @@ -158,7 +139,7 @@ def test_xslt_write_output_bytesio(self): res[0] = f.getvalue().decode('UTF-16') def test_xslt_write_output_failure(self): - class Writer(object): + class Writer: def write(self, data): raise ValueError("FAILED!") @@ -178,7 +159,7 @@ def test_xslt_write_output_file(self): res[0].write_output(f) finally: f.close() - with io.open(f.name, encoding='UTF-16') as f: + with open(f.name, encoding='UTF-16') as f: res[0] = f.read() finally: os.unlink(f.name) @@ -195,7 +176,7 @@ def test_xslt_write_output_file_path(self): res[0] = f.read().decode("UTF-16") finally: os.unlink(f.name) - + def test_xslt_write_output_file_pathlike(self): with self._xslt_setup() as res: f = NamedTemporaryFile(delete=False) @@ -241,7 +222,7 @@ def test_xslt_write_output_file_oserror(self): tempdir = mkdtemp() try: res[0].write_output(os.path.join(tempdir, 'missing_subdir', 'out.xml')) - except IOError: + except OSError: res[0] = '' else: self.fail("IOError not raised") @@ -251,14 +232,13 @@ def test_xslt_write_output_file_oserror(self): def test_xslt_unicode(self): expected = ''' - \\uF8D2 + \uF8D2 ''' with self._xslt_setup(expected=expected) as res: - res[0] = unicode(res[0]) + res[0] = str(res[0]) def test_xslt_unicode_standalone(self): - tree = self.parse(_bytes('\\uF8D2\\uF8D2' - ).decode("unicode_escape")) + tree = self.parse('\uF8D2\uF8D2') style = self.parse('''\ @@ -270,12 +250,12 @@ def test_xslt_unicode_standalone(self): st = etree.XSLT(style) res = st(tree) - expected = _bytes('''\ + expected = '''\ -\\uF8D2 -''').decode("unicode_escape") +\uF8D2 +''' self.assertEqual(expected, - unicode(res)) + str(res)) def test_xslt_input(self): style = self.parse('''\ @@ -434,7 +414,7 @@ def test_xslt_parameter_missing(self): st = etree.XSLT(style) # at least libxslt 1.1.28 produces this error, earlier ones (e.g. 1.1.18) might not ... - self.assertRaises(etree.XSLTApplyError, st.apply, tree) + self.assertRaises(etree.XSLTApplyError, st, tree) def test_xslt_multiple_parameters(self): tree = self.parse('BC') @@ -455,7 +435,7 @@ def test_xslt_multiple_parameters(self): BarBaz ''', str(res)) - + def test_xslt_parameter_xpath(self): tree = self.parse('BC') style = self.parse('''\ @@ -493,7 +473,7 @@ def test_xslt_parameter_xpath_object(self): B ''', str(res)) - + def test_xslt_default_parameters(self): tree = self.parse('BC') style = self.parse('''\ @@ -519,7 +499,7 @@ def test_xslt_default_parameters(self): Default ''', str(res)) - + def test_xslt_html_output(self): tree = self.parse('BC') style = self.parse('''\ @@ -562,12 +542,12 @@ def test_xslt_multiple_transforms(self): result = style(source) etree.tostring(result.getroot()) - + source = self.parse(xml) styledoc = self.parse(xslt) style = etree.XSLT(styledoc) result = style(source) - + etree.tostring(result.getroot()) def test_xslt_repeat_transform(self): @@ -662,9 +642,9 @@ def test_xslt_shortcut(self): result = tree.xslt(style, bar="'Bar'", baz="'Baz'") self.assertEqual( - _bytes('BarBaz'), + b'BarBaz', etree.tostring(result.getroot())) - + def test_multiple_elementrees(self): tree = self.parse('BC') style = self.parse('''\ @@ -676,30 +656,30 @@ def test_multiple_elementrees(self): ''') self.assertEqual(self._rootstring(tree), - _bytes('BC')) + b'BC') result = tree.xslt(style) self.assertEqual(self._rootstring(tree), - _bytes('BC')) + b'BC') self.assertEqual(self._rootstring(result), - _bytes('BC')) + b'BC') b_tree = etree.ElementTree(tree.getroot()[0]) self.assertEqual(self._rootstring(b_tree), - _bytes('B')) + b'B') result = b_tree.xslt(style) self.assertEqual(self._rootstring(tree), - _bytes('BC')) + b'BC') self.assertEqual(self._rootstring(result), - _bytes('B')) + b'B') c_tree = etree.ElementTree(tree.getroot()[1]) self.assertEqual(self._rootstring(c_tree), - _bytes('C')) + b'C') result = c_tree.xslt(style) self.assertEqual(self._rootstring(tree), - _bytes('BC')) + b'BC') self.assertEqual(self._rootstring(result), - _bytes('C')) + b'C') def test_xslt_document_XML(self): # make sure document('') works from parsed strings @@ -780,7 +760,7 @@ def resolve(self, url, id, context): parser = etree.XMLParser() parser.resolvers.add(TestResolver()) - xslt = etree.XSLT(etree.XML(_bytes("""\ + xslt = etree.XSLT(etree.XML(b"""\ @@ -801,7 +781,7 @@ def resolve(self, url, id, context): B -"""), parser)) +""", parser)) self.assertEqual(called['count'], 0) result = xslt(etree.XML('')) @@ -839,7 +819,7 @@ def resolve(self, url, id, context): called['count'] += 1 return self.resolve_string('', context) - stylesheet_xml = _bytes("""\ + stylesheet_xml = b"""\ @@ -847,7 +827,7 @@ def resolve(self, url, id, context): -""") +""" parser = etree.XMLParser() parser.resolvers.add(TestResolver()) @@ -926,12 +906,12 @@ def test_xslt_access_control_repr(self): repr(etree.XSLTAccessControl())) def test_xslt_move_result(self): - root = etree.XML(_bytes('''\ + root = etree.XML(b'''\ - ''')) + ''') - xslt = etree.XSLT(etree.XML(_bytes('''\ + xslt = etree.XSLT(etree.XML(b'''\ @@ -944,12 +924,12 @@ def test_xslt_move_result(self): - '''))) + ''')) result = xslt(root[0]) root[:] = result.getroot()[:] del root # segfaulted before - + def test_xslt_pi(self): tree = self.parse('''\ @@ -1183,7 +1163,7 @@ def test_exslt_math(self): str(res)) def test_exslt_regexp_test(self): - xslt = etree.XSLT(etree.XML(_bytes("""\ + xslt = etree.XSLT(etree.XML(b"""\ @@ -1191,8 +1171,8 @@ def test_exslt_regexp_test(self): -"""))) - result = xslt(etree.XML(_bytes('123098987'))) +""")) + result = xslt(etree.XML(b'123098987')) root = result.getroot() self.assertEqual(root.tag, 'test') @@ -1216,7 +1196,7 @@ def test_exslt_regexp_replace(self): """)) - result = xslt(etree.XML(_bytes('abdCdEeDed'))) + result = xslt(etree.XML(b'abdCdEeDed')) root = result.getroot() self.assertEqual(root.tag, 'test') @@ -1237,7 +1217,7 @@ def test_exslt_regexp_match(self): """)) - result = xslt(etree.XML(_bytes('abdCdEeDed'))) + result = xslt(etree.XML(b'abdCdEeDed')) root = result.getroot() self.assertEqual(root.tag, 'test') self.assertEqual(len(root), 3) @@ -1261,7 +1241,7 @@ def test_exslt_regexp_match(self): self.assertEqual(root[2][2].text, 'De') def test_exslt_regexp_match_groups(self): - xslt = etree.XSLT(etree.XML(_bytes("""\ + xslt = etree.XSLT(etree.XML(b"""\ @@ -1274,8 +1254,8 @@ def test_exslt_regexp_match_groups(self): -"""))) - result = xslt(etree.XML(_bytes(''))) +""")) + result = xslt(etree.XML(b'')) root = result.getroot() self.assertEqual(root.tag, 'test') self.assertEqual(len(root), 4) @@ -1287,7 +1267,7 @@ def test_exslt_regexp_match_groups(self): def test_exslt_regexp_match1(self): # taken from http://www.exslt.org/regexp/functions/match/index.html - xslt = etree.XSLT(etree.XML(_bytes("""\ + xslt = etree.XSLT(etree.XML(br""" @@ -1295,14 +1275,14 @@ def test_exslt_regexp_match1(self): + '(\w+):\/\/([^/:]+)(:\d*)?([^# ]*)')"> -"""))) - result = xslt(etree.XML(_bytes(''))) +""")) + result = xslt(etree.XML(b'')) root = result.getroot() self.assertEqual(root.tag, 'test') self.assertEqual(len(root), 5) @@ -1337,7 +1317,7 @@ def test_exslt_regexp_match2(self): """)) - result = xslt(etree.XML(_bytes(''))) + result = xslt(etree.XML(b'')) root = result.getroot() self.assertEqual(root.tag, 'test') self.assertEqual(len(root), 5) @@ -1351,7 +1331,7 @@ def test_exslt_regexp_match2(self): def _test_exslt_regexp_match3(self): # taken from http://www.exslt.org/regexp/functions/match/index.html # THIS IS NOT SUPPORTED! - xslt = etree.XSLT(etree.XML(_bytes("""\ + xslt = etree.XSLT(etree.XML(b"""\ @@ -1364,8 +1344,8 @@ def _test_exslt_regexp_match3(self): -"""))) - result = xslt(etree.XML(_bytes(''))) +""")) + result = xslt(etree.XML(b'')) root = result.getroot() self.assertEqual(root.tag, 'test') self.assertEqual(len(root), 4) @@ -1378,7 +1358,7 @@ def _test_exslt_regexp_match3(self): def _test_exslt_regexp_match4(self): # taken from http://www.exslt.org/regexp/functions/match/index.html # THIS IS NOT SUPPORTED! - xslt = etree.XSLT(etree.XML(_bytes("""\ + xslt = etree.XSLT(etree.XML(b"""\ @@ -1391,8 +1371,8 @@ def _test_exslt_regexp_match4(self): -"""))) - result = xslt(etree.XML(_bytes(''))) +""")) + result = xslt(etree.XML(b'')) root = result.getroot() self.assertEqual(root.tag, 'test') self.assertEqual(len(root), 4) @@ -1421,7 +1401,7 @@ def mytext(ctxt, values): result = tree.xslt(style, {('testns', 'mytext') : mytext}) self.assertEqual(self._rootstring(result), - _bytes('X')) + b'X') def test_extensions2(self): tree = self.parse('B') @@ -1441,7 +1421,7 @@ def mytext(ctxt, values): result = tree.xslt(style) self.assertEqual(self._rootstring(result), - _bytes('X')) + b'X') def test_variable_result_tree_fragment(self): tree = self.parse('B') @@ -1472,7 +1452,7 @@ def mytext(ctxt, values): result = tree.xslt(style) self.assertEqual(self._rootstring(result), - _bytes('bXb')) + b'bXb') def test_xpath_on_context_node(self): tree = self.parse('BC') @@ -1495,7 +1475,7 @@ def extfunc(ctxt): result = tree.xslt(style) self.assertEqual(self._rootstring(result), - _bytes('BxC')) + b'BxC') def test_xpath_on_foreign_context_node(self): # LP ticket 1354652 @@ -1530,7 +1510,7 @@ def extfunc(ctxt): result = transform(tree) self.assertEqual(self._rootstring(result), - _bytes('BxC')) + b'BxC') class ETreeXSLTExtElementTestCase(HelperTestCase): @@ -1559,7 +1539,7 @@ def execute(self, context, self_node, input_node, output_parent): result = tree.xslt(style, extensions=extensions) self.assertEqual(self._rootstring(result), - _bytes('X')) + b'X') def test_extension_element_doc_context(self): tree = self.parse('B') @@ -1663,7 +1643,7 @@ def execute(self, context, self_node, input_node, output_parent): result = tree.xslt(style, extensions=extensions) self.assertEqual(self._rootstring(result), - _bytes('Y')) + b'Y') def test_extension_element_apply_templates(self): tree = self.parse('B') @@ -1683,7 +1663,7 @@ class MyExt(etree.XSLTExtension): def execute(self, context, self_node, input_node, output_parent): for child in self_node: for result in self.apply_templates(context, child): - if isinstance(result, basestring): + if isinstance(result, str): el = etree.Element("T") el.text = result else: @@ -1694,7 +1674,7 @@ def execute(self, context, self_node, input_node, output_parent): result = tree.xslt(style, extensions=extensions) self.assertEqual(self._rootstring(result), - _bytes('YXYZ')) + b'YXYZ') def test_extension_element_apply_templates_elements_only(self): tree = self.parse('B') @@ -1715,14 +1695,14 @@ def execute(self, context, self_node, input_node, output_parent): for child in self_node: for result in self.apply_templates(context, child, elements_only=True): - assert not isinstance(result, basestring) + assert not isinstance(result, str) output_parent.append(result) extensions = { ('testns', 'myext') : MyExt() } result = tree.xslt(style, extensions=extensions) self.assertEqual(self._rootstring(result), - _bytes('')) + b'') def test_extension_element_apply_templates_remove_blank_text(self): tree = self.parse('B') @@ -1744,7 +1724,7 @@ def execute(self, context, self_node, input_node, output_parent): for child in self_node: for result in self.apply_templates(context, child, remove_blank_text=True): - if isinstance(result, basestring): + if isinstance(result, str): assert result.strip() el = etree.Element("T") el.text = result @@ -1756,7 +1736,7 @@ def execute(self, context, self_node, input_node, output_parent): result = tree.xslt(style, extensions=extensions) self.assertEqual(self._rootstring(result), - _bytes('XYZ')) + b'XYZ') def test_extension_element_apply_templates_target_node(self): tree = self.parse('B') @@ -1781,7 +1761,7 @@ def execute(self, context, self_node, input_node, output_parent): result = tree.xslt(style, extensions=extensions) self.assertEqual(self._rootstring(result), - _bytes('YXYZ')) + b'YXYZ') def test_extension_element_apply_templates_target_node_doc(self): tree = self.parse('B') @@ -1807,7 +1787,7 @@ def execute(self, context, self_node, input_node, output_parent): result = tree.xslt(style, extensions=extensions) self.assertEqual(etree.tostring(result), - _bytes('XYZ')) + b'XYZ') def test_extension_element_process_children(self): tree = self.parse('E') @@ -1844,7 +1824,7 @@ def execute(self, context, self_node, input_node, output_parent): result = tree.xslt(style, extensions=extensions) self.assertEqual(self._rootstring(result), - _bytes('E')) + b'E') def test_extension_element_process_children_to_append_only(self): tree = self.parse('') @@ -1868,7 +1848,7 @@ def execute(self, context, self_node, input_node, output_parent): result = tree.xslt(style, extensions=extensions) self.assertEqual(self._rootstring(result), - _bytes('')) + b'') def test_extension_element_process_children_to_read_only_raise(self): tree = self.parse('') @@ -1918,7 +1898,7 @@ def execute(self, context, self_node, input_node, output_parent): result = tree.xslt(style, extensions=extensions) self.assertEqual(self._rootstring(result), - _bytes('')) + b'') def test_extension_element_raise(self): tree = self.parse('B') @@ -2028,8 +2008,6 @@ def execute(self, context, self_node, input_node, output_parent): class Py3XSLTTestCase(HelperTestCase): """XSLT tests for etree under Python 3""" - pytestmark = skipif('sys.version_info < (3,0)') - def test_xslt_result_bytes(self): tree = self.parse('BC') style = self.parse('''\ @@ -2043,10 +2021,10 @@ def test_xslt_result_bytes(self): st = etree.XSLT(style) res = st(tree) - self.assertEqual(_bytes('''\ + self.assertEqual(b'''\ B -'''), +''', bytes(res)) def test_xslt_result_bytearray(self): @@ -2062,10 +2040,10 @@ def test_xslt_result_bytearray(self): st = etree.XSLT(style) res = st(tree) - self.assertEqual(_bytes('''\ + self.assertEqual(b'''\ B -'''), +''', bytearray(res)) def test_xslt_result_memoryview(self): @@ -2081,25 +2059,24 @@ def test_xslt_result_memoryview(self): st = etree.XSLT(style) res = st(tree) - self.assertEqual(_bytes('''\ + self.assertEqual(b'''\ B -'''), +''', bytes(memoryview(res))) def test_suite(): suite = unittest.TestSuite() - suite.addTests([unittest.makeSuite(ETreeXSLTTestCase)]) - suite.addTests([unittest.makeSuite(ETreeEXSLTTestCase)]) - suite.addTests([unittest.makeSuite(ETreeXSLTExtFuncTestCase)]) - suite.addTests([unittest.makeSuite(ETreeXSLTExtElementTestCase)]) - if is_python3: - suite.addTests([unittest.makeSuite(Py3XSLTTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeXSLTTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeEXSLTTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeXSLTExtFuncTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(ETreeXSLTExtElementTestCase)]) + suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(Py3XSLTTestCase)]) suite.addTests( - [make_doctest('../../../doc/extensions.txt')]) + [make_doctest('extensions.txt')]) suite.addTests( - [make_doctest('../../../doc/xpathxslt.txt')]) + [make_doctest('xpathxslt.txt')]) return suite if __name__ == '__main__': diff --git a/src/lxml/xinclude.pxi b/src/lxml/xinclude.pxi index 6bac82923..34787cb64 100644 --- a/src/lxml/xinclude.pxi +++ b/src/lxml/xinclude.pxi @@ -3,13 +3,13 @@ from lxml.includes cimport xinclude -cdef class XIncludeError(LxmlError): - u"""Error during XInclude processing. +class XIncludeError(LxmlError): + """Error during XInclude processing. """ cdef class XInclude: - u"""XInclude(self) + """XInclude(self) XInclude processor. Create an instance and call it on an Element to run XInclude @@ -25,7 +25,7 @@ cdef class XInclude: return self._error_log.copy() def __call__(self, _Element node not None): - u"__call__(self, node)" + "__call__(self, node)" # We cannot pass the XML_PARSE_NOXINCNODE option as this would free # the XInclude nodes - there may still be Python references to them! # Therefore, we allow XInclude nodes to be converted to @@ -33,35 +33,53 @@ cdef class XInclude: # siblings. Tree traversal will simply ignore them as they are not # typed as elements. The included fragment is added between the two, # i.e. as a sibling, which does not conflict with traversal. + cdef xinclude.xmlXIncludeCtxt* xctxt = NULL cdef int result _assertValidNode(node) assert self._error_log is not None, "XInclude processor not initialised" if node._doc._parser is not None: parse_options = node._doc._parser._parse_options context = node._doc._parser._getParserContext() - c_context = context + c_parser_ctxt = context._c_ctxt + context_ptr = context else: parse_options = 0 context = None - c_context = NULL + c_parser_ctxt = context_ptr = NULL - self._error_log.connect() - if tree.LIBXML_VERSION < 20704 or not c_context: - __GLOBAL_PARSER_CONTEXT.pushImpliedContext(context) - with nogil: - orig_loader = _register_document_loader() - if c_context: - result = xinclude.xmlXIncludeProcessTreeFlagsData( - node._c_node, parse_options, c_context) - else: - result = xinclude.xmlXIncludeProcessTree(node._c_node) - _reset_document_loader(orig_loader) - if tree.LIBXML_VERSION < 20704 or not c_context: - __GLOBAL_PARSER_CONTEXT.popImpliedContext() - self._error_log.disconnect() + if tree.LIBXML_VERSION >= 21400: + xctxt = xinclude.xmlXIncludeNewContext(node._c_node.doc) + if xctxt is NULL: + raise MemoryError() + + xinclude.xmlXIncludeSetResourceLoader( + xctxt, _local_resource_loader, c_parser_ctxt) + if parse_options: + xinclude.xmlXIncludeSetFlags(xctxt, parse_options) + + try: + self._error_log.connect() + old_loader = _register_resource_loader() + try: + doc = node._doc + doc.lock_write() + try: + with nogil: + if tree.LIBXML_VERSION >= 21400: + result = xinclude.xmlXIncludeProcessNode(xctxt, node._c_node) + else: + result = xinclude.xmlXIncludeProcessTreeFlagsData( + node._c_node, parse_options, context_ptr) + finally: + doc.unlock_write() + finally: + _reset_resource_loader(old_loader) + self._error_log.disconnect() + finally: + xinclude.xmlXIncludeFreeContext(xctxt) if result == -1: raise XIncludeError( self._error_log._buildExceptionMessage( - u"XInclude processing failed"), + "XInclude processing failed"), self._error_log) diff --git a/src/lxml/xmlerror.pxi b/src/lxml/xmlerror.pxi index 1b50444fb..8b08854d9 100644 --- a/src/lxml/xmlerror.pxi +++ b/src/lxml/xmlerror.pxi @@ -3,13 +3,13 @@ from lxml.includes cimport xmlerror from lxml cimport cvarargs -DEF GLOBAL_ERROR_LOG = u"_GlobalErrorLog" -DEF XSLT_ERROR_LOG = u"_XSLTErrorLog" +DEF GLOBAL_ERROR_LOG = "_GlobalErrorLog" +DEF XSLT_ERROR_LOG = "_XSLTErrorLog" # module level API functions def clear_error_log(): - u"""clear_error_log() + """clear_error_log() Clear the global error log. Note that this log is already bound to a fixed size. @@ -23,7 +23,7 @@ def clear_error_log(): # setup for global log: -cdef void _initThreadLogging(): +cdef void _initThreadLogging() noexcept: # Disable generic error lines from libxml2. _connectGenericErrorLog(None) @@ -38,7 +38,7 @@ cdef void _initThreadLogging(): cdef class _LogEntry: """A log message entry from an error log. - Attributes: + **Attributes:** - message: the message text - domain: the domain ID (see lxml.etree.ErrorDomains) @@ -52,7 +52,7 @@ cdef class _LogEntry: cdef readonly int domain cdef readonly int type cdef readonly int level - cdef readonly int line + cdef readonly long line cdef readonly int column cdef basestring _message cdef basestring _filename @@ -66,11 +66,11 @@ cdef class _LogEntry: tree.xmlFree(self._c_path) @cython.final - cdef _setError(self, xmlerror.xmlError* error): + cdef int _setError(self, const xmlerror.xmlError* error) except -1: self.domain = error.domain self.type = error.code self.level = error.level - self.line = error.line + self.line = error.line self.column = error.int2 self._c_message = NULL self._c_filename = NULL @@ -78,7 +78,7 @@ cdef class _LogEntry: if (error.message is NULL or error.message[0] == b'\0' or error.message[0] == b'\n' and error.message[1] == b'\0'): - self._message = u"unknown error" + self._message = "unknown error" else: self._message = None self._c_message = tree.xmlStrdup( @@ -86,7 +86,7 @@ cdef class _LogEntry: if not self._c_message: raise MemoryError() if error.file is NULL: - self._filename = u'' + self._filename = '' else: self._filename = None self._c_filename = tree.xmlStrdup( error.file) @@ -94,9 +94,12 @@ cdef class _LogEntry: raise MemoryError() if error.node is not NULL: self._c_path = tree.xmlGetNodePath( error.node) + c_line = tree.xmlGetLineNo( error.node) + if c_line > limits.INT_MAX: + self.line = c_line @cython.final - cdef _setGeneric(self, int domain, int type, int level, int line, + cdef _setGeneric(self, int domain, int type, int level, long line, message, filename): self.domain = domain self.type = type @@ -108,7 +111,7 @@ cdef class _LogEntry: self._c_path = NULL def __repr__(self): - return u"%s:%d:%d:%s:%s:%s: %s" % ( + return "%s:%d:%d:%s:%s:%s: %s" % ( self.filename, self.line, self.column, self.level_name, self.domain_name, self.type_name, self.message) @@ -116,7 +119,7 @@ cdef class _LogEntry: def domain_name(self): """The name of the error domain. See lxml.etree.ErrorDomains """ - return ErrorDomains._getName(self.domain, u"unknown") + return ErrorDomains._getName(self.domain, "unknown") @property def type_name(self): @@ -126,13 +129,13 @@ cdef class _LogEntry: getName = RelaxNGErrorTypes._getName else: getName = ErrorTypes._getName - return getName(self.type, u"unknown") + return getName(self.type, "unknown") @property def level_name(self): """The name of the error level. See lxml.etree.ErrorLevels """ - return ErrorLevels._getName(self.level, u"unknown") + return ErrorLevels._getName(self.level, "unknown") @property def message(self): @@ -144,7 +147,7 @@ cdef class _LogEntry: if self._c_message is NULL: return None size = cstring_h.strlen(self._c_message) - if size > 0 and self._c_message[size-1] == '\n': + if size > 0 and self._c_message[size-1] == b'\n': size -= 1 # strip EOL # cannot use funicode() here because the message may contain # byte encoded file paths etc. @@ -155,7 +158,7 @@ cdef class _LogEntry: self._message = self._c_message[:size].decode( 'ascii', 'backslashreplace') except UnicodeDecodeError: - self._message = u'' + self._message = '' if self._c_message: # clean up early tree.xmlFree(self._c_message) @@ -192,13 +195,13 @@ cdef class _BaseErrorLog: return _BaseErrorLog(self._first_error, self.last_error) def __repr__(self): - return u'' + return '' cpdef receive(self, _LogEntry entry): pass @cython.final - cdef void _receive(self, xmlerror.xmlError* error): + cdef int _receive(self, const xmlerror.xmlError* error) except -1: cdef bint is_error cdef _LogEntry entry cdef _BaseErrorLog global_log @@ -216,8 +219,8 @@ cdef class _BaseErrorLog: self.last_error = entry @cython.final - cdef void _receiveGeneric(self, int domain, int type, int level, int line, - message, filename): + cdef int _receiveGeneric(self, int domain, int type, int level, long line, + message, filename) except -1: cdef bint is_error cdef _LogEntry entry cdef _BaseErrorLog global_log @@ -272,7 +275,7 @@ cdef class _BaseErrorLog: return message cdef class _ListErrorLog(_BaseErrorLog): - u"Immutable base version of a list based error log." + "Immutable base version of a list based error log." cdef list _entries cdef int _offset def __init__(self, entries, first_error, last_error): @@ -285,7 +288,7 @@ cdef class _ListErrorLog(_BaseErrorLog): self._entries = entries cpdef copy(self): - u"""Creates a shallow copy of this error log. Reuses the list of + """Creates a shallow copy of this error log. Reuses the list of entries. """ cdef _ListErrorLog log = _ListErrorLog( @@ -300,7 +303,7 @@ cdef class _ListErrorLog(_BaseErrorLog): return iter(entries) def __repr__(self): - return u'\n'.join([repr(entry) for entry in self]) + return '\n'.join([repr(entry) for entry in self]) def __getitem__(self, index): if self._offset: @@ -319,45 +322,45 @@ cdef class _ListErrorLog(_BaseErrorLog): return True return False - def __nonzero__(self): + def __bool__(self): return len(self._entries) > self._offset def filter_domains(self, domains): - u"""Filter the errors by the given domains and return a new error log + """Filter the errors by the given domains and return a new error log containing the matches. """ cdef _LogEntry entry - if isinstance(domains, (int, long)): + if isinstance(domains, int): domains = (domains,) filtered = [entry for entry in self if entry.domain in domains] return _ListErrorLog(filtered, None, None) def filter_types(self, types): - u"""filter_types(self, types) + """filter_types(self, types) Filter the errors by the given types and return a new error log containing the matches. """ cdef _LogEntry entry - if isinstance(types, (int, long)): + if isinstance(types, int): types = (types,) filtered = [entry for entry in self if entry.type in types] return _ListErrorLog(filtered, None, None) def filter_levels(self, levels): - u"""filter_levels(self, levels) + """filter_levels(self, levels) Filter the errors by the given error levels and return a new error log containing the matches. """ cdef _LogEntry entry - if isinstance(levels, (int, long)): + if isinstance(levels, int): levels = (levels,) filtered = [entry for entry in self if entry.level in levels] return _ListErrorLog(filtered, None, None) def filter_from_level(self, level): - u"""filter_from_level(self, level) + """filter_from_level(self, level) Return a log with all messages of the requested level of worse. """ @@ -366,21 +369,21 @@ cdef class _ListErrorLog(_BaseErrorLog): return _ListErrorLog(filtered, None, None) def filter_from_fatals(self): - u"""filter_from_fatals(self) + """filter_from_fatals(self) Convenience method to get all fatal error messages. """ return self.filter_from_level(ErrorLevels.FATAL) - + def filter_from_errors(self): - u"""filter_from_errors(self) + """filter_from_errors(self) Convenience method to get all error messages or worse. """ return self.filter_from_level(ErrorLevels.ERROR) - + def filter_from_warnings(self): - u"""filter_from_warnings(self) + """filter_from_warnings(self) Convenience method to get all warnings or worse. """ @@ -444,40 +447,51 @@ cdef class _ErrorLog(_ListErrorLog): @cython.final cdef int connect(self) except -1: - self._first_error = None - del self._entries[:] + with cython.critical_section(self._entries): + self._first_error = None + del self._entries[:] cdef _ErrorLogContext context = _ErrorLogContext.__new__(_ErrorLogContext) - context.push_error_log(self) - self._logContexts.append(context) + with cython.critical_section(self._logContexts): + context.push_error_log(self) + self._logContexts.append(context) return 0 @cython.final cdef int disconnect(self) except -1: - cdef _ErrorLogContext context = self._logContexts.pop() - context.pop_error_log() + cdef _ErrorLogContext context + with cython.critical_section(self._logContexts): + context = self._logContexts.pop() + context.pop_error_log() return 0 cpdef clear(self): - self._first_error = None - self.last_error = None - self._offset = 0 - del self._entries[:] + with cython.critical_section(self._entries): + self._first_error = None + self.last_error = None + self._offset = 0 + del self._entries[:] cpdef copy(self): - u"""Creates a shallow copy of this error log and the list of entries. + """Creates a shallow copy of this error log and the list of entries. """ - return _ListErrorLog( - self._entries[self._offset:], - self._first_error, self.last_error) + with cython.critical_section(self._entries): + entries = self._entries[self._offset:] + first_error = self._first_error + last_error = self.last_error + + return _ListErrorLog(entries, first_error, last_error) def __iter__(self): - return iter(self._entries[self._offset:]) + with cython.critical_section(self._entries): + entries = self._entries[self._offset:] + return iter(entries) cpdef receive(self, _LogEntry entry): - if self._first_error is None and entry.level >= xmlerror.XML_ERR_ERROR: - self._first_error = entry - self._entries.append(entry) + with cython.critical_section(self._entries): + if self._first_error is None and entry.level >= xmlerror.XML_ERR_ERROR: + self._first_error = entry + self._entries.append(entry) cdef class _DomainErrorLog(_ErrorLog): def __init__(self, domains): @@ -495,19 +509,20 @@ cdef class _RotatingErrorLog(_ErrorLog): self._max_len = max_len cpdef receive(self, _LogEntry entry): - if self._first_error is None and entry.level >= xmlerror.XML_ERR_ERROR: - self._first_error = entry - self._entries.append(entry) - - if len(self._entries) > self._max_len: - self._offset += 1 - if self._offset > self._max_len // 3: - offset = self._offset - self._offset = 0 - del self._entries[:offset] + with cython.critical_section(self._entries): + if self._first_error is None and entry.level >= xmlerror.XML_ERR_ERROR: + self._first_error = entry + self._entries.append(entry) + + if len(self._entries) > self._max_len: + self._offset += 1 + if self._offset > self._max_len // 3: + offset = self._offset + self._offset = 0 + del self._entries[:offset] cdef class PyErrorLog(_BaseErrorLog): - u"""PyErrorLog(self, logger_name=None, logger=None) + """PyErrorLog(self, logger_name=None, logger=None) A global error log that connects to the Python stdlib logging package. The constructor accepts an optional logger name or a readily @@ -546,12 +561,12 @@ cdef class PyErrorLog(_BaseErrorLog): self._log = logger.log cpdef copy(self): - u"""Dummy method that returns an empty error log. + """Dummy method that returns an empty error log. """ return _ListErrorLog([], None, None) def log(self, log_entry, message, *args): - u"""log(self, log_entry, message, *args) + """log(self, log_entry, message, *args) Called by the .receive() method to log a _LogEntry instance to the Python logging system. This handles the error level @@ -568,7 +583,7 @@ cdef class PyErrorLog(_BaseErrorLog): ) cpdef receive(self, _LogEntry log_entry): - u"""receive(self, log_entry) + """receive(self, log_entry) Receive a _LogEntry instance from the logging system. Calls the .log() method with appropriate parameters:: @@ -587,7 +602,7 @@ cdef _BaseErrorLog __GLOBAL_ERROR_LOG = _RotatingErrorLog(__MAX_LOG_SIZE) cdef _BaseErrorLog _getThreadErrorLog(name): - u"""Retrieve the current error log with name 'name' of this thread.""" + """Retrieve the current error log with name 'name' of this thread.""" cdef python.PyObject* thread_dict thread_dict = python.PyThreadState_GetDict() if thread_dict is NULL: @@ -601,7 +616,7 @@ cdef _BaseErrorLog _getThreadErrorLog(name): cdef _setThreadErrorLog(name, _BaseErrorLog log): - u"""Set the global error log of this thread.""" + """Set the global error log of this thread.""" cdef python.PyObject* thread_dict thread_dict = python.PyThreadState_GetDict() if thread_dict is NULL: @@ -613,12 +628,12 @@ cdef _setThreadErrorLog(name, _BaseErrorLog log): cdef __copyGlobalErrorLog(): - u"Helper function for properties in exceptions." + "Helper function for properties in exceptions." return _getThreadErrorLog(GLOBAL_ERROR_LOG).copy() def use_global_python_log(PyErrorLog log not None): - u"""use_global_python_log(log) + """use_global_python_log(log) Replace the global error log by an etree.PyErrorLog that uses the standard Python logging package. @@ -634,7 +649,7 @@ def use_global_python_log(PyErrorLog log not None): # local log functions: forward error to logger object -cdef void _forwardError(void* c_log_handler, xmlerror.xmlError* error) with gil: +cdef void _forwardError(void* c_log_handler, const xmlerror.xmlError* error) noexcept with gil: cdef _BaseErrorLog log_handler if c_log_handler is not NULL: log_handler = <_BaseErrorLog>c_log_handler @@ -645,27 +660,27 @@ cdef void _forwardError(void* c_log_handler, xmlerror.xmlError* error) with gil: log_handler._receive(error) -cdef void _receiveError(void* c_log_handler, xmlerror.xmlError* error) nogil: +cdef void _receiveError(void* c_log_handler, const xmlerror.xmlError* error) noexcept nogil: # no Python objects here, may be called without thread context ! if __DEBUG: _forwardError(c_log_handler, error) -cdef void _receiveXSLTError(void* c_log_handler, char* msg, ...) nogil: +cdef void _receiveXSLTError(void* c_log_handler, char* msg, ...) noexcept nogil: # no Python objects here, may be called without thread context ! cdef cvarargs.va_list args cvarargs.va_start(args, msg) _receiveGenericError(c_log_handler, xmlerror.XML_FROM_XSLT, msg, args) cvarargs.va_end(args) -cdef void _receiveRelaxNGParseError(void* c_log_handler, char* msg, ...) nogil: +cdef void _receiveRelaxNGParseError(void* c_log_handler, char* msg, ...) noexcept nogil: # no Python objects here, may be called without thread context ! cdef cvarargs.va_list args cvarargs.va_start(args, msg) _receiveGenericError(c_log_handler, xmlerror.XML_FROM_RELAXNGP, msg, args) cvarargs.va_end(args) -cdef void _receiveRelaxNGValidationError(void* c_log_handler, char* msg, ...) nogil: +cdef void _receiveRelaxNGValidationError(void* c_log_handler, char* msg, ...) noexcept nogil: # no Python objects here, may be called without thread context ! cdef cvarargs.va_list args cvarargs.va_start(args, msg) @@ -673,11 +688,11 @@ cdef void _receiveRelaxNGValidationError(void* c_log_handler, char* msg, ...) no cvarargs.va_end(args) # dummy function: no log output at all -cdef void _nullGenericErrorFunc(void* ctxt, char* msg, ...) nogil: +cdef void _nullGenericErrorFunc(void* ctxt, char* msg, ...) noexcept nogil: pass -cdef void _connectGenericErrorLog(log, int c_domain=-1): +cdef void _connectGenericErrorLog(log, int c_domain=-1) noexcept: cdef xmlerror.xmlGenericErrorFunc error_func = NULL c_log = log if c_domain == xmlerror.XML_FROM_XSLT: @@ -694,14 +709,14 @@ cdef void _connectGenericErrorLog(log, int c_domain=-1): cdef void _receiveGenericError(void* c_log_handler, int c_domain, - char* msg, cvarargs.va_list args) nogil: + const char* msg, cvarargs.va_list args) noexcept nogil: # no Python objects here, may be called without thread context ! cdef xmlerror.xmlError c_error cdef char* c_text cdef char* c_message - cdef char* c_element - cdef char* c_pos - cdef char* c_name_pos + cdef const char* c_element + cdef const char* c_pos + cdef const char* c_name_pos cdef char* c_str cdef int text_size, element_size, format_count, c_int if not __DEBUG or msg is NULL: @@ -716,32 +731,32 @@ cdef void _receiveGenericError(void* c_log_handler, int c_domain, c_name_pos = c_pos = msg format_count = 0 while c_pos[0]: - if c_pos[0] == '%': + if c_pos[0] == b'%': c_pos += 1 - if c_pos[0] == 's': # "%s" + if c_pos[0] == b's': # "%s" format_count += 1 c_str = cvarargs.va_charptr(args) if c_pos == msg + 1: c_text = c_str # msg == "%s..." - elif c_name_pos[0] == 'e': + elif c_name_pos[0] == b'e': if cstring_h.strncmp(c_name_pos, 'element %s', 10) == 0: c_element = c_str - elif c_name_pos[0] == 'f': + elif c_name_pos[0] == b'f': if cstring_h.strncmp(c_name_pos, 'file %s', 7) == 0: if cstring_h.strncmp('string://__STRING__XSLT', c_str, 23) == 0: c_str = '' c_error.file = c_str - elif c_pos[0] == 'd': # "%d" + elif c_pos[0] == b'd': # "%d" format_count += 1 c_int = cvarargs.va_int(args) if cstring_h.strncmp(c_name_pos, 'line %d', 7) == 0: c_error.line = c_int - elif c_pos[0] != '%': # "%%" == "%" + elif c_pos[0] != b'%': # "%%" == "%" format_count += 1 break # unexpected format or end of string => abort - elif c_pos[0] == ' ': - if c_pos[1] != '%': + elif c_pos[0] == b' ': + if c_pos[1] != b'%': c_name_pos = c_pos + 1 c_pos += 1 @@ -753,8 +768,12 @@ cdef void _receiveGenericError(void* c_log_handler, int c_domain, element_size = cstring_h.strlen(c_element) c_message = stdlib.malloc( (text_size + element_size + 1) * sizeof(char)) - stdio.sprintf(c_message, msg, c_element) - c_error.message = c_message + if c_message is not NULL: + stdio.sprintf(c_message, msg, c_element) + c_error.message = c_message + else: + # Out of memory, report at least the original message. + c_error.message = msg else: c_error.message = '' elif c_element is NULL: @@ -764,8 +783,11 @@ cdef void _receiveGenericError(void* c_log_handler, int c_domain, element_size = cstring_h.strlen(c_element) c_message = stdlib.malloc( (text_size + 12 + element_size + 1) * sizeof(char)) - stdio.sprintf(c_message, "%s, element '%s'", c_text, c_element) - c_error.message = c_message + if c_message is NULL: + c_error.message = c_text + else: + stdio.sprintf(c_message, "%s, element '%s'", c_text, c_element) + c_error.message = c_message c_error.domain = c_domain c_error.code = xmlerror.XML_ERR_OK # what else? @@ -977,6 +999,12 @@ ERR_VERSION_MISMATCH=109 ERR_NAME_TOO_LONG=110 ERR_USER_STOP=111 ERR_COMMENT_ABRUPTLY_ENDED=112 +WAR_ENCODING_MISMATCH=113 +ERR_RESOURCE_LIMIT=114 +ERR_ARGUMENT=115 +ERR_SYSTEM=116 +ERR_REDECL_PREDEF_ENTITY=117 +ERR_INT_SUBSET_NOT_FINISHED=118 NS_ERR_XML_NAMESPACE=200 NS_ERR_UNDEFINED_NAMESPACE=201 NS_ERR_QNAME=202 @@ -1027,6 +1055,7 @@ DTD_XMLID_TYPE=540 DTD_DUP_TOKEN=541 HTML_STRUCURE_ERROR=800 HTML_UNKNOWN_TAG=801 +HTML_INCORRECTLY_OPENED_COMMENT=802 RNGP_ANYNAME_ATTR_ANCESTOR=1000 RNGP_ATTR_CONFLICT=1001 RNGP_ATTRIBUTE_CHILDREN=1002 @@ -1238,6 +1267,7 @@ IO_ENETUNREACH=1553 IO_EADDRINUSE=1554 IO_EALREADY=1555 IO_EAFNOSUPPORT=1556 +IO_UNSUPPORTED_PROTOCOL=1557 XINCLUDE_RECURSION=1600 XINCLUDE_PARSE_VALUE=1601 XINCLUDE_ENTITY_DEF_MISMATCH=1602 diff --git a/src/lxml/xmlid.pxi b/src/lxml/xmlid.pxi index c1f2bbf16..56d9789ce 100644 --- a/src/lxml/xmlid.pxi +++ b/src/lxml/xmlid.pxi @@ -1,7 +1,7 @@ cdef object _find_id_attributes def XMLID(text, parser=None, *, base_url=None): - u"""XMLID(text, parser=None, base_url=None) + """XMLID(text, parser=None, base_url=None) Parse the text and return a tuple (root node, ID dictionary). The root node is the same as returned by the XML() function. The dictionary @@ -12,17 +12,17 @@ def XMLID(text, parser=None, *, base_url=None): cdef dict dic global _find_id_attributes if _find_id_attributes is None: - _find_id_attributes = XPath(u'//*[string(@id)]') + _find_id_attributes = XPath('//*[string(@id)]') # ElementTree compatible implementation: parse and look for 'id' attributes root = XML(text, parser, base_url=base_url) dic = {} for elem in _find_id_attributes(root): - dic[elem.get(u'id')] = elem + dic[elem.get('id')] = elem return root, dic def XMLDTDID(text, parser=None, *, base_url=None): - u"""XMLDTDID(text, parser=None, base_url=None) + """XMLDTDID(text, parser=None, base_url=None) Parse the text and return a tuple (root node, ID dictionary). The root node is the same as returned by the XML() function. The dictionary @@ -42,7 +42,7 @@ def XMLDTDID(text, parser=None, *, base_url=None): return root, _IDDict(root) def parseid(source, parser=None, *, base_url=None): - u"""parseid(source, parser=None) + """parseid(source, parser=None) Parses the source into a tuple containing an ElementTree object and an ID dictionary. If no parser is provided as second argument, the default @@ -55,8 +55,9 @@ def parseid(source, parser=None, *, base_url=None): doc = _parseDocument(source, parser, base_url) return _elementTreeFactory(doc, None), _IDDict(doc) + cdef class _IDDict: - u"""IDDict(self, etree) + """IDDict(self, etree) A dictionary-like proxy class that mapps ID attributes to elements. The dictionary must be instantiated with the root element of a parsed XML @@ -64,13 +65,14 @@ cdef class _IDDict: that were created or modified 'by hand' are not supported. """ cdef _Document _doc - cdef object _keys - cdef object _items + cdef list _keys + cdef list _items + def __cinit__(self, etree): cdef _Document doc doc = _documentOrRaise(etree) if doc._c_doc.ids is NULL: - raise ValueError, u"No ID dictionary available." + raise ValueError, "No ID dictionary available." self._doc = doc self._keys = None self._items = None @@ -86,10 +88,10 @@ cdef class _IDDict: id_utf = _utf8(id_name) c_id = tree.xmlHashLookup(c_ids, _xcstr(id_utf)) if c_id is NULL: - raise KeyError, u"key not found." + raise KeyError, "key not found." c_attr = c_id.attr if c_attr is NULL or c_attr.parent is NULL: - raise KeyError, u"ID attribute not found." + raise KeyError, "ID attribute not found." return _elementFactory(self._doc, c_attr.parent) def get(self, id_name): @@ -137,32 +139,30 @@ cdef class _IDDict: return iter(self._items) def values(self): - cdef list values = [] if self._items is None: self._items = self._build_items() - for item in self._items: - value = python.PyTuple_GET_ITEM(item, 1) - python.Py_INCREF(value) - values.append(value) - return values + return [( item)[1] for item in self._items] def itervalues(self): return iter(self.values()) - cdef object _build_keys(self): + @cython.final + cdef list _build_keys(self): keys = [] tree.xmlHashScan(self._doc._c_doc.ids, _collectIdHashKeys, keys) return keys - cdef object _build_items(self): + @cython.final + cdef list _build_items(self): items = [] context = (items, self._doc) tree.xmlHashScan(self._doc._c_doc.ids, _collectIdHashItemList, context) return items -cdef void _collectIdHashItemList(void* payload, void* context, xmlChar* name): + +cdef void _collectIdHashItemList(void* payload, void* context, xmlChar* name) noexcept: # collect elements from ID attribute hash table cdef list lst c_id = payload @@ -172,7 +172,8 @@ cdef void _collectIdHashItemList(void* payload, void* context, xmlChar* name): element = _elementFactory(doc, c_id.attr.parent) lst.append( (funicode(name), element) ) -cdef void _collectIdHashKeys(void* payload, void* collect_list, xmlChar* name): + +cdef void _collectIdHashKeys(void* payload, void* collect_list, xmlChar* name) noexcept: c_id = payload if c_id is NULL or c_id.attr is NULL or c_id.attr.parent is NULL: return diff --git a/src/lxml/xmlschema.pxi b/src/lxml/xmlschema.pxi index fe7a2bacb..190433191 100644 --- a/src/lxml/xmlschema.pxi +++ b/src/lxml/xmlschema.pxi @@ -2,15 +2,15 @@ from lxml.includes cimport xmlschema -cdef class XMLSchemaError(LxmlError): +class XMLSchemaError(LxmlError): """Base class of all XML Schema errors """ -cdef class XMLSchemaParseError(XMLSchemaError): +class XMLSchemaParseError(XMLSchemaError): """Error while parsing an XML document as XML Schema. """ -cdef class XMLSchemaValidateError(XMLSchemaError): +class XMLSchemaValidateError(XMLSchemaError): """Error while validating an XML document with an XML Schema. """ @@ -19,12 +19,12 @@ cdef class XMLSchemaValidateError(XMLSchemaError): # XMLSchema cdef XPath _check_for_default_attributes = XPath( - u"boolean(//xs:attribute[@default or @fixed][1])", - namespaces={u'xs': u'http://www.w3.org/2001/XMLSchema'}) + "boolean(//xs:attribute[@default or @fixed][1])", + namespaces={'xs': 'http://www.w3.org/2001/XMLSchema'}) cdef class XMLSchema(_Validator): - u"""XMLSchema(self, etree=None, file=None) + """XMLSchema(self, etree=None, file=None) Turn a document into an XML Schema validator. Either pass a schema as Element or ElementTree, or pass a file or @@ -43,6 +43,9 @@ cdef class XMLSchema(_Validator): self._add_attribute_defaults = False def __init__(self, etree=None, *, file=None, bint attribute_defaults=False): + if self._c_schema is not NULL: + raise RuntimeError("Repeated call to XMLSchema.__init__()") + cdef xmlschema.xmlSchemaParserCtxt* parser_ctxt cdef xmlDoc* c_doc @@ -52,7 +55,11 @@ cdef class XMLSchema(_Validator): if etree is not None: doc = _documentOrRaise(etree) root_node = _rootNodeOrRaise(etree) - c_doc = _copyDocRoot(doc._c_doc, root_node._c_node) + doc.lock_read() + try: + c_doc = _copyDocRoot(doc._c_doc, root_node._c_node) + finally: + doc.unlock_read() self._doc = _documentFactory(c_doc, doc._parser) parser_ctxt = xmlschema.xmlSchemaNewDocParserCtxt(c_doc) elif file is not None: @@ -64,31 +71,38 @@ cdef class XMLSchema(_Validator): self._doc = _parseDocument(file, None, None) parser_ctxt = xmlschema.xmlSchemaNewDocParserCtxt(self._doc._c_doc) else: - raise XMLSchemaParseError, u"No tree or file given" + raise XMLSchemaParseError, "No tree or file given" if parser_ctxt is NULL: raise MemoryError() + # Need a cast here because older libxml2 releases do not use 'const' in the functype. xmlschema.xmlSchemaSetParserStructuredErrors( - parser_ctxt, _receiveError, self._error_log) - if self._doc is not None: - # calling xmlSchemaParse on a schema with imports or - # includes will cause libxml2 to create an internal - # context for parsing, so push an implied context to route - # resolve requests to the document's parser - __GLOBAL_PARSER_CONTEXT.pushImpliedContextFromParser(self._doc._parser) - with nogil: - orig_loader = _register_document_loader() - self._c_schema = xmlschema.xmlSchemaParse(parser_ctxt) - _reset_document_loader(orig_loader) - if self._doc is not None: - __GLOBAL_PARSER_CONTEXT.popImpliedContext() - xmlschema.xmlSchemaFreeParserCtxt(parser_ctxt) + parser_ctxt, _receiveError, self._error_log) + + try: + if self._doc is not None: + # calling xmlSchemaParse on a schema with imports or + # includes will cause libxml2 to create an internal + # context for parsing, so push an implied context to route + # resolve requests to the document's parser + __GLOBAL_PARSER_CONTEXT.pushImpliedContextFromParser(self._doc._parser) + + with nogil: + old_resource_loader = _register_xmlschema_resource_loader(parser_ctxt) + self._c_schema = xmlschema.xmlSchemaParse(parser_ctxt) + _reset_resource_loader(old_resource_loader) + + if self._doc is not None: + __GLOBAL_PARSER_CONTEXT.popImpliedContext() + + finally: + xmlschema.xmlSchemaFreeParserCtxt(parser_ctxt) if self._c_schema is NULL: raise XMLSchemaParseError( self._error_log._buildExceptionMessage( - u"Document is not valid XML Schema"), + "Document is not valid XML Schema"), self._error_log) if self._doc is not None: @@ -99,7 +113,7 @@ cdef class XMLSchema(_Validator): xmlschema.xmlSchemaFree(self._c_schema) def __call__(self, etree): - u"""__call__(self, etree) + """__call__(self, etree) Validate doc using XML Schema. @@ -119,25 +133,28 @@ cdef class XMLSchema(_Validator): if valid_ctxt is NULL: raise MemoryError() + doc.lock_fakedoc() try: if self._add_attribute_defaults: xmlschema.xmlSchemaSetValidOptions( valid_ctxt, xmlschema.XML_SCHEMA_VAL_VC_I_CREATE) self._error_log.clear() + # Need a cast here because older libxml2 releases do not use 'const' in the functype. xmlschema.xmlSchemaSetValidStructuredErrors( - valid_ctxt, _receiveError, self._error_log) + valid_ctxt, _receiveError, self._error_log) c_doc = _fakeRootDoc(doc._c_doc, root_node._c_node) with nogil: ret = xmlschema.xmlSchemaValidateDoc(valid_ctxt, c_doc) _destroyFakeDoc(doc._c_doc, c_doc) finally: + doc.unlock_fakedoc() xmlschema.xmlSchemaFreeValidCtxt(valid_ctxt) if ret == -1: raise XMLSchemaValidateError( - u"Internal error in XML Schema validation.", + "Internal error in XML Schema validation.", self._error_log) if ret == 0: return True @@ -153,6 +170,15 @@ cdef class XMLSchema(_Validator): add_default_attributes or self._add_attribute_defaults)) return context + +cdef xmlparser.xmlExternalEntityLoader _register_xmlschema_resource_loader(xmlschema.xmlSchemaParserCtxt *schema_ctxt) noexcept nogil: + if tree.LIBXML_VERSION < 21400: + return _register_resource_loader() + # libxml2 2.14 has per-context document loaders. + xmlschema.xmlSchemaSetResourceLoader(schema_ctxt, _local_resource_loader, NULL) + return NULL + + @cython.final @cython.internal cdef class _ParserSchemaValidationContext: @@ -175,7 +201,7 @@ cdef class _ParserSchemaValidationContext: return self._schema._newSaxValidator( self._add_default_attributes) - cdef void inject_default_attributes(self, xmlDoc* c_doc): + cdef void inject_default_attributes(self, xmlDoc* c_doc) noexcept: # we currently need to insert default attributes manually # after parsing, as libxml2 does not support this at parse # time @@ -193,12 +219,13 @@ cdef class _ParserSchemaValidationContext: xmlschema.xmlSchemaSetValidOptions( self._valid_ctxt, xmlschema.XML_SCHEMA_VAL_VC_I_CREATE) if error_log is not None: + # Need a cast here because older libxml2 releases do not use 'const' in the functype. xmlschema.xmlSchemaSetValidStructuredErrors( - self._valid_ctxt, _receiveError, error_log) + self._valid_ctxt, _receiveError, error_log) self._sax_plug = xmlschema.xmlSchemaSAXPlug( self._valid_ctxt, &c_ctxt.sax, &c_ctxt.userData) - cdef void disconnect(self): + cdef void disconnect(self) noexcept: if self._sax_plug is not NULL: xmlschema.xmlSchemaSAXUnplug(self._sax_plug) self._sax_plug = NULL @@ -206,7 +233,7 @@ cdef class _ParserSchemaValidationContext: xmlschema.xmlSchemaSetValidStructuredErrors( self._valid_ctxt, NULL, NULL) - cdef bint isvalid(self): + cdef bint isvalid(self) noexcept: if self._valid_ctxt is NULL: return 1 # valid return xmlschema.xmlSchemaIsValid(self._valid_ctxt) diff --git a/src/lxml/xpath.pxi b/src/lxml/xpath.pxi index a7cae4bff..cc7e2cf30 100644 --- a/src/lxml/xpath.pxi +++ b/src/lxml/xpath.pxi @@ -26,7 +26,7 @@ cdef object _XPATH_EVAL_ERRORS = ( xmlerror.XML_XPATH_INVALID_CTXT_POSITION ) -cdef int _register_xpath_function(void* ctxt, name_utf, ns_utf): +cdef int _register_xpath_function(void* ctxt, name_utf, ns_utf) noexcept: if ns_utf is None: return xpath.xmlXPathRegisterFunc( ctxt, _xcstr(name_utf), @@ -36,7 +36,7 @@ cdef int _register_xpath_function(void* ctxt, name_utf, ns_utf): ctxt, _xcstr(name_utf), _xcstr(ns_utf), _xpath_function_call) -cdef int _unregister_xpath_function(void* ctxt, name_utf, ns_utf): +cdef int _unregister_xpath_function(void* ctxt, name_utf, ns_utf) noexcept: if ns_utf is None: return xpath.xmlXPathRegisterFunc( ctxt, _xcstr(name_utf), NULL) @@ -78,7 +78,7 @@ cdef class _XPathContext(_BaseContext): xpath.xmlXPathRegisteredVariablesCleanup(self._xpathCtxt) self._cleanup_context() - cdef void registerExsltFunctions(self): + cdef void registerExsltFunctions(self) noexcept: if xslt.LIBXSLT_VERSION < 10125: # we'd only execute dummy functions anyway return @@ -99,35 +99,33 @@ cdef class _XPathContext(_BaseContext): cdef void _registerExsltFunctionsForNamespaces( - void* _c_href, void* _ctxt, const_xmlChar* c_prefix): + void* _c_href, void* _ctxt, const_xmlChar* c_prefix) noexcept: c_href = _c_href ctxt = _ctxt - if tree.xmlStrcmp(c_href, xslt.EXSLT_DATE_NAMESPACE) == 0: + if tree.xmlStrEqual(c_href, xslt.EXSLT_DATE_NAMESPACE): xslt.exsltDateXpathCtxtRegister(ctxt, c_prefix) - elif tree.xmlStrcmp(c_href, xslt.EXSLT_SETS_NAMESPACE) == 0: + elif tree.xmlStrEqual(c_href, xslt.EXSLT_SETS_NAMESPACE): xslt.exsltSetsXpathCtxtRegister(ctxt, c_prefix) - elif tree.xmlStrcmp(c_href, xslt.EXSLT_MATH_NAMESPACE) == 0: + elif tree.xmlStrEqual(c_href, xslt.EXSLT_MATH_NAMESPACE): xslt.exsltMathXpathCtxtRegister(ctxt, c_prefix) - elif tree.xmlStrcmp(c_href, xslt.EXSLT_STRINGS_NAMESPACE) == 0: + elif tree.xmlStrEqual(c_href, xslt.EXSLT_STRINGS_NAMESPACE): xslt.exsltStrXpathCtxtRegister(ctxt, c_prefix) cdef class _XPathEvaluatorBase: cdef xpath.xmlXPathContext* _xpathCtxt cdef _XPathContext _context - cdef python.PyThread_type_lock _eval_lock + cdef cython.pymutex _eval_lock cdef _ErrorLog _error_log + def __cinit__(self): - self._xpathCtxt = NULL - if config.ENABLE_THREADING: - self._eval_lock = python.PyThread_allocate_lock() - if self._eval_lock is NULL: - raise MemoryError() self._error_log = _ErrorLog() def __init__(self, namespaces, extensions, enable_regexp, smart_strings): + if self._context is not None: + raise RuntimeError("Repeated call to _XPathEvaluatorBase.__init__()") self._context = _XPathContext(namespaces, extensions, self._error_log, enable_regexp, None, smart_strings) @@ -139,30 +137,12 @@ cdef class _XPathEvaluatorBase: def __dealloc__(self): if self._xpathCtxt is not NULL: xpath.xmlXPathFreeContext(self._xpathCtxt) - if config.ENABLE_THREADING: - if self._eval_lock is not NULL: - python.PyThread_free_lock(self._eval_lock) cdef set_context(self, xpath.xmlXPathContext* xpathCtxt): self._xpathCtxt = xpathCtxt self._context.set_context(xpathCtxt) - def evaluate(self, _eval_arg, **_variables): - u"""evaluate(self, _eval_arg, **_variables) - - Evaluate an XPath expression. - - Instead of calling this method, you can also call the evaluator object - itself. - - Variables may be provided as keyword arguments. Note that namespaces - are currently not supported for variables. - - :deprecated: call the object, not its method. - """ - return self(_eval_arg, **_variables) - - cdef bint _checkAbsolutePath(self, char* path): + cdef bint _checkAbsolutePath(self, char* path) noexcept: cdef char c if path is NULL: return 0 @@ -173,20 +153,14 @@ cdef class _XPathEvaluatorBase: return c == c'/' @cython.final - cdef int _lock(self) except -1: - cdef int result - if config.ENABLE_THREADING and self._eval_lock != NULL: - with nogil: - result = python.PyThread_acquire_lock( - self._eval_lock, python.WAIT_LOCK) - if result == 0: - raise XPathError, u"XPath evaluator locking failed" - return 0 + cdef void _lock(self) noexcept: + if config.ENABLE_THREADING: + self._eval_lock.acquire() @cython.final - cdef void _unlock(self): - if config.ENABLE_THREADING and self._eval_lock != NULL: - python.PyThread_release_lock(self._eval_lock) + cdef void _unlock(self) noexcept: + if config.ENABLE_THREADING: + self._eval_lock.release() cdef _build_parse_error(self): cdef _BaseErrorLog entries @@ -196,7 +170,7 @@ cdef class _XPathEvaluatorBase: if message is not None: return XPathSyntaxError(message, self._error_log) return XPathSyntaxError( - self._error_log._buildExceptionMessage(u"Error in xpath expression"), + self._error_log._buildExceptionMessage("Error in xpath expression"), self._error_log) cdef _build_eval_error(self): @@ -209,7 +183,7 @@ cdef class _XPathEvaluatorBase: if message is not None: return XPathEvalError(message, self._error_log) return XPathEvalError( - self._error_log._buildExceptionMessage(u"Error in xpath expression"), + self._error_log._buildExceptionMessage("Error in xpath expression"), self._error_log) cdef object _handle_result(self, xpath.xmlXPathObject* xpathObj, _Document doc): @@ -234,7 +208,7 @@ cdef class _XPathEvaluatorBase: cdef class XPathElementEvaluator(_XPathEvaluatorBase): - u"""XPathElementEvaluator(self, element, namespaces=None, extensions=None, regexp=True, smart_strings=True) + """XPathElementEvaluator(self, element, namespaces=None, extensions=None, regexp=True, smart_strings=True) Create an XPath evaluator for an element. Absolute XPath expressions (starting with '/') will be evaluated against @@ -247,8 +221,12 @@ cdef class XPathElementEvaluator(_XPathEvaluatorBase): you pass ``smart_strings=False``. """ cdef _Element _element + def __init__(self, _Element element not None, *, namespaces=None, extensions=None, regexp=True, smart_strings=True): + if self._xpathCtxt is not NULL: + raise RuntimeError("Repeated call to XPathElementEvaluator.__init__()") + cdef xpath.xmlXPathContext* xpathCtxt cdef int ns_register_status cdef _Document doc @@ -264,20 +242,20 @@ cdef class XPathElementEvaluator(_XPathEvaluatorBase): self.set_context(xpathCtxt) def register_namespace(self, prefix, uri): - u"""Register a namespace with the XPath context. + """Register a namespace with the XPath context. """ assert self._xpathCtxt is not NULL, "XPath context not initialised" self._context.addNamespace(prefix, uri) def register_namespaces(self, namespaces): - u"""Register a prefix -> uri dict. + """Register a prefix -> uri dict. """ assert self._xpathCtxt is not NULL, "XPath context not initialised" for prefix, uri in namespaces.items(): self._context.addNamespace(prefix, uri) def __call__(self, _path, **_variables): - u"""__call__(self, _path, **_variables) + """__call__(self, _path, **_variables) Evaluate an XPath expression on the document. @@ -293,16 +271,33 @@ cdef class XPathElementEvaluator(_XPathEvaluatorBase): path = _utf8(_path) doc = self._element._doc + # FIXME: as long as we cannot upgrade a read lock to a write lock, + # we assume that we need a write lock if the user provided extensions. + # Must do this after context.register_context() ! + use_write_lock = self._context.has_user_extensions + self._lock() self._xpathCtxt.node = self._element._c_node try: self._context.register_context(doc) self._context.registerVariables(_variables) - c_path = _xcstr(path) - with nogil: - xpathObj = xpath.xmlXPathEvalExpression( - c_path, self._xpathCtxt) - result = self._handle_result(xpathObj, doc) + + if use_write_lock: + doc.lock_write() + else: + doc.lock_read() + + try: + c_path = _xcstr(path) + with nogil: + xpathObj = xpath.xmlXPathEvalExpression( + c_path, self._xpathCtxt) + result = self._handle_result(xpathObj, doc) + finally: + if use_write_lock: + doc.unlock_write() + else: + doc.unlock_read() finally: self._context.unregister_context() self._unlock() @@ -311,7 +306,7 @@ cdef class XPathElementEvaluator(_XPathEvaluatorBase): cdef class XPathDocumentEvaluator(XPathElementEvaluator): - u"""XPathDocumentEvaluator(self, etree, namespaces=None, extensions=None, regexp=True, smart_strings=True) + """XPathDocumentEvaluator(self, etree, namespaces=None, extensions=None, regexp=True, smart_strings=True) Create an XPath evaluator for an ElementTree. Additional namespace declarations can be passed with the @@ -323,12 +318,12 @@ cdef class XPathDocumentEvaluator(XPathElementEvaluator): def __init__(self, _ElementTree etree not None, *, namespaces=None, extensions=None, regexp=True, smart_strings=True): XPathElementEvaluator.__init__( - self, etree._context_node, namespaces=namespaces, + self, etree._context_node, namespaces=namespaces, extensions=extensions, regexp=regexp, smart_strings=smart_strings) def __call__(self, _path, **_variables): - u"""__call__(self, _path, **_variables) + """__call__(self, _path, **_variables) Evaluate an XPath expression on the document. @@ -345,18 +340,37 @@ cdef class XPathDocumentEvaluator(XPathElementEvaluator): self._lock() try: self._context.register_context(doc) - c_doc = _fakeRootDoc(doc._c_doc, self._element._c_node) + + # FIXME: as long as we cannot upgrade a read lock to a write lock, + # we assume that we need a write lock if the user provided extensions. + # Must do this after context.register_context() ! + use_write_lock = self._context.has_user_extensions + + if use_write_lock: + doc.lock_write() + else: + doc.lock_fakedoc() + try: - self._context.registerVariables(_variables) - c_path = _xcstr(path) - with nogil: - self._xpathCtxt.doc = c_doc - self._xpathCtxt.node = tree.xmlDocGetRootElement(c_doc) - xpathObj = xpath.xmlXPathEvalExpression( - c_path, self._xpathCtxt) - result = self._handle_result(xpathObj, doc) + c_doc = _fakeRootDoc(doc._c_doc, self._element._c_node) + try: + self._context.registerVariables(_variables) + c_path = _xcstr(path) + with nogil: + self._xpathCtxt.doc = c_doc + self._xpathCtxt.node = tree.xmlDocGetRootElement(c_doc) + xpathObj = xpath.xmlXPathEvalExpression( + c_path, self._xpathCtxt) + result = self._handle_result(xpathObj, doc) + finally: + _destroyFakeDoc(doc._c_doc, c_doc) + finally: - _destroyFakeDoc(doc._c_doc, c_doc) + if use_write_lock: + doc.unlock_write() + else: + doc.unlock_fakedoc() + self._context.unregister_context() finally: self._unlock() @@ -366,7 +380,7 @@ cdef class XPathDocumentEvaluator(XPathElementEvaluator): def XPathEvaluator(etree_or_element, *, namespaces=None, extensions=None, regexp=True, smart_strings=True): - u"""XPathEvaluator(etree_or_element, namespaces=None, extensions=None, regexp=True, smart_strings=True) + """XPathEvaluator(etree_or_element, namespaces=None, extensions=None, regexp=True, smart_strings=True) Creates an XPath evaluator for an ElementTree or an Element. @@ -390,7 +404,7 @@ def XPathEvaluator(etree_or_element, *, namespaces=None, extensions=None, cdef class XPath(_XPathEvaluatorBase): - u"""XPath(self, path, namespaces=None, extensions=None, regexp=True, smart_strings=True) + """XPath(self, path, namespaces=None, extensions=None, regexp=True, smart_strings=True) A compiled XPath expression that can be called on Elements and ElementTrees. Besides the XPath expression, you can pass prefix-namespace @@ -403,11 +417,12 @@ cdef class XPath(_XPathEvaluatorBase): """ cdef xpath.xmlXPathCompExpr* _xpath cdef bytes _path - def __cinit__(self): - self._xpath = NULL def __init__(self, path, *, namespaces=None, extensions=None, regexp=True, smart_strings=True): + if self._xpath is not NULL: + return + cdef xpath.xmlXPathContext* xpathCtxt _XPathEvaluatorBase.__init__(self, namespaces, extensions, regexp, smart_strings) @@ -421,36 +436,53 @@ cdef class XPath(_XPathEvaluatorBase): raise self._build_parse_error() def __call__(self, _etree_or_element, **_variables): - u"__call__(self, _etree_or_element, **_variables)" + "__call__(self, _etree_or_element, **_variables)" cdef xpath.xmlXPathObject* xpathObj - cdef _Document document - cdef _Element element assert self._xpathCtxt is not NULL, "XPath context not initialised" - document = _documentOrRaise(_etree_or_element) + doc = _documentOrRaise(_etree_or_element) element = _rootNodeOrRaise(_etree_or_element) self._lock() - self._xpathCtxt.doc = document._c_doc - self._xpathCtxt.node = element._c_node - try: - self._context.register_context(document) + self._xpathCtxt.doc = doc._c_doc + self._xpathCtxt.node = element._c_node + + self._context.register_context(doc) self._context.registerVariables(_variables) - with nogil: - xpathObj = xpath.xmlXPathCompiledEval( - self._xpath, self._xpathCtxt) - result = self._handle_result(xpathObj, document) + + # FIXME: as long as we cannot upgrade a read lock to a write lock, + # we assume that we need a write lock if the user provided extensions. + # Must do this after context.register_context() ! + use_write_lock = self._context.has_user_extensions + + if use_write_lock: + doc.lock_write() + else: + doc.lock_read() + + try: + with nogil: + xpathObj = xpath.xmlXPathCompiledEval( + self._xpath, self._xpathCtxt) + result = self._handle_result(xpathObj, doc) + finally: + if use_write_lock: + doc.unlock_write() + else: + doc.unlock_read() + finally: self._context.unregister_context() self._unlock() + return result @property def path(self): """The literal XPath expression. """ - return self._path.decode(u'UTF-8') + return self._path.decode('UTF-8') def __dealloc__(self): if self._xpath is not NULL: @@ -464,7 +496,7 @@ cdef object _replace_strings = re.compile(b'("[^"]*")|(\'[^\']*\')').sub cdef object _find_namespaces = re.compile(b'({[^}]+})').findall cdef class ETXPath(XPath): - u"""ETXPath(self, path, extensions=None, regexp=True, smart_strings=True) + """ETXPath(self, path, extensions=None, regexp=True, smart_strings=True) Special XPath class that supports the ElementTree {uri} notation for namespaces. Note that this class does not accept the ``namespace`` keyword diff --git a/src/lxml/xslt.pxi b/src/lxml/xslt.pxi index d483cfa30..e69d09187 100644 --- a/src/lxml/xslt.pxi +++ b/src/lxml/xslt.pxi @@ -1,17 +1,16 @@ - # XSLT from lxml.includes cimport xslt -cdef class XSLTError(LxmlError): +class XSLTError(LxmlError): """Base class of all XSLT errors. """ -cdef class XSLTParseError(XSLTError): +class XSLTParseError(XSLTError): """Error parsing a stylesheet document. """ -cdef class XSLTApplyError(XSLTError): +class XSLTApplyError(XSLTError): """Error running an XSL transformation. """ @@ -19,7 +18,7 @@ class XSLTSaveError(XSLTError, SerialisationError): """Error serialising an XSLT result. """ -cdef class XSLTExtensionError(XSLTError): +class XSLTExtensionError(XSLTError): """Error registering an XSLT extension. """ @@ -50,6 +49,7 @@ cdef class _XSLTResolverContext(_ResolverContext): cdef xmlDoc* _c_style_doc cdef _BaseParser _parser + @cython.final cdef _XSLTResolverContext _copy(self): cdef _XSLTResolverContext context context = _XSLTResolverContext() @@ -57,14 +57,16 @@ cdef class _XSLTResolverContext(_ResolverContext): context._c_style_doc = self._c_style_doc return context + cdef _initXSLTResolverContext(_XSLTResolverContext context, _BaseParser parser): _initResolverContext(context, parser.resolvers) context._parser = parser context._c_style_doc = NULL + cdef xmlDoc* _xslt_resolve_from_python(const_xmlChar* c_uri, void* c_context, - int parse_options, int* error) with gil: + int parse_options, int* error) noexcept with gil: # call the Python document loaders cdef _XSLTResolverContext context cdef _ResolverRegistry resolvers @@ -79,13 +81,13 @@ cdef xmlDoc* _xslt_resolve_from_python(const_xmlChar* c_uri, void* c_context, c_doc = context._c_style_doc try: if c_doc is not NULL and c_doc.URL is not NULL: - if tree.xmlStrcmp(c_uri, c_doc.URL) == 0: + if tree.xmlStrEqual(c_uri, c_doc.URL): c_return_doc = _copyDoc(c_doc, 1) return c_return_doc # 'goto', see 'finally' below # delegate to the Python resolvers resolvers = context._resolvers - if tree.xmlStrncmp('string://__STRING__XSLT__/', c_uri, 26) == 0: + if cstring_h.strncmp('string://__STRING__XSLT__/', c_uri, 26) == 0: c_uri += 26 uri = _decodeFilename(c_uri) doc_ref = resolvers.resolve(uri, None, context) @@ -102,6 +104,7 @@ cdef xmlDoc* _xslt_resolve_from_python(const_xmlChar* c_uri, void* c_context, doc_ref._file, doc_ref._filename, context._parser) elif doc_ref._type == PARSER_DATA_EMPTY: c_return_doc = _newXMLDoc() + context._parser.initDocDict(c_return_doc) if c_return_doc is not NULL and c_return_doc.URL is NULL: c_return_doc.URL = tree.xmlStrdup(c_uri) except: @@ -112,7 +115,7 @@ cdef xmlDoc* _xslt_resolve_from_python(const_xmlChar* c_uri, void* c_context, cdef void _xslt_store_resolver_exception(const_xmlChar* c_uri, void* context, - xslt.xsltLoadType c_type) with gil: + xslt.xsltLoadType c_type) noexcept with gil: try: message = f"Cannot resolve URI {_decodeFilename(c_uri)}" if c_type == xslt.XSLT_LOAD_DOCUMENT: @@ -128,7 +131,7 @@ cdef void _xslt_store_resolver_exception(const_xmlChar* c_uri, void* context, cdef xmlDoc* _xslt_doc_loader(const_xmlChar* c_uri, tree.xmlDict* c_dict, int parse_options, void* c_ctxt, - xslt.xsltLoadType c_type) nogil: + xslt.xsltLoadType c_type) noexcept nogil: # nogil => no Python objects here, may be called without thread context ! cdef xmlDoc* c_doc cdef xmlDoc* result @@ -163,11 +166,12 @@ cdef xmlDoc* _xslt_doc_loader(const_xmlChar* c_uri, tree.xmlDict* c_dict, cdef xslt.xsltDocLoaderFunc XSLT_DOC_DEFAULT_LOADER = xslt.xsltDocDefaultLoader xslt.xsltSetLoaderFunc(_xslt_doc_loader) + ################################################################################ # XSLT file/network access control cdef class XSLTAccessControl: - u"""XSLTAccessControl(self, read_file=True, write_file=True, create_dir=True, read_network=True, write_network=True) + """XSLTAccessControl(self, read_file=True, write_file=True, create_dir=True, read_network=True, write_network=True) Access control for XSLT: reading/writing files, directories and network I/O. Access to a type of resource is granted or denied by @@ -223,18 +227,18 @@ cdef class XSLTAccessControl: xslt.xsltSetSecurityPrefs(self._prefs, option, function) @cython.final - cdef void _register_in_context(self, xslt.xsltTransformContext* ctxt): + cdef void _register_in_context(self, xslt.xsltTransformContext* ctxt) noexcept: xslt.xsltSetCtxtSecurityPrefs(self._prefs, ctxt) @property def options(self): """The access control configuration as a map of options.""" return { - u'read_file': self._optval(xslt.XSLT_SECPREF_READ_FILE), - u'write_file': self._optval(xslt.XSLT_SECPREF_WRITE_FILE), - u'create_dir': self._optval(xslt.XSLT_SECPREF_CREATE_DIRECTORY), - u'read_network': self._optval(xslt.XSLT_SECPREF_READ_NETWORK), - u'write_network': self._optval(xslt.XSLT_SECPREF_WRITE_NETWORK), + 'read_file': self._optval(xslt.XSLT_SECPREF_READ_FILE), + 'write_file': self._optval(xslt.XSLT_SECPREF_WRITE_FILE), + 'create_dir': self._optval(xslt.XSLT_SECPREF_CREATE_DIRECTORY), + 'read_network': self._optval(xslt.XSLT_SECPREF_READ_NETWORK), + 'write_network': self._optval(xslt.XSLT_SECPREF_WRITE_NETWORK), } @cython.final @@ -250,14 +254,15 @@ cdef class XSLTAccessControl: def __repr__(self): items = sorted(self.options.items()) - return u"%s(%s)" % ( - python._fqtypename(self).decode('UTF-8').split(u'.')[-1], - u', '.join([u"%s=%r" % item for item in items])) + return "%s(%s)" % ( + python._typename(self), + ', '.join(["%s=%r" % item for item in items])) + ################################################################################ # XSLT -cdef int _register_xslt_function(void* ctxt, name_utf, ns_utf): +cdef int _register_xslt_function(void* ctxt, name_utf, ns_utf) noexcept: if ns_utf is None: return 0 # libxml2 internalises the strings if ctxt has a dict @@ -265,16 +270,18 @@ cdef int _register_xslt_function(void* ctxt, name_utf, ns_utf): ctxt, _xcstr(name_utf), _xcstr(ns_utf), _xpath_function_call) + cdef dict EMPTY_DICT = {} + @cython.final @cython.internal cdef class _XSLTContext(_BaseContext): cdef xslt.xsltTransformContext* _xsltCtxt cdef _ReadOnlyElementProxy _extension_element_proxy cdef dict _extension_elements + def __cinit__(self): - self._xsltCtxt = NULL self._extension_elements = EMPTY_DICT def __init__(self, namespaces, extensions, error_log, enable_regexp, @@ -283,7 +290,7 @@ cdef class _XSLTContext(_BaseContext): for ns_name_tuple, extension in extensions.items(): if ns_name_tuple[0] is None: raise XSLTExtensionError, \ - u"extensions must not have empty namespaces" + "extensions must not have empty namespaces" if isinstance(extension, XSLTExtension): if self._extension_elements is EMPTY_DICT: self._extension_elements = {} @@ -308,7 +315,8 @@ cdef class _XSLTContext(_BaseContext): self._register_context(doc) self.registerLocalFunctions(xsltCtxt, _register_xslt_function) self.registerGlobalFunctions(xsltCtxt, _register_xslt_function) - _registerXSLTExtensions(xsltCtxt, self._extension_elements) + if self._extension_elements is not EMPTY_DICT: + _registerXSLTExtensions(xsltCtxt, self._extension_elements) cdef free_context(self): self._cleanup_context() @@ -323,17 +331,18 @@ cdef class _XSLTContext(_BaseContext): @cython.internal @cython.freelist(8) cdef class _XSLTQuotedStringParam: - u"""A wrapper class for literal XSLT string parameters that require + """A wrapper class for literal XSLT string parameters that require quote escaping. """ cdef bytes strval + def __cinit__(self, strval): self.strval = _utf8(strval) @cython.no_gc_clear cdef class XSLT: - u"""XSLT(self, xslt_input, extensions=None, regexp=True, access_control=None) + """XSLT(self, xslt_input, extensions=None, regexp=True, access_control=None) Turn an XSL document into an XSLT object. @@ -342,18 +351,18 @@ cdef class XSLT: transform = etree.XSLT(xsl_tree) result = transform(xml_tree) - Keyword arguments of the constructor: + **Keyword arguments of the constructor:** - - extensions: a dict mapping ``(namespace, name)`` pairs to - extension functions or extension elements - - regexp: enable exslt regular expression support in XPath - (default: True) - - access_control: access restrictions for network or file - system (see `XSLTAccessControl`) + - extensions: a dict mapping ``(namespace, name)`` pairs to extension + functions or extension elements + - regexp: enable exslt regular expression support in XPath (default: True) + - access_control: access restrictions for network or file system + (see `XSLTAccessControl`) - Keyword arguments of the XSLT call: + **Keyword arguments of the XSLT call:** - - profile_run: enable XSLT profiling (default: False) + - profile_run: enable XSLT profiling and make the profile available as XML + document in ``result.xslt_profile`` (default: False) Other keyword arguments of the call are passed to the stylesheet as parameters. @@ -364,11 +373,11 @@ cdef class XSLT: cdef XSLTAccessControl _access_control cdef _ErrorLog _error_log - def __cinit__(self): - self._c_style = NULL - def __init__(self, xslt_input, *, extensions=None, regexp=True, access_control=None): + if self._c_style is not NULL: + raise RuntimeError("Repeated call to XSLT.__init__()") + cdef xslt.xsltStylesheet* c_style = NULL cdef xmlDoc* c_doc cdef _Document doc @@ -381,7 +390,11 @@ cdef class XSLT: self._access_control = access_control # make a copy of the document as stylesheet parsing modifies it - c_doc = _copyDocRoot(doc._c_doc, root_node._c_node) + doc.lock_read() + try: + c_doc = _copyDocRoot(doc._c_doc, root_node._c_node) + finally: + doc.unlock_read() # make sure we always have a stylesheet URL if c_doc.URL is NULL: @@ -396,15 +409,24 @@ cdef class XSLT: self._xslt_resolver_context._c_style_doc = _copyDoc(c_doc, 1) c_doc._private = self._xslt_resolver_context - with self._error_log: - orig_loader = _register_document_loader() - c_style = xslt.xsltParseStylesheetDoc(c_doc) - _reset_document_loader(orig_loader) + if xslt.LIBXSLT_VERSION >= 10134: + c_style = xslt.xsltNewStylesheet() + if c_style is NULL: + raise MemoryError() - if c_style is NULL or c_style.errors: + with self._error_log, lxml_document_loader, nogil: + if xslt.LIBXSLT_VERSION >= 10134: + if xslt.xsltParseStylesheetUser(c_style, c_doc) != 0 or c_style.errors: + xslt.xsltFreeStylesheet(c_style) + c_style = NULL + else: + c_style = xslt.xsltParseStylesheetDoc(c_doc) + if c_style is not NULL and c_style.errors: + xslt.xsltFreeStylesheet(c_style) + c_style = NULL + + if c_style is NULL: tree.xmlFreeDoc(c_doc) - if c_style is not NULL: - xslt.xsltFreeStylesheet(c_style) self._xslt_resolver_context._raise_if_stored() # last error seems to be the most accurate here if self._error_log.last_error is not None and \ @@ -414,7 +436,7 @@ cdef class XSLT: else: raise XSLTParseError( self._error_log._buildExceptionMessage( - u"Cannot parse stylesheet"), + "Cannot parse stylesheet"), self._error_log) c_doc._private = NULL # no longer used! @@ -436,7 +458,7 @@ cdef class XSLT: @staticmethod def strparam(strval): - u"""strparam(strval) + """strparam(strval) Mark an XSLT string parameter that requires quote escaping before passing it into the transformation. Use it like this:: @@ -450,7 +472,7 @@ cdef class XSLT: @staticmethod def set_global_max_depth(int max_depth): - u"""set_global_max_depth(max_depth) + """set_global_max_depth(max_depth) The maximum traversal depth that the stylesheet engine will allow. This does not only count the template recursion depth but also takes @@ -468,14 +490,8 @@ cdef class XSLT: raise ValueError("cannot set a maximum stylesheet traversal depth < 0") xslt.xsltMaxDepth = max_depth - def apply(self, _input, *, profile_run=False, **kw): - u"""apply(self, _input, profile_run=False, **kw) - - :deprecated: call the object, not this method.""" - return self(_input, profile_run=profile_run, **kw) - def tostring(self, _ElementTree result_tree): - u"""tostring(self, result_tree) + """tostring(self, result_tree) Save result doc to string based on stylesheet output method. @@ -490,7 +506,7 @@ cdef class XSLT: return _copyXSLT(self) def __call__(self, _input, *, profile_run=False, **kw): - u"""__call__(self, _input, profile_run=False, **kw) + """__call__(self, _input, profile_run=False, **kw) Execute the XSL transformation on a tree or Element. @@ -515,11 +531,27 @@ cdef class XSLT: input_doc = _documentOrRaise(_input) root_node = _rootNodeOrRaise(_input) - c_doc = _fakeRootDoc(input_doc._c_doc, root_node._c_node) + cdef bint use_write_lock = self._context._extensions + if use_write_lock: + input_doc.lock_write() + else: + input_doc.lock_fakedoc() + try: + c_doc = _fakeRootDoc(input_doc._c_doc, root_node._c_node) + except: + if use_write_lock: + input_doc.unlock_write() + else: + input_doc.unlock_fakedoc() + raise transform_ctxt = xslt.xsltNewTransformContext(self._c_style, c_doc) if transform_ctxt is NULL: _destroyFakeDoc(input_doc._c_doc, c_doc) + if use_write_lock: + input_doc.unlock_write() + else: + input_doc.unlock_fakedoc() raise MemoryError() # using the stylesheet dict is safer than using a possibly @@ -527,17 +559,22 @@ cdef class XSLT: # non-input tag/attr names will come from the stylesheet # anyway. if transform_ctxt.dict is not NULL: - xmlparser.xmlDictFree(transform_ctxt.dict) + tree.xmlDictFree(transform_ctxt.dict) if kw: # parameter values are stored in the dict # => avoid unnecessarily cluttering the global dict - transform_ctxt.dict = xmlparser.xmlDictCreateSub(self._c_style.doc.dict) + transform_ctxt.dict = tree.xmlDictCreateSub(self._c_style.doc.dict) if transform_ctxt.dict is NULL: xslt.xsltFreeTransformContext(transform_ctxt) + _destroyFakeDoc(input_doc._c_doc, c_doc) + if use_write_lock: + input_doc.unlock_write() + else: + input_doc.unlock_fakedoc() raise MemoryError() else: transform_ctxt.dict = self._c_style.doc.dict - xmlparser.xmlDictReference(transform_ctxt.dict) + tree.xmlDictReference(transform_ctxt.dict) xslt.xsltSetCtxtParseOptions( transform_ctxt, input_doc._parser._parse_options) @@ -546,7 +583,12 @@ cdef class XSLT: transform_ctxt.profile = 1 try: - context = self._context._copy() + try: + context = self._context._copy() + except: + xslt.xsltFreeTransformContext(transform_ctxt) + raise + context.register_context(transform_ctxt, input_doc) resolver_context = self._xslt_resolver_context._copy() @@ -555,9 +597,6 @@ cdef class XSLT: _convert_xslt_parameters(transform_ctxt, kw, ¶ms) c_result = self._run_transform( c_doc, params, context, transform_ctxt) - if params is not NULL: - # deallocate space for parameters - python.lxml_free(params) if transform_ctxt.state != xslt.XSLT_STATE_OK: if c_result is not NULL: @@ -570,9 +609,16 @@ cdef class XSLT: profile_doc = _documentFactory( c_profile_doc, input_doc._parser) finally: + if params is not NULL: + # deallocate space for parameters + python.lxml_free(params) if context is not None: context.free_context() _destroyFakeDoc(input_doc._c_doc, c_doc) + if use_write_lock: + input_doc.unlock_write() + else: + input_doc.unlock_fakedoc() try: if resolver_context is not None and resolver_context._has_raised(): @@ -598,47 +644,47 @@ cdef class XSLT: elif error is not None and error.line > 0: message = f"Error applying stylesheet, line {error.line}" else: - message = u"Error applying stylesheet" + message = "Error applying stylesheet" raise XSLTApplyError(message, self._error_log) finally: if resolver_context is not None: resolver_context.clear() + c_dict = c_result.dict + tree.xmlDictReference(c_dict) + result_doc = _documentFactory(c_result, input_doc._parser) + result_doc.initDict() - c_dict = c_result.dict - xmlparser.xmlDictReference(c_dict) - __GLOBAL_PARSER_CONTEXT.initThreadDictRef(&c_result.dict) - if c_dict is not c_result.dict or \ - self._c_style.doc.dict is not c_result.dict or \ - input_doc._c_doc.dict is not c_result.dict: + if self._c_style.doc.dict is not c_result.dict: with nogil: - if c_dict is not c_result.dict: - fixThreadDictNames(c_result, - c_dict, c_result.dict) if self._c_style.doc.dict is not c_result.dict: fixThreadDictNames(c_result, self._c_style.doc.dict, c_result.dict) + + if input_doc._c_doc.dict is not c_result.dict: + input_doc.lock_read() + with nogil: if input_doc._c_doc.dict is not c_result.dict: fixThreadDictNames(c_result, input_doc._c_doc.dict, c_result.dict) - xmlparser.xmlDictFree(c_dict) + input_doc.unlock_read() + + tree.xmlDictFree(c_dict) return _xsltResultTreeFactory(result_doc, self, profile_doc) cdef xmlDoc* _run_transform(self, xmlDoc* c_input_doc, const_char** params, _XSLTContext context, - xslt.xsltTransformContext* transform_ctxt): + xslt.xsltTransformContext* transform_ctxt) except? NULL: cdef xmlDoc* c_result xslt.xsltSetTransformErrorFunc(transform_ctxt, self._error_log, _receiveXSLTError) if self._access_control is not None: self._access_control._register_in_context(transform_ctxt) - with self._error_log, nogil: - orig_loader = _register_document_loader() + with self._error_log, lxml_document_loader, nogil: c_result = xslt.xsltApplyStylesheetUser( self._c_style, c_input_doc, params, NULL, NULL, transform_ctxt) - _reset_document_loader(orig_loader) return c_result @@ -670,9 +716,16 @@ cdef _convert_xslt_parameters(xslt.xsltTransformContext* transform_ctxt, v = (value)._path else: v = _utf8(value) - params[i] = tree.xmlDictLookup(c_dict, _xcstr(k), len(k)) + + c_len = len(k) + if c_len > limits.INT_MAX: + raise ValueError("Parameter name too long") + params[i] = tree.xmlDictLookup(c_dict, _xcstr(k), c_len) i += 1 - params[i] = tree.xmlDictLookup(c_dict, _xcstr(v), len(v)) + c_len = len(v) + if c_len > limits.INT_MAX: + raise ValueError("Parameter value too long") + params[i] = tree.xmlDictLookup(c_dict, _xcstr(v), c_len) i += 1 except: python.lxml_free(params) @@ -680,6 +733,7 @@ cdef _convert_xslt_parameters(xslt.xsltTransformContext* transform_ctxt, params[i] = NULL params_ptr[0] = params + cdef XSLT _copyXSLT(XSLT stylesheet): cdef XSLT new_xslt cdef xmlDoc* c_doc @@ -694,13 +748,15 @@ cdef XSLT _copyXSLT(XSLT stylesheet): stylesheet._xslt_resolver_context._c_style_doc, 1) c_doc = _copyDoc(stylesheet._c_style.doc, 1) - new_xslt._c_style = xslt.xsltParseStylesheetDoc(c_doc) + with nogil: + new_xslt._c_style = xslt.xsltParseStylesheetDoc(c_doc) if new_xslt._c_style is NULL: tree.xmlFreeDoc(c_doc) raise MemoryError() return new_xslt + @cython.final cdef class _XSLTResultTree(_ElementTree): """The result of an XSLT evaluation. @@ -723,22 +779,19 @@ cdef class _XSLTResultTree(_ElementTree): the result as defined by the ```` tag. """ cdef _FilelikeWriter writer = None - cdef _Document doc cdef int r, rclose, c_compression cdef const_xmlChar* c_encoding = NULL cdef tree.xmlOutputBuffer* c_buffer - if self._context_node is not None: - doc = self._context_node._doc - else: - doc = None + cdef _Document doc = self._get_result_doc() if doc is None: - doc = self._doc - if doc is None: - raise XSLTSaveError("No document to serialise") + raise XSLTSaveError("No document to serialise") + c_compression = compression or 0 xslt.LXML_GET_XSLT_ENCODING(c_encoding, self._xslt._c_style) - writer = _create_output_buffer(file, c_encoding, compression, &c_buffer, close=False) + writer = _create_output_buffer(file, c_encoding, c_compression, &c_buffer, close=False) + + doc.lock_read() if writer is None: with nogil: r = xslt.xsltSaveResultTo(c_buffer, doc._c_doc, self._xslt._c_style) @@ -746,89 +799,82 @@ cdef class _XSLTResultTree(_ElementTree): else: r = xslt.xsltSaveResultTo(c_buffer, doc._c_doc, self._xslt._c_style) rclose = tree.xmlOutputBufferClose(c_buffer) + doc.unlock_read() + if writer is not None: writer._exc_context._raise_if_stored() if r < 0 or rclose == -1: python.PyErr_SetFromErrno(IOError) # raises IOError - cdef _saveToStringAndSize(self, xmlChar** s, int* l): - cdef _Document doc + cdef _Document _get_result_doc(self): + return self._context_node._doc if self._context_node is not None else self._doc + + cdef int _saveToStringAndSize(self, xmlDoc *c_doc, xmlChar** s, int* size) noexcept: cdef int r - if self._context_node is not None: - doc = self._context_node._doc - else: - doc = None - if doc is None: - doc = self._doc - if doc is None: - s[0] = NULL - return with nogil: - r = xslt.xsltSaveResultToString(s, l, doc._c_doc, - self._xslt._c_style) - if r == -1: - raise MemoryError() + r = xslt.xsltSaveResultToString(s, size, c_doc, self._xslt._c_style) + return r != -1 def __str__(self): + cdef xmlChar* encoding cdef xmlChar* s = NULL - cdef int l = 0 - if not python.IS_PYTHON2: - return self.__unicode__() - self._saveToStringAndSize(&s, &l) - if s is NULL: + cdef int size = 0 + + doc = self._get_result_doc() + if doc is None: return '' - # we must not use 'funicode()' here as this is not always UTF-8 + + # XSLT serialisation needs exclusive document access. + doc.lock_write() try: - result = s[:l] + if not self._saveToStringAndSize(doc._c_doc, &s, &size): + raise MemoryError() finally: - tree.xmlFree(s) - return result + doc.unlock_write() - def __unicode__(self): - cdef xmlChar* encoding - cdef xmlChar* s = NULL - cdef int l = 0 - self._saveToStringAndSize(&s, &l) - if s is NULL: - return u'' encoding = self._xslt._c_style.encoding try: - if encoding is NULL: - result = s[:l].decode('UTF-8') - else: - result = s[:l].decode(encoding) + result = s[:size].decode('UTF-8') if encoding is NULL else s[:size].decode(encoding) finally: tree.xmlFree(s) return _stripEncodingDeclaration(result) def __getbuffer__(self, Py_buffer* buffer, int flags): - cdef int l = 0 + cdef int size = 0 if buffer is NULL: return - if self._buffer is NULL or flags & python.PyBUF_WRITABLE: - self._saveToStringAndSize(&buffer.buf, &l) - buffer.len = l - if self._buffer is NULL and not flags & python.PyBUF_WRITABLE: - self._buffer = buffer.buf - self._buffer_len = l - self._buffer_refcnt = 1 - else: - buffer.buf = self._buffer - buffer.len = self._buffer_len - self._buffer_refcnt += 1 - if flags & python.PyBUF_WRITABLE: - buffer.readonly = 0 - else: - buffer.readonly = 1 - if flags & python.PyBUF_FORMAT: - buffer.format = "B" - else: - buffer.format = NULL + + doc = self._get_result_doc() + if doc is None: + raise XSLTSaveError("No document to serialise") + + # XSLT serialisation needs exclusive document access. + doc.lock_write() + try: + with cython.critical_section(self): + if self._buffer is NULL or flags & python.PyBUF_WRITABLE: + if not self._saveToStringAndSize(doc._c_doc, &buffer.buf, &size): + raise MemoryError() + buffer.len = size + if self._buffer is NULL and not flags & python.PyBUF_WRITABLE: + self._buffer = buffer.buf + self._buffer_len = size + self._buffer_refcnt = 1 + else: + buffer.buf = self._buffer + buffer.len = self._buffer_len + self._buffer_refcnt += 1 + finally: + doc.unlock_write() + + buffer.readonly = (flags & python.PyBUF_WRITABLE) == 0 + buffer.format = b"B" if flags & python.PyBUF_FORMAT else NULL + buffer.ndim = 0 buffer.shape = NULL buffer.strides = NULL buffer.suboffsets = NULL - buffer.itemsize = 1 + buffer.itemsize = sizeof(xmlChar) buffer.internal = NULL if buffer.obj is not self: # set by Cython? buffer.obj = self @@ -836,14 +882,19 @@ cdef class _XSLTResultTree(_ElementTree): def __releasebuffer__(self, Py_buffer* buffer): if buffer is NULL: return - if buffer.buf is self._buffer: - self._buffer_refcnt -= 1 - if self._buffer_refcnt == 0: - tree.xmlFree(self._buffer) - self._buffer = NULL + cdef void* c_buffer_to_release = NULL + if buffer.buf is not self._buffer: + c_buffer_to_release = buffer.buf else: - tree.xmlFree(buffer.buf) + with cython.critical_section(self): + self._buffer_refcnt -= 1 + if self._buffer_refcnt == 0: + c_buffer_to_release = self._buffer + self._buffer = NULL + buffer.buf = NULL + if c_buffer_to_release is not NULL: + tree.xmlFree(c_buffer_to_release) property xslt_profile: """Return an ElementTree with profiling data for the stylesheet run. @@ -860,6 +911,7 @@ cdef class _XSLTResultTree(_ElementTree): def __del__(self): self._profile = None + cdef _xsltResultTreeFactory(_Document doc, XSLT xslt, _Document profile): cdef _XSLTResultTree result result = <_XSLTResultTree>_newElementTree(doc, None, _XSLTResultTree) @@ -867,6 +919,7 @@ cdef _xsltResultTreeFactory(_Document doc, XSLT xslt, _Document profile): result._profile = profile return result + # functions like "output" and "write" are a potential security risk, but we # rely on the user to configure XSLTAccessControl as needed xslt.xsltRegisterAllExtras() @@ -878,22 +931,24 @@ xslt.exsltRegisterAll() ################################################################################ # XSLT PI support -cdef object _RE_PI_HREF = re.compile(ur'\s+href\s*=\s*(?:\'([^\']*)\'|"([^"]*)")') +cdef object _RE_PI_HREF = re.compile(r'\s+href\s*=\s*(?:\'([^\']*)\'|"([^"]*)")') cdef object _FIND_PI_HREF = _RE_PI_HREF.findall cdef object _REPLACE_PI_HREF = _RE_PI_HREF.sub cdef XPath __findStylesheetByID = None + cdef _findStylesheetByID(_Document doc, id): global __findStylesheetByID if __findStylesheetByID is None: __findStylesheetByID = XPath( - u"//xsl:stylesheet[@xml:id = $id]", - namespaces={u"xsl" : u"http://www.w3.org/1999/XSL/Transform"}) + "//xsl:stylesheet[@xml:id = $id]", + namespaces={"xsl" : "http://www.w3.org/1999/XSL/Transform"}) return __findStylesheetByID(doc, id=id) + cdef class _XSLTProcessingInstruction(PIBase): def parseXSL(self, parser=None): - u"""parseXSL(self, parser=None) + """parseXSL(self, parser=None) Try to parse the stylesheet referenced by this PI and return an ElementTree for it. If the stylesheet is embedded in the @@ -908,21 +963,29 @@ cdef class _XSLTProcessingInstruction(PIBase): cdef bytes href_utf cdef const_xmlChar* c_href cdef xmlAttr* c_attr + _assertValidNode(self) - if self._c_node.content is NULL: - raise ValueError, u"PI lacks content" - hrefs = _FIND_PI_HREF(u' ' + (self._c_node.content).decode('UTF-8')) + self._doc.lock_read() + try: + if self._c_node.content is NULL: + raise ValueError, "PI lacks content" + hrefs = _FIND_PI_HREF(' ' + (self._c_node.content).decode('UTF-8')) + finally: + self._doc.unlock_read() + if len(hrefs) != 1: - raise ValueError, u"malformed PI attributes" + raise ValueError, "malformed PI attributes" hrefs = hrefs[0] href_utf = utf8(hrefs[0] or hrefs[1]) c_href = _xcstr(href_utf) if c_href[0] != c'#': # normal URL, try to parse from it + self._doc.lock_read() c_href = tree.xmlBuildURI( c_href, tree.xmlNodeGetBase(self._c_node.doc, self._c_node)) + self._doc.unlock_read() if c_href is not NULL: try: href_utf = c_href @@ -935,36 +998,41 @@ cdef class _XSLTProcessingInstruction(PIBase): # try XML:ID lookup _assertValidDoc(self._doc) c_href += 1 # skip leading '#' - c_attr = tree.xmlGetID(self._c_node.doc, c_href) - if c_attr is not NULL and c_attr.doc is self._c_node.doc: - result_node = _elementFactory(self._doc, c_attr.parent) - return _elementTreeFactory(result_node._doc, result_node) + + self._doc.lock_read() + try: + c_attr = tree.xmlGetID(self._c_node.doc, c_href) + if c_attr is not NULL and c_attr.doc is self._c_node.doc: + result_node = _elementFactory(self._doc, c_attr.parent) + return _elementTreeFactory(result_node._doc, result_node) + finally: + self._doc.unlock_read() # try XPath search root = _findStylesheetByID(self._doc, funicode(c_href)) if not root: - raise ValueError, u"reference to non-existing embedded stylesheet" + raise ValueError, "reference to non-existing embedded stylesheet" elif len(root) > 1: - raise ValueError, u"ambiguous reference to embedded stylesheet" + raise ValueError, "ambiguous reference to embedded stylesheet" result_node = root[0] return _elementTreeFactory(result_node._doc, result_node) def set(self, key, value): - u"""set(self, key, value) + """set(self, key, value) Supports setting the 'href' pseudo-attribute in the text of the processing instruction. """ - if key != u"href": + if key != "href": raise AttributeError, \ - u"only setting the 'href' attribute is supported on XSLT-PIs" + "only setting the 'href' attribute is supported on XSLT-PIs" if value is None: - attrib = u"" - elif u'"' in value or u'>' in value: - raise ValueError, u"Invalid URL, must not contain '\"' or '>'" + attrib = "" + elif '"' in value or '>' in value: + raise ValueError, "Invalid URL, must not contain '\"' or '>'" else: attrib = f' href="{value}"' - text = u' ' + self.text + text = ' ' + self.text if _FIND_PI_HREF(text): self.text = _REPLACE_PI_HREF(attrib, text) else: diff --git a/src/lxml/xsltext.pxi b/src/lxml/xsltext.pxi index c98ae1ff4..0786df0de 100644 --- a/src/lxml/xsltext.pxi +++ b/src/lxml/xsltext.pxi @@ -1,10 +1,17 @@ # XSLT extension elements +cdef bint is_blank_text(unicode s): + for ch in s: + if not ch.isspace(): + return False + return True + + cdef class XSLTExtension: - u"""Base class of an XSLT extension element. + """Base class of an XSLT extension element. """ def execute(self, context, self_node, input_node, output_parent): - u"""execute(self, context, self_node, input_node, output_parent) + """execute(self, context, self_node, input_node, output_parent) Execute this extension element. Subclasses must override this method. They may append @@ -21,7 +28,7 @@ cdef class XSLTExtension: def apply_templates(self, _XSLTContext context not None, node, output_parent=None, *, elements_only=False, remove_blank_text=False): - u"""apply_templates(self, context, node, output_parent=None, elements_only=False, remove_blank_text=False) + """apply_templates(self, context, node, output_parent=None, elements_only=False, remove_blank_text=False) Call this method to retrieve the result of applying templates to an element. @@ -73,7 +80,7 @@ cdef class XSLTExtension: def process_children(self, _XSLTContext context not None, output_parent=None, *, elements_only=False, remove_blank_text=False): - u"""process_children(self, context, output_parent=None, elements_only=False, remove_blank_text=False) + """process_children(self, context, output_parent=None, elements_only=False, remove_blank_text=False) Call this method to process the XSLT content of the extension element itself. @@ -129,13 +136,14 @@ cdef class XSLTExtension: cdef xmlNode* c_next cdef _ReadOnlyProxy proxy cdef list results = [] # or maybe _collectAttributes(c_parent, 2) ? + c_node = c_parent.children while c_node is not NULL: c_next = c_node.next if c_node.type == tree.XML_TEXT_NODE: if not elements_only: s = funicode(c_node.content) - if not remove_blank_text or s.strip(): + if not (remove_blank_text and is_blank_text(s)): results.append(s) s = None elif c_node.type == tree.XML_ELEMENT_NODE: @@ -159,16 +167,17 @@ cdef _registerXSLTExtensions(xslt.xsltTransformContext* c_ctxt, c_ctxt, _xcstr(name_utf), _xcstr(ns_utf), _callExtensionElement) + cdef void _callExtensionElement(xslt.xsltTransformContext* c_ctxt, xmlNode* c_context_node, xmlNode* c_inst_node, - void* dummy) with gil: + void* dummy) noexcept with gil: cdef _XSLTContext context cdef XSLTExtension extension - cdef python.PyObject* dict_result cdef xmlNode* c_node cdef _ReadOnlyProxy context_node = None, self_node = None cdef object output_parent # not restricted to ro-nodes + c_uri = _getNs(c_inst_node) if c_uri is NULL: # not allowed, and should never happen @@ -176,14 +185,13 @@ cdef void _callExtensionElement(xslt.xsltTransformContext* c_ctxt, if c_ctxt.xpathCtxt.userData is NULL: # just for safety, should never happen return + context = <_XSLTContext>c_ctxt.xpathCtxt.userData try: try: - dict_result = python.PyDict_GetItem( - context._extension_elements, (c_uri, c_inst_node.name)) - if dict_result is NULL: + extension = context._extension_elements.get((c_uri, c_inst_node.name)) + if extension is None: raise KeyError, f"extension element {funicode(c_inst_node.name)} not found" - extension = dict_result try: # build the context proxy nodes @@ -219,9 +227,9 @@ cdef void _callExtensionElement(xslt.xsltTransformContext* c_ctxt, _freeReadOnlyProxies(self_node) except Exception as e: try: - e = unicode(e).encode(u"UTF-8") + e = unicode(e).encode("UTF-8") except: - e = repr(e).encode(u"UTF-8") + e = repr(e).encode("UTF-8") message = python.PyBytes_FromFormat( "Error executing extension element '%s': %s", c_inst_node.name, _cstr(e)) diff --git a/test.py b/test.py index d523e7084..9dc160f82 100644 --- a/test.py +++ b/test.py @@ -72,6 +72,11 @@ import unittest import traceback +try: + import tracemalloc +except ImportError: + tracemalloc = None + from unittest import TextTestResult __metaclass__ = type @@ -86,6 +91,7 @@ class Options: # test location basedir = '' # base directory for tests (defaults to # basedir of argv[0] + 'src'), must be absolute + src_in_path = True # add 'src/' to sys.path follow_symlinks = True # should symlinks to subdirectories be # followed? (hardcoded, may cause loops) @@ -479,7 +485,7 @@ def main(argv): # Option processing opts, args = getopt.gnu_getopt(argv[1:], 'hvpqufw', ['list-files', 'list-tests', 'list-hooks', - 'level=', 'all-levels', 'coverage']) + 'level=', 'all-levels', 'coverage', 'no-src']) for k, v in opts: if k == '-h': print(__doc__) @@ -511,6 +517,8 @@ def main(argv): cfg.run_tests = False elif k == '--coverage': cfg.coverage = True + elif k == '--no-src': + cfg.src_in_path = False elif k == '--level': try: cfg.level = int(v) @@ -536,7 +544,8 @@ def main(argv): cfg.unit_tests = True # Set up the python path - sys.path[0] = cfg.basedir + if cfg.src_in_path: + sys.path[0] = cfg.basedir # Set up tracing before we start importing things cov = None @@ -544,6 +553,10 @@ def main(argv): from coverage import Coverage cov = Coverage(omit=['test.py']) + # Configure tracemalloc. + if tracemalloc is not None: + tracemalloc.start() + # Finding and importing test_files = get_test_files(cfg) diff --git a/tools/ci-run.sh b/tools/ci-run.sh index f9b43fbdd..199bb71e4 100644 --- a/tools/ci-run.sh +++ b/tools/ci-run.sh @@ -1,13 +1,25 @@ #!/usr/bin/bash -GCC_VERSION=${GCC_VERSION:=8} +set -x + +GCC_VERSION=${GCC_VERSION:=9} +TEST_CFLAGS= +EXTRA_CFLAGS= +EXTRA_LDFLAGS= +SAVED_GITHUB_API_TOKEN="${GITHUB_API_TOKEN}" +unset GITHUB_API_TOKEN # remove from env # Set up compilers if [ -z "${OS_NAME##ubuntu*}" ]; then echo "Installing requirements [apt]" sudo apt-add-repository -y "ppa:ubuntu-toolchain-r/test" sudo apt-get update -y -q - sudo apt-get install -y -q ccache gcc-$GCC_VERSION "libxml2=2.9.4*" "libxml2-dev=2.9.4*" libxslt1.1 libxslt1-dev || exit 1 + sudo apt-get install -y -q ccache gcc-$GCC_VERSION || exit 1 + if [ -n "${STATIC_DEPS##true}" ]; then + # Ubuntu 22.04 has libxml2 2.9.13, Ubuntu 24.04 has 2.9.14 + sudo apt-get install -y -q "libxml2=2.9.14*" "libxml2-dev=2.9.14*" libxslt1.1 libxslt1-dev \ + || sudo apt-get install -y -q "libxml2=2.9.13*" "libxml2-dev=2.9.13*" libxslt1.1 libxslt1-dev + fi sudo /usr/sbin/update-ccache-symlinks echo "/usr/lib/ccache" >> $GITHUB_PATH # export ccache to path @@ -15,9 +27,14 @@ if [ -z "${OS_NAME##ubuntu*}" ]; then export CC="gcc" export PATH="/usr/lib/ccache:$PATH" + TEST_CFLAGS="-Og -g -fPIC" + EXTRA_CFLAGS="-Wall -Wextra -DLXML_DEBUG_ATOMICS=1" elif [ -z "${OS_NAME##macos*}" ]; then export CC="clang -Wno-deprecated-declarations" + TEST_CFLAGS="-Og -g -fPIC -arch arm64 -arch x86_64" + EXTRA_LDFLAGS="-arch arm64 -arch x86_64" + EXTRA_CFLAGS="-Wall -Wextra -arch arm64 -arch x86_64 -DLXML_DEBUG_ATOMICS=1" fi # Log versions in use @@ -25,48 +42,47 @@ echo "====================" echo "|VERSIONS INSTALLED|" echo "====================" python -c 'import sys; print("Python %s" % (sys.version,))' -if [ "$CC" ]; then +if [[ "$CC" ]]; then which ${CC%% *} ${CC%% *} --version fi -pkg-config --modversion libxml-2.0 libxslt +if [ -z "${OS_NAME##win*}" ]; then + pkg-config --modversion libxml-2.0 libxslt +fi echo "====================" ccache -s || true # Install python requirements echo "Installing requirements [python]" -python -m pip install -U pip setuptools wheel +python -m pip install -U pip setuptools + if [ -z "${PYTHON_VERSION##*-dev}" ]; - then python -m pip install --install-option=--cython-compile-minimal https://github.com/cython/cython/archive/master.zip; + then CYTHON_COMPILE=false python -m pip install https://github.com/cython/cython/archive/master.zip; else python -m pip install -r requirements.txt; fi -if [ -z "${PYTHON_VERSION##2*}" ]; then - python -m pip install -U beautifulsoup4==4.9.3 cssselect==1.1.0 html5lib==1.1 rnc2rng==2.6.5 ${EXTRA_DEPS} || exit 1 -else - python -m pip install -U beautifulsoup4 cssselect html5lib rnc2rng ${EXTRA_DEPS} || exit 1 -fi -if [ "$COVERAGE" == "true" ]; then + +python -m pip install -U beautifulsoup4 cssselect html5lib rnc2rng ${EXTRA_DEPS} || exit 1 +python -m pip install --no-deps lxml_html_clean || exit 1 + +if [[ "$COVERAGE" == "true" ]]; then python -m pip install "coverage<5" || exit 1 - python -m pip install --pre 'Cython>=3.0a0' || exit 1 fi # Build -CFLAGS="-Og -g -fPIC -Wall -Wextra" python -u setup.py build_ext --inplace \ - $(if [ -n "${PYTHON_VERSION##2.*}" ]; then echo -n " -j7 "; fi ) \ - $(if [ "$COVERAGE" == "true" ]; then echo -n " --with-coverage"; fi ) \ +GITHUB_API_TOKEN="${SAVED_GITHUB_API_TOKEN}" \ + CFLAGS="$CFLAGS $TEST_CFLAGS $EXTRA_CFLAGS" \ + LDFLAGS="$LDFLAGS $EXTRA_LDFLAGS" \ + python -u setup.py build_ext --inplace --warnings -j4 \ + $(if [[ "$COVERAGE" == "true" ]]; then echo -n " --with-coverage"; fi ) \ || exit 1 -ccache -s || true - # Run tests -CFLAGS="-Og -g -fPIC" PYTHONUNBUFFERED=x make test || exit 1 - -python setup.py install || exit 1 -python -c "from lxml import etree" || exit 1 - -CFLAGS="-O3 -g1 -mtune=generic -fPIC -flto" \ - LDFLAGS="-flto" \ - make clean wheel || exit 1 +echo "Running the tests ..." +GITHUB_API_TOKEN="${SAVED_GITHUB_API_TOKEN}" \ + CFLAGS="$TEST_CFLAGS $EXTRA_CFLAGS" \ + LDFLAGS="$LDFLAGS $EXTRA_LDFLAGS" \ + PYTHONUNBUFFERED=x \ + make test || exit 1 ccache -s || true diff --git a/tools/manylinux/build-wheels.sh b/tools/manylinux/build-wheels.sh index 7192ee58a..dac15198e 100755 --- a/tools/manylinux/build-wheels.sh +++ b/tools/manylinux/build-wheels.sh @@ -30,16 +30,22 @@ build_wheel() { run_tests() { # Install packages and test for PYBIN in /opt/python/${PYTHON_BUILD_VERSION}/bin/; do - ${PYBIN}/python -m pip install $PACKAGE --no-index -f /io/$WHEELHOUSE || exit 1 + ${PYBIN}/python -m pip install $PACKAGE --no-index -f /io/$WHEELHOUSE | tee install.txt || exit 1 # check import as a quick test - (cd $HOME; ${PYBIN}/python -c 'import lxml.etree, lxml.objectify') + (cd $HOME; ${PYBIN}/python -c 'import lxml.etree, lxml.objectify') || { + # Allow PyPy to fail the import due to C-API differences for 'str' (PyVarObject or not). + echo "${PYBIN}" | fgrep -q pypy || exit 1 + echo "Import failed - deleting wheel" + sed -ne '/Processing .*\.whl/s|Processing ||p' install.txt | (cd /io/$WHEELHOUSE && xargs rm) + } done } prepare_system() { #yum install -y zlib-devel - yum -y install xz || true + yum --version 2>/dev/null && yum -y install xz || true + apt-get --version 2>/dev/null && apt-get install xz-utils || true #rm -fr /opt/python/cp34-* echo "Python versions found: $(cd /opt/python && echo cp* | sed -e 's|[^ ]*-||g')" ${CC:-gcc} --version diff --git a/tools/pypistats.py b/tools/pypistats.py new file mode 100644 index 000000000..5c11e78dd --- /dev/null +++ b/tools/pypistats.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +import json +from collections import defaultdict +from urllib.request import urlopen +import ssl + +PACKAGE = "lxml" + + +def get_stats(stats_type, package=PACKAGE, period="month"): + stats_url = f"https://pypistats.org/api/packages/{package}/{stats_type}?period={period}" + + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + + with urlopen(stats_url, context=ctx) as stats: + data = json.load(stats) + return data + + +def aggregate(stats): + counts = defaultdict(int) + days = defaultdict(int) + for entry in stats['data']: + category = entry['category'] + counts[category] += entry['downloads'] + days[category] += 1 + return {category: counts[category] / days[category] for category in counts} + + +def version_sorter(version_and_count): + version = version_and_count[0] + return tuple(map(int, version.split("."))) if version.replace(".", "").isdigit() else (2**32,) + + +def system_sorter(name_and_count): + order = ('linux', 'windows', 'darwin')[::-1] + system = name_and_count[0] + try: + return order.index(system.lower()) + except ValueError: + return 0 + + +def print_agg_stats(stats, sort_key=None): + total = sum(stats.values()) + max_len = max(len(category) for category in stats) + agg_sum = 0.0 + for category, count in sorted(stats.items(), key=sort_key, reverse=True): + agg_sum += count + print(f" {category:{max_len}}: {count:-12.1f} / day ({agg_sum / total * 100:-5.1f}%)") + + +def main(): + import sys + package_name = sys.argv[1] if len(sys.argv) > 1 else PACKAGE + print("Package:", package_name) + + counts = get_stats("python_minor", package=package_name) + stats = aggregate(counts) + print("Downloads by Python version:") + print_agg_stats(stats, sort_key=version_sorter) + + print() + counts = get_stats("system", package=package_name) + stats = aggregate(counts) + print("Downloads by system:") + print_agg_stats(stats, sort_key=system_sorter) + + total = sum(stats.values()) + days = {"month": 30, "week": 7, "day": 1} + print(f"Total downloads per month: {total * days['month']:-12,.1f}") + + +if __name__ == '__main__': + main() diff --git a/tox.ini b/tox.ini index 063a68044..15b6576a2 100644 --- a/tox.ini +++ b/tox.ini @@ -4,17 +4,22 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py35, py36, py37, py38, py39, py310 +envlist = py39, py310, py311, py312, py313, py314, pypy3 [testenv] allowlist_externals = make setenv = - CFLAGS = -g -O0 + CFLAGS = -g -Og -fPIC + STATIC_DEPS = true +# WITHOUT_ZLIB = true commands = {envpython} setup.py clean {envpython} setup.py build_ext --inplace - make test + make PYTHON={envpython} test install_command = pip install {opts} {packages} deps = -r{toxinidir}/requirements.txt html5lib + lxml_html_clean + cssselect + setuptools;python_version >= '3.12' diff --git a/update-error-constants.py b/update-error-constants.py index 02928400c..8520af5f6 100644 --- a/update-error-constants.py +++ b/update-error-constants.py @@ -1,9 +1,8 @@ -#!/usr/bin/env python - -from __future__ import print_function, absolute_import +#!/usr/bin/env python3 import operator import os.path +import pathlib import sys import xml.etree.ElementTree as ET @@ -73,8 +72,8 @@ def regenerate_file(filename, result): return True -def parse_enums(doc_dir, api_filename, enum_dict): - tree = ET.parse(os.path.join(doc_dir, api_filename)) +def parse_from_api_xml(api_xml_path, enum_dict): + tree = ET.parse(str(api_xml_path)) for enum in tree.iterfind('symbols/enum'): enum_type = enum.get('type') if enum_type not in ENUM_MAP: @@ -90,15 +89,33 @@ def parse_enums(doc_dir, api_filename, enum_dict): )) -def main(doc_dir): - enum_dict = {} - parse_enums(doc_dir, 'libxml2-api.xml', enum_dict) - #parse_enums(doc_dir, 'libxml-xmlerror.html', enum_dict) - #parse_enums(doc_dir, 'libxml-xpath.html', enum_dict) - #parse_enums(doc_dir, 'libxml-xmlschemas.html', enum_dict) - #parse_enums(doc_dir, 'libxml-relaxng.html', enum_dict) - - # regenerate source files +def parse_from_doxygen_xml(doxygen_xml_path, enum_dict): + for xml_file in doxygen_xml_path.glob("*_8h.xml"): + for _, compound in ET.iterparse(xml_file): + if compound.tag != 'compounddef': + continue + if not compound.findtext('compoundname', '').endswith('.h'): + break + for memberdef in compound.iterfind('sectiondef[@kind = "enum"]/memberdef'): + enum_type = memberdef.findtext('name') + if enum_type not in ENUM_MAP: + continue + entries = enum_dict.get(enum_type) + if not entries: + print("Found enum", enum_type) + entries = enum_dict[enum_type] = [] + + enum_value = 0 + for enum in memberdef.iterfind('enumvalue'): + enum_value = int(enum.findtext('initializer', '').lstrip('= ') or enum_value + 1) + entries.append(( + enum.findtext('name'), + enum_value, + enum.findtext('briefdescription/para', '').rstrip('. ').strip(), + )) + + +def generate_source_files(enum_dict): pxi_result = [] append_pxi = pxi_result.append pxd_result = [] @@ -150,6 +167,23 @@ def main(doc_dir): print("Done") +def main(doc_dir): + doc_path = pathlib.Path(doc_dir) + api_xml_path = doc_path / 'libxml2-api.xml' + doxygen_xml_path = doc_path / 'xml' + + enum_dict = {} + if api_xml_path.exists(): + parse_from_api_xml(api_xml_path, enum_dict) + elif doxygen_xml_path.exists(): + parse_from_doxygen_xml(doxygen_xml_path, enum_dict) + else: + print(f"XML files for libxml2 API not found - did you generate the libxml2 documentation in {doc_dir}?") + return + + generate_source_files(enum_dict) + + if __name__ == "__main__": if len(sys.argv) < 2 or sys.argv[1].lower() in ('-h', '--help'): print("This script generates the constants in file %s" % BUILD_SOURCE_FILE)