From e71b0a81420ed5a7d1bbd9afba09c74dc6a47b28 Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Sun, 21 Mar 2021 16:59:51 +0100 Subject: [PATCH 01/74] Prevent duplicated downloads. --- download_artefacts.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/download_artefacts.py b/download_artefacts.py index 450251788..10d47b853 100755 --- a/download_artefacts.py +++ b/download_artefacts.py @@ -90,6 +90,14 @@ def download(urls, dest_dir, jobs=PARALLEL_DOWNLOADS): raise +def dedup(it): + seen = set() + for value in it: + if value not in seen: + seen.add(value) + yield value + + def roundrobin(*iterables): "roundrobin('ABC', 'D', 'EF') --> A D E B F C" # Recipe credited to George Sakkis @@ -117,10 +125,10 @@ def main(*args): dest_dir.mkdir() start_time = datetime.datetime.now().replace(microsecond=0) - urls = roundrobin( + urls = roundrobin(*map(dedup, [ find_github_files(version), find_appveyor_files(version), - ) + ])) count = sum(1 for _ in enumerate(download(urls, dest_dir))) duration = datetime.datetime.now().replace(microsecond=0) - start_time logger.info(f"Downloaded {count} files in {duration}.") From 40caae02ad3b5e820a90e533ce9c009b6b390545 Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Sun, 21 Mar 2021 19:40:00 +0100 Subject: [PATCH 02/74] Avoid race conditions when downloading artefacts. --- download_artefacts.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/download_artefacts.py b/download_artefacts.py index 10d47b853..cf82b4c0a 100755 --- a/download_artefacts.py +++ b/download_artefacts.py @@ -65,14 +65,16 @@ def download1(wheel_url, dest_dir): and file_path.stat().st_size == int(w.headers["Content-Length"])): logger.info(f"Already have {wheel_name}") else: + temp_file_path = file_path.with_suffix(".tmp") try: - with open(file_path, "wb") as f: + with open(temp_file_path, "wb") as f: shutil.copyfileobj(w, f) except: - if file_path.exists(): - file_path.unlink() + if temp_file_path.exists(): + temp_file_path.unlink() raise else: + temp_file_path.replace(file_path) logger.info(f"Finished downloading {wheel_name}") return wheel_name From ea954da3c87bd8f6874f6bf4203e2ef5269ea383 Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Mon, 29 Mar 2021 22:30:25 +0200 Subject: [PATCH 03/74] Clarify that the ET compatibility difference for the '*' tag filter applies not only to ".iter()" but also to ".find*()". --- doc/compatibility.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/compatibility.txt b/doc/compatibility.txt index e23d18171..654cb7c4e 100644 --- a/doc/compatibility.txt +++ b/doc/compatibility.txt @@ -146,11 +146,11 @@ ElementTree. Nonetheless, some differences and incompatibilities exist: not. This means that a comment text "text" that ElementTree serializes as "" will become "" in lxml. -* When the string '*' is used as tag filter in the ``Element.getiterator()`` - method, ElementTree returns all elements in the tree, including comments and - processing instructions. lxml.etree only returns real Elements, i.e. tree - nodes that have a string tag name. Without a filter, both libraries iterate - over all nodes. +* When the string ``'*'`` is used as tag filter in the ``Element.iter()`` and + ``.find*()`` methods, ElementTree returns all elements in the tree, including + comments and processing instructions. lxml.etree only returns real Elements, + i.e. tree nodes that have a string tag name. Without a filter, both libraries + iterate over all nodes. Note that currently only lxml.etree supports passing the ``Element`` factory function as filter to select only Elements. Both libraries support passing From b3e3b1fcc6388e45c0d8bbba9dd6b32c547db362 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 24 Apr 2021 19:55:38 +0200 Subject: [PATCH 04/74] Add CPython nightly builds (currently Py3.10) to the travis build matrix (GH-315) --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 13ec41be7..291c40377 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ cache: - libs python: + - nightly - 3.9 - 2.7 - 3.8 @@ -61,6 +62,7 @@ matrix: env: STATIC_DEPS=true arch: ppc64le allow_failures: + - python: nightly - python: pypy - python: pypy3 @@ -79,3 +81,5 @@ script: - ccache -s || true - CFLAGS="-O0 -g -fPIC" PYTHONUNBUFFERED=x make test - ccache -s || true + - python setup.py install + - python -c "from lxml import etree" From d03c0dc090e06d5e16a2194aa41b576ecd69fa64 Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Sun, 2 May 2021 15:01:20 +0200 Subject: [PATCH 05/74] Include manylinux 2.24 wheel builds because they feature a newer C compiler. --- Makefile | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index a8c9de829..944260752 100644 --- a/Makefile +++ b/Makefile @@ -16,9 +16,15 @@ MANYLINUX_LIBXML2_VERSION=2.9.10 MANYLINUX_LIBXSLT_VERSION=1.1.34 MANYLINUX_CFLAGS=-O3 -g1 -pipe -fPIC -flto MANYLINUX_LDFLAGS=-flto -MANYLINUX_IMAGE_X86_64=quay.io/pypa/manylinux1_x86_64 -MANYLINUX_IMAGE_686=quay.io/pypa/manylinux1_i686 -MANYLINUX_IMAGE_AARCH64=quay.io/pypa/manylinux2014_aarch64 + +MANYLINUX_IMAGES= \ + manylinux1_x86_64 \ + manylinux1_i686 \ + manylinux_2_24_x86_64 \ + manylinux_2_24_i686 \ + manylinux_2_24_aarch64 \ + manylinux_2_24_ppc64le \ + manylinux_2_24_s390x AARCH64_ENV=-e AR="/opt/rh/devtoolset-9/root/usr/bin/gcc-ar" \ -e NM="/opt/rh/devtoolset-9/root/usr/bin/gcc-nm" \ @@ -55,19 +61,22 @@ require-cython: qemu-user-static: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes -wheel_manylinux: wheel_manylinux64 wheel_manylinux32 wheel_manylinuxaarch64 -wheel_manylinuxaarch64: qemu-user-static +wheel_manylinux: $(addprefix wheel_,$(MANYLINUX_IMAGES)) +$(addprefix wheel_,$(filter-out %_x86_64, $(filter-out %_i686, $(MANYLINUX_IMAGES)))): qemu-user-static -wheel_manylinux32 wheel_manylinux64 wheel_manylinuxaarch64: dist/lxml-$(LXMLVERSION).tar.gz +wheel_%: dist/lxml-$(LXMLVERSION).tar.gz time docker run --rm -t \ -v $(shell pwd):/io \ - $(if $(patsubst %aarch64,,$@),,$(AARCH64_ENV)) \ + -e AR=gcc-ar \ + -e NM=gcc-nm \ + -e RANLIB=gcc-ranlib \ -e CFLAGS="$(MANYLINUX_CFLAGS) $(if $(patsubst %aarch64,,$@),-march=core2,-march=armv8-a -mtune=cortex-a72)" \ -e LDFLAGS="$(MANYLINUX_LDFLAGS)" \ -e LIBXML2_VERSION="$(MANYLINUX_LIBXML2_VERSION)" \ -e LIBXSLT_VERSION="$(MANYLINUX_LIBXSLT_VERSION)" \ - -e WHEELHOUSE=wheelhouse_$(subst wheel_,,$@) \ - $(if $(filter $@,wheel_manylinuxaarch64),$(MANYLINUX_IMAGE_AARCH64),$(if $(patsubst %32,,$@),$(MANYLINUX_IMAGE_X86_64),$(MANYLINUX_IMAGE_686))) \ + -e WHEELHOUSE=$(subst wheel_,wheelhouse/,$@) \ + $(if $(patsubst %aarch64,,$@),,$(AARCH64_ENV)) \ + quay.io/pypa/$(subst wheel_,,$@) \ bash /io/tools/manylinux/build-wheels.sh /io/$< wheel: From f163e6395668e315c74489183070ce2ed3878e83 Mon Sep 17 00:00:00 2001 From: Joel Date: Sat, 8 May 2021 15:21:08 +0200 Subject: [PATCH 06/74] Enable access to the system_url of DTD entity declarations (GH-317) --- src/lxml/dtd.pxi | 5 +++++ src/lxml/tests/test_dtd.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/src/lxml/dtd.pxi b/src/lxml/dtd.pxi index 5dcb80c46..2b4bf762f 100644 --- a/src/lxml/dtd.pxi +++ b/src/lxml/dtd.pxi @@ -258,6 +258,11 @@ cdef class _DTDEntityDecl: _assertValidDTDNode(self, self._c_node) return funicodeOrNone(self._c_node.content) + @property + def system_url(self): + _assertValidDTDNode(self, self._c_node) + return funicodeOrNone(self._c_node.SystemID) + ################################################################################ # DTD diff --git a/src/lxml/tests/test_dtd.py b/src/lxml/tests/test_dtd.py index 0f06b7399..779f9e849 100644 --- a/src/lxml/tests/test_dtd.py +++ b/src/lxml/tests/test_dtd.py @@ -403,6 +403,14 @@ def test_comment_before_dtd(self): self.assertEqual(etree.tostring(doc), _bytes(data)) + def test_entity_system_url(self): + xml = etree.parse(BytesIO(' ]>')) + self.assertEqual(xml.docinfo.internalDTD.entities()[0].system_url, "./foo.bar") + + def test_entity_system_url_none(self): + xml = etree.parse(BytesIO(' ]>')) + self.assertEqual(xml.docinfo.internalDTD.entities()[0].system_url, None) + def test_suite(): suite = unittest.TestSuite() From a3741bc3d5b083e6503fc62ac45a48014c5ae6f4 Mon Sep 17 00:00:00 2001 From: DavidKorczynski Date: Sat, 8 May 2021 14:37:11 +0100 Subject: [PATCH 07/74] Add initial Atheris fuzzer. (GH-313) --- src/lxml/tests/fuzz_xml_parse.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/lxml/tests/fuzz_xml_parse.py diff --git a/src/lxml/tests/fuzz_xml_parse.py b/src/lxml/tests/fuzz_xml_parse.py new file mode 100644 index 000000000..a7c3ef499 --- /dev/null +++ b/src/lxml/tests/fuzz_xml_parse.py @@ -0,0 +1,23 @@ +""" +Fuzzes the lxml.etree.XML function with the Atheris fuzzer. + +The goal is to catch unhandled exceptions and potential +memory corruption issues in auto-generated code. +""" + +import atheris +import sys + +from lxml import etree + +def test_etree_xml(data): + fdp = atheris.FuzzedDataProvider(data) + try: + root = etree.XML(fdp.ConsumeUnicode(sys.maxsize)) + except etree.XMLSyntaxError: + pass + return + +if __name__ == "__main__": + atheris.Setup(sys.argv, test_etree_xml, enable_python_coverage=True) + atheris.Fuzz() From b3b09fcd1962409c2f7867fcadd636c38579b81d Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Sat, 8 May 2021 16:25:30 +0200 Subject: [PATCH 08/74] Clean up fuzzer test. --- src/lxml/tests/fuzz_xml_parse.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lxml/tests/fuzz_xml_parse.py b/src/lxml/tests/fuzz_xml_parse.py index a7c3ef499..980d8d0b8 100644 --- a/src/lxml/tests/fuzz_xml_parse.py +++ b/src/lxml/tests/fuzz_xml_parse.py @@ -10,14 +10,16 @@ from lxml import etree + def test_etree_xml(data): fdp = atheris.FuzzedDataProvider(data) try: - root = etree.XML(fdp.ConsumeUnicode(sys.maxsize)) + etree.XML(fdp.ConsumeUnicode(sys.maxsize)) except etree.XMLSyntaxError: pass return + if __name__ == "__main__": atheris.Setup(sys.argv, test_etree_xml, enable_python_coverage=True) atheris.Fuzz() From 37eae21e132241e67d05776447d7394c153e82f0 Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Sat, 8 May 2021 16:26:16 +0200 Subject: [PATCH 09/74] Add a "make fuzz" target to run the fuzzer test. --- Makefile | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Makefile b/Makefile index 944260752..2b5f386de 100644 --- a/Makefile +++ b/Makefile @@ -98,6 +98,15 @@ valgrind_test_inplace: inplace valgrind --tool=memcheck --leak-check=full --num-callers=30 --suppressions=valgrind-python.supp \ $(PYTHON) test.py +fuzz: clean + $(MAKE) \ + CC="/usr/bin/clang" \ + 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 + gdb_test_inplace: inplace @echo "file $(PYTHON)\nrun test.py" > .gdb.command gdb -x .gdb.command -d src -d src/lxml From 1ea55a8550ca123d9adb4ab9ebc82fa1527f0149 Mon Sep 17 00:00:00 2001 From: Bob Kline Date: Sat, 15 May 2021 15:28:44 -0400 Subject: [PATCH 10/74] Avoid text overlaps on website banner (GH-318) --- doc/html/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/html/style.css b/doc/html/style.css index 4cc454aac..b399b3d0e 100644 --- a/doc/html/style.css +++ b/doc/html/style.css @@ -164,7 +164,7 @@ div.banner { border: 2px solid darkred; color: darkgreen; line-height: 1em; - margin: 1ex; + margin: 3ex 1ex 1ex; padding: 3pt; } From 70b7ddbb516c10624bedc87f3d4af887ad55bc19 Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Thu, 13 May 2021 20:54:50 +0200 Subject: [PATCH 11/74] Switch to libxml2 2.9.11 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2b5f386de..cd2922826 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ PY3_WITH_CYTHON?=$(shell $(PYTHON3) -c 'import Cython.Build.Dependencies' >/dev/ 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) -MANYLINUX_LIBXML2_VERSION=2.9.10 +MANYLINUX_LIBXML2_VERSION=2.9.11 MANYLINUX_LIBXSLT_VERSION=1.1.34 MANYLINUX_CFLAGS=-O3 -g1 -pipe -fPIC -flto MANYLINUX_LDFLAGS=-flto From a7efa314e0dfc8738a80b60e984eed762a98803b Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Thu, 13 May 2021 22:19:20 +0200 Subject: [PATCH 12/74] Work around a bug in the configure script of libxslt. See https://gitlab.gnome.org/GNOME/libxslt/-/commit/90c34c8bb90e095a8a8fe8b2ce368bd9ff1837cc --- buildlibxml.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/buildlibxml.py b/buildlibxml.py index f45c86086..169502bd7 100644 --- a/buildlibxml.py +++ b/buildlibxml.py @@ -437,6 +437,15 @@ def has_current_lib(name, build_dir, _build_all_following=[False]): if not has_current_lib("libxml2", libxml2_dir): cmmi(libxml2_configure_cmd, libxml2_dir, multicore, **call_setup) + # Fix up libxslt configure script (needed up to and including 1.1.34) + # https://gitlab.gnome.org/GNOME/libxslt/-/commit/90c34c8bb90e095a8a8fe8b2ce368bd9ff1837cc + with open(os.path.join(libxslt_dir, "configure"), 'rb') as f: + config_script = f.read() + if b' --libs print ' in config_script: + config_script = config_script.replace(b' --libs print ', b' --libs ') + with open(os.path.join(libxslt_dir, "configure"), 'wb') as f: + f.write(config_script) + # build libxslt libxslt_configure_cmd = configure_cmd + [ '--without-python', From 6aad8dff217ad902e0bb27eacf8612474c6812fd Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Thu, 13 May 2021 22:21:13 +0200 Subject: [PATCH 13/74] Switch to libxml2 2.9.12. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index cd2922826..4cb99a009 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ PY3_WITH_CYTHON?=$(shell $(PYTHON3) -c 'import Cython.Build.Dependencies' >/dev/ 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) -MANYLINUX_LIBXML2_VERSION=2.9.11 +MANYLINUX_LIBXML2_VERSION=2.9.12 MANYLINUX_LIBXSLT_VERSION=1.1.34 MANYLINUX_CFLAGS=-O3 -g1 -pipe -fPIC -flto MANYLINUX_LDFLAGS=-flto From 0faced0a3b14e4b8b7575b1c63bb9e756ccbef1c Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Sat, 15 May 2021 22:04:11 +0200 Subject: [PATCH 14/74] Add project income report for 2020. --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index 3ad1ba177..ce0898c5c 100644 --- a/README.rst +++ b/README.rst @@ -69,6 +69,12 @@ Another supporter of the lxml project is Project income report --------------------- +* Total project income in 2020: EUR 6065,86 (506.49 € / month) + + - Tidelift: EUR 4064.77 + - Paypal: EUR 1401.09 + - other: EUR 600.00 + * Total project income in 2019: EUR 717.52 (59.79 € / month) - Tidelift: EUR 360.30 From 852ed1092bd80b6b9a51db24371047ec88843031 Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Tue, 18 May 2021 22:02:02 +0200 Subject: [PATCH 15/74] Adapt a test to a behavioural change in libxml2 2.9.11+. --- src/lxml/tests/test_etree.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lxml/tests/test_etree.py b/src/lxml/tests/test_etree.py index 9cf70604b..42613dcbe 100644 --- a/src/lxml/tests/test_etree.py +++ b/src/lxml/tests/test_etree.py @@ -3036,7 +3036,10 @@ def test_subelement_nsmap(self): def test_html_prefix_nsmap(self): etree = self.etree el = etree.HTML('aa').find('.//page-description') - self.assertEqual({'hha': None}, el.nsmap) + if etree.LIBXML_VERSION < (2, 9, 11): + self.assertEqual({'hha': None}, el.nsmap) + else: + self.assertEqual({}, el.nsmap) def test_getchildren(self): Element = self.etree.Element From 5ecb40bc6d0711aa570fed5c2788f87049513c84 Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Wed, 19 May 2021 00:14:15 +0200 Subject: [PATCH 16/74] Add Py3.9 to tox.ini. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 575d7a144..4fb8f3a32 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py35, py36, py37, py38 +envlist = py27, py35, py36, py37, py38, py39 [testenv] setenv = From 450487092251816b4252a0e8694bf50abb1d4046 Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Wed, 19 May 2021 01:04:49 +0200 Subject: [PATCH 17/74] Switch back to libxml2 2.9.10 since 2.9.11/12 are incompatible. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4cb99a009..2b5f386de 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ PY3_WITH_CYTHON?=$(shell $(PYTHON3) -c 'import Cython.Build.Dependencies' >/dev/ 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) -MANYLINUX_LIBXML2_VERSION=2.9.12 +MANYLINUX_LIBXML2_VERSION=2.9.10 MANYLINUX_LIBXSLT_VERSION=1.1.34 MANYLINUX_CFLAGS=-O3 -g1 -pipe -fPIC -flto MANYLINUX_LDFLAGS=-flto From c9cf865d2e5f4ea4952d0ea6d4e0e2e2120649b7 Mon Sep 17 00:00:00 2001 From: Isaac Jurado Date: Wed, 19 May 2021 09:50:53 +0200 Subject: [PATCH 18/74] Allow passing STATIC_* setup variables from the environment. (GH-314) For very customized static builds of lxml, the only way to succeed is by patching the setup.py file. This change makes it a little more convenient to make static builds directly from the pip command line. --- setup.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 845c0d9c0..cba548095 100644 --- a/setup.py +++ b/setup.py @@ -25,10 +25,13 @@ # override these and pass --static for a static build. See # doc/build.txt for more information. If you do not pass --static # changing this will have no effect. -STATIC_INCLUDE_DIRS = [] -STATIC_LIBRARY_DIRS = [] -STATIC_CFLAGS = [] -STATIC_BINARIES = [] +def static_env_list(name, separator=None): + return [x.strip() for x in os.environ.get(name, "").split(separator) if x.strip()] + +STATIC_INCLUDE_DIRS = static_env_list("LXML_STATIC_INCLUDE_DIRS", separator=os.pathsep) +STATIC_LIBRARY_DIRS = static_env_list("LXML_STATIC_LIBRARY_DIRS", separator=os.pathsep) +STATIC_CFLAGS = static_env_list("LXML_STATIC_CFLAGS") +STATIC_BINARIES = static_env_list("LXML_STATIC_BINARIES", separator=os.pathsep) # create lxml-version.h file versioninfo.create_version_h() From 247e55e6f23643c13ff1ebbae2d52d3fe105084a Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Wed, 19 May 2021 10:06:19 +0200 Subject: [PATCH 19/74] Remove unused image file. --- doc/html/flattr-badge-large.png | Bin 1639 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 doc/html/flattr-badge-large.png diff --git a/doc/html/flattr-badge-large.png b/doc/html/flattr-badge-large.png deleted file mode 100644 index 1105305850621343d54022dd422415ddf1f659e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1639 zcmV-t2AKJYP)t_els_}nNMyeFqp|I+&`C*Q)S_473%8C!<642`wf;WpUb9?&s{RBQi9P}iuC*<{RE8g)9KOwr* zeUgCwig=9xJ+fpsgS9-;Gw3PWLx`<&`^S!{ai!P)%~2{B1Qpr-r!^>F0@{&yy#W2( zC0DkMEYi#*^tXaBr};wMSH8i-6SXMxJ7ZVpq1U=e`e=&9A_wMMAt2ipXDb{f?~75p zA|USvdKHUJI;hZCBemfO>U%DV*6_>OE;*NwOLjO>U~RG<;Y*>r6K@cphZpYRpwC?o z!HmjW+;yAr@{t;B&ah9i_>7gfE5eLA+6b9#-JYU@g@9QuBE$EHHZ`2D4ou zLi}Pxjq=W2C~mV`5_)puuZU6amNZpmSJINXu*(kc&fJD0NfyjOMRqKoQ0WaKmJ2D~ z!No^Yw%~Z46^*%?Ug=vwRoD~*{~SwX8=#-KXd~32?TjWbP}TOPv-?6zcMARFK9=je zr^e8;yC&C3`0~(C=Q(lE-{pVFJku4!;<-m%m@3`^{fmBFe^7utoei}p^xGpXNUA@G zXqBs^8Q{*OC39H~ff#u{fDfu@O=bCos_V_*;x|*1ZN&aORW{6r45uv~Lr35K5DHMvg0mxq2T$AN?XCf%HQx+~R) z^VKM*x(D--skNdOh2A%O1qpQr5T$gLG}T2-G!7?hrWSGv0-d(o?SLRs9S_cO1(#5$MCu z228G#@a3TgXZ^_S>67v1%v0^D^YPn9LSN_oJS5j!GK=ZvP%{#0e?er`E=hkRA*bj_ zWbFurGyE=C;%M@>h$!+D#*luYEf!N{H2O-^6>6|?!o9|r8%NdBSBc;p7x-jtr#bed zUmt8XxlY2Dhki1{o&zuuY0f;=l8Pr2_XyyfLSNJEWW=ezrxyQ}aacDGz0yU>hD9YT znG49YLwkQU#*JG7&C`CNEfzEA4L*OTE*mG@YmB*ZifgfWH~kt^-2)ng7Tcq-HxGRe zTTHH#@a3TgrtRRimlvVt7>~5T=v@`$HTH;9eg|ED4jTHBXq!2%)*fQIJ#rNZH3xA{ z;UZ}&MSH@Mxu-8j&^b_pxaxgKR2~Bn&C`CN&Bp?C$-0#tiL@<1W_`w(n@eD>HNwlC zkl)Pqc6Lp!_CmfU%;Y)=UmkiuimeEA3k=^;AxdS3(-m7qt>}&f5qE6RH>5*+wI?FV zoh5Df(&Tc5ni!#7P#=Xuv0p>kk-_57OFL2B&9*$2Kap$0#q-d8CF`augW#REjg2D` z`i!x0erHM?P}QCe?<6aP7wiYs1+70yb#ZjdF}Y5{mxu0`WW)VM`tX(tVU@NxS++&g zm#>{;XEnU^=zY)xxpF_kFFQ+`0oI*alp7oW6XUNRv$pnTf$`Hb+{!-Q*&2VuR_$eD zsMI}aTuhZ*dt|A)^tFW7q~nN$J|Q;FkLK7Hvq5xTtiNZlUYM(t311$%Z-Og{mN~7OnDFJH{}FHfoE~`1&dp#s)b~(_xGGPaEp=M)4l8zqPrU7G?ij#FZg>88 zh8-w)A_9Nfu-)kY-GSH8Rmdwn5mw@~qJ6r^5tM77+`XAQ@CuJTxz<qoN$>h<83Fu9Wj&==)>+1|1j>LP2Jy!k6-ecgoiTfu8Yp?`+C)OMG lo{g*iWN$F=Tq6KC_%D-CKj~#=%!>d3002ovPDHLkV1i<{B$EID From ee05daf1094997b62ed34092abd8607a8efb2485 Mon Sep 17 00:00:00 2001 From: Wen Bo Li <50884368+wenovus@users.noreply.github.com> Date: Wed, 19 May 2021 01:33:47 -0700 Subject: [PATCH 20/74] Removed unused Zope Public License from docs folder (GH-312) --- doc/licenses/ZopePublicLicense.txt | 59 ------------------------------ 1 file changed, 59 deletions(-) delete mode 100644 doc/licenses/ZopePublicLicense.txt diff --git a/doc/licenses/ZopePublicLicense.txt b/doc/licenses/ZopePublicLicense.txt deleted file mode 100644 index 44e0648b3..000000000 --- a/doc/licenses/ZopePublicLicense.txt +++ /dev/null @@ -1,59 +0,0 @@ -Zope Public License (ZPL) Version 2.0 ------------------------------------------------ - -This software is Copyright (c) Zope Corporation (tm) and -Contributors. All rights reserved. - -This license has been certified as open source. It has also -been designated as GPL compatible by the Free Software -Foundation (FSF). - -Redistribution and use in source and binary forms, with or -without modification, are permitted provided that the -following conditions are met: - -1. Redistributions in 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 distribution. - -3. The name Zope Corporation (tm) must not be used to - endorse or promote products derived from this software - without prior written permission from Zope Corporation. - -4. The right to distribute this software or to use it for - any purpose does not give you the right to use Servicemarks - (sm) or Trademarks (tm) of Zope Corporation. Use of them is - covered in a separate agreement (see - http://www.zope.com/Marks). - -5. If any files are modified, you must cause the modified - files to carry prominent notices stating that you changed - the files and the date of any change. - -Disclaimer - - THIS SOFTWARE IS PROVIDED BY ZOPE CORPORATION ``AS IS'' - AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT - NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY - AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN - NO EVENT SHALL ZOPE CORPORATION OR ITS CONTRIBUTORS BE - LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE - OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH - DAMAGE. - - -This software consists of contributions made by Zope -Corporation and many individuals on behalf of Zope -Corporation. Specific attributions are listed in the -accompanying credits file. From 6321f9de9b3cdca136bce63ea40816e077b9005f Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Wed, 19 May 2021 15:04:14 +0200 Subject: [PATCH 21/74] Avoid direct C-API call. --- src/lxml/serializer.pxi | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lxml/serializer.pxi b/src/lxml/serializer.pxi index d66f59a7e..e5cd36748 100644 --- a/src/lxml/serializer.pxi +++ b/src/lxml/serializer.pxi @@ -68,8 +68,7 @@ cdef _textToString(xmlNode* c_node, encoding, bint with_tail): needs_conversion = 1 if needs_conversion: - text = python.PyUnicode_DecodeUTF8( - c_text, tree.xmlBufferLength(c_buffer), 'strict') + text = (c_text)[:tree.xmlBufferLength(c_buffer)].decode('utf8') if encoding is not unicode: encoding = _utf8(encoding) text = python.PyUnicode_AsEncodedString( From 65e8dd679f5fe21d860bb0e4a43743c63125a814 Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Tue, 29 Jun 2021 15:09:06 +0200 Subject: [PATCH 22/74] Allow building the HTML docs without the donation section/button. Debian doesn't like non-free content. --- doc/mkhtml.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/doc/mkhtml.py b/doc/mkhtml.py index c65233563..36da5de99 100644 --- a/doc/mkhtml.py +++ b/doc/mkhtml.py @@ -194,7 +194,7 @@ def insert_link(match): out_file.close() -def publish(dirname, lxml_path, release): +def publish(dirname, lxml_path, release, with_donations=True): if not os.path.exists(dirname): os.mkdir(dirname) @@ -245,7 +245,8 @@ def publish(dirname, lxml_path, release): menu = Element("div", {'class': 'sidemenu', 'id': 'sidemenu'}) SubElement(menu, 'div', {'class': 'menutrigger', 'onclick': 'trigger_menu(event)'}).text = "Menu" menu_div = SubElement(menu, 'div', {'class': 'menu'}) - inject_banner(menu_div) + if with_donations: + inject_banner(menu_div) # build HTML pages and parse them back for section, text_files in SITE_STRUCTURE: @@ -266,13 +267,14 @@ def publish(dirname, lxml_path, release): rest2html(script, path, outpath, stylesheet_url) tree = parse(outpath) - page_div = tree.getroot()[1][0] # html->body->div[class=document] - inject_banner(page_div) + if with_donations: + page_div = tree.getroot()[1][0] # html->body->div[class=document] + inject_banner(page_div) - if filename == 'main.txt': - # inject donation buttons - #inject_flatter_button(tree) - inject_donate_buttons(lxml_path, script, tree) + if filename == 'main.txt': + # inject donation buttons + #inject_flatter_button(tree) + inject_donate_buttons(lxml_path, script, tree) trees[filename] = (tree, basename, outpath) build_menu(tree, basename, section_head) @@ -324,4 +326,7 @@ def publish(dirname, lxml_path, release): if __name__ == '__main__': - publish(sys.argv[1], sys.argv[2], sys.argv[3]) + no_donations = '--no-donations' in sys.argv[1:] + if no_donations: + sys.argv.remove('--no-donations') + publish(sys.argv[1], sys.argv[2], sys.argv[3], with_donations=not no_donations) From 9e8f18f051c7b3c3165366308f2eb86b18034116 Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Sun, 4 Jul 2021 22:14:29 +0200 Subject: [PATCH 23/74] Make the note about the (faster) .find*() methods in the XPath section stick out to suggest their use. --- doc/html/style.css | 12 ++++++++++++ doc/xpathxslt.txt | 11 ++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/doc/html/style.css b/doc/html/style.css index b399b3d0e..7d1b0e675 100644 --- a/doc/html/style.css +++ b/doc/html/style.css @@ -321,6 +321,18 @@ html > .pagequote { position: fixed; } +div.admonition { + border: solid 1px; + border-radius: 1ex; + margin: 0.5ex; + padding: 0.5ex 1.5ex 0.5ex 1.5ex; + background: lightyellow; +} + +div.admonition > .admonition-title { + background: yellow; +} + code { color: Black; background-color: #f0f0f0; diff --git a/doc/xpathxslt.txt b/doc/xpathxslt.txt index 8b2870e51..9eb9bcf79 100644 --- a/doc/xpathxslt.txt +++ b/doc/xpathxslt.txt @@ -63,9 +63,14 @@ comparison`_ to learn when to use which. Their semantics when used on Elements and ElementTrees are the same as for the ``xpath()`` method described here. -Note that the ``.find*()`` methods are usually faster than the full-blown XPath -support. They also support incremental tree processing through the ``.iterfind()`` -method, whereas XPath always collects all results before returning them. +.. note:: + + The ``.find*()`` methods are usually *faster* than the full-blown XPath + support. They also support incremental tree processing through the + ``.iterfind()`` method, whereas XPath always collects all results before + returning them. They are therefore recommended over XPath for both speed + and memory reasons, whenever there is no need for highly selective XPath + queries. .. _`performance comparison`: performance.html#xpath From 885765dc99124199e686b9fabd162872624dfbf0 Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Sun, 4 Jul 2021 22:44:07 +0200 Subject: [PATCH 24/74] Revive benchmarks. --- benchmark/bench_etree.py | 3 ++- benchmark/benchbase.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/benchmark/bench_etree.py b/benchmark/bench_etree.py index 0f66db8e9..69ac5208e 100644 --- a/benchmark/bench_etree.py +++ b/benchmark/bench_etree.py @@ -1,9 +1,10 @@ import copy +from io import BytesIO from itertools import * import benchbase from benchbase import (with_attributes, with_text, onlylib, - serialized, children, nochange, BytesIO) + serialized, children, nochange) TEXT = "some ASCII text" UTEXT = u"some klingon: \F8D2" diff --git a/benchmark/benchbase.py b/benchmark/benchbase.py index e34e61036..48aee2128 100644 --- a/benchmark/benchbase.py +++ b/benchmark/benchbase.py @@ -1,4 +1,4 @@ -import sys, re, string, time, copy, gc +import sys, re, string, copy, gc from itertools import * import time @@ -474,6 +474,7 @@ def main(benchmark_class): if import_lxml: from lxml import etree _etrees.append(etree) + print("Using lxml %s" % etree.__version__) try: sys.argv.remove('-fel') @@ -521,6 +522,8 @@ def main(benchmark_class): print("No library to test. Exiting.") sys.exit(1) + print("Running benchmarks in Python %s" % (sys.version_info,)) + print("Preparing test suites and trees ...") selected = set( sys.argv[1:] ) benchmark_suites, benchmarks = \ From 32d52bee3ea4117b0fcb4dab994b707c7aba9d3a Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Sun, 4 Jul 2021 23:38:10 +0200 Subject: [PATCH 25/74] Update benchmark results in doc/performance.txt to lxml 4.6.3. --- doc/performance.txt | 297 +++++++++++++++++++++----------------------- 1 file changed, 145 insertions(+), 152 deletions(-) diff --git a/doc/performance.txt b/doc/performance.txt index 1a0c9ad6b..6e01812ba 100644 --- a/doc/performance.txt +++ b/doc/performance.txt @@ -88,18 +88,11 @@ very easy to add as tiny test methods, so if you write a performance test for a specific part of the API yourself, please consider sending it to the lxml mailing list. -The timings presented below compare lxml 3.1.1 (with libxml2 2.9.0) to the +The timings presented below compare lxml 4.6.3 (with libxml2 2.9.10) to the latest released versions of ElementTree (with cElementTree as accelerator -module) in the standard library of CPython 3.3.0. They were run -single-threaded on a 2.9GHz 64bit double core Intel i7 machine under -Ubuntu Linux 12.10 (Quantal). The C libraries were compiled with the -same platform specific optimisation flags. The Python interpreter was -also manually compiled for the platform. Note that many of the following -ElementTree timings are therefore better than what a normal Python -installation with the standard library (c)ElementTree modules would yield. -Note also that CPython 2.7 and 3.2+ come with a newer ElementTree version, -so older Python installations will not perform as good for (c)ElementTree, -and sometimes substantially worse. +module) in the standard library of CPython 3.8.10. They were run +single-threaded on a 2.3GHz 64bit double core Intel i5 machine under +Ubuntu Linux 20.04 (Focal). .. _`bench_etree.py`: https://github.com/lxml/lxml/blob/master/benchmark/bench_etree.py .. _`bench_xpath.py`: https://github.com/lxml/lxml/blob/master/benchmark/bench_xpath.py @@ -141,50 +134,50 @@ is native to libxml2. While 20 to 40 times faster than (c)ElementTree lxml is still more than 10 times as fast as the much improved ElementTree 1.3 in recent Python versions:: - lxe: tostring_utf16 (S-TR T1) 7.9958 msec/pass - cET: tostring_utf16 (S-TR T1) 83.1358 msec/pass + lxe: tostring_utf16 (S-TR T1) 5.8763 msec/pass + cET: tostring_utf16 (S-TR T1) 38.0461 msec/pass - lxe: tostring_utf16 (UATR T1) 8.3222 msec/pass - cET: tostring_utf16 (UATR T1) 84.4688 msec/pass + lxe: tostring_utf16 (UATR T1) 6.0940 msec/pass + cET: tostring_utf16 (UATR T1) 37.8058 msec/pass - lxe: tostring_utf16 (S-TR T2) 8.2297 msec/pass - cET: tostring_utf16 (S-TR T2) 87.3415 msec/pass + lxe: tostring_utf16 (S-TR T2) 6.1204 msec/pass + cET: tostring_utf16 (S-TR T2) 40.0257 msec/pass - lxe: tostring_utf8 (S-TR T2) 6.5677 msec/pass - cET: tostring_utf8 (S-TR T2) 76.2064 msec/pass + lxe: tostring_utf8 (S-TR T2) 4.7486 msec/pass + cET: tostring_utf8 (S-TR T2) 30.3330 msec/pass - lxe: tostring_utf8 (U-TR T3) 1.1952 msec/pass - cET: tostring_utf8 (U-TR T3) 22.0058 msec/pass + lxe: tostring_utf8 (U-TR T3) 1.2028 msec/pass + cET: tostring_utf8 (U-TR T3) 8.9505 msec/pass The difference is somewhat smaller for plain text serialisation:: - lxe: tostring_text_ascii (S-TR T1) 2.7738 msec/pass - cET: tostring_text_ascii (S-TR T1) 4.7629 msec/pass + lxe: tostring_text_ascii (S-TR T1) 2.4126 msec/pass + cET: tostring_text_ascii (S-TR T1) 3.1371 msec/pass - lxe: tostring_text_ascii (S-TR T3) 0.8273 msec/pass - cET: tostring_text_ascii (S-TR T3) 1.5273 msec/pass + lxe: tostring_text_ascii (S-TR T3) 0.8945 msec/pass + cET: tostring_text_ascii (S-TR T3) 1.2043 msec/pass - lxe: tostring_text_utf16 (S-TR T1) 2.7659 msec/pass - cET: tostring_text_utf16 (S-TR T1) 10.5038 msec/pass + lxe: tostring_text_utf16 (S-TR T1) 2.5816 msec/pass + cET: tostring_text_utf16 (S-TR T1) 7.3011 msec/pass - lxe: tostring_text_utf16 (U-TR T1) 2.8017 msec/pass - cET: tostring_text_utf16 (U-TR T1) 10.5207 msec/pass + lxe: tostring_text_utf16 (U-TR T1) 2.7902 msec/pass + cET: tostring_text_utf16 (U-TR T1) 7.4139 msec/pass The ``tostring()`` function also supports serialisation to a Python unicode string object, which is currently faster in ElementTree -under CPython 3.3:: +under CPython 3.8:: - lxe: tostring_text_unicode (S-TR T1) 2.6896 msec/pass - cET: tostring_text_unicode (S-TR T1) 1.0056 msec/pass + lxe: tostring_text_unicode (S-TR T1) 2.5883 msec/pass + cET: tostring_text_unicode (S-TR T1) 1.1873 msec/pass - lxe: tostring_text_unicode (U-TR T1) 2.7366 msec/pass - cET: tostring_text_unicode (U-TR T1) 1.0154 msec/pass + lxe: tostring_text_unicode (U-TR T1) 2.8777 msec/pass + cET: tostring_text_unicode (U-TR T1) 1.1592 msec/pass - lxe: tostring_text_unicode (S-TR T3) 0.7997 msec/pass - cET: tostring_text_unicode (S-TR T3) 0.3154 msec/pass + lxe: tostring_text_unicode (S-TR T3) 0.6495 msec/pass + cET: tostring_text_unicode (S-TR T3) 0.4494 msec/pass - lxe: tostring_text_unicode (U-TR T4) 0.0048 msec/pass - cET: tostring_text_unicode (U-TR T4) 0.0160 msec/pass + lxe: tostring_text_unicode (U-TR T4) 0.0050 msec/pass + cET: tostring_text_unicode (U-TR T4) 0.0131 msec/pass For parsing, lxml.etree and cElementTree compete for the medal. Depending on the input, either of the two can be faster. The (c)ET @@ -192,14 +185,14 @@ libraries use a very thin layer on top of the expat parser, which is known to be very fast. Here are some timings from the benchmarking suite:: - lxe: parse_bytesIO (SAXR T1) 13.0246 msec/pass - cET: parse_bytesIO (SAXR T1) 8.2929 msec/pass + lxe: parse_bytesIO (SAXR T1) 15.2328 msec/pass + cET: parse_bytesIO (SAXR T1) 7.5498 msec/pass - lxe: parse_bytesIO (S-XR T3) 1.3542 msec/pass - cET: parse_bytesIO (S-XR T3) 2.4023 msec/pass + lxe: parse_bytesIO (S-XR T3) 1.5039 msec/pass + cET: parse_bytesIO (S-XR T3) 2.1725 msec/pass - lxe: parse_bytesIO (UAXR T3) 7.5610 msec/pass - cET: parse_bytesIO (UAXR T3) 11.2455 msec/pass + lxe: parse_bytesIO (UAXR T3) 8.7409 msec/pass + cET: parse_bytesIO (UAXR T3) 12.4905 msec/pass And another couple of timings `from a benchmark`_ that Fredrik Lundh `used to promote cElementTree`_, comparing a number of different @@ -277,26 +270,26 @@ rather close to each other, usually within a factor of two, with winners well distributed over both sides. Similar timings can be observed for the ``iterparse()`` function:: - lxe: iterparse_bytesIO (SAXR T1) 17.9198 msec/pass - cET: iterparse_bytesIO (SAXR T1) 14.4982 msec/pass + lxe: iterparse_bytesIO (SAXR T1) 20.9262 msec/pass + cET: iterparse_bytesIO (SAXR T1) 10.3736 msec/pass - lxe: iterparse_bytesIO (UAXR T3) 8.8522 msec/pass - cET: iterparse_bytesIO (UAXR T3) 12.9857 msec/pass + lxe: iterparse_bytesIO (UAXR T3) 11.0531 msec/pass + cET: iterparse_bytesIO (UAXR T3) 13.2461 msec/pass However, if you benchmark the complete round-trip of a serialise-parse cycle, the numbers will look similar to these:: - lxe: write_utf8_parse_bytesIO (S-TR T1) 19.8867 msec/pass - cET: write_utf8_parse_bytesIO (S-TR T1) 80.7259 msec/pass + lxe: write_utf8_parse_bytesIO (S-TR T1) 19.3429 msec/pass + cET: write_utf8_parse_bytesIO (S-TR T1) 35.5511 msec/pass - lxe: write_utf8_parse_bytesIO (UATR T2) 23.7896 msec/pass - cET: write_utf8_parse_bytesIO (UATR T2) 98.0766 msec/pass + lxe: write_utf8_parse_bytesIO (UATR T2) 22.8314 msec/pass + cET: write_utf8_parse_bytesIO (UATR T2) 42.3915 msec/pass - lxe: write_utf8_parse_bytesIO (S-TR T3) 3.0684 msec/pass - cET: write_utf8_parse_bytesIO (S-TR T3) 24.6122 msec/pass + lxe: write_utf8_parse_bytesIO (S-TR T3) 3.4230 msec/pass + cET: write_utf8_parse_bytesIO (S-TR T3) 11.1156 msec/pass - lxe: write_utf8_parse_bytesIO (SATR T4) 0.3495 msec/pass - cET: write_utf8_parse_bytesIO (SATR T4) 1.9610 msec/pass + lxe: write_utf8_parse_bytesIO (SATR T4) 0.4215 msec/pass + cET: write_utf8_parse_bytesIO (SATR T4) 0.9992 msec/pass For applications that require a high parser throughput of large files, and that do little to no serialization, both cET and lxml.etree are a @@ -379,30 +372,30 @@ The same tree overhead makes operations like collecting children as in a shallow copy of their list of children, lxml has to create a Python object for each child and collect them in a list:: - lxe: root_list_children (--TR T1) 0.0038 msec/pass - cET: root_list_children (--TR T1) 0.0010 msec/pass + lxe: root_list_children (--TR T1) 0.0033 msec/pass + cET: root_list_children (--TR T1) 0.0007 msec/pass - lxe: root_list_children (--TR T2) 0.0455 msec/pass - cET: root_list_children (--TR T2) 0.0050 msec/pass + lxe: root_list_children (--TR T2) 0.0596 msec/pass + cET: root_list_children (--TR T2) 0.0055 msec/pass This handicap is also visible when accessing single children:: - lxe: first_child (--TR T2) 0.0424 msec/pass - cET: first_child (--TR T2) 0.0384 msec/pass + lxe: first_child (--TR T2) 0.0615 msec/pass + cET: first_child (--TR T2) 0.0548 msec/pass - lxe: last_child (--TR T1) 0.0477 msec/pass - cET: last_child (--TR T1) 0.0467 msec/pass + lxe: last_child (--TR T1) 0.0603 msec/pass + cET: last_child (--TR T1) 0.0563 msec/pass ... unless you also add the time to find a child index in a bigger list. ET and cET use Python lists here, which are based on arrays. The data structure used by libxml2 is a linked tree, and thus, a linked list of children:: - lxe: middle_child (--TR T1) 0.0710 msec/pass - cET: middle_child (--TR T1) 0.0420 msec/pass + lxe: middle_child (--TR T1) 0.0918 msec/pass + cET: middle_child (--TR T1) 0.0513 msec/pass - lxe: middle_child (--TR T2) 1.7393 msec/pass - cET: middle_child (--TR T2) 0.0396 msec/pass + lxe: middle_child (--TR T2) 2.3277 msec/pass + cET: middle_child (--TR T2) 0.0484 msec/pass Element creation @@ -412,18 +405,18 @@ As opposed to ET, libxml2 has a notion of documents that each element must be in. This results in a major performance difference for creating independent Elements that end up in independently created documents:: - lxe: create_elements (--TC T2) 1.0045 msec/pass - cET: create_elements (--TC T2) 0.0753 msec/pass + lxe: create_elements (--TC T2) 0.8178 msec/pass + cET: create_elements (--TC T2) 0.0668 msec/pass Therefore, it is always preferable to create Elements for the document they are supposed to end up in, either as SubElements of an Element or using the explicit ``Element.makeelement()`` call:: - lxe: makeelement (--TC T2) 1.0586 msec/pass - cET: makeelement (--TC T2) 0.1483 msec/pass + lxe: makeelement (--TC T2) 0.8020 msec/pass + cET: makeelement (--TC T2) 0.0618 msec/pass - lxe: create_subelements (--TC T2) 0.8826 msec/pass - cET: create_subelements (--TC T2) 0.0827 msec/pass + lxe: create_subelements (--TC T2) 0.7782 msec/pass + cET: create_subelements (--TC T2) 0.0865 msec/pass So, if the main performance bottleneck of an application is creating large XML trees in memory through calls to Element and SubElement, cET is the best @@ -440,11 +433,11 @@ requires lxml to do recursive adaptations throughout the moved tree structure. The following benchmark appends all root children of the second tree to the root of the first tree:: - lxe: append_from_document (--TR T1,T2) 1.0812 msec/pass - cET: append_from_document (--TR T1,T2) 0.1104 msec/pass + lxe: append_from_document (--TR T1,T2) 1.3409 msec/pass + cET: append_from_document (--TR T1,T2) 0.0539 msec/pass - lxe: append_from_document (--TR T3,T4) 0.0155 msec/pass - cET: append_from_document (--TR T3,T4) 0.0060 msec/pass + lxe: append_from_document (--TR T3,T4) 0.0203 msec/pass + cET: append_from_document (--TR T3,T4) 0.0031 msec/pass Although these are fairly small numbers compared to parsing, this easily shows the different performance classes for lxml and (c)ET. Where the latter do not @@ -455,19 +448,19 @@ with the size of the tree that is moved. This difference is not always as visible, but applies to most parts of the API, like inserting newly created elements:: - lxe: insert_from_document (--TR T1,T2) 3.9763 msec/pass - cET: insert_from_document (--TR T1,T2) 0.1459 msec/pass + lxe: insert_from_document (--TR T1,T2) 4.9999 msec/pass + cET: insert_from_document (--TR T1,T2) 0.0696 msec/pass or replacing the child slice by a newly created element:: - lxe: replace_children_element (--TC T1) 0.0749 msec/pass - cET: replace_children_element (--TC T1) 0.0081 msec/pass + lxe: replace_children_element (--TC T1) 0.0653 msec/pass + cET: replace_children_element (--TC T1) 0.0098 msec/pass as opposed to replacing the slice with an existing element from the same document:: - lxe: replace_children (--TC T1) 0.0052 msec/pass - cET: replace_children (--TC T1) 0.0036 msec/pass + lxe: replace_children (--TC T1) 0.0069 msec/pass + cET: replace_children (--TC T1) 0.0043 msec/pass While these numbers are too small to provide a major performance impact in practice, you should keep this difference in mind when you @@ -481,14 +474,14 @@ deepcopy Deep copying a tree is fast in lxml:: - lxe: deepcopy_all (--TR T1) 3.1650 msec/pass - cET: deepcopy_all (--TR T1) 53.9973 msec/pass + lxe: deepcopy_all (--TR T1) 4.0150 msec/pass + cET: deepcopy_all (--TR T1) 2.4621 msec/pass - lxe: deepcopy_all (-ATR T2) 3.7365 msec/pass - cET: deepcopy_all (-ATR T2) 61.6267 msec/pass + lxe: deepcopy_all (-ATR T2) 4.7412 msec/pass + cET: deepcopy_all (-ATR T2) 2.8064 msec/pass - lxe: deepcopy_all (S-TR T3) 0.7913 msec/pass - cET: deepcopy_all (S-TR T3) 13.6220 msec/pass + lxe: deepcopy_all (S-TR T3) 1.1363 msec/pass + cET: deepcopy_all (S-TR T3) 0.5484 msec/pass So, for example, if you have a database-like scenario where you parse in a large tree and then search and copy independent subtrees from it for further @@ -504,31 +497,31 @@ traversal of the XML tree and especially if few elements are of interest or the target element tag name is known, the ``.iter()`` method is a good choice:: - lxe: iter_all (--TR T1) 1.0529 msec/pass - cET: iter_all (--TR T1) 0.2635 msec/pass + lxe: iter_all (--TR T1) 1.3881 msec/pass + cET: iter_all (--TR T1) 0.2708 msec/pass - lxe: iter_islice (--TR T2) 0.0110 msec/pass - cET: iter_islice (--TR T2) 0.0050 msec/pass + lxe: iter_islice (--TR T2) 0.0124 msec/pass + cET: iter_islice (--TR T2) 0.0036 msec/pass - lxe: iter_tag (--TR T2) 0.0079 msec/pass - cET: iter_tag (--TR T2) 0.0112 msec/pass + lxe: iter_tag (--TR T2) 0.0105 msec/pass + cET: iter_tag (--TR T2) 0.0083 msec/pass - lxe: iter_tag_all (--TR T2) 0.1822 msec/pass - cET: iter_tag_all (--TR T2) 0.5343 msec/pass + lxe: iter_tag_all (--TR T2) 0.7262 msec/pass + cET: iter_tag_all (--TR T2) 0.4537 msec/pass This translates directly into similar timings for ``Element.findall()``:: - lxe: findall (--TR T2) 1.7176 msec/pass - cET: findall (--TR T2) 0.9973 msec/pass + lxe: findall (--TR T2) 4.0147 msec/pass + cET: findall (--TR T2) 0.9193 msec/pass - lxe: findall (--TR T3) 0.3967 msec/pass - cET: findall (--TR T3) 0.2525 msec/pass + lxe: findall (--TR T3) 0.4113 msec/pass + cET: findall (--TR T3) 0.2377 msec/pass - lxe: findall_tag (--TR T2) 0.2258 msec/pass - cET: findall_tag (--TR T2) 0.5770 msec/pass + lxe: findall_tag (--TR T2) 0.7253 msec/pass + cET: findall_tag (--TR T2) 0.4904 msec/pass - lxe: findall_tag (--TR T3) 0.1085 msec/pass - cET: findall_tag (--TR T3) 0.1919 msec/pass + lxe: findall_tag (--TR T3) 0.1092 msec/pass + cET: findall_tag (--TR T3) 0.1757 msec/pass Note that all three libraries currently use the same Python implementation for ``.findall()``, except for their native tree @@ -548,38 +541,38 @@ provides more than one way of accessing it and you should take care which part of the lxml API you use. The most straight forward way is to call the ``xpath()`` method on an Element or ElementTree:: - lxe: xpath_method (--TC T1) 0.3982 msec/pass - lxe: xpath_method (--TC T2) 7.8895 msec/pass - lxe: xpath_method (--TC T3) 0.0477 msec/pass - lxe: xpath_method (--TC T4) 0.3982 msec/pass + lxe: xpath_method (--TC T1) 0.2763 msec/pass + lxe: xpath_method (--TC T2) 5.3439 msec/pass + lxe: xpath_method (--TC T3) 0.0315 msec/pass + lxe: xpath_method (--TC T4) 0.2587 msec/pass This is well suited for testing and when the XPath expressions are as diverse as the trees they are called on. However, if you have a single XPath expression that you want to apply to a larger number of different elements, the ``XPath`` class is the most efficient way to do it:: - lxe: xpath_class (--TC T1) 0.0713 msec/pass - lxe: xpath_class (--TC T2) 1.1325 msec/pass - lxe: xpath_class (--TC T3) 0.0215 msec/pass - lxe: xpath_class (--TC T4) 0.0722 msec/pass + lxe: xpath_class (--TC T1) 0.0610 msec/pass + lxe: xpath_class (--TC T2) 0.6981 msec/pass + lxe: xpath_class (--TC T3) 0.0141 msec/pass + lxe: xpath_class (--TC T4) 0.0432 msec/pass Note that this still allows you to use variables in the expression, so you can parse it once and then adapt it through variables at call time. In other cases, where you have a fixed Element or ElementTree and want to run different expressions on it, you should consider the ``XPathEvaluator``:: - lxe: xpath_element (--TR T1) 0.1101 msec/pass - lxe: xpath_element (--TR T2) 2.0473 msec/pass - lxe: xpath_element (--TR T3) 0.0267 msec/pass - lxe: xpath_element (--TR T4) 0.1087 msec/pass + lxe: xpath_element (--TR T1) 0.0598 msec/pass + lxe: xpath_element (--TR T2) 0.9737 msec/pass + lxe: xpath_element (--TR T3) 0.0167 msec/pass + lxe: xpath_element (--TR T4) 0.0606 msec/pass While it looks slightly slower, creating an XPath object for each of the expressions generates a much higher overhead here:: - lxe: xpath_class_repeat (--TC T1 ) 0.3884 msec/pass - lxe: xpath_class_repeat (--TC T2 ) 7.6182 msec/pass - lxe: xpath_class_repeat (--TC T3 ) 0.0465 msec/pass - lxe: xpath_class_repeat (--TC T4 ) 0.3877 msec/pass + lxe: xpath_class_repeat (--TC T1 ) 0.2658 msec/pass + lxe: xpath_class_repeat (--TC T2 ) 5.0316 msec/pass + lxe: xpath_class_repeat (--TC T3 ) 0.0319 msec/pass + lxe: xpath_class_repeat (--TC T4 ) 0.2749 msec/pass Note that tree iteration can be substantially faster than XPath if your code short-circuits after the first couple of elements were @@ -589,25 +582,25 @@ regardless of how much of it will actually be used. Here is an example where only the first matching element is being searched, a case for which XPath has syntax support as well:: - lxe: find_single (--TR T2) 0.0184 msec/pass - cET: find_single (--TR T2) 0.0052 msec/pass + lxe: find_single (--TR T2) 0.0045 msec/pass + cET: find_single (--TR T2) 0.0029 msec/pass - lxe: iter_single (--TR T2) 0.0024 msec/pass - cET: iter_single (--TR T2) 0.0007 msec/pass + lxe: iter_single (--TR T2) 0.0019 msec/pass + cET: iter_single (--TR T2) 0.0005 msec/pass - lxe: xpath_single (--TR T2) 0.0033 msec/pass + lxe: xpath_single (--TR T2) 0.0844 msec/pass When looking for the first two elements out of many, the numbers explode for XPath, as restricting the result subset requires a more complex expression:: - lxe: iterfind_two (--TR T2) 0.0184 msec/pass - cET: iterfind_two (--TR T2) 0.0062 msec/pass + lxe: iterfind_two (--TR T2) 0.0050 msec/pass + cET: iterfind_two (--TR T2) 0.0031 msec/pass lxe: iter_two (--TR T2) 0.0029 msec/pass - cET: iter_two (--TR T2) 0.0017 msec/pass + cET: iter_two (--TR T2) 0.0012 msec/pass - lxe: xpath_two (--TR T2) 0.2768 msec/pass + lxe: xpath_two (--TR T2) 0.0706 msec/pass A longer example @@ -774,21 +767,21 @@ ObjectPath can be used to speed up the access to elements that are deep in the tree. It avoids step-by-step Python element instantiations along the path, which can substantially improve the access time:: - lxe: attribute (--TR T1) 4.1828 msec/pass - lxe: attribute (--TR T2) 17.3802 msec/pass - lxe: attribute (--TR T4) 3.8657 msec/pass + lxe: attribute (--TR T1) 2.6822 msec/pass + lxe: attribute (--TR T2) 16.4094 msec/pass + lxe: attribute (--TR T4) 2.4951 msec/pass - lxe: objectpath (--TR T1) 0.9289 msec/pass - lxe: objectpath (--TR T2) 13.3109 msec/pass - lxe: objectpath (--TR T4) 0.9289 msec/pass + lxe: objectpath (--TR T1) 1.1985 msec/pass + lxe: objectpath (--TR T2) 14.7083 msec/pass + lxe: objectpath (--TR T4) 1.2503 msec/pass - lxe: attributes_deep (--TR T1) 6.2900 msec/pass - lxe: attributes_deep (--TR T2) 20.4713 msec/pass - lxe: attributes_deep (--TR T4) 6.1679 msec/pass + lxe: attributes_deep (--TR T1) 3.9361 msec/pass + lxe: attributes_deep (--TR T2) 17.9017 msec/pass + lxe: attributes_deep (--TR T4) 3.7947 msec/pass - lxe: objectpath_deep (--TR T1) 1.3049 msec/pass - lxe: objectpath_deep (--TR T2) 14.0815 msec/pass - lxe: objectpath_deep (--TR T4) 1.3051 msec/pass + lxe: objectpath_deep (--TR T1) 1.6170 msec/pass + lxe: objectpath_deep (--TR T2) 15.3167 msec/pass + lxe: objectpath_deep (--TR T4) 1.5836 msec/pass Note, however, that parsing ObjectPath expressions is not for free either, so this is most effective for frequently accessing the same element. @@ -818,17 +811,17 @@ expressions to be more selective. By choosing the right trees (or even subtrees and elements) to cache, you can trade memory usage against access speed:: - lxe: attribute_cached (--TR T1) 3.1357 msec/pass - lxe: attribute_cached (--TR T2) 15.8911 msec/pass - lxe: attribute_cached (--TR T4) 2.9194 msec/pass + lxe: attribute_cached (--TR T1) 1.9312 msec/pass + lxe: attribute_cached (--TR T2) 15.1188 msec/pass + lxe: attribute_cached (--TR T4) 1.9250 msec/pass - lxe: attributes_deep_cached (--TR T1) 3.8984 msec/pass - lxe: attributes_deep_cached (--TR T2) 16.8300 msec/pass - lxe: attributes_deep_cached (--TR T4) 3.6936 msec/pass + lxe: attributes_deep_cached (--TR T1) 2.6906 msec/pass + lxe: attributes_deep_cached (--TR T2) 16.4149 msec/pass + lxe: attributes_deep_cached (--TR T4) 2.5618 msec/pass - lxe: objectpath_deep_cached (--TR T1) 0.7496 msec/pass - lxe: objectpath_deep_cached (--TR T2) 12.3763 msec/pass - lxe: objectpath_deep_cached (--TR T4) 0.7427 msec/pass + lxe: objectpath_deep_cached (--TR T1) 1.0054 msec/pass + lxe: objectpath_deep_cached (--TR T2) 14.3306 msec/pass + lxe: objectpath_deep_cached (--TR T4) 0.8924 msec/pass Things to note: you cannot currently use ``weakref.WeakKeyDictionary`` objects for this as lxml's element objects do not support weak references (which are From 1f4cbdf7f833ee79158c9536bdf44c572b356f84 Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Mon, 5 Jul 2021 00:04:12 +0200 Subject: [PATCH 26/74] Update benchmark results in doc/performance.txt to lxml 4.6.3, with a static LTO build (since that is what the Linux wheels are using). --- doc/performance.txt | 290 ++++++++++++++++++++++---------------------- 1 file changed, 145 insertions(+), 145 deletions(-) diff --git a/doc/performance.txt b/doc/performance.txt index 6e01812ba..6518c6e47 100644 --- a/doc/performance.txt +++ b/doc/performance.txt @@ -134,50 +134,50 @@ is native to libxml2. While 20 to 40 times faster than (c)ElementTree lxml is still more than 10 times as fast as the much improved ElementTree 1.3 in recent Python versions:: - lxe: tostring_utf16 (S-TR T1) 5.8763 msec/pass - cET: tostring_utf16 (S-TR T1) 38.0461 msec/pass + lxe: tostring_utf16 (S-TR T1) 5.9340 msec/pass + cET: tostring_utf16 (S-TR T1) 38.3270 msec/pass - lxe: tostring_utf16 (UATR T1) 6.0940 msec/pass - cET: tostring_utf16 (UATR T1) 37.8058 msec/pass + lxe: tostring_utf16 (UATR T1) 6.2032 msec/pass + cET: tostring_utf16 (UATR T1) 37.7944 msec/pass - lxe: tostring_utf16 (S-TR T2) 6.1204 msec/pass - cET: tostring_utf16 (S-TR T2) 40.0257 msec/pass + lxe: tostring_utf16 (S-TR T2) 6.1841 msec/pass + cET: tostring_utf16 (S-TR T2) 40.2577 msec/pass - lxe: tostring_utf8 (S-TR T2) 4.7486 msec/pass - cET: tostring_utf8 (S-TR T2) 30.3330 msec/pass + lxe: tostring_utf8 (S-TR T2) 4.6697 msec/pass + cET: tostring_utf8 (S-TR T2) 30.5173 msec/pass - lxe: tostring_utf8 (U-TR T3) 1.2028 msec/pass - cET: tostring_utf8 (U-TR T3) 8.9505 msec/pass + lxe: tostring_utf8 (U-TR T3) 1.2085 msec/pass + cET: tostring_utf8 (U-TR T3) 9.0246 msec/pass The difference is somewhat smaller for plain text serialisation:: - lxe: tostring_text_ascii (S-TR T1) 2.4126 msec/pass - cET: tostring_text_ascii (S-TR T1) 3.1371 msec/pass + lxe: tostring_text_ascii (S-TR T1) 2.6727 msec/pass + cET: tostring_text_ascii (S-TR T1) 2.9683 msec/pass - lxe: tostring_text_ascii (S-TR T3) 0.8945 msec/pass - cET: tostring_text_ascii (S-TR T3) 1.2043 msec/pass + lxe: tostring_text_ascii (S-TR T3) 0.6952 msec/pass + cET: tostring_text_ascii (S-TR T3) 1.0073 msec/pass - lxe: tostring_text_utf16 (S-TR T1) 2.5816 msec/pass - cET: tostring_text_utf16 (S-TR T1) 7.3011 msec/pass + lxe: tostring_text_utf16 (S-TR T1) 2.7366 msec/pass + cET: tostring_text_utf16 (S-TR T1) 7.3647 msec/pass - lxe: tostring_text_utf16 (U-TR T1) 2.7902 msec/pass - cET: tostring_text_utf16 (U-TR T1) 7.4139 msec/pass + lxe: tostring_text_utf16 (U-TR T1) 3.0322 msec/pass + cET: tostring_text_utf16 (U-TR T1) 7.5922 msec/pass The ``tostring()`` function also supports serialisation to a Python unicode string object, which is currently faster in ElementTree under CPython 3.8:: - lxe: tostring_text_unicode (S-TR T1) 2.5883 msec/pass - cET: tostring_text_unicode (S-TR T1) 1.1873 msec/pass + lxe: tostring_text_unicode (S-TR T1) 2.7645 msec/pass + cET: tostring_text_unicode (S-TR T1) 1.1806 msec/pass - lxe: tostring_text_unicode (U-TR T1) 2.8777 msec/pass - cET: tostring_text_unicode (U-TR T1) 1.1592 msec/pass + lxe: tostring_text_unicode (U-TR T1) 2.9871 msec/pass + cET: tostring_text_unicode (U-TR T1) 1.1659 msec/pass - lxe: tostring_text_unicode (S-TR T3) 0.6495 msec/pass - cET: tostring_text_unicode (S-TR T3) 0.4494 msec/pass + lxe: tostring_text_unicode (S-TR T3) 0.7446 msec/pass + cET: tostring_text_unicode (S-TR T3) 0.4532 msec/pass - lxe: tostring_text_unicode (U-TR T4) 0.0050 msec/pass - cET: tostring_text_unicode (U-TR T4) 0.0131 msec/pass + lxe: tostring_text_unicode (U-TR T4) 0.0048 msec/pass + cET: tostring_text_unicode (U-TR T4) 0.0134 msec/pass For parsing, lxml.etree and cElementTree compete for the medal. Depending on the input, either of the two can be faster. The (c)ET @@ -185,14 +185,14 @@ libraries use a very thin layer on top of the expat parser, which is known to be very fast. Here are some timings from the benchmarking suite:: - lxe: parse_bytesIO (SAXR T1) 15.2328 msec/pass - cET: parse_bytesIO (SAXR T1) 7.5498 msec/pass + lxe: parse_bytesIO (SAXR T1) 14.2074 msec/pass + cET: parse_bytesIO (SAXR T1) 7.9336 msec/pass - lxe: parse_bytesIO (S-XR T3) 1.5039 msec/pass - cET: parse_bytesIO (S-XR T3) 2.1725 msec/pass + lxe: parse_bytesIO (S-XR T3) 1.4477 msec/pass + cET: parse_bytesIO (S-XR T3) 2.1925 msec/pass - lxe: parse_bytesIO (UAXR T3) 8.7409 msec/pass - cET: parse_bytesIO (UAXR T3) 12.4905 msec/pass + lxe: parse_bytesIO (UAXR T3) 8.4128 msec/pass + cET: parse_bytesIO (UAXR T3) 12.2926 msec/pass And another couple of timings `from a benchmark`_ that Fredrik Lundh `used to promote cElementTree`_, comparing a number of different @@ -270,26 +270,26 @@ rather close to each other, usually within a factor of two, with winners well distributed over both sides. Similar timings can be observed for the ``iterparse()`` function:: - lxe: iterparse_bytesIO (SAXR T1) 20.9262 msec/pass - cET: iterparse_bytesIO (SAXR T1) 10.3736 msec/pass + lxe: iterparse_bytesIO (SAXR T1) 20.3598 msec/pass + cET: iterparse_bytesIO (SAXR T1) 10.8948 msec/pass - lxe: iterparse_bytesIO (UAXR T3) 11.0531 msec/pass - cET: iterparse_bytesIO (UAXR T3) 13.2461 msec/pass + lxe: iterparse_bytesIO (UAXR T3) 10.1640 msec/pass + cET: iterparse_bytesIO (UAXR T3) 12.9926 msec/pass However, if you benchmark the complete round-trip of a serialise-parse cycle, the numbers will look similar to these:: - lxe: write_utf8_parse_bytesIO (S-TR T1) 19.3429 msec/pass - cET: write_utf8_parse_bytesIO (S-TR T1) 35.5511 msec/pass + lxe: write_utf8_parse_bytesIO (S-TR T1) 18.9857 msec/pass + cET: write_utf8_parse_bytesIO (S-TR T1) 35.7475 msec/pass - lxe: write_utf8_parse_bytesIO (UATR T2) 22.8314 msec/pass - cET: write_utf8_parse_bytesIO (UATR T2) 42.3915 msec/pass + lxe: write_utf8_parse_bytesIO (UATR T2) 22.4853 msec/pass + cET: write_utf8_parse_bytesIO (UATR T2) 42.6254 msec/pass - lxe: write_utf8_parse_bytesIO (S-TR T3) 3.4230 msec/pass - cET: write_utf8_parse_bytesIO (S-TR T3) 11.1156 msec/pass + lxe: write_utf8_parse_bytesIO (S-TR T3) 3.3801 msec/pass + cET: write_utf8_parse_bytesIO (S-TR T3) 11.2493 msec/pass - lxe: write_utf8_parse_bytesIO (SATR T4) 0.4215 msec/pass - cET: write_utf8_parse_bytesIO (SATR T4) 0.9992 msec/pass + lxe: write_utf8_parse_bytesIO (SATR T4) 0.4263 msec/pass + cET: write_utf8_parse_bytesIO (SATR T4) 1.0326 msec/pass For applications that require a high parser throughput of large files, and that do little to no serialization, both cET and lxml.etree are a @@ -345,14 +345,14 @@ restructuring. This can be seen from the tree setup times of the benchmark (given in seconds):: lxe: -- S- U- -A SA UA - T1: 0.0299 0.0343 0.0344 0.0293 0.0345 0.0342 - T2: 0.0368 0.0423 0.0418 0.0427 0.0474 0.0459 - T3: 0.0088 0.0084 0.0086 0.0251 0.0258 0.0261 - T4: 0.0002 0.0002 0.0002 0.0005 0.0006 0.0006 + T1: 0.0219 0.0254 0.0257 0.0216 0.0259 0.0259 + T2: 0.0234 0.0279 0.0283 0.0271 0.0318 0.0307 + T3: 0.0051 0.0050 0.0058 0.0218 0.0233 0.0231 + T4: 0.0001 0.0001 0.0001 0.0004 0.0004 0.0004 cET: -- S- U- -A SA UA - T1: 0.0050 0.0045 0.0093 0.0044 0.0043 0.0043 - T2: 0.0073 0.0075 0.0074 0.0201 0.0075 0.0074 - T3: 0.0033 0.0213 0.0032 0.0034 0.0033 0.0035 + T1: 0.0035 0.0029 0.0078 0.0031 0.0031 0.0029 + T2: 0.0047 0.0051 0.0053 0.0046 0.0055 0.0048 + T3: 0.0016 0.0216 0.0027 0.0021 0.0023 0.0026 T4: 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 The timings are somewhat close to each other, although cET can be @@ -372,30 +372,30 @@ The same tree overhead makes operations like collecting children as in a shallow copy of their list of children, lxml has to create a Python object for each child and collect them in a list:: - lxe: root_list_children (--TR T1) 0.0033 msec/pass - cET: root_list_children (--TR T1) 0.0007 msec/pass + lxe: root_list_children (--TR T1) 0.0036 msec/pass + cET: root_list_children (--TR T1) 0.0005 msec/pass - lxe: root_list_children (--TR T2) 0.0596 msec/pass - cET: root_list_children (--TR T2) 0.0055 msec/pass + lxe: root_list_children (--TR T2) 0.0634 msec/pass + cET: root_list_children (--TR T2) 0.0086 msec/pass This handicap is also visible when accessing single children:: - lxe: first_child (--TR T2) 0.0615 msec/pass + lxe: first_child (--TR T2) 0.0601 msec/pass cET: first_child (--TR T2) 0.0548 msec/pass - lxe: last_child (--TR T1) 0.0603 msec/pass - cET: last_child (--TR T1) 0.0563 msec/pass + lxe: last_child (--TR T1) 0.0570 msec/pass + cET: last_child (--TR T1) 0.0534 msec/pass ... unless you also add the time to find a child index in a bigger list. ET and cET use Python lists here, which are based on arrays. The data structure used by libxml2 is a linked tree, and thus, a linked list of children:: - lxe: middle_child (--TR T1) 0.0918 msec/pass - cET: middle_child (--TR T1) 0.0513 msec/pass + lxe: middle_child (--TR T1) 0.0892 msec/pass + cET: middle_child (--TR T1) 0.0510 msec/pass - lxe: middle_child (--TR T2) 2.3277 msec/pass - cET: middle_child (--TR T2) 0.0484 msec/pass + lxe: middle_child (--TR T2) 2.3038 msec/pass + cET: middle_child (--TR T2) 0.0508 msec/pass Element creation @@ -405,18 +405,18 @@ As opposed to ET, libxml2 has a notion of documents that each element must be in. This results in a major performance difference for creating independent Elements that end up in independently created documents:: - lxe: create_elements (--TC T2) 0.8178 msec/pass - cET: create_elements (--TC T2) 0.0668 msec/pass + lxe: create_elements (--TC T2) 0.8032 msec/pass + cET: create_elements (--TC T2) 0.0675 msec/pass Therefore, it is always preferable to create Elements for the document they are supposed to end up in, either as SubElements of an Element or using the explicit ``Element.makeelement()`` call:: - lxe: makeelement (--TC T2) 0.8020 msec/pass - cET: makeelement (--TC T2) 0.0618 msec/pass + lxe: makeelement (--TC T2) 0.8030 msec/pass + cET: makeelement (--TC T2) 0.0625 msec/pass - lxe: create_subelements (--TC T2) 0.7782 msec/pass - cET: create_subelements (--TC T2) 0.0865 msec/pass + lxe: create_subelements (--TC T2) 0.8621 msec/pass + cET: create_subelements (--TC T2) 0.0923 msec/pass So, if the main performance bottleneck of an application is creating large XML trees in memory through calls to Element and SubElement, cET is the best @@ -433,11 +433,11 @@ requires lxml to do recursive adaptations throughout the moved tree structure. The following benchmark appends all root children of the second tree to the root of the first tree:: - lxe: append_from_document (--TR T1,T2) 1.3409 msec/pass - cET: append_from_document (--TR T1,T2) 0.0539 msec/pass + lxe: append_from_document (--TR T1,T2) 1.3800 msec/pass + cET: append_from_document (--TR T1,T2) 0.0513 msec/pass - lxe: append_from_document (--TR T3,T4) 0.0203 msec/pass - cET: append_from_document (--TR T3,T4) 0.0031 msec/pass + lxe: append_from_document (--TR T3,T4) 0.0150 msec/pass + cET: append_from_document (--TR T3,T4) 0.0026 msec/pass Although these are fairly small numbers compared to parsing, this easily shows the different performance classes for lxml and (c)ET. Where the latter do not @@ -448,19 +448,19 @@ with the size of the tree that is moved. This difference is not always as visible, but applies to most parts of the API, like inserting newly created elements:: - lxe: insert_from_document (--TR T1,T2) 4.9999 msec/pass - cET: insert_from_document (--TR T1,T2) 0.0696 msec/pass + lxe: insert_from_document (--TR T1,T2) 5.2345 msec/pass + cET: insert_from_document (--TR T1,T2) 0.0732 msec/pass or replacing the child slice by a newly created element:: - lxe: replace_children_element (--TC T1) 0.0653 msec/pass - cET: replace_children_element (--TC T1) 0.0098 msec/pass + lxe: replace_children_element (--TC T1) 0.0720 msec/pass + cET: replace_children_element (--TC T1) 0.0105 msec/pass as opposed to replacing the slice with an existing element from the same document:: - lxe: replace_children (--TC T1) 0.0069 msec/pass - cET: replace_children (--TC T1) 0.0043 msec/pass + lxe: replace_children (--TC T1) 0.0060 msec/pass + cET: replace_children (--TC T1) 0.0050 msec/pass While these numbers are too small to provide a major performance impact in practice, you should keep this difference in mind when you @@ -474,14 +474,14 @@ deepcopy Deep copying a tree is fast in lxml:: - lxe: deepcopy_all (--TR T1) 4.0150 msec/pass - cET: deepcopy_all (--TR T1) 2.4621 msec/pass + lxe: deepcopy_all (--TR T1) 4.1246 msec/pass + cET: deepcopy_all (--TR T1) 2.5451 msec/pass - lxe: deepcopy_all (-ATR T2) 4.7412 msec/pass - cET: deepcopy_all (-ATR T2) 2.8064 msec/pass + lxe: deepcopy_all (-ATR T2) 4.7867 msec/pass + cET: deepcopy_all (-ATR T2) 2.7504 msec/pass - lxe: deepcopy_all (S-TR T3) 1.1363 msec/pass - cET: deepcopy_all (S-TR T3) 0.5484 msec/pass + lxe: deepcopy_all (S-TR T3) 1.0097 msec/pass + cET: deepcopy_all (S-TR T3) 0.6278 msec/pass So, for example, if you have a database-like scenario where you parse in a large tree and then search and copy independent subtrees from it for further @@ -497,31 +497,31 @@ traversal of the XML tree and especially if few elements are of interest or the target element tag name is known, the ``.iter()`` method is a good choice:: - lxe: iter_all (--TR T1) 1.3881 msec/pass - cET: iter_all (--TR T1) 0.2708 msec/pass + lxe: iter_all (--TR T1) 1.3661 msec/pass + cET: iter_all (--TR T1) 0.2670 msec/pass - lxe: iter_islice (--TR T2) 0.0124 msec/pass - cET: iter_islice (--TR T2) 0.0036 msec/pass + lxe: iter_islice (--TR T2) 0.0122 msec/pass + cET: iter_islice (--TR T2) 0.0033 msec/pass - lxe: iter_tag (--TR T2) 0.0105 msec/pass - cET: iter_tag (--TR T2) 0.0083 msec/pass + lxe: iter_tag (--TR T2) 0.0098 msec/pass + cET: iter_tag (--TR T2) 0.0086 msec/pass - lxe: iter_tag_all (--TR T2) 0.7262 msec/pass - cET: iter_tag_all (--TR T2) 0.4537 msec/pass + lxe: iter_tag_all (--TR T2) 0.6840 msec/pass + cET: iter_tag_all (--TR T2) 0.4323 msec/pass This translates directly into similar timings for ``Element.findall()``:: - lxe: findall (--TR T2) 4.0147 msec/pass - cET: findall (--TR T2) 0.9193 msec/pass + lxe: findall (--TR T2) 3.9611 msec/pass + cET: findall (--TR T2) 0.9227 msec/pass - lxe: findall (--TR T3) 0.4113 msec/pass - cET: findall (--TR T3) 0.2377 msec/pass + lxe: findall (--TR T3) 0.3989 msec/pass + cET: findall (--TR T3) 0.2670 msec/pass - lxe: findall_tag (--TR T2) 0.7253 msec/pass - cET: findall_tag (--TR T2) 0.4904 msec/pass + lxe: findall_tag (--TR T2) 0.7420 msec/pass + cET: findall_tag (--TR T2) 0.4942 msec/pass - lxe: findall_tag (--TR T3) 0.1092 msec/pass - cET: findall_tag (--TR T3) 0.1757 msec/pass + lxe: findall_tag (--TR T3) 0.1099 msec/pass + cET: findall_tag (--TR T3) 0.1748 msec/pass Note that all three libraries currently use the same Python implementation for ``.findall()``, except for their native tree @@ -541,38 +541,38 @@ provides more than one way of accessing it and you should take care which part of the lxml API you use. The most straight forward way is to call the ``xpath()`` method on an Element or ElementTree:: - lxe: xpath_method (--TC T1) 0.2763 msec/pass - lxe: xpath_method (--TC T2) 5.3439 msec/pass - lxe: xpath_method (--TC T3) 0.0315 msec/pass - lxe: xpath_method (--TC T4) 0.2587 msec/pass + lxe: xpath_method (--TC T1) 0.2828 msec/pass + lxe: xpath_method (--TC T2) 5.4705 msec/pass + lxe: xpath_method (--TC T3) 0.0324 msec/pass + lxe: xpath_method (--TC T4) 0.2804 msec/pass This is well suited for testing and when the XPath expressions are as diverse as the trees they are called on. However, if you have a single XPath expression that you want to apply to a larger number of different elements, the ``XPath`` class is the most efficient way to do it:: - lxe: xpath_class (--TC T1) 0.0610 msec/pass - lxe: xpath_class (--TC T2) 0.6981 msec/pass - lxe: xpath_class (--TC T3) 0.0141 msec/pass - lxe: xpath_class (--TC T4) 0.0432 msec/pass + lxe: xpath_class (--TC T1) 0.0570 msec/pass + lxe: xpath_class (--TC T2) 0.6924 msec/pass + lxe: xpath_class (--TC T3) 0.0148 msec/pass + lxe: xpath_class (--TC T4) 0.0446 msec/pass Note that this still allows you to use variables in the expression, so you can parse it once and then adapt it through variables at call time. In other cases, where you have a fixed Element or ElementTree and want to run different expressions on it, you should consider the ``XPathEvaluator``:: - lxe: xpath_element (--TR T1) 0.0598 msec/pass - lxe: xpath_element (--TR T2) 0.9737 msec/pass - lxe: xpath_element (--TR T3) 0.0167 msec/pass - lxe: xpath_element (--TR T4) 0.0606 msec/pass + lxe: xpath_element (--TR T1) 0.0684 msec/pass + lxe: xpath_element (--TR T2) 1.0865 msec/pass + lxe: xpath_element (--TR T3) 0.0174 msec/pass + lxe: xpath_element (--TR T4) 0.0665 msec/pass While it looks slightly slower, creating an XPath object for each of the expressions generates a much higher overhead here:: - lxe: xpath_class_repeat (--TC T1 ) 0.2658 msec/pass - lxe: xpath_class_repeat (--TC T2 ) 5.0316 msec/pass - lxe: xpath_class_repeat (--TC T3 ) 0.0319 msec/pass - lxe: xpath_class_repeat (--TC T4 ) 0.2749 msec/pass + lxe: xpath_class_repeat (--TC T1 ) 0.2813 msec/pass + lxe: xpath_class_repeat (--TC T2 ) 5.4042 msec/pass + lxe: xpath_class_repeat (--TC T3 ) 0.0339 msec/pass + lxe: xpath_class_repeat (--TC T4 ) 0.2706 msec/pass Note that tree iteration can be substantially faster than XPath if your code short-circuits after the first couple of elements were @@ -582,25 +582,25 @@ regardless of how much of it will actually be used. Here is an example where only the first matching element is being searched, a case for which XPath has syntax support as well:: - lxe: find_single (--TR T2) 0.0045 msec/pass - cET: find_single (--TR T2) 0.0029 msec/pass + lxe: find_single (--TR T2) 0.0031 msec/pass + cET: find_single (--TR T2) 0.0026 msec/pass lxe: iter_single (--TR T2) 0.0019 msec/pass - cET: iter_single (--TR T2) 0.0005 msec/pass + cET: iter_single (--TR T2) 0.0002 msec/pass - lxe: xpath_single (--TR T2) 0.0844 msec/pass + lxe: xpath_single (--TR T2) 0.0861 msec/pass When looking for the first two elements out of many, the numbers explode for XPath, as restricting the result subset requires a more complex expression:: lxe: iterfind_two (--TR T2) 0.0050 msec/pass - cET: iterfind_two (--TR T2) 0.0031 msec/pass + cET: iterfind_two (--TR T2) 0.0036 msec/pass - lxe: iter_two (--TR T2) 0.0029 msec/pass - cET: iter_two (--TR T2) 0.0012 msec/pass + lxe: iter_two (--TR T2) 0.0021 msec/pass + cET: iter_two (--TR T2) 0.0014 msec/pass - lxe: xpath_two (--TR T2) 0.0706 msec/pass + lxe: xpath_two (--TR T2) 0.0916 msec/pass A longer example @@ -767,21 +767,21 @@ ObjectPath can be used to speed up the access to elements that are deep in the tree. It avoids step-by-step Python element instantiations along the path, which can substantially improve the access time:: - lxe: attribute (--TR T1) 2.6822 msec/pass - lxe: attribute (--TR T2) 16.4094 msec/pass - lxe: attribute (--TR T4) 2.4951 msec/pass + lxe: attribute (--TR T1) 2.4018 msec/pass + lxe: attribute (--TR T2) 16.3755 msec/pass + lxe: attribute (--TR T4) 2.3725 msec/pass - lxe: objectpath (--TR T1) 1.1985 msec/pass - lxe: objectpath (--TR T2) 14.7083 msec/pass - lxe: objectpath (--TR T4) 1.2503 msec/pass + lxe: objectpath (--TR T1) 1.1816 msec/pass + lxe: objectpath (--TR T2) 14.4675 msec/pass + lxe: objectpath (--TR T4) 1.2276 msec/pass - lxe: attributes_deep (--TR T1) 3.9361 msec/pass - lxe: attributes_deep (--TR T2) 17.9017 msec/pass - lxe: attributes_deep (--TR T4) 3.7947 msec/pass + lxe: attributes_deep (--TR T1) 3.7086 msec/pass + lxe: attributes_deep (--TR T2) 17.5436 msec/pass + lxe: attributes_deep (--TR T4) 3.8407 msec/pass - lxe: objectpath_deep (--TR T1) 1.6170 msec/pass - lxe: objectpath_deep (--TR T2) 15.3167 msec/pass - lxe: objectpath_deep (--TR T4) 1.5836 msec/pass + lxe: objectpath_deep (--TR T1) 1.4980 msec/pass + lxe: objectpath_deep (--TR T2) 14.7266 msec/pass + lxe: objectpath_deep (--TR T4) 1.4834 msec/pass Note, however, that parsing ObjectPath expressions is not for free either, so this is most effective for frequently accessing the same element. @@ -811,17 +811,17 @@ expressions to be more selective. By choosing the right trees (or even subtrees and elements) to cache, you can trade memory usage against access speed:: - lxe: attribute_cached (--TR T1) 1.9312 msec/pass - lxe: attribute_cached (--TR T2) 15.1188 msec/pass - lxe: attribute_cached (--TR T4) 1.9250 msec/pass + lxe: attribute_cached (--TR T1) 1.9207 msec/pass + lxe: attribute_cached (--TR T2) 15.6903 msec/pass + lxe: attribute_cached (--TR T4) 1.8718 msec/pass - lxe: attributes_deep_cached (--TR T1) 2.6906 msec/pass - lxe: attributes_deep_cached (--TR T2) 16.4149 msec/pass - lxe: attributes_deep_cached (--TR T4) 2.5618 msec/pass + lxe: attributes_deep_cached (--TR T1) 2.6512 msec/pass + lxe: attributes_deep_cached (--TR T2) 16.7937 msec/pass + lxe: attributes_deep_cached (--TR T4) 2.5539 msec/pass - lxe: objectpath_deep_cached (--TR T1) 1.0054 msec/pass - lxe: objectpath_deep_cached (--TR T2) 14.3306 msec/pass - lxe: objectpath_deep_cached (--TR T4) 0.8924 msec/pass + lxe: objectpath_deep_cached (--TR T1) 0.8519 msec/pass + lxe: objectpath_deep_cached (--TR T2) 13.9337 msec/pass + lxe: objectpath_deep_cached (--TR T4) 0.8645 msec/pass Things to note: you cannot currently use ``weakref.WeakKeyDictionary`` objects for this as lxml's element objects do not support weak references (which are From 1cbffa9312843d2537f80700864fe0d2ed5537a5 Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Mon, 5 Jul 2021 00:16:56 +0200 Subject: [PATCH 27/74] Show libxml2 version in benchmark output. --- benchmark/benchbase.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/benchmark/benchbase.py b/benchmark/benchbase.py index 48aee2128..a9f9ad857 100644 --- a/benchmark/benchbase.py +++ b/benchmark/benchbase.py @@ -474,7 +474,8 @@ def main(benchmark_class): if import_lxml: from lxml import etree _etrees.append(etree) - print("Using lxml %s" % etree.__version__) + print("Using lxml %s (with libxml2 %s)" % ( + etree.__version__, '.'.join(map(str, etree.LIBXML_VERSION)))) try: sys.argv.remove('-fel') From fa790231bcbf50e179dde5d42d2c8a34597f3851 Mon Sep 17 00:00:00 2001 From: Stefan Behnel Date: Mon, 5 Jul 2021 00:17:43 +0200 Subject: [PATCH 28/74] Add a script to update the benchmark results in doc/performance.txt after a new benchmark run. --- doc/update_performance_results.py | 58 +++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 doc/update_performance_results.py diff --git a/doc/update_performance_results.py b/doc/update_performance_results.py new file mode 100644 index 000000000..cf0f45bbc --- /dev/null +++ b/doc/update_performance_results.py @@ -0,0 +1,58 @@ +import operator +import re + +_parse_result_line = re.compile( + "\s*(?P\w+):\s*(?P\w+)\s+\((?P[-\w]+\s[\w,]+)\s*\)\s+(?P