diff options
author | David Lord <davidism@gmail.com> | 2023-01-26 07:18:50 -0800 |
---|---|---|
committer | David Lord <davidism@gmail.com> | 2023-01-26 07:18:50 -0800 |
commit | 45dbe44a36a2abe1b0b62b8ff9e32653fe91fb83 (patch) | |
tree | 8469681beac3314ed3d660fdb6e3dd9f5a1ced95 | |
parent | 00fea0a771ed63169933684283862e50ff3497aa (diff) | |
download | markupsafe-rust.tar.gz |
initial rust implementationrust
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | MANIFEST.in | 2 | ||||
-rw-r--r-- | bench.py | 39 | ||||
-rw-r--r-- | bench/bench_basic.py | 5 | ||||
-rw-r--r-- | bench/bench_largestring.py | 6 | ||||
-rw-r--r-- | bench/bench_long_empty_string.py | 6 | ||||
-rw-r--r-- | bench/bench_long_suffix.py | 6 | ||||
-rw-r--r-- | bench/bench_short_empty_string.py | 5 | ||||
-rw-r--r-- | bench/runbench.py | 38 | ||||
-rw-r--r-- | pyproject.toml | 2 | ||||
-rw-r--r-- | setup.py | 91 | ||||
-rw-r--r-- | src/markupsafe/_rust_speedups.pyi | 9 | ||||
-rw-r--r-- | src/rust/Cargo.lock | 273 | ||||
-rw-r--r-- | src/rust/Cargo.toml | 12 | ||||
-rw-r--r-- | src/rust/src/lib.rs | 54 | ||||
-rw-r--r-- | tests/conftest.py | 2 |
16 files changed, 409 insertions, 142 deletions
@@ -15,3 +15,4 @@ __pycache__/ /htmlcov/ /docs/_build/ /.mypy_cache/ +/src/rust/target/ diff --git a/MANIFEST.in b/MANIFEST.in index 7dfa3f6..35d1a31 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,4 +6,6 @@ prune docs/_build graft tests include src/markupsafe/py.typed include src/markupsafe/*.pyi +graft src/rust +prune src/rust/target global-exclude *.pyc diff --git a/bench.py b/bench.py new file mode 100644 index 0000000..1a1f9bc --- /dev/null +++ b/bench.py @@ -0,0 +1,39 @@ +import pyperf + +runner = pyperf.Runner() + +for name in ("native", "rust_speedups"): # ("native", "speedups", "rust_speedups"): + runner.timeit( + f"short escape {name}", + setup=f"from markupsafe._{name} import escape", + stmt='escape("<strong>Hello, World!</strong>")', + ) + # runner.timeit( + # f"long escape {name}", + # setup=( + # f"from markupsafe._{name} import escape;\n" + # "s = \"Hello, World!\" * 1000" + # ), + # stmt="escape(s)", + # ) + # runner.timeit( + # f"short empty {name}", + # setup=f"from markupsafe._{name} import escape", + # stmt="escape(\"Hello, World!\")", + # ) + # runner.timeit( + # f"long empty {name}", + # setup=( + # f"from markupsafe._{name} import escape;\n" + # "s = \"<strong>Hello, World!</strong>\" * 1000" + # ), + # stmt="escape(s)", + # ) + # runner.timeit( + # f"long suffix {name}", + # setup=( + # f"from markupsafe._{name} import escape;\n" + # "s = \"<strong>Hello, World!</strong>\" + \"x\" * 100000" + # ), + # stmt="escape(s)", + # ) diff --git a/bench/bench_basic.py b/bench/bench_basic.py deleted file mode 100644 index b67eda4..0000000 --- a/bench/bench_basic.py +++ /dev/null @@ -1,5 +0,0 @@ -from markupsafe import escape - - -def run(): - escape("<strong>Hello World!</strong>") diff --git a/bench/bench_largestring.py b/bench/bench_largestring.py deleted file mode 100644 index 9ced67f..0000000 --- a/bench/bench_largestring.py +++ /dev/null @@ -1,6 +0,0 @@ -from markupsafe import escape - - -def run(): - string = "<strong>Hello World!</strong>" * 1000 - escape(string) diff --git a/bench/bench_long_empty_string.py b/bench/bench_long_empty_string.py deleted file mode 100644 index ad2480c..0000000 --- a/bench/bench_long_empty_string.py +++ /dev/null @@ -1,6 +0,0 @@ -from markupsafe import escape - - -def run(): - string = "Hello World!" * 1000 - escape(string) diff --git a/bench/bench_long_suffix.py b/bench/bench_long_suffix.py deleted file mode 100644 index 35f38da..0000000 --- a/bench/bench_long_suffix.py +++ /dev/null @@ -1,6 +0,0 @@ -from markupsafe import escape - - -def run(): - string = "<strong>Hello World!</strong>" + "x" * 100000 - escape(string) diff --git a/bench/bench_short_empty_string.py b/bench/bench_short_empty_string.py deleted file mode 100644 index 0664a6f..0000000 --- a/bench/bench_short_empty_string.py +++ /dev/null @@ -1,5 +0,0 @@ -from markupsafe import escape - - -def run(): - escape("Hello World!") diff --git a/bench/runbench.py b/bench/runbench.py deleted file mode 100644 index f20cd49..0000000 --- a/bench/runbench.py +++ /dev/null @@ -1,38 +0,0 @@ -import os -import re -import sys -from subprocess import Popen - -_filename_re = re.compile(r"^bench_(.*?)\.py$") -bench_directory = os.path.abspath(os.path.dirname(__file__)) - - -def list_benchmarks(): - result = [] - for name in os.listdir(bench_directory): - match = _filename_re.match(name) - if match is not None: - result.append(match.group(1)) - result.sort(key=lambda x: (x.startswith("logging_"), x.lower())) - return result - - -def run_bench(name): - print(name) - Popen( - [sys.executable, "-m", "timeit", "-s", f"from bench_{name} import run", "run()"] - ).wait() - - -def main(): - print("=" * 80) - print("Running benchmark for MarkupSafe") - print("-" * 80) - os.chdir(bench_directory) - for bench in list_benchmarks(): - run_bench(bench) - print("-" * 80) - - -if __name__ == "__main__": - main() diff --git a/pyproject.toml b/pyproject.toml index 5fdf079..9244e1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ Twitter = "https://twitter.com/PalletsTeam" Chat = "https://discord.gg/pallets" [build-system] -requires = ["setuptools"] +requires = ["setuptools", "setuptools-rust"] build-backend = "setuptools.build_meta" [tool.setuptools.dynamic] @@ -1,84 +1,27 @@ import os import platform -import sys -from distutils.errors import CCompilerError -from distutils.errors import DistutilsExecError -from distutils.errors import DistutilsPlatformError from setuptools import Extension from setuptools import setup -from setuptools.command.build_ext import build_ext +from setuptools_rust import RustExtension -ext_modules = [Extension("markupsafe._speedups", ["src/markupsafe/_speedups.c"])] - - -class BuildFailed(Exception): - pass - - -class ve_build_ext(build_ext): - """This class allows C extension building to fail.""" - - def run(self): - try: - build_ext.run(self) - except DistutilsPlatformError as e: - raise BuildFailed() from e - - def build_extension(self, ext): - try: - build_ext.build_extension(self, ext) - except (CCompilerError, DistutilsExecError, DistutilsPlatformError) as e: - raise BuildFailed() from e - except ValueError as e: - # this can happen on Windows 64 bit, see Python issue 7511 - if "'path'" in str(sys.exc_info()[1]): # works with Python 2 and 3 - raise BuildFailed() from e - raise - - -def run_setup(with_binary): - setup( - cmdclass={"build_ext": ve_build_ext}, - ext_modules=ext_modules if with_binary else [], - ) - - -def show_message(*lines): - print("=" * 74) - for line in lines: - print(line) - print("=" * 74) - - -supports_speedups = platform.python_implementation() not in { +if platform.python_implementation() not in { "PyPy", "Jython", "GraalVM", -} - -if os.environ.get("CIBUILDWHEEL", "0") == "1" and supports_speedups: - run_setup(True) -elif supports_speedups: - try: - run_setup(True) - except BuildFailed: - show_message( - "WARNING: The C extension could not be compiled, speedups" - " are not enabled.", - "Failure information, if any, is above.", - "Retrying the build without the C extension now.", - ) - run_setup(False) - show_message( - "WARNING: The C extension could not be compiled, speedups" - " are not enabled.", - "Plain-Python build succeeded.", - ) -else: - run_setup(False) - show_message( - "WARNING: C extensions are not supported on this Python" - " platform, speedups are not enabled.", - "Plain-Python build succeeded.", +}: + local = os.environ.get("CIBUIDWHEEL", "0") != "1" + setup( + ext_modules=[ + Extension( + "markupsafe._speedups", ["src/markupsafe/_speedups.c"], optional=local + ) + ], + rust_extensions=[ + RustExtension( + "markupsafe._rust_speedups", "src/rust/Cargo.toml", optional=local + ) + ], ) +else: + setup() diff --git a/src/markupsafe/_rust_speedups.pyi b/src/markupsafe/_rust_speedups.pyi new file mode 100644 index 0000000..f673240 --- /dev/null +++ b/src/markupsafe/_rust_speedups.pyi @@ -0,0 +1,9 @@ +from typing import Any +from typing import Optional + +from . import Markup + +def escape(s: Any) -> Markup: ... +def escape_silent(s: Optional[Any]) -> Markup: ... +def soft_str(s: Any) -> str: ... +def soft_unicode(s: Any) -> str: ... diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock new file mode 100644 index 0000000..4b91064 --- /dev/null +++ b/src/rust/Cargo.lock @@ -0,0 +1,273 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "indoc" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da2d6f23ffea9d7e76c53eee25dfb67bcd8fde7f1198b0855350698c9f07c780" + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "markupsafe-rust" +version = "0.1.0" +dependencies = [ + "pyo3", +] + +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "proc-macro2" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd4149c8c3975099622b4e1962dac27565cf5663b76452c3e2b66e0b6824277" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "parking_lot", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd09fe469834db21ee60e0051030339e5d361293d8cb5ec02facf7fdcf52dbf" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c427c9a96b9c5b12156dbc11f76b14f49e9aae8905ca783ea87c249044ef137" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b822bbba9d60630a44d2109bc410489bb2f439b33e3a14ddeb8a40b378a7c4" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84ae898104f7c99db06231160770f3e40dad6eb9021daddc0fedfa3e41dff10a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9410d0f6853b1d94f0e519fb95df60f29d2c1eff2d921ffdf01a4c8a3b54f12d" + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unindent" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml new file mode 100644 index 0000000..6e1a4fd --- /dev/null +++ b/src/rust/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "markupsafe-rust" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +pyo3 = "0.18.0" + +[lib] +name = "_rust_speedups" +crate-type = ["cdylib"] diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs new file mode 100644 index 0000000..281280e --- /dev/null +++ b/src/rust/src/lib.rs @@ -0,0 +1,54 @@ +use pyo3::prelude::*; +use pyo3::types::{PyBool, PyFloat, PyLong, PyString}; +use pyo3::{intern, PyTypeInfo}; + +fn escape_str(s: &str) -> String { + s.replace('&', "&") + .replace('>', ">") + .replace('<', "<") + .replace('\'', "'") + .replace('"', """) +} + +#[pyfunction] +fn escape<'p>(py: Python<'p>, s: &'p PyAny) -> PyResult<&'p PyAny> { + let cls = PyModule::import(py, "markupsafe")?.getattr("Markup")?; + + if PyLong::is_exact_type_of(s) || PyFloat::is_exact_type_of(s) || PyBool::is_exact_type_of(s) { + cls.call1((s,)) + } else if s.hasattr(intern!(py, "__html__"))? { + cls.call1((s.call_method0("__html__")?,)) + } else if PyString::is_exact_type_of(s) { + cls.call1((escape_str(s.extract()?),)) + } else { + cls.call1((escape_str(s.str()?.extract()?),)) + } +} + +#[pyfunction] +fn escape_silent<'p>(py: Python<'p>, s: &'p PyAny) -> PyResult<&'p PyAny> { + if s.is_none() { + let cls = PyModule::import(py, "markupsafe")?.getattr("Markup")?; + cls.call0() + } else { + escape(py, s) + } +} + +#[pyfunction] +fn soft_str(s: &PyAny) -> &PyAny { + if PyString::is_type_of(s) { + s + } else { + s.str().unwrap() + } +} + +#[pymodule] +#[pyo3(name = "_rust_speedups")] +fn speedups(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(escape, m)?)?; + m.add_function(wrap_pyfunction!(escape_silent, m)?)?; + m.add_function(wrap_pyfunction!(soft_str, m)?)?; + Ok(()) +} diff --git a/tests/conftest.py b/tests/conftest.py index d040ea8..f05285d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import pytest from markupsafe import _native try: - from markupsafe import _speedups + from markupsafe import _rust_speedups as _speedups except ImportError: _speedups = None # type: ignore |