diff options
author | David Lord <davidism@gmail.com> | 2020-01-30 08:22:06 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-01-30 08:22:06 -0800 |
commit | 32b2a46f04e7752b5aedb05f0e1b3864e16fcee3 (patch) | |
tree | ca951757a32e038c5904a770215a4385a9b438de | |
parent | f43bf5f567cafbafb379382439e4be14ebf2506e (diff) | |
parent | c3d647d951981135e1e4a501a40fb6481b4b534a (diff) | |
download | markupsafe-32b2a46f04e7752b5aedb05f0e1b3864e16fcee3.tar.gz |
Merge pull request #115 from pallets/drop-python2
Drop Python 2
-rw-r--r-- | .azure-pipelines.yml | 16 | ||||
-rw-r--r-- | .editorconfig | 3 | ||||
-rw-r--r-- | .pre-commit-config.yaml | 5 | ||||
-rw-r--r-- | CHANGES.rst | 8 | ||||
-rw-r--r-- | README.rst | 10 | ||||
-rw-r--r-- | bench/runbench.py | 11 | ||||
-rw-r--r-- | docs/conf.py | 4 | ||||
-rw-r--r-- | docs/escaping.rst | 2 | ||||
-rw-r--r-- | docs/formatting.rst | 14 | ||||
-rw-r--r-- | docs/html.rst | 10 | ||||
-rw-r--r-- | docs/index.rst | 10 | ||||
-rw-r--r-- | setup.cfg | 9 | ||||
-rw-r--r-- | setup.py | 23 | ||||
-rw-r--r-- | src/markupsafe/__init__.py | 171 | ||||
-rw-r--r-- | src/markupsafe/_compat.py | 33 | ||||
-rw-r--r-- | src/markupsafe/_constants.py | 9 | ||||
-rw-r--r-- | src/markupsafe/_native.py | 32 | ||||
-rw-r--r-- | src/markupsafe/_speedups.c | 312 | ||||
-rw-r--r-- | tests/conftest.py | 5 | ||||
-rw-r--r-- | tests/test_escape.py | 23 | ||||
-rw-r--r-- | tests/test_exception_custom_html.py | 7 | ||||
-rw-r--r-- | tests/test_leak.py | 9 | ||||
-rw-r--r-- | tests/test_markupsafe.py | 62 | ||||
-rw-r--r-- | tox.ini | 2 |
24 files changed, 307 insertions, 483 deletions
diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 1f258e1..bb5e977 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -9,6 +9,7 @@ trigger: jobs: - job: Test + variables: vmImage: 'ubuntu-latest' python.version: '3.8' @@ -28,10 +29,6 @@ jobs: python.version: '3.7' Python 3.6 Linux: python.version: '3.6' - Python 3.5 Linux: - python.version: '3.5' - Python 2.7 Linux: - python.version: '2.7' Docs: TOXENV: 'docs' Style: @@ -56,16 +53,15 @@ jobs: dependsOn: Test condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/') + variables: + CIBW_SKIP: 'cp27-* cp35-*' + strategy: matrix: Linux: vmImage: 'ubuntu-latest' Windows: vmImage: 'windows-latest' - CIBW_SKIP: 'cp35-*' - Windows Python 3.5: - vmImage: 'vs2017-win2016' - CIBW_BUILD: 'cp35-*' Mac: vmImage: 'macos-latest' @@ -76,10 +72,6 @@ jobs: - task: UsePythonVersion@0 displayName: Use Python - - script: choco install vcpython27 -f -y - displayName: Install Visual C++ for Python 2.7 - condition: eq(variables['vmImage'], 'windows-latest') - - script: pip install cibuildwheel displayName: Install cibuildwheel diff --git a/.editorconfig b/.editorconfig index e32c802..220a591 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,3 +11,6 @@ max_line_length = 88 [*.{yml,yaml,json,js,css,html}] indent_size = 2 + +[*.c] +indent_style = tab diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cdbcfed..7f8d643 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,9 @@ repos: + - repo: https://github.com/asottile/pyupgrade + rev: v1.26.2 + hooks: + - id: pyupgrade + args: ["--py36-plus"] - repo: https://github.com/asottile/reorder_python_imports rev: v1.9.0 hooks: diff --git a/CHANGES.rst b/CHANGES.rst index 63ecd67..a846912 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,11 @@ +Version 2.0.0 +------------- + +Unreleased + +- Drop Python 2.7, 3.4, and 3.5 support. + + Version 1.1.1 ------------- @@ -27,17 +27,17 @@ Examples >>> from markupsafe import Markup, escape >>> # escape replaces special characters and wraps in Markup - >>> escape('<script>alert(document.cookie);</script>') + >>> escape("<script>alert(document.cookie);</script>") Markup(u'<script>alert(document.cookie);</script>') >>> # wrap in Markup to mark text "safe" and prevent escaping - >>> Markup('<strong>Hello</strong>') + >>> Markup("<strong>Hello</strong>") Markup('<strong>hello</strong>') - >>> escape(Markup('<strong>Hello</strong>')) + >>> escape(Markup("<strong>Hello</strong>")) Markup('<strong>hello</strong>') - >>> # Markup is a text subclass (str on Python 3, unicode on Python 2) + >>> # Markup is a str subclass >>> # methods and operators escape their arguments >>> template = Markup("Hello <em>%s</em>") - >>> template % '"World"' + >>> template % ('"World"',) Markup('Hello <em>"World"</em>') diff --git a/bench/runbench.py b/bench/runbench.py index 38cf128..f20cd49 100644 --- a/bench/runbench.py +++ b/bench/runbench.py @@ -1,9 +1,3 @@ -#!/usr/bin/env python -""" - Runs the benchmarks -""" -from __future__ import print_function - import os import re import sys @@ -24,10 +18,9 @@ def list_benchmarks(): def run_bench(name): - sys.stdout.write("%-32s" % name) - sys.stdout.flush() + print(name) Popen( - [sys.executable, "-mtimeit", "-s", "from bench_%s import run" % name, "run()"] + [sys.executable, "-m", "timeit", "-s", f"from bench_{name} import run", "run()"] ).wait() diff --git a/docs/conf.py b/docs/conf.py index f349847..46123a3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -32,11 +32,11 @@ html_sidebars = { "**": ["localtoc.html", "relations.html", "searchbox.html"], } singlehtml_sidebars = {"index": ["project.html", "localtoc.html"]} -html_title = "MarkupSafe Documentation ({})".format(version) +html_title = f"MarkupSafe Documentation ({version})" html_show_sourcelink = False # LaTeX ---------------------------------------------------------------- latex_documents = [ - (master_doc, "MarkupSafe-{}.tex".format(version), html_title, author, "manual") + (master_doc, f"MarkupSafe-{version}.tex", html_title, author, "manual") ] diff --git a/docs/escaping.rst b/docs/escaping.rst index d99674d..9e7000a 100644 --- a/docs/escaping.rst +++ b/docs/escaping.rst @@ -18,4 +18,4 @@ Optional Values Convert an Object to a String ----------------------------- -.. autofunction:: soft_unicode +.. autofunction:: soft_str diff --git a/docs/formatting.rst b/docs/formatting.rst index c425134..c14f917 100644 --- a/docs/formatting.rst +++ b/docs/formatting.rst @@ -26,7 +26,7 @@ to use an ``__html_format__`` method. 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 +``span`` tag, and adds a link when using the ``"link"`` format specifier: .. code-block:: python @@ -37,12 +37,12 @@ specifier: self.name = name def __html_format__(self, format_spec): - if format_spec == 'link': + if format_spec == "link": return Markup( '<a href="/user/{}">{}</a>' ).format(self.id, self.__html__()) elif format_spec: - raise ValueError('Invalid format spec') + raise ValueError("Invalid format spec") return self.__html__() def __html__(self): @@ -53,10 +53,10 @@ specifier: .. code-block:: pycon - >>> user = User(3, '<script>') + >>> user = User(3, "<script>") >>> escape(user) Markup('<span class="user"><script></span>') - >>> Markup('<p>User: {user:link}').format(user=user) + >>> Markup("<p>User: {user:link}").format(user=user) Markup('<p>User: <a href="/user/3"><span class="user"><script></span></a> See Python's docs on :ref:`format string syntax <python:formatstrings>`. @@ -70,8 +70,8 @@ formatting. .. code-block:: pycon - >>> user = User(3, '<script>') - >>> Markup('<a href="/user/%d">"%s</a>') % (user.id, user.name) + >>> user = User(3, "<script>") + >>> Markup('<a href="/user/%d">%s</a>') % (user.id, user.name) Markup('<a href="/user/3"><script></a>') See Python's docs on :ref:`printf-style formatting <python:old-string-formatting>`. diff --git a/docs/html.rst b/docs/html.rst index 3a0c11b..dec87af 100644 --- a/docs/html.rst +++ b/docs/html.rst @@ -20,11 +20,11 @@ For example, an ``Image`` class might automatically generate an self.url = url def __html__(self): - return '<img src="%s">' % self.url + return f'<img src="{self.url}">' .. code-block:: pycon - >>> img = Image('/static/logo.png') + >>> img = Image("/static/logo.png") >>> Markup(img) Markup('<img src="/static/logo.png">') @@ -40,12 +40,10 @@ should still be escaped: self.name = name def __html__(self): - return '<a href="/user/{}">{}</a>'.format( - self.id, escape(self.name) - ) + return f'<a href="/user/{self.id}">{escape(self.name)}</a>' .. code-block:: pycon - >>> user = User(3, '<script>') + >>> user = User(3, "<script>") >>> escape(user) Markup('<a href="/users/3"><script></a>') diff --git a/docs/index.rst b/docs/index.rst index 3db77ef..5c45e64 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,20 +13,14 @@ 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 = escape("<em>Hello</em>") >>> hello Markup('<em>Hello</em>') >>> escape(hello) Markup('<em>Hello</em>') ->>> hello + ' <strong>World</strong>' +>>> hello + " <strong>World</strong>" Markup('<em>Hello</em> <strong>World</strong>') -.. note:: - - The docs assume you're using Python 3. The terms "text" and "string" - refer to the :class:`str` class. In Python 2, this would be the - ``unicode`` class instead. - Installing ---------- @@ -1,5 +1,6 @@ [metadata] license_file = LICENSE.rst +long_description = file:README.rst long_description_content_type = text/x-rst [tool:pytest] @@ -16,9 +17,8 @@ source = [coverage:paths] source = - src/markupsafe - .tox/*/lib/python*/site-packages/markupsafe - .tox/*/site-packages/markupsafe + src + */site-packages [flake8] # B = bugbear @@ -38,6 +38,3 @@ ignore = W503 # up to 88 allowed by bugbear B950 max-line-length = 80 -# _compat names and imports will always look bad, ignore warnings -exclude = - src/markupsafe/_compat.py @@ -1,6 +1,4 @@ -from __future__ import print_function - -import io +import platform import re import sys from distutils.errors import CCompilerError @@ -12,15 +10,9 @@ from setuptools import find_packages from setuptools import setup from setuptools.command.build_ext import build_ext -with io.open("README.rst", "rt", encoding="utf8") as f: - readme = f.read() - -with io.open("src/markupsafe/__init__.py", "rt", encoding="utf8") as f: +with open("src/markupsafe/__init__.py", encoding="utf8") as f: version = re.search(r'__version__ = "(.*?)"', f.read()).group(1) -is_jython = "java" in sys.platform -is_pypy = hasattr(sys, "pypy_version_info") - ext_modules = [Extension("markupsafe._speedups", ["src/markupsafe/_speedups.c"])] @@ -60,12 +52,9 @@ def run_setup(with_binary): "Issue tracker": "https://github.com/pallets/markupsafe/issues", }, license="BSD-3-Clause", - author="Armin Ronacher", - author_email="armin.ronacher@active-4.com", - maintainer="The Pallets Team", + maintainer="Pallets", maintainer_email="contact@palletsprojects.com", description="Safely add untrusted strings to HTML/XML markup.", - long_description=readme, classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", @@ -73,8 +62,6 @@ def run_setup(with_binary): "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 3", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Text Processing :: Markup :: HTML", @@ -82,7 +69,7 @@ def run_setup(with_binary): packages=find_packages("src"), package_dir={"": "src"}, include_package_data=True, - python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*", + python_requires=">=3.6", cmdclass={"build_ext": ve_build_ext}, ext_modules=ext_modules if with_binary else [], ) @@ -95,7 +82,7 @@ def show_message(*lines): print("=" * 74) -if not (is_pypy or is_jython): +if platform.python_implementation() not in {"PyPy", "Jython"}: try: run_setup(True) except BuildFailed: diff --git a/src/markupsafe/__init__.py b/src/markupsafe/__init__.py index da05ed3..39f468b 100644 --- a/src/markupsafe/__init__.py +++ b/src/markupsafe/__init__.py @@ -1,34 +1,20 @@ -# -*- coding: utf-8 -*- """ -markupsafe -~~~~~~~~~~ - Implements an escape function and a Markup string to replace HTML special characters with safe representations. - -:copyright: 2010 Pallets -:license: BSD-3-Clause """ import re import string +from collections import abc -from ._compat import int_types -from ._compat import iteritems -from ._compat import Mapping -from ._compat import PY2 -from ._compat import string_types -from ._compat import text_type -from ._compat import unichr +__version__ = "2.0.0a1" -__version__ = "1.1.1" - -__all__ = ["Markup", "soft_unicode", "escape", "escape_silent"] +__all__ = ["Markup", "escape", "escape_silent", "soft_str", "soft_unicode"] _striptags_re = re.compile(r"(<!--.*?-->|<[^>]*>)") _entity_re = re.compile(r"&([^& ;]+);") -class Markup(text_type): +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. @@ -37,11 +23,11 @@ class Markup(text_type): 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('Hello, <em>World</em>!') >>> Markup(42) Markup('42') - >>> Markup.escape('Hello, <em>World</em>!') + >>> Markup.escape("Hello, <em>World</em>!") Markup('Hello <em>World</em>!') This implements the ``__html__()`` interface that some frameworks @@ -55,41 +41,40 @@ class Markup(text_type): >>> Markup(Foo()) Markup('<a href="/foo">foo</a>') - This is a subclass of the text type (``str`` in Python 3, - ``unicode`` in Python 2). It has the same methods as that type, but - all methods escape their arguments and return a ``Markup`` instance. + 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>%s</em>") % ("foo & bar",) Markup('<em>foo & bar</em>') - >>> Markup('<em>Hello</em> ') + '<foo>' + >>> Markup("<em>Hello</em> ") + "<foo>" Markup('<em>Hello</em> <foo>') """ __slots__ = () - def __new__(cls, base=u"", encoding=None, errors="strict"): + def __new__(cls, base="", encoding=None, errors="strict"): if hasattr(base, "__html__"): base = base.__html__() if encoding is None: - return text_type.__new__(cls, base) - return text_type.__new__(cls, base, encoding, errors) + return super().__new__(cls, base) + return super().__new__(cls, base, encoding, errors) def __html__(self): return self def __add__(self, other): - if isinstance(other, string_types) or hasattr(other, "__html__"): - return self.__class__(super(Markup, self).__add__(self.escape(other))) + if isinstance(other, str) or hasattr(other, "__html__"): + return self.__class__(super().__add__(self.escape(other))) return NotImplemented def __radd__(self, other): - if hasattr(other, "__html__") or isinstance(other, string_types): + if isinstance(other, str) or hasattr(other, "__html__"): return self.escape(other).__add__(self) return NotImplemented def __mul__(self, num): - if isinstance(num, int_types): - return self.__class__(text_type.__mul__(self, num)) + if isinstance(num, int): + return self.__class__(super().__mul__(num)) return NotImplemented __rmul__ = __mul__ @@ -99,36 +84,36 @@ class Markup(text_type): arg = tuple(_MarkupEscapeHelper(x, self.escape) for x in arg) else: arg = _MarkupEscapeHelper(arg, self.escape) - return self.__class__(text_type.__mod__(self, arg)) + return self.__class__(super().__mod__(arg)) def __repr__(self): - return "%s(%s)" % (self.__class__.__name__, text_type.__repr__(self)) + return f"{self.__class__.__name__}({super().__repr__()})" def join(self, seq): - return self.__class__(text_type.join(self, map(self.escape, seq))) + return self.__class__(super().join(map(self.escape, seq))) - join.__doc__ = text_type.join.__doc__ + join.__doc__ = str.join.__doc__ def split(self, *args, **kwargs): - return list(map(self.__class__, text_type.split(self, *args, **kwargs))) + return list(map(self.__class__, super().split(*args, **kwargs))) - split.__doc__ = text_type.split.__doc__ + split.__doc__ = str.split.__doc__ def rsplit(self, *args, **kwargs): - return list(map(self.__class__, text_type.rsplit(self, *args, **kwargs))) + return list(map(self.__class__, super().rsplit(*args, **kwargs))) - rsplit.__doc__ = text_type.rsplit.__doc__ + rsplit.__doc__ = str.rsplit.__doc__ def splitlines(self, *args, **kwargs): - return list(map(self.__class__, text_type.splitlines(self, *args, **kwargs))) + return list(map(self.__class__, super().splitlines(*args, **kwargs))) - splitlines.__doc__ = text_type.splitlines.__doc__ + 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 » <em>About</em>').unescape() + >>> Markup("Main » <em>About</em>").unescape() 'Main » <em>About</em>' """ from ._constants import HTML_ENTITIES @@ -136,27 +121,27 @@ class Markup(text_type): def handle_match(m): name = m.group(1) if name in HTML_ENTITIES: - return unichr(HTML_ENTITIES[name]) + return chr(HTML_ENTITIES[name]) try: if name[:2] in ("#x", "#X"): - return unichr(int(name[2:], 16)) + return chr(int(name[2:], 16)) elif name.startswith("#"): - return unichr(int(name[1:])) + return chr(int(name[1:])) except ValueError: pass # Don't modify unexpected input. return m.group() - return _entity_re.sub(handle_match, text_type(self)) + return _entity_re.sub(handle_match, str(self)) def striptags(self): """:meth:`unescape` the markup, remove tags, and normalize whitespace to single spaces. - >>> Markup('Main »\t<em>About</em>').striptags() + >>> Markup("Main »\t<em>About</em>").striptags() 'Main » About' """ - stripped = u" ".join(_striptags_re.sub("", self).split()) + stripped = " ".join(_striptags_re.sub("", self).split()) return Markup(stripped).unescape() @classmethod @@ -170,11 +155,11 @@ class Markup(text_type): return rv def make_simple_escaping_wrapper(name): # noqa: B902 - orig = getattr(text_type, name) + orig = getattr(str, name) def func(self, *args, **kwargs): args = _escape_argspec(list(args), enumerate(args), self.escape) - _escape_argspec(kwargs, iteritems(kwargs), self.escape) + _escape_argspec(kwargs, kwargs.items(), self.escape) return self.__class__(orig(self, *args, **kwargs)) func.__name__ = orig.__name__ @@ -201,11 +186,13 @@ class Markup(text_type): ): locals()[method] = make_simple_escaping_wrapper(method) + del method, make_simple_escaping_wrapper + def partition(self, sep): - return tuple(map(self.__class__, text_type.partition(self, self.escape(sep)))) + return tuple(map(self.__class__, super().partition(self.escape(sep)))) def rpartition(self, sep): - return tuple(map(self.__class__, text_type.rpartition(self, self.escape(sep)))) + return tuple(map(self.__class__, super().rpartition(self.escape(sep)))) def format(self, *args, **kwargs): formatter = EscapeFormatter(self.escape) @@ -214,17 +201,11 @@ class Markup(text_type): def __html_format__(self, format_spec): if format_spec: - raise ValueError("Unsupported format specification " "for Markup.") + raise ValueError("Unsupported format specification for Markup.") return self - # not in python 3 - if hasattr(text_type, "__getslice__"): - __getslice__ = make_simple_escaping_wrapper("__getslice__") - - del method, make_simple_escaping_wrapper - -class _MagicFormatMapping(Mapping): +class _MagicFormatMapping(abc.Mapping): """This class implements a dummy wrapper to fix a bug in the Python standard library for string formatting. @@ -255,43 +236,38 @@ class _MagicFormatMapping(Mapping): return len(self._kwargs) -if hasattr(text_type, "format"): - - 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( - "Format specifier {0} given, but {1} does not" - " define __html_format__. A class that defines" - " __html__ must define __html_format__ to work" - " with format specifiers.".format(format_spec, type(value)) - ) - rv = value.__html__() - else: - # We need to make sure the format spec is unicode here as - # otherwise the wrong callback methods are invoked. For - # instance a byte string there would invoke __str__ and - # not __unicode__. - rv = string.Formatter.format_field(self, value, text_type(format_spec)) - return text_type(self.escape(rv)) +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 hasattr(value, "__html__") or isinstance(value, string_types): + if isinstance(value, str) or hasattr(value, "__html__"): obj[key] = escape(value) return obj -class _MarkupEscapeHelper(object): - """Helper for Markup.__mod__""" +class _MarkupEscapeHelper: + """Helper for :meth:`Markup.__mod__`.""" def __init__(self, obj, escape): self.obj = obj @@ -301,9 +277,7 @@ class _MarkupEscapeHelper(object): return _MarkupEscapeHelper(self.obj[item], self.escape) def __str__(self): - return text_type(self.escape(self.obj)) - - __unicode__ = __str__ + return str(self.escape(self.obj)) def __repr__(self): return str(self.escape(repr(self.obj))) @@ -315,13 +289,8 @@ class _MarkupEscapeHelper(object): return float(self.obj) -# we have to import it down here as the speedups and native -# modules imports the markup type which is define above. +# circular import try: - from ._speedups import escape, escape_silent, soft_unicode + from ._speedups import escape, escape_silent, soft_str, soft_unicode except ImportError: - from ._native import escape, escape_silent, soft_unicode - -if not PY2: - soft_str = soft_unicode - __all__.append("soft_str") + from ._native import escape, escape_silent, soft_str, soft_unicode diff --git a/src/markupsafe/_compat.py b/src/markupsafe/_compat.py deleted file mode 100644 index bc05090..0000000 --- a/src/markupsafe/_compat.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -""" -markupsafe._compat -~~~~~~~~~~~~~~~~~~ - -:copyright: 2010 Pallets -:license: BSD-3-Clause -""" -import sys - -PY2 = sys.version_info[0] == 2 - -if not PY2: - text_type = str - string_types = (str,) - unichr = chr - int_types = (int,) - - def iteritems(x): - return iter(x.items()) - - from collections.abc import Mapping - -else: - text_type = unicode - string_types = (str, unicode) - unichr = unichr - int_types = (int, long) - - def iteritems(x): - return x.iteritems() - - from collections import Mapping diff --git a/src/markupsafe/_constants.py b/src/markupsafe/_constants.py index 7c57c2d..7638937 100644 --- a/src/markupsafe/_constants.py +++ b/src/markupsafe/_constants.py @@ -1,12 +1,3 @@ -# -*- coding: utf-8 -*- -""" -markupsafe._constants -~~~~~~~~~~~~~~~~~~~~~ - -:copyright: 2010 Pallets -:license: BSD-3-Clause -""" - HTML_ENTITIES = { "AElig": 198, "Aacute": 193, diff --git a/src/markupsafe/_native.py b/src/markupsafe/_native.py index cd08752..87cefcf 100644 --- a/src/markupsafe/_native.py +++ b/src/markupsafe/_native.py @@ -1,15 +1,7 @@ -# -*- coding: utf-8 -*- """ -markupsafe._native -~~~~~~~~~~~~~~~~~~ - Native Python implementation used when the C module is not compiled. - -:copyright: 2010 Pallets -:license: BSD-3-Clause """ from . import Markup -from ._compat import text_type def escape(s): @@ -26,7 +18,7 @@ def escape(s): if hasattr(s, "__html__"): return Markup(s.__html__()) return Markup( - text_type(s) + str(s) .replace("&", "&") .replace(">", ">") .replace("<", "<") @@ -50,20 +42,32 @@ def escape_silent(s): return escape(s) -def soft_unicode(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 = escape("<User 1>") >>> value Markup('<User 1>') >>> escape(str(value)) Markup('&lt;User 1&gt;') - >>> escape(soft_unicode(value)) + >>> escape(soft_str(value)) Markup('<User 1>') """ - if not isinstance(s, text_type): - s = text_type(s) + 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 index 12d2c4a..0f2e7b2 100644 --- a/src/markupsafe/_speedups.c +++ b/src/markupsafe/_speedups.c @@ -1,23 +1,9 @@ /** - * markupsafe._speedups - * ~~~~~~~~~~~~~~~~~~~~ - * * C implementation of escaping for better performance. Used instead of * the native Python implementation when compiled. - * - * :copyright: 2010 Pallets - * :license: BSD-3-Clause */ #include <Python.h> -#if PY_MAJOR_VERSION < 3 -#define ESCAPED_CHARS_TABLE_SIZE 63 -#define UNICHR(x) (PyUnicode_AS_UNICODE((PyUnicodeObject*)PyUnicode_DecodeASCII(x, strlen(x), NULL))); - -static Py_ssize_t escaped_chars_delta_len[ESCAPED_CHARS_TABLE_SIZE]; -static Py_UNICODE *escaped_chars_repl[ESCAPED_CHARS_TABLE_SIZE]; -#endif - static PyObject* markup; static int @@ -25,21 +11,6 @@ init_constants(void) { PyObject *module; -#if PY_MAJOR_VERSION < 3 - /* mapping of characters to replace */ - escaped_chars_repl['"'] = UNICHR("""); - escaped_chars_repl['\''] = UNICHR("'"); - escaped_chars_repl['&'] = UNICHR("&"); - escaped_chars_repl['<'] = UNICHR("<"); - escaped_chars_repl['>'] = UNICHR(">"); - - /* lengths of those characters when replaced - 1 */ - memset(escaped_chars_delta_len, 0, sizeof (escaped_chars_delta_len)); - escaped_chars_delta_len['"'] = escaped_chars_delta_len['\''] = \ - escaped_chars_delta_len['&'] = 4; - escaped_chars_delta_len['<'] = escaped_chars_delta_len['>'] = 3; -#endif - /* import markup type so that we can mark the return value */ module = PyImport_ImportModule("markupsafe"); if (!module) @@ -50,137 +21,74 @@ init_constants(void) return 1; } -#if PY_MAJOR_VERSION < 3 -static PyObject* -escape_unicode(PyUnicodeObject *in) -{ - PyUnicodeObject *out; - Py_UNICODE *inp = PyUnicode_AS_UNICODE(in); - const Py_UNICODE *inp_end = PyUnicode_AS_UNICODE(in) + PyUnicode_GET_SIZE(in); - Py_UNICODE *next_escp; - Py_UNICODE *outp; - Py_ssize_t delta=0, erepl=0, delta_len=0; - - /* First we need to figure out how long the escaped string will be */ - while (*(inp) || inp < inp_end) { - if (*inp < ESCAPED_CHARS_TABLE_SIZE) { - delta += escaped_chars_delta_len[*inp]; - erepl += !!escaped_chars_delta_len[*inp]; - } - ++inp; - } - - /* Do we need to escape anything at all? */ - if (!erepl) { - Py_INCREF(in); - return (PyObject*)in; - } - - out = (PyUnicodeObject*)PyUnicode_FromUnicode(NULL, PyUnicode_GET_SIZE(in) + delta); - if (!out) - return NULL; - - outp = PyUnicode_AS_UNICODE(out); - inp = PyUnicode_AS_UNICODE(in); - while (erepl-- > 0) { - /* look for the next substitution */ - next_escp = inp; - while (next_escp < inp_end) { - if (*next_escp < ESCAPED_CHARS_TABLE_SIZE && - (delta_len = escaped_chars_delta_len[*next_escp])) { - ++delta_len; - break; - } - ++next_escp; - } - - if (next_escp > inp) { - /* copy unescaped chars between inp and next_escp */ - Py_UNICODE_COPY(outp, inp, next_escp-inp); - outp += next_escp - inp; - } - - /* escape 'next_escp' */ - Py_UNICODE_COPY(outp, escaped_chars_repl[*next_escp], delta_len); - outp += delta_len; - - inp = next_escp + 1; - } - if (inp < inp_end) - Py_UNICODE_COPY(outp, inp, PyUnicode_GET_SIZE(in) - (inp - PyUnicode_AS_UNICODE(in))); - - return (PyObject*)out; -} -#else /* PY_MAJOR_VERSION < 3 */ - #define GET_DELTA(inp, inp_end, delta) \ - while (inp < inp_end) { \ - switch (*inp++) { \ - case '"': \ - case '\'': \ - case '&': \ - delta += 4; \ - break; \ - case '<': \ - case '>': \ - delta += 3; \ - break; \ - } \ + 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 '"': \ + { \ + 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 '\'': \ + *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 '&': \ + *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 '<': \ + *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 '>': \ + *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: \ + *outp++ = '&'; \ + *outp++ = 'g'; \ + *outp++ = 't'; \ + *outp++ = ';'; \ + break; \ + default: \ ncopy++; \ - } \ - inp++; \ - } \ + } \ + inp++; \ + } \ memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \ } @@ -278,7 +186,6 @@ escape_unicode(PyUnicodeObject *in) assert(0); /* shouldn't happen */ return NULL; } -#endif /* PY_MAJOR_VERSION < 3 */ static PyObject* escape(PyObject *self, PyObject *text) @@ -287,11 +194,7 @@ escape(PyObject *self, PyObject *text) PyObject *s = NULL, *rv = NULL, *html; if (id_html == NULL) { -#if PY_MAJOR_VERSION < 3 - id_html = PyString_InternFromString("__html__"); -#else id_html = PyUnicode_InternFromString("__html__"); -#endif if (id_html == NULL) { return NULL; } @@ -299,11 +202,8 @@ escape(PyObject *self, PyObject *text) /* we don't have to escape integers, bools or floats */ if (PyLong_CheckExact(text) || -#if PY_MAJOR_VERSION < 3 - PyInt_CheckExact(text) || -#endif - PyFloat_CheckExact(text) || PyBool_Check(text) || - text == Py_None) + 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 */ @@ -323,11 +223,7 @@ escape(PyObject *self, PyObject *text) /* otherwise make the object unicode if it isn't, then escape */ PyErr_Clear(); if (!PyUnicode_Check(text)) { -#if PY_MAJOR_VERSION < 3 - PyObject *unicode = PyObject_Unicode(text); -#else PyObject *unicode = PyObject_Str(text); -#endif if (!unicode) return NULL; s = escape_unicode((PyUnicodeObject*)unicode); @@ -353,54 +249,80 @@ escape_silent(PyObject *self, PyObject *text) static PyObject* -soft_unicode(PyObject *self, PyObject *s) +soft_str(PyObject *self, PyObject *s) { if (!PyUnicode_Check(s)) -#if PY_MAJOR_VERSION < 3 - return PyObject_Unicode(s); -#else return PyObject_Str(s); -#endif Py_INCREF(s); return s; } -static PyMethodDef module_methods[] = { - {"escape", (PyCFunction)escape, METH_O, - "escape(s) -> markup\n\n" - "Convert the characters &, <, >, ', and \" in string s to HTML-safe\n" - "sequences. Use this if you need to display text that might contain\n" - "such characters in HTML. Marks return value as markup string."}, - {"escape_silent", (PyCFunction)escape_silent, METH_O, - "escape_silent(s) -> markup\n\n" - "Like escape but converts None to an empty string."}, - {"soft_unicode", (PyCFunction)soft_unicode, METH_O, - "soft_unicode(object) -> string\n\n" - "Make a string unicode if it isn't already. That way a markup\n" - "string is not converted back to unicode."}, - {NULL, NULL, 0, NULL} /* Sentinel */ -}; - - -#if PY_MAJOR_VERSION < 3 - -#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ -#define PyMODINIT_FUNC void -#endif -PyMODINIT_FUNC -init_speedups(void) +static PyObject* +soft_unicode(PyObject *self, PyObject *s) { - if (!init_constants()) - return; - - Py_InitModule3("markupsafe._speedups", module_methods, ""); + 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); } -#else /* Python 3.x module initialization */ + +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('<User 1>')\n" + ">>> escape(str(value))\n" + "Markup('&lt;User 1&gt;')\n" + ">>> escape(soft_str(value))\n" + "Markup('<User 1>')\n" + }, + { + "soft_unicode", + (PyCFunction)soft_unicode, + METH_O, + "" + }, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; static struct PyModuleDef module_definition = { - PyModuleDef_HEAD_INIT, + PyModuleDef_HEAD_INIT, "markupsafe._speedups", NULL, -1, @@ -419,5 +341,3 @@ PyInit__speedups(void) return PyModule_Create(&module_definition); } - -#endif diff --git a/tests/conftest.py b/tests/conftest.py index 296cd58..7141547 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -34,4 +34,9 @@ def escape_silent(_mod): @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 index 788134a..bf53fac 100644 --- a/tests/test_escape.py +++ b/tests/test_escape.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import pytest from markupsafe import Markup @@ -8,22 +7,22 @@ from markupsafe import Markup ("value", "expect"), ( # empty - (u"", u""), + ("", ""), # ascii - (u"abcd&><'\"efgh", u"abcd&><'"efgh"), - (u"&><'\"efgh", u"&><'"efgh"), - (u"abcd&><'\"", u"abcd&><'""), + ("abcd&><'\"efgh", "abcd&><'"efgh"), + ("&><'\"efgh", "&><'"efgh"), + ("abcd&><'\"", "abcd&><'""), # 2 byte - (u"こんにちは&><'\"こんばんは", u"こんにちは&><'"こんばんは"), - (u"&><'\"こんばんは", u"&><'"こんばんは"), - (u"こんにちは&><'\"", u"こんにちは&><'""), + ("こんにちは&><'\"こんばんは", "こんにちは&><'"こんばんは"), + ("&><'\"こんばんは", "&><'"こんばんは"), + ("こんにちは&><'\"", "こんにちは&><'""), # 4 byte ( - u"\U0001F363\U0001F362&><'\"\U0001F37A xyz", - u"\U0001F363\U0001F362&><'"\U0001F37A xyz", + "\U0001F363\U0001F362&><'\"\U0001F37A xyz", + "\U0001F363\U0001F362&><'"\U0001F37A xyz", ), - (u"&><'\"\U0001F37A xyz", u"&><'"\U0001F37A xyz"), - (u"\U0001F363\U0001F362&><'\"", u"\U0001F363\U0001F362&><'""), + ("&><'\"\U0001F37A xyz", "&><'"\U0001F37A xyz"), + ("\U0001F363\U0001F362&><'\"", "\U0001F363\U0001F362&><'""), ), ) def test_escape(escape, value, expect): diff --git a/tests/test_exception_custom_html.py b/tests/test_exception_custom_html.py index 5f9ffde..ec2f10b 100644 --- a/tests/test_exception_custom_html.py +++ b/tests/test_exception_custom_html.py @@ -1,15 +1,12 @@ -# -*- coding: utf-8 -*- import pytest -from markupsafe import escape - -class CustomHtmlThatRaises(object): +class CustomHtmlThatRaises: def __html__(self): raise ValueError(123) -def test_exception_custom_html(): +def test_exception_custom_html(escape): """Checks whether exceptions in custom __html__ implementations are propagated correctly. diff --git a/tests/test_leak.py b/tests/test_leak.py index b36a4ce..55b10b9 100644 --- a/tests/test_leak.py +++ b/tests/test_leak.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- import gc -import sys +import platform import pytest @@ -18,10 +17,10 @@ def test_markup_leaks(): for _j in range(1000): escape("foo") escape("<foo>") - escape(u"foo") - escape(u"<foo>") + escape("foo") + escape("<foo>") - if hasattr(sys, "pypy_version_info"): + if platform.python_implementation() == "PyPy": gc.collect() counts.add(len(gc.get_objects())) diff --git a/tests/test_markupsafe.py b/tests/test_markupsafe.py index 5b08006..bd42d98 100644 --- a/tests/test_markupsafe.py +++ b/tests/test_markupsafe.py @@ -1,17 +1,12 @@ -# -*- coding: utf-8 -*- import pytest -from markupsafe import escape -from markupsafe import escape_silent from markupsafe import Markup -from markupsafe._compat import PY2 -from markupsafe._compat import text_type -def test_adding(): +def test_adding(escape): unsafe = '<script type="application/x-some-script">alert("foo");</script>' safe = Markup("<em>username</em>") - assert unsafe + safe == text_type(escape(unsafe)) + text_type(safe) + assert unsafe + safe == str(escape(unsafe)) + str(safe) @pytest.mark.parametrize( @@ -38,15 +33,13 @@ def test_type_behavior(): def test_html_interop(): - class Foo(object): + class Foo: def __html__(self): return "<em>awesome</em>" - def __unicode__(self): + def __str__(self): return "awesome" - __str__ = __unicode__ - assert Markup(Foo()) == "<em>awesome</em>" result = Markup("<strong>%s</strong>") % Foo() assert result == "<strong><em>awesome</em></strong>" @@ -54,21 +47,21 @@ def test_html_interop(): def test_tuple_interpol(): result = Markup("<em>%s:%s</em>") % ("<foo>", "<bar>") - expect = Markup(u"<em><foo>:<bar></em>") + expect = Markup("<em><foo>:<bar></em>") assert result == expect def test_dict_interpol(): result = Markup("<em>%(foo)s</em>") % {"foo": "<foo>"} - expect = Markup(u"<em><foo></em>") + expect = Markup("<em><foo></em>") assert result == expect result = Markup("<em>%(foo)s:%(bar)s</em>") % {"foo": "<foo>", "bar": "<bar>"} - expect = Markup(u"<em><foo>:<bar></em>") + expect = Markup("<em><foo>:<bar></em>") assert result == expect -def test_escaping(): +def test_escaping(escape): assert escape("\"<>&'") == ""<>&'" assert Markup("<em>Foo & Bar</em>").striptags() == "Foo & Bar" @@ -105,11 +98,11 @@ def test_formatting_empty(): def test_custom_formatting(): - class HasHTMLOnly(object): + class HasHTMLOnly: def __html__(self): return Markup("<foo>") - class HasHTMLAndFormat(object): + class HasHTMLAndFormat: def __html__(self): return Markup("<foo>") @@ -121,7 +114,7 @@ def test_custom_formatting(): def test_complex_custom_formatting(): - class User(object): + class User: def __init__(self, id, username): self.id = id self.username = username @@ -146,19 +139,11 @@ def test_complex_custom_formatting(): def test_formatting_with_objects(): - class Stringable(object): - def __unicode__(self): - return u"строка" - - if PY2: - - def __str__(self): - return "some other value" - - else: - __str__ = __unicode__ + class Stringable: + def __str__(self): + return "строка" - assert Markup("{s}").format(s=Stringable()) == Markup(u"строка") + assert Markup("{s}").format(s=Stringable()) == Markup("строка") def test_all_set(): @@ -168,10 +153,10 @@ def test_all_set(): getattr(markup, item) -def test_escape_silent(): +def test_escape_silent(escape, escape_silent): assert escape_silent(None) == Markup() assert escape(None) == Markup(None) - assert escape_silent("<foo>") == Markup(u"<foo>") + assert escape_silent("<foo>") == Markup("<foo>") def test_splitting(): @@ -185,7 +170,7 @@ def test_mul(): assert Markup("a") * 3 == Markup("aaa") -def test_escape_return_type(): +def test_escape_return_type(escape): assert isinstance(escape("a"), Markup) assert isinstance(escape(Markup("a")), Markup) @@ -194,3 +179,14 @@ def test_escape_return_type(): 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 @@ -1,6 +1,6 @@ [tox] envlist = - py{38,37,36,35,27,py3,py} + py{38,37,36,py3} style docs skip_missing_interpreters = true |