aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIris Yang <irisykyang@google.com>2020-09-01 14:43:44 +0800
committerIris Yang <irisykyang@google.com>2020-09-01 14:43:44 +0800
commitd5d7b04cfc953da1f9b326f599a610ed2dd14704 (patch)
tree0dde3def694ea284074bc6a1fd24fb6b66c4bd5b
parent3e87759c5e2a3bdafcca3d02c3cc6a89b1dd8b7c (diff)
parent122f69384488f031b9b781018e092c8560aa4cb9 (diff)
downloadmarkupsafe-d5d7b04cfc953da1f9b326f599a610ed2dd14704.tar.gz
Import external/python/markupsafe into master
Merge branch 'upstream-master' of sso://googleplex-android/platform/external/python/markupsafe Third-party review: ag/12415762 Bug: 165703675 Change-Id: Id1e1cc64feccc247be68dc1caa7ebebb76a5f607
-rw-r--r--.editorconfig16
-rw-r--r--.github/workflows/build.yaml28
-rw-r--r--.github/workflows/tests.yaml52
-rw-r--r--.gitignore18
-rw-r--r--.pre-commit-config.yaml26
-rw-r--r--.readthedocs.yaml8
-rw-r--r--CHANGES.rst101
-rw-r--r--LICENSE.rst28
-rw-r--r--MANIFEST.in7
-rw-r--r--README.rst68
-rw-r--r--bench/bench_basic.py5
-rw-r--r--bench/bench_largestring.py6
-rw-r--r--bench/bench_long_empty_string.py6
-rw-r--r--bench/bench_long_suffix.py6
-rw-r--r--bench/bench_short_empty_string.py5
-rw-r--r--bench/runbench.py38
-rw-r--r--docs/Makefile19
-rw-r--r--docs/changes.rst4
-rw-r--r--docs/conf.py49
-rw-r--r--docs/escaping.rst21
-rw-r--r--docs/formatting.rst77
-rw-r--r--docs/html.rst49
-rw-r--r--docs/index.rst47
-rw-r--r--docs/license.rst4
-rw-r--r--docs/make.bat35
-rw-r--r--requirements/dev.in5
-rw-r--r--requirements/dev.txt31
-rw-r--r--requirements/docs.in4
-rw-r--r--requirements/docs.txt36
-rw-r--r--requirements/tests.in1
-rw-r--r--requirements/tests.txt16
-rw-r--r--setup.cfg70
-rw-r--r--setup.py79
-rw-r--r--src/markupsafe/__init__.py281
-rw-r--r--src/markupsafe/_native.py70
-rw-r--r--src/markupsafe/_speedups.c339
-rw-r--r--tests/conftest.py42
-rw-r--r--tests/test_escape.py29
-rw-r--r--tests/test_exception_custom_html.py18
-rw-r--r--tests/test_leak.py28
-rw-r--r--tests/test_markupsafe.py185
-rw-r--r--tox.ini19
42 files changed, 1976 insertions, 0 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..220a591
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,16 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+insert_final_newline = true
+trim_trailing_whitespace = true
+end_of_line = lf
+charset = utf-8
+max_line_length = 88
+
+[*.{yml,yaml,json,js,css,html}]
+indent_size = 2
+
+[*.c]
+indent_style = tab
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
new file mode 100644
index 0000000..17581fa
--- /dev/null
+++ b/.github/workflows/build.yaml
@@ -0,0 +1,28 @@
+name: Build
+on:
+ push:
+ branches:
+ - master
+ - '*.x'
+ tags:
+ - '*'
+jobs:
+ wheels:
+ name: ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ env:
+ CIBW_SKIP: 'cp27-* pp27-* cp35-*'
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ with:
+ python-version: '3.8'
+ - run: pip install cibuildwheel
+ - run: cibuildwheel
+ - uses: actions/upload-artifact@v2
+ with:
+ path: ./wheelhouse
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
new file mode 100644
index 0000000..4c457e1
--- /dev/null
+++ b/.github/workflows/tests.yaml
@@ -0,0 +1,52 @@
+name: Tests
+on:
+ push:
+ branches:
+ - master
+ - '*.x'
+ pull_request:
+ branches:
+ - master
+ - '*.x'
+jobs:
+ tests:
+ name: ${{ matrix.name }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - {name: Linux, python: '3.8', os: ubuntu-latest, tox: py38}
+ - {name: Windows, python: '3.8', os: windows-latest, tox: py38}
+ - {name: Mac, python: '3.8', os: macos-latest, tox: py38}
+ - {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37}
+ - {name: '3.6', python: '3.6', os: ubuntu-latest, tox: py36}
+ - {name: 'PyPy', python: pypy3, os: ubuntu-latest, tox: pypy3}
+ - {name: Style, python: '3.8', os: ubuntu-latest, tox: style}
+ - {name: Docs, python: '3.8', os: ubuntu-latest, tox: docs}
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python }}
+ - name: update pip
+ run: |
+ pip install -U wheel
+ pip install -U setuptools
+ python -m pip install -U pip
+ - name: get pip cache dir
+ id: pip-cache
+ run: echo "::set-output name=dir::$(pip cache dir)"
+ - name: cache pip
+ uses: actions/cache@v1
+ with:
+ path: ${{ steps.pip-cache.outputs.dir }}
+ key: pip|${{ runner.os }}|${{ matrix.python }}|${{ hashFiles('setup.py') }}|${{ hashFiles('requirements/*.txt') }}
+ - name: cache pre-commit
+ uses: actions/cache@v1
+ with:
+ path: ~/.cache/pre-commit
+ key: pre-commit|${{ matrix.python }}|${{ hashFiles('.pre-commit-config.yaml') }}
+ if: matrix.tox == 'style'
+ - run: pip install tox
+ - run: tox -e ${{ matrix.tox }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ba7609d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+.DS_Store
+*.pyc
+*.pyo
+*.o
+*.so
+env/
+venv/
+dist/
+build/
+*.egg-info/
+.tox/
+.cache/
+.pytest_cache/
+.coverage
+.coverage.*
+htmlcov/
+docs/_build/
+.idea/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..cd7a684
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,26 @@
+repos:
+ - repo: https://github.com/asottile/pyupgrade
+ rev: v2.4.3
+ hooks:
+ - id: pyupgrade
+ args: ["--py36-plus"]
+ - repo: https://github.com/asottile/reorder_python_imports
+ rev: v2.3.0
+ hooks:
+ - id: reorder-python-imports
+ args: ["--application-directories", "src"]
+ - repo: https://github.com/python/black
+ rev: 19.10b0
+ hooks:
+ - id: black
+ - repo: https://gitlab.com/pycqa/flake8
+ rev: 3.8.2
+ hooks:
+ - id: flake8
+ additional_dependencies: [flake8-bugbear]
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v3.1.0
+ hooks:
+ - id: check-byte-order-marker
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 0000000..f4dd25b
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,8 @@
+version: 2
+python:
+ install:
+ - method: pip
+ path: .
+ - requirements: requirements/docs.txt
+sphinx:
+ builder: dirhtml
diff --git a/CHANGES.rst b/CHANGES.rst
new file mode 100644
index 0000000..7f729e7
--- /dev/null
+++ b/CHANGES.rst
@@ -0,0 +1,101 @@
+Version 2.0.0
+-------------
+
+Unreleased
+
+- Drop Python 2.7, 3.4, and 3.5 support.
+- ``Markup.unescape`` uses :func:`html.unescape` to support HTML5
+ character references. :pr:`117`
+
+
+Version 1.1.1
+-------------
+
+Released 2019-02-23
+
+- Fix segfault when ``__html__`` method raises an exception when using
+ the C speedups. The exception is now propagated correctly. :pr:`109`
+
+
+Version 1.1.0
+-------------
+
+Released 2018-11-05
+
+- Drop support for Python 2.6 and 3.3.
+- Build wheels for Linux, Mac, and Windows, allowing systems without
+ a compiler to take advantage of the C extension speedups. :pr:`104`
+- Use newer CPython API on Python 3, resulting in a 1.5x speedup.
+ :pr`64`
+- ``escape`` wraps ``__html__`` result in ``Markup``, consistent with
+ documented behavior. :pr:`69`
+
+
+Version 1.0
+-----------
+
+Released 2017-03-07
+
+- Fixed custom types not invoking ``__unicode__`` when used with
+ ``format()``.
+- Added ``__version__`` module attribute.
+- Improve unescape code to leave lone ampersands alone.
+
+
+Version 0.18
+------------
+
+Released 2013-05-22
+
+- Fixed ``__mul__`` and string splitting on Python 3.
+
+
+Version 0.17
+------------
+
+Released 2013-05-21
+
+- Fixed a bug with broken interpolation on tuples.
+
+
+Version 0.16
+------------
+
+Released 2013-05-20
+
+- Improved Python 3 Support and removed 2to3.
+- Removed support for Python 3.2 and 2.5.
+
+
+Version 0.15
+------------
+
+Released 2011-07-20
+
+- Fixed a typo that caused the library to fail to install on pypy and
+ jython.
+
+
+Version 0.14
+------------
+
+Released 2011-07-20
+
+- Release fix for 0.13.
+
+
+Version 0.13
+------------
+
+Released 2011-07-20
+
+- Do not attempt to compile extension for PyPy or Jython.
+- Work around some 64bit Windows issues.
+
+
+Version 0.12
+------------
+
+Released 2011-02-17
+
+- Improved PyPy compatibility.
diff --git a/LICENSE.rst b/LICENSE.rst
new file mode 100644
index 0000000..9d227a0
--- /dev/null
+++ b/LICENSE.rst
@@ -0,0 +1,28 @@
+Copyright 2010 Pallets
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS 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 THE COPYRIGHT
+HOLDER OR 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.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..278d994
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,7 @@
+include CHANGES.rst
+include tox.ini
+include requirements/*.txt
+graft docs
+prune docs/_build
+graft tests
+global-exclude *.pyc
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..26f2e44
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,68 @@
+MarkupSafe
+==========
+
+MarkupSafe implements a text object that escapes characters so it is
+safe to use in HTML and XML. Characters that have special meanings are
+replaced so that they display as the actual characters. This mitigates
+injection attacks, meaning untrusted user input can safely be displayed
+on a page.
+
+
+Installing
+----------
+
+Install and update using `pip`_:
+
+.. code-block:: text
+
+ pip install -U MarkupSafe
+
+.. _pip: https://pip.pypa.io/en/stable/quickstart/
+
+
+Examples
+--------
+
+.. code-block:: pycon
+
+ >>> from markupsafe import Markup, escape
+
+ >>> # escape replaces special characters and wraps in Markup
+ >>> escape("<script>alert(document.cookie);</script>")
+ Markup('&lt;script&gt;alert(document.cookie);&lt;/script&gt;')
+
+ >>> # wrap in Markup to mark text "safe" and prevent escaping
+ >>> Markup("<strong>Hello</strong>")
+ Markup('<strong>hello</strong>')
+
+ >>> escape(Markup("<strong>Hello</strong>"))
+ Markup('<strong>hello</strong>')
+
+ >>> # Markup is a str subclass
+ >>> # methods and operators escape their arguments
+ >>> template = Markup("Hello <em>{name}</em>")
+ >>> template.format(name='"World"')
+ Markup('Hello <em>&#34;World&#34;</em>')
+
+
+Donate
+------
+
+The Pallets organization develops and supports MarkupSafe and other
+libraries that use it. In order to grow the community of contributors
+and users, and allow the maintainers to devote more time to the
+projects, `please donate today`_.
+
+.. _please donate today: https://palletsprojects.com/donate
+
+
+Links
+-----
+
+* Website: https://palletsprojects.com/p/markupsafe/
+* Documentation: https://markupsafe.palletsprojects.com/
+* Releases: https://pypi.org/project/MarkupSafe/
+* Code: https://github.com/pallets/markupsafe
+* Issue tracker: https://github.com/pallets/markupsafe/issues
+* Test status: https://dev.azure.com/pallets/markupsafe/_build
+* Official chat: https://discord.gg/t6rrQZH
diff --git a/bench/bench_basic.py b/bench/bench_basic.py
new file mode 100644
index 0000000..b67eda4
--- /dev/null
+++ b/bench/bench_basic.py
@@ -0,0 +1,5 @@
+from markupsafe import escape
+
+
+def run():
+ escape("<strong>Hello World!</strong>")
diff --git a/bench/bench_largestring.py b/bench/bench_largestring.py
new file mode 100644
index 0000000..9ced67f
--- /dev/null
+++ b/bench/bench_largestring.py
@@ -0,0 +1,6 @@
+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
new file mode 100644
index 0000000..ad2480c
--- /dev/null
+++ b/bench/bench_long_empty_string.py
@@ -0,0 +1,6 @@
+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
new file mode 100644
index 0000000..35f38da
--- /dev/null
+++ b/bench/bench_long_suffix.py
@@ -0,0 +1,6 @@
+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
new file mode 100644
index 0000000..0664a6f
--- /dev/null
+++ b/bench/bench_short_empty_string.py
@@ -0,0 +1,5 @@
+from markupsafe import escape
+
+
+def run():
+ escape("Hello World!")
diff --git a/bench/runbench.py b/bench/runbench.py
new file mode 100644
index 0000000..f20cd49
--- /dev/null
+++ b/bench/runbench.py
@@ -0,0 +1,38 @@
+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/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..5128596
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,19 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/changes.rst b/docs/changes.rst
new file mode 100644
index 0000000..955deaf
--- /dev/null
+++ b/docs/changes.rst
@@ -0,0 +1,4 @@
+Changes
+=======
+
+.. include:: ../CHANGES.rst
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..881f1ab
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,49 @@
+from pallets_sphinx_themes import get_version
+from pallets_sphinx_themes import ProjectLink
+
+# Project --------------------------------------------------------------
+
+project = "MarkupSafe"
+copyright = "2010 Pallets"
+author = "Pallets"
+release, version = get_version("MarkupSafe")
+
+# General --------------------------------------------------------------
+
+master_doc = "index"
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx.ext.intersphinx",
+ "pallets_sphinx_themes",
+ "sphinxcontrib.log_cabinet",
+ "sphinx_issues",
+]
+intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)}
+issues_github_path = "pallets/markupsafe"
+
+# HTML -----------------------------------------------------------------
+
+html_theme = "jinja"
+html_theme_options = {"index_sidebar_logo": False}
+html_context = {
+ "project_links": [
+ ProjectLink("Donate to Pallets", "https://palletsprojects.com/donate"),
+ ProjectLink("Website", "https://palletsprojects.com/p/markupsafe/"),
+ ProjectLink("PyPI releases", "https://pypi.org/project/MarkupSafe/"),
+ ProjectLink("Source Code", "https://github.com/pallets/markupsafe/"),
+ ProjectLink("Issue Tracker", "https://github.com/pallets/markupsafe/issues/"),
+ ]
+}
+html_sidebars = {
+ "index": ["project.html", "localtoc.html", "searchbox.html"],
+ "**": ["localtoc.html", "relations.html", "searchbox.html"],
+}
+singlehtml_sidebars = {"index": ["project.html", "localtoc.html"]}
+html_title = f"MarkupSafe Documentation ({version})"
+html_show_sourcelink = False
+
+# LaTeX ----------------------------------------------------------------
+
+latex_documents = [
+ (master_doc, f"MarkupSafe-{version}.tex", html_title, author, "manual")
+]
diff --git a/docs/escaping.rst b/docs/escaping.rst
new file mode 100644
index 0000000..9e7000a
--- /dev/null
+++ b/docs/escaping.rst
@@ -0,0 +1,21 @@
+.. module:: markupsafe
+
+Working With Safe Text
+======================
+
+.. autofunction:: escape
+
+.. autoclass:: Markup
+ :members: escape, unescape, striptags
+
+
+Optional Values
+---------------
+
+.. autofunction:: escape_silent
+
+
+Convert an Object to a String
+-----------------------------
+
+.. autofunction:: soft_str
diff --git a/docs/formatting.rst b/docs/formatting.rst
new file mode 100644
index 0000000..c14f917
--- /dev/null
+++ b/docs/formatting.rst
@@ -0,0 +1,77 @@
+.. currentmodule:: markupsafe
+
+String Formatting
+=================
+
+The :class:`Markup` class can be used as a format string. Objects
+formatted into a markup string will be escaped first.
+
+
+Format Method
+-------------
+
+The ``format`` method extends the standard :meth:`str.format` behavior
+to use an ``__html_format__`` method.
+
+#. If an object has an ``__html_format__`` method, it is called as a
+ replacement for the ``__format__`` method. It is passed a format
+ specifier if it's given. The method must return a string or
+ :class:`Markup` instance.
+
+#. If an object has an ``__html__`` method, it is called. If a format
+ specifier was passed and the class defined ``__html__`` but not
+ ``__html_format__``, a ``ValueError`` is raised.
+
+#. Otherwise Python's default format behavior is used and the result
+ is escaped.
+
+For example, to implement a ``User`` that wraps its ``name`` in a
+``span`` tag, and adds a link when using the ``"link"`` format
+specifier:
+
+.. code-block:: python
+
+ class User(object):
+ def __init__(self, id, name):
+ self.id = id
+ self.name = name
+
+ def __html_format__(self, format_spec):
+ if format_spec == "link":
+ return Markup(
+ '<a href="/user/{}">{}</a>'
+ ).format(self.id, self.__html__())
+ elif format_spec:
+ raise ValueError("Invalid format spec")
+ return self.__html__()
+
+ def __html__(self):
+ return Markup(
+ '<span class="user">{0}</span>'
+ ).format(self.name)
+
+
+.. code-block:: pycon
+
+ >>> user = User(3, "<script>")
+ >>> escape(user)
+ Markup('<span class="user">&lt;script&gt;</span>')
+ >>> Markup("<p>User: {user:link}").format(user=user)
+ Markup('<p>User: <a href="/user/3"><span class="user">&lt;script&gt;</span></a>
+
+See Python's docs on :ref:`format string syntax <python:formatstrings>`.
+
+
+printf-style Formatting
+-----------------------
+
+Besides escaping, there's no special behavior involved with percent
+formatting.
+
+.. code-block:: pycon
+
+ >>> user = User(3, "<script>")
+ >>> Markup('<a href="/user/%d">%s</a>') % (user.id, user.name)
+ Markup('<a href="/user/3">&lt;script&gt;</a>')
+
+See Python's docs on :ref:`printf-style formatting <python:old-string-formatting>`.
diff --git a/docs/html.rst b/docs/html.rst
new file mode 100644
index 0000000..dec87af
--- /dev/null
+++ b/docs/html.rst
@@ -0,0 +1,49 @@
+.. currentmodule:: markupsafe
+
+HTML Representations
+====================
+
+In many frameworks, if a class implements an ``__html__`` method it
+will be used to get the object's representation in HTML. MarkupSafe's
+:func:`escape` function and :class:`Markup` class understand and
+implement this method. If an object has an ``__html__`` method it will
+be called rather than converting the object to a string, and the result
+will be assumed safe and not escaped.
+
+For example, an ``Image`` class might automatically generate an
+``<img>`` tag:
+
+.. code-block:: python
+
+ class Image:
+ def __init__(self, url):
+ self.url = url
+
+ def __html__(self):
+ return f'<img src="{self.url}">'
+
+.. code-block:: pycon
+
+ >>> img = Image("/static/logo.png")
+ >>> Markup(img)
+ Markup('<img src="/static/logo.png">')
+
+Since this bypasses escaping, you need to be careful about using
+user-provided data in the output. For example, a user's display name
+should still be escaped:
+
+.. code-block:: python
+
+ class User:
+ def __init__(self, id, name):
+ self.id = id
+ self.name = name
+
+ def __html__(self):
+ return f'<a href="/user/{self.id}">{escape(self.name)}</a>'
+
+.. code-block:: pycon
+
+ >>> user = User(3, "<script>")
+ >>> escape(user)
+ Markup('<a href="/users/3">&lt;script&gt;</a>')
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..5c45e64
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,47 @@
+.. currentmodule:: markupsafe
+
+MarkupSafe
+==========
+
+MarkupSafe escapes characters so text is safe to use in HTML and XML.
+Characters that have special meanings are replaced so that they display
+as the actual characters. This mitigates injection attacks, meaning
+untrusted user input can safely be displayed on a page.
+
+The :func:`escape` function escapes text and returns a :class:`Markup`
+object. The object won't be escaped anymore, but any text that is used
+with it will be, ensuring that the result remains safe to use in HTML.
+
+>>> from markupsafe import escape
+>>> hello = escape("<em>Hello</em>")
+>>> hello
+Markup('&lt;em&gt;Hello&lt;/em&gt;')
+>>> escape(hello)
+Markup('&lt;em&gt;Hello&lt;/em&gt;')
+>>> hello + " <strong>World</strong>"
+Markup('&lt;em&gt;Hello&lt;/em&gt; &lt;strong&gt;World&lt;/strong&gt;')
+
+
+Installing
+----------
+
+Install and update using `pip`_:
+
+.. code-block:: text
+
+ pip install -U MarkupSafe
+
+.. _pip: https://pip.pypa.io/en/stable/quickstart/
+
+
+Table of Contents
+-----------------
+
+.. toctree::
+ :maxdepth: 2
+
+ escaping
+ html
+ formatting
+ license
+ changes
diff --git a/docs/license.rst b/docs/license.rst
new file mode 100644
index 0000000..a53a98c
--- /dev/null
+++ b/docs/license.rst
@@ -0,0 +1,4 @@
+BSD-3-Clause License
+====================
+
+.. include:: ../LICENSE.rst
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 0000000..7893348
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
+
+:end
+popd
diff --git a/requirements/dev.in b/requirements/dev.in
new file mode 100644
index 0000000..bcc48da
--- /dev/null
+++ b/requirements/dev.in
@@ -0,0 +1,5 @@
+# -r docs.in # can't include due to Sphinx/Jinja mutual dependency
+-r tests.in
+pip-tools
+pre-commit
+tox
diff --git a/requirements/dev.txt b/requirements/dev.txt
new file mode 100644
index 0000000..1959dc5
--- /dev/null
+++ b/requirements/dev.txt
@@ -0,0 +1,31 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+# pip-compile requirements/dev.in
+#
+appdirs==1.4.4 # via virtualenv
+attrs==19.3.0 # via pytest
+cfgv==3.1.0 # via pre-commit
+click==7.1.2 # via pip-tools
+distlib==0.3.0 # via virtualenv
+filelock==3.0.12 # via tox, virtualenv
+identify==1.4.16 # via pre-commit
+iniconfig==1.0.0 # via pytest
+more-itertools==8.3.0 # via pytest
+nodeenv==1.3.5 # via pre-commit
+packaging==20.4 # via pytest, tox
+pip-tools==5.3.1 # via -r requirements/dev.in
+pluggy==0.13.1 # via pytest, tox
+pre-commit==2.6.0 # via -r requirements/dev.in
+py==1.9.0 # via pytest, tox
+pyparsing==2.4.7 # via packaging
+pytest==6.0.1 # via -r requirements/tests.in
+pyyaml==5.3.1 # via pre-commit
+six==1.15.0 # via packaging, pip-tools, tox, virtualenv
+toml==0.10.1 # via pre-commit, pytest, tox
+tox==3.19.0 # via -r requirements/dev.in
+virtualenv==20.0.21 # via pre-commit, tox
+
+# The following packages are considered to be unsafe in a requirements file:
+# pip
diff --git a/requirements/docs.in b/requirements/docs.in
new file mode 100644
index 0000000..7ec501b
--- /dev/null
+++ b/requirements/docs.in
@@ -0,0 +1,4 @@
+Pallets-Sphinx-Themes
+Sphinx
+sphinx-issues
+sphinxcontrib-log-cabinet
diff --git a/requirements/docs.txt b/requirements/docs.txt
new file mode 100644
index 0000000..9b6ca1d
--- /dev/null
+++ b/requirements/docs.txt
@@ -0,0 +1,36 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+# pip-compile requirements/docs.in
+#
+alabaster==0.7.12 # via sphinx
+babel==2.8.0 # via sphinx
+certifi==2020.4.5.1 # via requests
+chardet==3.0.4 # via requests
+docutils==0.16 # via sphinx
+idna==2.9 # via requests
+imagesize==1.2.0 # via sphinx
+jinja2==2.11.2 # via sphinx
+markupsafe==1.1.1 # via jinja2
+packaging==20.4 # via pallets-sphinx-themes, sphinx
+pallets-sphinx-themes==1.2.3 # via -r requirements/docs.in
+pygments==2.6.1 # via sphinx
+pyparsing==2.4.7 # via packaging
+pytz==2020.1 # via babel
+requests==2.23.0 # via sphinx
+six==1.15.0 # via packaging
+snowballstemmer==2.0.0 # via sphinx
+sphinx-issues==1.2.0 # via -r requirements/docs.in
+sphinx==3.2.0 # via -r requirements/docs.in, pallets-sphinx-themes, sphinx-issues, sphinxcontrib-log-cabinet
+sphinxcontrib-applehelp==1.0.2 # via sphinx
+sphinxcontrib-devhelp==1.0.2 # via sphinx
+sphinxcontrib-htmlhelp==1.0.3 # via sphinx
+sphinxcontrib-jsmath==1.0.1 # via sphinx
+sphinxcontrib-log-cabinet==1.0.1 # via -r requirements/docs.in
+sphinxcontrib-qthelp==1.0.3 # via sphinx
+sphinxcontrib-serializinghtml==1.1.4 # via sphinx
+urllib3==1.25.9 # via requests
+
+# The following packages are considered to be unsafe in a requirements file:
+# setuptools
diff --git a/requirements/tests.in b/requirements/tests.in
new file mode 100644
index 0000000..e079f8a
--- /dev/null
+++ b/requirements/tests.in
@@ -0,0 +1 @@
+pytest
diff --git a/requirements/tests.txt b/requirements/tests.txt
new file mode 100644
index 0000000..233bc81
--- /dev/null
+++ b/requirements/tests.txt
@@ -0,0 +1,16 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+# pip-compile requirements/tests.in
+#
+attrs==19.3.0 # via pytest
+iniconfig==1.0.0 # via pytest
+more-itertools==8.3.0 # via pytest
+packaging==20.4 # via pytest
+pluggy==0.13.1 # via pytest
+py==1.9.0 # via pytest
+pyparsing==2.4.7 # via packaging
+pytest==6.0.1 # via -r requirements/tests.in
+six==1.15.0 # via packaging
+toml==0.10.1 # via pytest
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..08801c7
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,70 @@
+[metadata]
+name = MarkupSafe
+version = attr: markupsafe.__version__
+url = https://palletsprojects.com/p/markupsafe/
+project_urls =
+ Documentation = https://markupsafe.palletsprojects.com/
+ Code = https://github.com/pallets/markupsafe
+ Issue tracker = https://github.com/pallets/markupsafe/issues
+license = BSD-3-Clause
+license_files = LICENSE.rst
+author = Armin Ronacher
+author_email = armin.ronacher@active-4.com
+maintainer = Pallets
+maintainer_email = contact@palletsprojects.com
+description = Safely add untrusted strings to HTML/XML markup.
+long_description = file: README.rst
+long_description_content_type = text/x-rst
+classifiers =
+ Development Status :: 5 - Production/Stable
+ Environment :: Web Environment
+ Intended Audience :: Developers
+ License :: OSI Approved :: BSD License
+ Operating System :: OS Independent
+ Programming Language :: Python
+ Topic :: Internet :: WWW/HTTP :: Dynamic Content
+ Topic :: Text Processing :: Markup :: HTML
+
+[options]
+packages = find:
+package_dir = = src
+include_package_data = true
+python_requires = >= 3.6
+
+[options.packages.find]
+where = src
+
+[tool:pytest]
+testpaths = tests
+filterwarnings =
+ error
+
+[coverage:run]
+branch = true
+source =
+ markupsafe
+ tests
+
+[coverage:paths]
+source =
+ src
+ */site-packages
+
+[flake8]
+# B = bugbear
+# E = pycodestyle errors
+# F = flake8 pyflakes
+# W = pycodestyle warnings
+# B9 = bugbear opinions
+select = B, E, F, W, B9
+ignore =
+ # slice notation whitespace, invalid
+ E203
+ # line length, handled by bugbear B950
+ E501
+ # bare except, handled by bugbear B001
+ E722
+ # bin op line break, invalid
+ W503
+# up to 88 allowed by bugbear B950
+max-line-length = 80
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..2a8743f
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,79 @@
+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
+
+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:
+ raise BuildFailed()
+
+ def build_extension(self, ext):
+ try:
+ build_ext.build_extension(self, ext)
+ except (CCompilerError, DistutilsExecError, DistutilsPlatformError):
+ raise BuildFailed()
+ except ValueError:
+ # 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()
+ raise
+
+
+def run_setup(with_binary):
+ setup(
+ name="MarkupSafe",
+ 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)
+
+
+if os.environ.get("CIBUILDWHEEL", "0") == "1":
+ run_setup(True)
+elif platform.python_implementation() not in {"PyPy", "Jython"}:
+ 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.",
+ )
diff --git a/src/markupsafe/__init__.py b/src/markupsafe/__init__.py
new file mode 100644
index 0000000..54d32bf
--- /dev/null
+++ b/src/markupsafe/__init__.py
@@ -0,0 +1,281 @@
+import re
+import string
+from collections import abc
+
+__version__ = "2.0.0a1"
+
+_striptags_re = re.compile(r"(<!--.*?-->|<[^>]*>)")
+
+
+class Markup(str):
+ """A string that is ready to be safely inserted into an HTML or XML
+ document, either because it was escaped or because it was marked
+ safe.
+
+ Passing an object to the constructor converts it to text and wraps
+ it to mark it safe without escaping. To escape the text, use the
+ :meth:`escape` class method instead.
+
+ >>> Markup("Hello, <em>World</em>!")
+ Markup('Hello, <em>World</em>!')
+ >>> Markup(42)
+ Markup('42')
+ >>> Markup.escape("Hello, <em>World</em>!")
+ Markup('Hello &lt;em&gt;World&lt;/em&gt;!')
+
+ This implements the ``__html__()`` interface that some frameworks
+ use. Passing an object that implements ``__html__()`` will wrap the
+ output of that method, marking it safe.
+
+ >>> class Foo:
+ ... def __html__(self):
+ ... return '<a href="/foo">foo</a>'
+ ...
+ >>> Markup(Foo())
+ Markup('<a href="/foo">foo</a>')
+
+ This is a subclass of :class:`str`. It has the same methods, but
+ escapes their arguments and returns a ``Markup`` instance.
+
+ >>> Markup("<em>%s</em>") % ("foo & bar",)
+ Markup('<em>foo &amp; bar</em>')
+ >>> Markup("<em>Hello</em> ") + "<foo>"
+ Markup('<em>Hello</em> &lt;foo&gt;')
+ """
+
+ __slots__ = ()
+
+ def __new__(cls, base="", encoding=None, errors="strict"):
+ if hasattr(base, "__html__"):
+ base = base.__html__()
+ if encoding is None:
+ return super().__new__(cls, base)
+ return super().__new__(cls, base, encoding, errors)
+
+ def __html__(self):
+ return self
+
+ def __add__(self, other):
+ if isinstance(other, str) or hasattr(other, "__html__"):
+ return self.__class__(super().__add__(self.escape(other)))
+ return NotImplemented
+
+ def __radd__(self, other):
+ if isinstance(other, str) or hasattr(other, "__html__"):
+ return self.escape(other).__add__(self)
+ return NotImplemented
+
+ def __mul__(self, num):
+ if isinstance(num, int):
+ return self.__class__(super().__mul__(num))
+ return NotImplemented
+
+ __rmul__ = __mul__
+
+ def __mod__(self, arg):
+ if isinstance(arg, tuple):
+ arg = tuple(_MarkupEscapeHelper(x, self.escape) for x in arg)
+ else:
+ arg = _MarkupEscapeHelper(arg, self.escape)
+ return self.__class__(super().__mod__(arg))
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}({super().__repr__()})"
+
+ def join(self, seq):
+ return self.__class__(super().join(map(self.escape, seq)))
+
+ join.__doc__ = str.join.__doc__
+
+ def split(self, *args, **kwargs):
+ return list(map(self.__class__, super().split(*args, **kwargs)))
+
+ split.__doc__ = str.split.__doc__
+
+ def rsplit(self, *args, **kwargs):
+ return list(map(self.__class__, super().rsplit(*args, **kwargs)))
+
+ rsplit.__doc__ = str.rsplit.__doc__
+
+ def splitlines(self, *args, **kwargs):
+ return list(map(self.__class__, super().splitlines(*args, **kwargs)))
+
+ splitlines.__doc__ = str.splitlines.__doc__
+
+ def unescape(self):
+ """Convert escaped markup back into a text string. This replaces
+ HTML entities with the characters they represent.
+
+ >>> Markup("Main &raquo; <em>About</em>").unescape()
+ 'Main » <em>About</em>'
+ """
+ from html import unescape
+
+ return unescape(str(self))
+
+ def striptags(self):
+ """:meth:`unescape` the markup, remove tags, and normalize
+ whitespace to single spaces.
+
+ >>> Markup("Main &raquo;\t<em>About</em>").striptags()
+ 'Main » About'
+ """
+ stripped = " ".join(_striptags_re.sub("", self).split())
+ return Markup(stripped).unescape()
+
+ @classmethod
+ def escape(cls, s):
+ """Escape a string. Calls :func:`escape` and ensures that for
+ subclasses the correct type is returned.
+ """
+ rv = escape(s)
+ if rv.__class__ is not cls:
+ return cls(rv)
+ return rv
+
+ def make_simple_escaping_wrapper(name): # noqa: B902
+ orig = getattr(str, name)
+
+ def func(self, *args, **kwargs):
+ args = _escape_argspec(list(args), enumerate(args), self.escape)
+ _escape_argspec(kwargs, kwargs.items(), self.escape)
+ return self.__class__(orig(self, *args, **kwargs))
+
+ func.__name__ = orig.__name__
+ func.__doc__ = orig.__doc__
+ return func
+
+ for method in (
+ "__getitem__",
+ "capitalize",
+ "title",
+ "lower",
+ "upper",
+ "replace",
+ "ljust",
+ "rjust",
+ "lstrip",
+ "rstrip",
+ "center",
+ "strip",
+ "translate",
+ "expandtabs",
+ "swapcase",
+ "zfill",
+ ):
+ locals()[method] = make_simple_escaping_wrapper(method)
+
+ del method, make_simple_escaping_wrapper
+
+ def partition(self, sep):
+ return tuple(map(self.__class__, super().partition(self.escape(sep))))
+
+ def rpartition(self, sep):
+ return tuple(map(self.__class__, super().rpartition(self.escape(sep))))
+
+ def format(self, *args, **kwargs):
+ formatter = EscapeFormatter(self.escape)
+ kwargs = _MagicFormatMapping(args, kwargs)
+ return self.__class__(formatter.vformat(self, args, kwargs))
+
+ def __html_format__(self, format_spec):
+ if format_spec:
+ raise ValueError("Unsupported format specification for Markup.")
+ return self
+
+
+class _MagicFormatMapping(abc.Mapping):
+ """This class implements a dummy wrapper to fix a bug in the Python
+ standard library for string formatting.
+
+ See http://bugs.python.org/issue13598 for information about why
+ this is necessary.
+ """
+
+ def __init__(self, args, kwargs):
+ self._args = args
+ self._kwargs = kwargs
+ self._last_index = 0
+
+ def __getitem__(self, key):
+ if key == "":
+ idx = self._last_index
+ self._last_index += 1
+ try:
+ return self._args[idx]
+ except LookupError:
+ pass
+ key = str(idx)
+ return self._kwargs[key]
+
+ def __iter__(self):
+ return iter(self._kwargs)
+
+ def __len__(self):
+ return len(self._kwargs)
+
+
+class EscapeFormatter(string.Formatter):
+ def __init__(self, escape):
+ self.escape = escape
+
+ def format_field(self, value, format_spec):
+ if hasattr(value, "__html_format__"):
+ rv = value.__html_format__(format_spec)
+ elif hasattr(value, "__html__"):
+ if format_spec:
+ raise ValueError(
+ f"Format specifier {format_spec} given, but {type(value)} does not"
+ " define __html_format__. A class that defines __html__ must define"
+ " __html_format__ to work with format specifiers."
+ )
+ rv = value.__html__()
+ else:
+ # We need to make sure the format spec is str here as
+ # otherwise the wrong callback methods are invoked.
+ rv = string.Formatter.format_field(self, value, str(format_spec))
+ return str(self.escape(rv))
+
+
+def _escape_argspec(obj, iterable, escape):
+ """Helper for various string-wrapped functions."""
+ for key, value in iterable:
+ if isinstance(value, str) or hasattr(value, "__html__"):
+ obj[key] = escape(value)
+ return obj
+
+
+class _MarkupEscapeHelper:
+ """Helper for :meth:`Markup.__mod__`."""
+
+ def __init__(self, obj, escape):
+ self.obj = obj
+ self.escape = escape
+
+ def __getitem__(self, item):
+ return _MarkupEscapeHelper(self.obj[item], self.escape)
+
+ def __str__(self):
+ return str(self.escape(self.obj))
+
+ def __repr__(self):
+ return str(self.escape(repr(self.obj)))
+
+ def __int__(self):
+ return int(self.obj)
+
+ def __float__(self):
+ return float(self.obj)
+
+
+# circular import
+try:
+ from ._speedups import escape
+ from ._speedups import escape_silent
+ from ._speedups import soft_str
+ from ._speedups import soft_unicode
+except ImportError:
+ from ._native import escape
+ from ._native import escape_silent # noqa: F401
+ from ._native import soft_str # noqa: F401
+ from ._native import soft_unicode # noqa: F401
diff --git a/src/markupsafe/_native.py b/src/markupsafe/_native.py
new file mode 100644
index 0000000..7722296
--- /dev/null
+++ b/src/markupsafe/_native.py
@@ -0,0 +1,70 @@
+from . import Markup
+
+
+def escape(s):
+ """Replace the characters ``&``, ``<``, ``>``, ``'``, and ``"`` in
+ the string with HTML-safe sequences. Use this if you need to display
+ text that might contain such characters in HTML.
+
+ If the object has an ``__html__`` method, it is called and the
+ return value is assumed to already be safe for HTML.
+
+ :param s: An object to be converted to a string and escaped.
+ :return: A :class:`Markup` string with the escaped text.
+ """
+ if hasattr(s, "__html__"):
+ return Markup(s.__html__())
+ return Markup(
+ str(s)
+ .replace("&", "&amp;")
+ .replace(">", "&gt;")
+ .replace("<", "&lt;")
+ .replace("'", "&#39;")
+ .replace('"', "&#34;")
+ )
+
+
+def escape_silent(s):
+ """Like :func:`escape` but treats ``None`` as the empty string.
+ Useful with optional values, as otherwise you get the string
+ ``'None'`` when the value is ``None``.
+
+ >>> escape(None)
+ Markup('None')
+ >>> escape_silent(None)
+ Markup('')
+ """
+ if s is None:
+ return Markup()
+ return escape(s)
+
+
+def soft_str(s):
+ """Convert an object to a string if it isn't already. This preserves
+ a :class:`Markup` string rather than converting it back to a basic
+ string, so it will still be marked as safe and won't be escaped
+ again.
+
+ >>> value = escape("<User 1>")
+ >>> value
+ Markup('&lt;User 1&gt;')
+ >>> escape(str(value))
+ Markup('&amp;lt;User 1&amp;gt;')
+ >>> escape(soft_str(value))
+ Markup('&lt;User 1&gt;')
+ """
+ if not isinstance(s, str):
+ return str(s)
+ return s
+
+
+def soft_unicode(s):
+ import warnings
+
+ warnings.warn(
+ "'soft_unicode' has been renamed to 'soft_str'. The old name"
+ " will be removed in version 2.1.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return soft_str(s)
diff --git a/src/markupsafe/_speedups.c b/src/markupsafe/_speedups.c
new file mode 100644
index 0000000..7b7e57f
--- /dev/null
+++ b/src/markupsafe/_speedups.c
@@ -0,0 +1,339 @@
+#include <Python.h>
+
+static PyObject* markup;
+
+static int
+init_constants(void)
+{
+ PyObject *module;
+
+ /* import markup type so that we can mark the return value */
+ module = PyImport_ImportModule("markupsafe");
+ if (!module)
+ return 0;
+ markup = PyObject_GetAttrString(module, "Markup");
+ Py_DECREF(module);
+
+ return 1;
+}
+
+#define GET_DELTA(inp, inp_end, delta) \
+ while (inp < inp_end) { \
+ switch (*inp++) { \
+ case '"': \
+ case '\'': \
+ case '&': \
+ delta += 4; \
+ break; \
+ case '<': \
+ case '>': \
+ delta += 3; \
+ break; \
+ } \
+ }
+
+#define DO_ESCAPE(inp, inp_end, outp) \
+ { \
+ Py_ssize_t ncopy = 0; \
+ while (inp < inp_end) { \
+ switch (*inp) { \
+ case '"': \
+ memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
+ outp += ncopy; ncopy = 0; \
+ *outp++ = '&'; \
+ *outp++ = '#'; \
+ *outp++ = '3'; \
+ *outp++ = '4'; \
+ *outp++ = ';'; \
+ break; \
+ case '\'': \
+ memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
+ outp += ncopy; ncopy = 0; \
+ *outp++ = '&'; \
+ *outp++ = '#'; \
+ *outp++ = '3'; \
+ *outp++ = '9'; \
+ *outp++ = ';'; \
+ break; \
+ case '&': \
+ memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
+ outp += ncopy; ncopy = 0; \
+ *outp++ = '&'; \
+ *outp++ = 'a'; \
+ *outp++ = 'm'; \
+ *outp++ = 'p'; \
+ *outp++ = ';'; \
+ break; \
+ case '<': \
+ memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
+ outp += ncopy; ncopy = 0; \
+ *outp++ = '&'; \
+ *outp++ = 'l'; \
+ *outp++ = 't'; \
+ *outp++ = ';'; \
+ break; \
+ case '>': \
+ memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
+ outp += ncopy; ncopy = 0; \
+ *outp++ = '&'; \
+ *outp++ = 'g'; \
+ *outp++ = 't'; \
+ *outp++ = ';'; \
+ break; \
+ default: \
+ ncopy++; \
+ } \
+ inp++; \
+ } \
+ memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
+ }
+
+static PyObject*
+escape_unicode_kind1(PyUnicodeObject *in)
+{
+ Py_UCS1 *inp = PyUnicode_1BYTE_DATA(in);
+ Py_UCS1 *inp_end = inp + PyUnicode_GET_LENGTH(in);
+ Py_UCS1 *outp;
+ PyObject *out;
+ Py_ssize_t delta = 0;
+
+ GET_DELTA(inp, inp_end, delta);
+ if (!delta) {
+ Py_INCREF(in);
+ return (PyObject*)in;
+ }
+
+ out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta,
+ PyUnicode_IS_ASCII(in) ? 127 : 255);
+ if (!out)
+ return NULL;
+
+ inp = PyUnicode_1BYTE_DATA(in);
+ outp = PyUnicode_1BYTE_DATA(out);
+ DO_ESCAPE(inp, inp_end, outp);
+ return out;
+}
+
+static PyObject*
+escape_unicode_kind2(PyUnicodeObject *in)
+{
+ Py_UCS2 *inp = PyUnicode_2BYTE_DATA(in);
+ Py_UCS2 *inp_end = inp + PyUnicode_GET_LENGTH(in);
+ Py_UCS2 *outp;
+ PyObject *out;
+ Py_ssize_t delta = 0;
+
+ GET_DELTA(inp, inp_end, delta);
+ if (!delta) {
+ Py_INCREF(in);
+ return (PyObject*)in;
+ }
+
+ out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta, 65535);
+ if (!out)
+ return NULL;
+
+ inp = PyUnicode_2BYTE_DATA(in);
+ outp = PyUnicode_2BYTE_DATA(out);
+ DO_ESCAPE(inp, inp_end, outp);
+ return out;
+}
+
+
+static PyObject*
+escape_unicode_kind4(PyUnicodeObject *in)
+{
+ Py_UCS4 *inp = PyUnicode_4BYTE_DATA(in);
+ Py_UCS4 *inp_end = inp + PyUnicode_GET_LENGTH(in);
+ Py_UCS4 *outp;
+ PyObject *out;
+ Py_ssize_t delta = 0;
+
+ GET_DELTA(inp, inp_end, delta);
+ if (!delta) {
+ Py_INCREF(in);
+ return (PyObject*)in;
+ }
+
+ out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta, 1114111);
+ if (!out)
+ return NULL;
+
+ inp = PyUnicode_4BYTE_DATA(in);
+ outp = PyUnicode_4BYTE_DATA(out);
+ DO_ESCAPE(inp, inp_end, outp);
+ return out;
+}
+
+static PyObject*
+escape_unicode(PyUnicodeObject *in)
+{
+ if (PyUnicode_READY(in))
+ return NULL;
+
+ switch (PyUnicode_KIND(in)) {
+ case PyUnicode_1BYTE_KIND:
+ return escape_unicode_kind1(in);
+ case PyUnicode_2BYTE_KIND:
+ return escape_unicode_kind2(in);
+ case PyUnicode_4BYTE_KIND:
+ return escape_unicode_kind4(in);
+ }
+ assert(0); /* shouldn't happen */
+ return NULL;
+}
+
+static PyObject*
+escape(PyObject *self, PyObject *text)
+{
+ static PyObject *id_html;
+ PyObject *s = NULL, *rv = NULL, *html;
+
+ if (id_html == NULL) {
+ id_html = PyUnicode_InternFromString("__html__");
+ if (id_html == NULL) {
+ return NULL;
+ }
+ }
+
+ /* we don't have to escape integers, bools or floats */
+ if (PyLong_CheckExact(text) ||
+ PyFloat_CheckExact(text) || PyBool_Check(text) ||
+ text == Py_None)
+ return PyObject_CallFunctionObjArgs(markup, text, NULL);
+
+ /* if the object has an __html__ method that performs the escaping */
+ html = PyObject_GetAttr(text ,id_html);
+ if (html) {
+ s = PyObject_CallObject(html, NULL);
+ Py_DECREF(html);
+ if (s == NULL) {
+ return NULL;
+ }
+ /* Convert to Markup object */
+ rv = PyObject_CallFunctionObjArgs(markup, (PyObject*)s, NULL);
+ Py_DECREF(s);
+ return rv;
+ }
+
+ /* otherwise make the object unicode if it isn't, then escape */
+ PyErr_Clear();
+ if (!PyUnicode_Check(text)) {
+ PyObject *unicode = PyObject_Str(text);
+ if (!unicode)
+ return NULL;
+ s = escape_unicode((PyUnicodeObject*)unicode);
+ Py_DECREF(unicode);
+ }
+ else
+ s = escape_unicode((PyUnicodeObject*)text);
+
+ /* convert the unicode string into a markup object. */
+ rv = PyObject_CallFunctionObjArgs(markup, (PyObject*)s, NULL);
+ Py_DECREF(s);
+ return rv;
+}
+
+
+static PyObject*
+escape_silent(PyObject *self, PyObject *text)
+{
+ if (text != Py_None)
+ return escape(self, text);
+ return PyObject_CallFunctionObjArgs(markup, NULL);
+}
+
+
+static PyObject*
+soft_str(PyObject *self, PyObject *s)
+{
+ if (!PyUnicode_Check(s))
+ return PyObject_Str(s);
+ Py_INCREF(s);
+ return s;
+}
+
+
+static PyObject*
+soft_unicode(PyObject *self, PyObject *s)
+{
+ PyErr_WarnEx(
+ PyExc_DeprecationWarning,
+ "'soft_unicode' has been renamed to 'soft_str'. The old name"
+ " will be removed in version 2.1.",
+ 2
+ );
+ return soft_str(self, s);
+}
+
+
+static PyMethodDef module_methods[] = {
+ {
+ "escape",
+ (PyCFunction)escape,
+ METH_O,
+ "Replace the characters ``&``, ``<``, ``>``, ``'``, and ``\"`` in"
+ " the string with HTML-safe sequences. Use this if you need to display"
+ " text that might contain such characters in HTML.\n\n"
+ "If the object has an ``__html__`` method, it is called and the"
+ " return value is assumed to already be safe for HTML.\n\n"
+ ":param s: An object to be converted to a string and escaped.\n"
+ ":return: A :class:`Markup` string with the escaped text.\n"
+ },
+ {
+ "escape_silent",
+ (PyCFunction)escape_silent,
+ METH_O,
+ "Like :func:`escape` but treats ``None`` as the empty string."
+ " Useful with optional values, as otherwise you get the string"
+ " ``'None'`` when the value is ``None``.\n\n"
+ ">>> escape(None)\n"
+ "Markup('None')\n"
+ ">>> escape_silent(None)\n"
+ "Markup('')\n"
+ },
+ {
+ "soft_str",
+ (PyCFunction)soft_str,
+ METH_O,
+ "Convert an object to a string if it isn't already. This preserves"
+ " a :class:`Markup` string rather than converting it back to a basic"
+ " string, so it will still be marked as safe and won't be escaped"
+ " again.\n\n"
+ ">>> value = escape(\"<User 1>\")\n"
+ ">>> value\n"
+ "Markup('&lt;User 1&gt;')\n"
+ ">>> escape(str(value))\n"
+ "Markup('&amp;lt;User 1&amp;gt;')\n"
+ ">>> escape(soft_str(value))\n"
+ "Markup('&lt;User 1&gt;')\n"
+ },
+ {
+ "soft_unicode",
+ (PyCFunction)soft_unicode,
+ METH_O,
+ ""
+ },
+ {NULL, NULL, 0, NULL} /* Sentinel */
+};
+
+static struct PyModuleDef module_definition = {
+ PyModuleDef_HEAD_INIT,
+ "markupsafe._speedups",
+ NULL,
+ -1,
+ module_methods,
+ NULL,
+ NULL,
+ NULL,
+ NULL
+};
+
+PyMODINIT_FUNC
+PyInit__speedups(void)
+{
+ if (!init_constants())
+ return NULL;
+
+ return PyModule_Create(&module_definition);
+}
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..7141547
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,42 @@
+import pytest
+
+from markupsafe import _native
+
+try:
+ from markupsafe import _speedups
+except ImportError:
+ _speedups = None
+
+
+@pytest.fixture(
+ scope="session",
+ params=(
+ _native,
+ pytest.param(
+ _speedups,
+ marks=pytest.mark.skipif(_speedups is None, reason="speedups unavailable"),
+ ),
+ ),
+)
+def _mod(request):
+ return request.param
+
+
+@pytest.fixture(scope="session")
+def escape(_mod):
+ return _mod.escape
+
+
+@pytest.fixture(scope="session")
+def escape_silent(_mod):
+ return _mod.escape_silent
+
+
+@pytest.fixture(scope="session")
+def soft_str(_mod):
+ return _mod.soft_str
+
+
+@pytest.fixture(scope="session")
+def soft_unicode(_mod):
+ return _mod.soft_unicode
diff --git a/tests/test_escape.py b/tests/test_escape.py
new file mode 100644
index 0000000..bf53fac
--- /dev/null
+++ b/tests/test_escape.py
@@ -0,0 +1,29 @@
+import pytest
+
+from markupsafe import Markup
+
+
+@pytest.mark.parametrize(
+ ("value", "expect"),
+ (
+ # empty
+ ("", ""),
+ # ascii
+ ("abcd&><'\"efgh", "abcd&amp;&gt;&lt;&#39;&#34;efgh"),
+ ("&><'\"efgh", "&amp;&gt;&lt;&#39;&#34;efgh"),
+ ("abcd&><'\"", "abcd&amp;&gt;&lt;&#39;&#34;"),
+ # 2 byte
+ ("こんにちは&><'\"こんばんは", "こんにちは&amp;&gt;&lt;&#39;&#34;こんばんは"),
+ ("&><'\"こんばんは", "&amp;&gt;&lt;&#39;&#34;こんばんは"),
+ ("こんにちは&><'\"", "こんにちは&amp;&gt;&lt;&#39;&#34;"),
+ # 4 byte
+ (
+ "\U0001F363\U0001F362&><'\"\U0001F37A xyz",
+ "\U0001F363\U0001F362&amp;&gt;&lt;&#39;&#34;\U0001F37A xyz",
+ ),
+ ("&><'\"\U0001F37A xyz", "&amp;&gt;&lt;&#39;&#34;\U0001F37A xyz"),
+ ("\U0001F363\U0001F362&><'\"", "\U0001F363\U0001F362&amp;&gt;&lt;&#39;&#34;"),
+ ),
+)
+def test_escape(escape, value, expect):
+ assert escape(value) == Markup(expect)
diff --git a/tests/test_exception_custom_html.py b/tests/test_exception_custom_html.py
new file mode 100644
index 0000000..ec2f10b
--- /dev/null
+++ b/tests/test_exception_custom_html.py
@@ -0,0 +1,18 @@
+import pytest
+
+
+class CustomHtmlThatRaises:
+ def __html__(self):
+ raise ValueError(123)
+
+
+def test_exception_custom_html(escape):
+ """Checks whether exceptions in custom __html__ implementations are
+ propagated correctly.
+
+ There was a bug in the native implementation at some point:
+ https://github.com/pallets/markupsafe/issues/108
+ """
+ obj = CustomHtmlThatRaises()
+ with pytest.raises(ValueError):
+ escape(obj)
diff --git a/tests/test_leak.py b/tests/test_leak.py
new file mode 100644
index 0000000..55b10b9
--- /dev/null
+++ b/tests/test_leak.py
@@ -0,0 +1,28 @@
+import gc
+import platform
+
+import pytest
+
+from markupsafe import escape
+
+
+@pytest.mark.skipif(
+ escape.__module__ == "markupsafe._native",
+ reason="only test memory leak with speedups",
+)
+def test_markup_leaks():
+ counts = set()
+
+ for _i in range(20):
+ for _j in range(1000):
+ escape("foo")
+ escape("<foo>")
+ escape("foo")
+ escape("<foo>")
+
+ if platform.python_implementation() == "PyPy":
+ gc.collect()
+
+ counts.add(len(gc.get_objects()))
+
+ assert len(counts) == 1
diff --git a/tests/test_markupsafe.py b/tests/test_markupsafe.py
new file mode 100644
index 0000000..fcd9347
--- /dev/null
+++ b/tests/test_markupsafe.py
@@ -0,0 +1,185 @@
+import pytest
+
+from markupsafe import Markup
+
+
+def test_adding(escape):
+ unsafe = '<script type="application/x-some-script">alert("foo");</script>'
+ safe = Markup("<em>username</em>")
+ assert unsafe + safe == str(escape(unsafe)) + str(safe)
+
+
+@pytest.mark.parametrize(
+ ("template", "data", "expect"),
+ (
+ ("<em>%s</em>", "<bad user>", "<em>&lt;bad user&gt;</em>"),
+ (
+ "<em>%(username)s</em>",
+ {"username": "<bad user>"},
+ "<em>&lt;bad user&gt;</em>",
+ ),
+ ("%i", 3.14, "3"),
+ ("%.2f", 3.14, "3.14"),
+ ),
+)
+def test_string_interpolation(template, data, expect):
+ assert Markup(template) % data == expect
+
+
+def test_type_behavior():
+ assert type(Markup("foo") + "bar") is Markup
+ x = Markup("foo")
+ assert x.__html__() is x
+
+
+def test_html_interop():
+ class Foo:
+ def __html__(self):
+ return "<em>awesome</em>"
+
+ def __str__(self):
+ return "awesome"
+
+ assert Markup(Foo()) == "<em>awesome</em>"
+ result = Markup("<strong>%s</strong>") % Foo()
+ assert result == "<strong><em>awesome</em></strong>"
+
+
+def test_tuple_interpol():
+ result = Markup("<em>%s:%s</em>") % ("<foo>", "<bar>")
+ expect = Markup("<em>&lt;foo&gt;:&lt;bar&gt;</em>")
+ assert result == expect
+
+
+def test_dict_interpol():
+ result = Markup("<em>%(foo)s</em>") % {"foo": "<foo>"}
+ expect = Markup("<em>&lt;foo&gt;</em>")
+ assert result == expect
+
+ result = Markup("<em>%(foo)s:%(bar)s</em>") % {"foo": "<foo>", "bar": "<bar>"}
+ expect = Markup("<em>&lt;foo&gt;:&lt;bar&gt;</em>")
+ assert result == expect
+
+
+def test_escaping(escape):
+ assert escape("\"<>&'") == "&#34;&lt;&gt;&amp;&#39;"
+ assert Markup("<em>Foo &amp; Bar</em>").striptags() == "Foo & Bar"
+
+
+def test_unescape():
+ assert Markup("&lt;test&gt;").unescape() == "<test>"
+
+ result = Markup("jack & tavi are cooler than mike &amp; russ").unescape()
+ expect = "jack & tavi are cooler than mike & russ"
+ assert result == expect
+
+ original = "&foo&#x3b;"
+ once = Markup(original).unescape()
+ twice = Markup(once).unescape()
+ expect = "&foo;"
+ assert once == expect
+ assert twice == expect
+
+
+def test_format():
+ result = Markup("<em>{awesome}</em>").format(awesome="<awesome>")
+ assert result == "<em>&lt;awesome&gt;</em>"
+
+ result = Markup("{0[1][bar]}").format([0, {"bar": "<bar/>"}])
+ assert result == "&lt;bar/&gt;"
+
+ result = Markup("{0[1][bar]}").format([0, {"bar": Markup("<bar/>")}])
+ assert result == "<bar/>"
+
+
+def test_formatting_empty():
+ formatted = Markup("{}").format(0)
+ assert formatted == Markup("0")
+
+
+def test_custom_formatting():
+ class HasHTMLOnly:
+ def __html__(self):
+ return Markup("<foo>")
+
+ class HasHTMLAndFormat:
+ def __html__(self):
+ return Markup("<foo>")
+
+ def __html_format__(self, spec):
+ return Markup("<FORMAT>")
+
+ assert Markup("{0}").format(HasHTMLOnly()) == Markup("<foo>")
+ assert Markup("{0}").format(HasHTMLAndFormat()) == Markup("<FORMAT>")
+
+
+def test_complex_custom_formatting():
+ class User:
+ def __init__(self, id, username):
+ self.id = id
+ self.username = username
+
+ def __html_format__(self, format_spec):
+ if format_spec == "link":
+ return Markup('<a href="/user/{0}">{1}</a>').format(
+ self.id, self.__html__()
+ )
+ elif format_spec:
+ raise ValueError("Invalid format spec")
+
+ return self.__html__()
+
+ def __html__(self):
+ return Markup("<span class=user>{0}</span>").format(self.username)
+
+ user = User(1, "foo")
+ result = Markup("<p>User: {0:link}").format(user)
+ expect = Markup('<p>User: <a href="/user/1"><span class=user>foo</span></a>')
+ assert result == expect
+
+
+def test_formatting_with_objects():
+ class Stringable:
+ def __str__(self):
+ return "строка"
+
+ assert Markup("{s}").format(s=Stringable()) == Markup("строка")
+
+
+def test_escape_silent(escape, escape_silent):
+ assert escape_silent(None) == Markup()
+ assert escape(None) == Markup(None)
+ assert escape_silent("<foo>") == Markup("&lt;foo&gt;")
+
+
+def test_splitting():
+ expect = [Markup("a"), Markup("b")]
+ assert Markup("a b").split() == expect
+ assert Markup("a b").rsplit() == expect
+ assert Markup("a\nb").splitlines() == expect
+
+
+def test_mul():
+ assert Markup("a") * 3 == Markup("aaa")
+
+
+def test_escape_return_type(escape):
+ assert isinstance(escape("a"), Markup)
+ assert isinstance(escape(Markup("a")), Markup)
+
+ class Foo:
+ def __html__(self):
+ return "<strong>Foo</strong>"
+
+ assert isinstance(escape(Foo()), Markup)
+
+
+def test_soft_str(soft_str):
+ assert type(soft_str("")) is str
+ assert type(soft_str(Markup())) is Markup
+ assert type(soft_str(15)) is str
+
+
+def test_soft_unicode_deprecated(soft_unicode):
+ with pytest.warns(DeprecationWarning):
+ assert type(soft_unicode(Markup())) is Markup
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..9b6d471
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,19 @@
+[tox]
+envlist =
+ py{38,37,36,py3}
+ style
+ docs
+skip_missing_interpreters = true
+
+[testenv]
+deps = -r requirements/tests.txt
+commands = pytest --tb=short --basetemp={envtmpdir} {posargs}
+
+[testenv:style]
+deps = pre-commit
+skip_install = true
+commands = pre-commit run --all-files --show-diff-on-failure
+
+[testenv:docs]
+deps = -r requirements/docs.txt
+commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html