diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 398ff08..0000000 --- a/.coveragerc +++ /dev/null @@ -1,2 +0,0 @@ -[run] -branch = True diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 828b2bf..5730b5d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,14 +6,15 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["pypy3", "3.6", "3.7", "3.8", "3.9"] + python-version: ["pypy-3.7", "3.7", "3.8", "3.9", "3.10"] steps: - name: Check out repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + check-latest: true - name: Install dependencies run: python -m pip install tox - name: Run tests @@ -23,25 +24,29 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@v1 + uses: actions/checkout@v3 - name: Install Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: "3.x" + check-latest: true - name: Install dependencies run: python -m pip install tox - name: Run linting run: python -m tox -e pep8 + - name: Run mypy + run: python -m tox -e mypy packaging: name: Packaging runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@v1 + uses: actions/checkout@v3 - name: Install Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: "3.x" + check-latest: true - name: Install dependencies run: python -m pip install tox - name: Test packaging diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5bb5123..be12984 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,11 +7,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: ref: ${{ github.event.release.tag_name }} - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 + with: + python-version: "3.x" + check-latest: true - name: Install build dependencies run: pip install -U setuptools wheel build - name: Build diff --git a/.gitignore b/.gitignore index 142b10c..962e2f9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,7 @@ htmlcov/ *.eggs *.py[co] .pytest_cache/ +.python-version .idea/ +.vscode/ diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000..8c924e6 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,23 @@ +# See https://www.gitpod.io/docs/references/gitpod-yml for full reference + +tasks: + - name: Setup Development and run Tests + + init: | + # Upgrade pyenv itself + pyenv update + + export PY_VERSIONS="3.7 3.8 3.9 3.10" + + # Install all supported Python versions + for py in $PY_VERSIONS; + do pyenv install "$py":latest --skip-existing ; + done + + # Make versions available via $PATH, exclude GitPod default + pyenv global $(pyenv versions --bare | grep -v 3.8.13) + + # Install `tox` test orchestrator + pip install tox + + command: tox diff --git a/CHANGES.rst b/CHANGES.rst index aab1da9..66c63de 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,64 @@ Changes ======= +37.2 (2022-09-24) + +* Allow HTML5 `s` tag through cleaner (#261) + +37.1 (2022-09-03) +----------------- + +* Allow HTML5 `nav` tag through cleaner (#259) + +37.0 (2022-08-21) +----------------- + +* Remove command line example from docs (#197) +* Multiple pyproject.toml fixes (#251) +* Confirm handling multiple inline strong (#252) +* Convert RST output to HTML5 (#253) +* Add Typing to classifiers (#254) +* Development tweaks - coverage reporting, actions updates (#255) +* Add test confirming behavior with unknown lexers (#256) + +36.0 (2022-08-06) +----------------- + +* Enable gitpod development (#238) +* Allow rst admonitions to render (#242) +* Add badges to README (#243) +* Update codebase for modern Python (#244) +* Fix table cell spans (#245) +* Allow ``math`` directive in rst (#246) +* Preserve ``lang`` attribute in ``pre`` (#247) + +35.0 (2022-04-19) +----------------- + +* Add py.typed to the built wheel (#228) +* Use isolated build for tox (#229) +* Fix renderer ignore (#230) +* Remove legacy check command and distutils (#233) +* Emit a warning when no content is rendered (#231) +* Drop support for Python 3.6 (#236) +* Update html attribute order in tests (#235) + +34.0 (2022-03-11) +----------------- + +* Add static types (#225) + +33.0 (2022-03-05) +----------------- + +* Support cmarkgfm>=0.8.0 (#224) + +33.0 (2022-02-05) +----------------- + +* Support cmarkgfm>=0.8.0 (#224) +* Support Python 3.10 + 32.0 (2021-12-13) ----------------- diff --git a/MANIFEST.in b/MANIFEST.in index cfa0b3a..5242524 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include LICENSE README.rst CHANGES.rst -include tox.ini .coveragerc pytest.ini +include tox.ini +include readme_renderer/py.typed recursive-include tests *.html recursive-include tests *.py diff --git a/README.rst b/README.rst index 1dde903..33bbfac 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,12 @@ Readme Renderer =============== +.. image:: https://badge.fury.io/py/readme-renderer.svg + :target: https://badge.fury.io/py/readme-renderer + +.. image:: https://github.com/pypa/readme_renderer/actions/workflows/ci.yml/badge.svg + :target: https://github.com/pypa/readme_renderer/actions/workflows/ci.yml + Readme Renderer is a library that will safely render arbitrary ``README`` files into HTML. It is designed to be used in Warehouse_ to render the ``long_description`` for packages. It can handle Markdown, @@ -16,14 +22,6 @@ To locally check whether your long descriptions will render on PyPI, first build your distributions, and then use the |twine check|_ command. -Render rST Description Locally ------------------------------- - -You can use ``readme_renderer`` on the command line to render an rST file as -HTML like this: :: - - python -m readme_renderer README.rst -o /tmp/README.html - Code of Conduct --------------- diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..609a620 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[build-system] +requires = ["setuptools>=40.8.0"] +build-backend = "setuptools.build_meta" + +# TODO: Remove when https://github.com/mgedmin/check-manifest/pull/155 released +[tool.check-manifest] +ignore = [".gitpod.yml"] + +[tool.coverage.run] +branch = true + +[tool.mypy] +strict = true +warn_unused_configs = true +show_error_codes = true +enable_error_code = [ + "ignore-without-code" +] + +[[tool.mypy.overrides]] +# These modules do not yet have types available. +module = [ + "cmarkgfm.*" +] +ignore_missing_imports = true diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 5d6dbaf..0000000 --- a/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -[pytest] -markers = - filterwarnings: built-in maker to silence warnings, not recognized by --strict. diff --git a/readme_renderer/__about__.py b/readme_renderer/__about__.py index 3baf259..8ac7c8f 100644 --- a/readme_renderer/__about__.py +++ b/readme_renderer/__about__.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import absolute_import, division, print_function __all__ = [ "__title__", @@ -30,7 +29,7 @@ ) __uri__ = "https://github.com/pypa/readme_renderer" -__version__ = "32.0" +__version__ = "37.2" __author__ = "The Python Packaging Authority" __email__ = "admin@mail.pypi.org" diff --git a/readme_renderer/__init__.py b/readme_renderer/__init__.py index c3eb860..cdec169 100644 --- a/readme_renderer/__init__.py +++ b/readme_renderer/__init__.py @@ -11,4 +11,3 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import absolute_import, division, print_function diff --git a/readme_renderer/__main__.py b/readme_renderer/__main__.py index 527d96d..74983fc 100644 --- a/readme_renderer/__main__.py +++ b/readme_renderer/__main__.py @@ -1,4 +1,3 @@ -from __future__ import absolute_import, print_function import argparse from readme_renderer.rst import render import sys diff --git a/readme_renderer/clean.py b/readme_renderer/clean.py index 190f423..386e684 100644 --- a/readme_renderer/clean.py +++ b/readme_renderer/clean.py @@ -11,9 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import absolute_import, division, print_function import functools +from typing import Any, Dict, Iterator, List, Optional import bleach import bleach.callbacks @@ -30,7 +30,7 @@ "br", "caption", "cite", "col", "colgroup", "dd", "del", "details", "div", "dl", "dt", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "img", "p", "pre", "span", "sub", "summary", "sup", "table", "tbody", "td", "th", "thead", - "tr", "tt", "kbd", "var", "input", + "tr", "tt", "kbd", "var", "input", "section", "aside", "nav", "s", ] ALLOWED_ATTRIBUTES = { @@ -44,9 +44,9 @@ "hr": ["class"], "img": ["src", "width", "height", "alt", "align", "class"], "span": ["class"], - "th": ["align"], - "td": ["align"], - "div": ["align"], + "th": ["align", "class"], + "td": ["align", "colspan", "rowspan"], + "div": ["align", "class"], "h1": ["align"], "h2": ["align"], "h3": ["align"], @@ -54,20 +54,26 @@ "h5": ["align"], "h6": ["align"], "code": ["class"], - "p": ["align"], + "p": ["align", "class"], + "pre": ["lang"], "ol": ["start"], "input": ["type", "checked", "disabled"], + "aside": ["class"], + "dd": ["class"], + "dl": ["class"], + "dt": ["class"], + "ul": ["class"], + "nav": ["class"], } -ALLOWED_STYLES = [ -] - class DisabledCheckboxInputsFilter: - def __init__(self, source): + # The typeshed for bleach (html5lib) filters is incomplete, use `typing.Any` + # See https://github.com/python/typeshed/blob/505ea726415016e53638c8b584b8fdc9c722cac1/stubs/bleach/bleach/html5lib_shim.pyi#L7-L8 # noqa E501 + def __init__(self, source: Any) -> None: self.source = source - def __iter__(self): + def __iter__(self) -> Iterator[Dict[str, Optional[str]]]: for token in self.source: if token.get("name") == "input": # only allow disabled checkbox inputs @@ -85,23 +91,24 @@ def __iter__(self): else: yield token - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any: return getattr(self.source, name) -def clean(html, tags=None, attributes=None, styles=None): +def clean( + html: str, + tags: Optional[List[str]] = None, + attributes: Optional[Dict[str, List[str]]] = None +) -> Optional[str]: if tags is None: tags = ALLOWED_TAGS if attributes is None: attributes = ALLOWED_ATTRIBUTES - if styles is None: - styles = ALLOWED_STYLES # Clean the output using Bleach cleaner = bleach.sanitizer.Cleaner( tags=tags, attributes=attributes, - styles=styles, filters=[ # Bleach Linkify makes it easy to modify links, however, we will # not be using it to create additional links. diff --git a/readme_renderer/integration/__init__.py b/readme_renderer/integration/__init__.py deleted file mode 100644 index d6051a0..0000000 --- a/readme_renderer/integration/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright 2015 Donald Stufft -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from __future__ import absolute_import, division, print_function diff --git a/readme_renderer/integration/distutils.py b/readme_renderer/integration/distutils.py deleted file mode 100644 index eb04460..0000000 --- a/readme_renderer/integration/distutils.py +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright 2015 Donald Stufft -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from __future__ import absolute_import, division, print_function - -import cgi -import io -import re - -import distutils.log -from distutils.command.check import check as _check -from distutils.core import Command - -from ..rst import render - - -# Regular expression used to capture and reformat doctuils warnings into -# something that a human can understand. This is loosely borrowed from -# Sphinx: https://github.com/sphinx-doc/sphinx/blob -# /c35eb6fade7a3b4a6de4183d1dd4196f04a5edaf/sphinx/util/docutils.py#L199 -_REPORT_RE = re.compile( - r'^:(?P(?:\d+)?): ' - r'\((?PDEBUG|INFO|WARNING|ERROR|SEVERE)/(\d+)?\) ' - r'(?P.*)', re.DOTALL | re.MULTILINE) - - -class _WarningStream(object): - def __init__(self): - self.output = io.StringIO() - - def write(self, text): - matched = _REPORT_RE.search(text) - - if not matched: - self.output.write(text) - return - - self.output.write( - u"line {line}: {level_text}: {message}\n".format( - level_text=matched.group('level').capitalize(), - line=matched.group('line'), - message=matched.group('message').rstrip('\r\n'))) - - def __str__(self): - return self.output.getvalue() - - -class Check(_check): - def check_restructuredtext(self): - """ - Checks if the long string fields are reST-compliant. - """ - # Warn that this command is deprecated - # Don't use self.warn() because it will cause the check to fail. - Command.warn( - self, - "This command has been deprecated. Use `twine check` instead: " - "https://packaging.python.org/guides/making-a-pypi-friendly-readme" - "#validating-restructuredtext-markup" - ) - - data = self.distribution.get_long_description() - content_type = getattr( - self.distribution.metadata, 'long_description_content_type', None) - - if content_type: - content_type, _ = cgi.parse_header(content_type) - if content_type != 'text/x-rst': - self.warn( - "Not checking long description content type '%s', this " - "command only checks 'text/x-rst'." % content_type) - return - - # None or empty string should both trigger this branch. - if not data or data == 'UNKNOWN': - self.warn( - "The project's long_description is either missing or empty.") - return - - stream = _WarningStream() - markup = render(data, stream=stream) - - if markup is None: - self.warn( - "The project's long_description has invalid markup which will " - "not be rendered on PyPI. The following syntax errors were " - "detected:\n%s" % stream) - return - - self.announce( - "The project's long description is valid RST.", - level=distutils.log.INFO) diff --git a/readme_renderer/markdown.py b/readme_renderer/markdown.py index fdf84ed..6e71a43 100644 --- a/readme_renderer/markdown.py +++ b/readme_renderer/markdown.py @@ -11,12 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import absolute_import, division, print_function import re import warnings +from typing import cast, Any, Dict, Callable, Match, Optional -from html.parser import unescape +from html import unescape import pygments import pygments.lexers @@ -32,13 +32,13 @@ try: import cmarkgfm from cmarkgfm.cmark import Options as cmarkgfmOptions - variants = { - "GFM": lambda raw: cmarkgfm.github_flavored_markdown_to_html( + variants: Dict[str, Callable[[str], str]] = { + "GFM": lambda raw: cast(str, cmarkgfm.github_flavored_markdown_to_html( raw, options=cmarkgfmOptions.CMARK_OPT_UNSAFE - ), - "CommonMark": lambda raw: cmarkgfm.markdown_to_html( + )), + "CommonMark": lambda raw: cast(str, cmarkgfm.markdown_to_html( raw, options=cmarkgfmOptions.CMARK_OPT_UNSAFE - ), + )), } except ImportError: warnings.warn(_EXTRA_WARNING) @@ -51,7 +51,11 @@ } -def render(raw, variant="GFM", **kwargs): +def render( + raw: str, + variant: str = "GFM", + **kwargs: Any +) -> Optional[str]: if not variants: warnings.warn(_EXTRA_WARNING) return None @@ -71,7 +75,7 @@ def render(raw, variant="GFM", **kwargs): return cleaned -def _highlight(html): +def _highlight(html: str) -> str: """Syntax-highlights HTML-rendered Markdown. Plucks sections to highlight that conform the the GitHub fenced code info @@ -94,7 +98,7 @@ def _highlight(html): '(?(in_code)|)(?P.+?)' r'', re.DOTALL) - def replacer(match): + def replacer(match: Match[Any]) -> str: try: lang = match.group('lang') lang = _LANG_ALIASES.get(lang, lang) @@ -112,7 +116,7 @@ def replacer(match): highlighted = pygments.highlight(code, lexer, formatter) - return '
{}
'.format(highlighted) + return f'
{highlighted}
' result = code_expr.sub(replacer, html) diff --git a/readme_renderer/py.typed b/readme_renderer/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/readme_renderer/rst.py b/readme_renderer/rst.py index d78394c..9a80c68 100644 --- a/readme_renderer/rst.py +++ b/readme_renderer/rst.py @@ -11,23 +11,30 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import absolute_import, division, print_function import io +from typing import Any, Dict, IO, Optional, Union from docutils.core import publish_parts -from docutils.writers.html4css1 import HTMLTranslator, Writer +from docutils.nodes import colspec, image +from docutils.writers.html5_polyglot import HTMLTranslator, Writer from docutils.utils import SystemMessage from .clean import clean -class ReadMeHTMLTranslator(HTMLTranslator): +class ReadMeHTMLTranslator(HTMLTranslator): # type: ignore[misc] # docutils is incomplete, returns `Any` python/typeshed#7256 # noqa E501 # Overrides base class not to output `` tag for SVG images. - object_image_types = {} - - def emptytag(self, node, tagname, suffix="\n", **attributes): + object_image_types: Dict[str, str] = {} + + def emptytag( + self, + node: Union[colspec, image], + tagname: str, + suffix: str = "\n", + **attributes: Any + ) -> Any: """Override this to add back the width/height attributes.""" if tagname == "img": if "width" in node: @@ -35,7 +42,7 @@ def emptytag(self, node, tagname, suffix="\n", **attributes): if "height" in node: attributes["height"] = node["height"] - return super(ReadMeHTMLTranslator, self).emptytag( + return super().emptytag( node, tagname, suffix, **attributes ) @@ -69,7 +76,8 @@ def emptytag(self, node, tagname, suffix="\n", **attributes): # Output math blocks as LaTeX that can be interpreted by MathJax for # a prettier display of Math formulas. - "math_output": "MathJax", + # Pass a dummy path to supress docutils warning and emit HTML. + "math_output": "MathJax /dummy.js", # Disable raw html as enabling it is a security risk, we do not want # people to be able to include any old HTML in the final output. @@ -95,7 +103,11 @@ def emptytag(self, node, tagname, suffix="\n", **attributes): } -def render(raw, stream=None, **kwargs): +def render( + raw: str, + stream: Optional[IO[str]] = None, + **kwargs: Any +) -> Optional[str]: if stream is None: # Use a io.StringIO as the warning stream to prevent warnings from # being printed to sys.stderr. @@ -117,4 +129,7 @@ def render(raw, stream=None, **kwargs): if rendered: return clean(rendered) else: + # If the warnings stream is empty, docutils had none, so add ours. + if not stream.tell(): + stream.write("No content rendered from RST source.") return None diff --git a/readme_renderer/txt.py b/readme_renderer/txt.py index 3218922..5af4805 100644 --- a/readme_renderer/txt.py +++ b/readme_renderer/txt.py @@ -11,21 +11,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import absolute_import, division, print_function -import sys +from typing import Any, Optional from .clean import clean -if sys.version_info >= (3,): - from html import escape as html_escape -else: - from cgi import escape +from html import escape as html_escape - def html_escape(s): - return escape(s, quote=True).replace("'", ''') - -def render(raw, **kwargs): +def render(raw: str, **kwargs: Any) -> Optional[str]: rendered = html_escape(raw).replace("\n", "
") return clean(rendered, tags=["br"]) diff --git a/setup.cfg b/setup.cfg index 0c9e0fc..8183238 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,2 @@ [metadata] -license_file = LICENSE +license_files = LICENSE diff --git a/setup.py b/setup.py index 39d61ff..3a17e84 100644 --- a/setup.py +++ b/setup.py @@ -11,19 +11,18 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import absolute_import, division, print_function -import os +import pathlib import setuptools -base_dir = os.path.dirname(__file__) +base_dir = pathlib.Path(__file__).parent -with open(os.path.join(base_dir, "readme_renderer", "__about__.py")) as f: +with open(base_dir.joinpath("readme_renderer", "__about__.py")) as f: about = {} exec(f.read(), about) -with open(os.path.join(base_dir, "README.rst")) as f: +with open(base_dir.joinpath("README.rst")) as f: long_description = f.read() @@ -48,18 +47,17 @@ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", + "Typing :: Typed", ], install_requires=["bleach>=2.1.0", "docutils>=0.13.1", "Pygments>=2.5.1"], - entry_points={ - "distutils.commands": ["check = readme_renderer.integration.distutils:Check"], - }, - extras_require={"md": "cmarkgfm>=0.5.0,<0.7.0"}, + include_package_data=True, + extras_require={"md": "cmarkgfm>=0.8.0"}, packages=setuptools.find_packages(exclude=["tests", "tests.*"]), - python_requires=">=3.6", + python_requires=">=3.7", ) diff --git a/tests/fixtures/test_CommonMark_008.html b/tests/fixtures/test_CommonMark_008.html index 3126beb..b941909 100644 --- a/tests/fixtures/test_CommonMark_008.html +++ b/tests/fixtures/test_CommonMark_008.html @@ -1,5 +1,5 @@

Here is some Python code for a Dog:

-
class Dog(Animal):
+
class Dog(Animal):
     def __init__(self, name):
         self.name = name
 
@@ -9,7 +9,7 @@
 dog = Dog('Fido')
 

and then here is some bash:

-
if [ "$1" = "--help" ]; then
+
if [ "$1" = "--help" ]; then
     echo "OK"
 fi
 
diff --git a/tests/fixtures/test_CommonMark_strong.html b/tests/fixtures/test_CommonMark_strong.html new file mode 100644 index 0000000..4801621 --- /dev/null +++ b/tests/fixtures/test_CommonMark_strong.html @@ -0,0 +1 @@ +

sphinx: a password Store that Perfectly Hides from Itself (No Xaggeration)

diff --git a/tests/fixtures/test_CommonMark_strong.md b/tests/fixtures/test_CommonMark_strong.md new file mode 100644 index 0000000..f19ba19 --- /dev/null +++ b/tests/fixtures/test_CommonMark_strong.md @@ -0,0 +1 @@ +sphinx: a password **S**tore that **P**erfectly **H**ides from **I**tself (**N**o **X**aggeration) diff --git a/tests/fixtures/test_GFM_024.html b/tests/fixtures/test_GFM_024.html index c4c8930..df9cabe 100644 --- a/tests/fixtures/test_GFM_024.html +++ b/tests/fixtures/test_GFM_024.html @@ -1,6 +1,6 @@
    -
  • Valid unchecked checkbox
  • -
  • Valid checked checkbox
  • +
  • Valid unchecked checkbox
  • +
  • Valid checked checkbox
  • Invalid enabled checkbox
  • diff --git a/tests/fixtures/test_GFM_doublequotes.html b/tests/fixtures/test_GFM_doublequotes.html index f9a435e..546fd39 100644 --- a/tests/fixtures/test_GFM_doublequotes.html +++ b/tests/fixtures/test_GFM_doublequotes.html @@ -1,11 +1,11 @@

    This is normal text.

    This is code text.
     
    -
    def this_is_python():
    +
    def this_is_python():
         """This is a docstring."""
         pass
     
    -
    func ThisIsGo(){
    -    return
    -}
    +
    func ThisIsGo(){
    +    return
    +}
     
    diff --git a/tests/fixtures/test_GFM_highlight.html b/tests/fixtures/test_GFM_highlight.html index 90a82f4..4094325 100644 --- a/tests/fixtures/test_GFM_highlight.html +++ b/tests/fixtures/test_GFM_highlight.html @@ -1,10 +1,12 @@

    This is normal text.

    This is code text.
     
    -
    def this_is_python():
    +
    def this_is_python():
         pass
     
    -
    func ThisIsGo(){
    -    return
    -}
    +
    func ThisIsGo(){
    +    return
    +}
    +
    +
    An unknown code fence block
     
    diff --git a/tests/fixtures/test_GFM_highlight.md b/tests/fixtures/test_GFM_highlight.md index 3829fcf..2b0cba6 100644 --- a/tests/fixtures/test_GFM_highlight.md +++ b/tests/fixtures/test_GFM_highlight.md @@ -14,3 +14,7 @@ func ThisIsGo(){ return } ``` + +```abc +An unknown code fence block +``` diff --git a/tests/fixtures/test_GFM_highlight_default_py.html b/tests/fixtures/test_GFM_highlight_default_py.html index a995f63..d85d6ce 100644 --- a/tests/fixtures/test_GFM_highlight_default_py.html +++ b/tests/fixtures/test_GFM_highlight_default_py.html @@ -1,4 +1,4 @@ -
    async def this_is_python():
    +
    async def this_is_python():
         pass
     
     print(await this_is_python())
    diff --git a/tests/fixtures/test_GFM_img.html b/tests/fixtures/test_GFM_img.html
    index 6a88bb2..a04360d 100644
    --- a/tests/fixtures/test_GFM_img.html
    +++ b/tests/fixtures/test_GFM_img.html
    @@ -1,4 +1,4 @@
    -

    Image of Yaktocat

    +

    Image of Yaktocat

    - Image of Yaktocat + Image of Yaktocat

    diff --git a/tests/fixtures/test_GFM_malicious_pre.html b/tests/fixtures/test_GFM_malicious_pre.html index a8d2378..7194eee 100644 --- a/tests/fixtures/test_GFM_malicious_pre.html +++ b/tests/fixtures/test_GFM_malicious_pre.html @@ -1,5 +1,5 @@

    This is normal text.

    -
    def this_is_python():
    +
    def this_is_python():
         """This is a docstring."""
         pass
     <script type="text/javascript">alert('I am evil.');</script>
    diff --git a/tests/fixtures/test_rst_003.html b/tests/fixtures/test_rst_003.html
    index e135ee9..113e64c 100644
    --- a/tests/fixtures/test_rst_003.html
    +++ b/tests/fixtures/test_rst_003.html
    @@ -1,8 +1,8 @@
    -
    +

    Required packages

    To run the PyPI software, you need Python 2.5+ and PostgreSQL

    -
    -
    + +

    Quick development setup

    Make sure you are sitting

    -
    + diff --git a/tests/fixtures/test_rst_008.html b/tests/fixtures/test_rst_008.html index 81d2f48..621a70c 100644 --- a/tests/fixtures/test_rst_008.html +++ b/tests/fixtures/test_rst_008.html @@ -1,16 +1,15 @@ -

    Here is some Python code for a Dog:

    -
    class Dog(Animal):
    +

    Here is some Python code for a Dog:

    +
    class Dog(Animal):
         def __init__(self, name):
             self.name = name
     
         def make_sound(self):
             print('Ruff!')
     
    -dog = Dog('Fido')
    -
    +dog = Dog('Fido')

    and then here is some bash:

    -
    if [ "$1" = "--help" ]; then
    +
    if [ "$1" = "--help" ]; then
         echo "OK"
    -fi
    -
    +fi

    or click SurveyMonkey

    +
    An unknown code fence block
    diff --git a/tests/fixtures/test_rst_008.rst b/tests/fixtures/test_rst_008.rst index 183512c..8397635 100644 --- a/tests/fixtures/test_rst_008.rst +++ b/tests/fixtures/test_rst_008.rst @@ -20,3 +20,8 @@ and then here is some bash: fi or click `SurveyMonkey `_ + + +.. code-block:: abc + + An unknown code fence block diff --git a/tests/fixtures/test_rst_admonitions.html b/tests/fixtures/test_rst_admonitions.html new file mode 100644 index 0000000..11e4805 --- /dev/null +++ b/tests/fixtures/test_rst_admonitions.html @@ -0,0 +1,13 @@ + + + diff --git a/tests/fixtures/test_rst_admonitions.rst b/tests/fixtures/test_rst_admonitions.rst new file mode 100644 index 0000000..9de1da7 --- /dev/null +++ b/tests/fixtures/test_rst_admonitions.rst @@ -0,0 +1,11 @@ +.. danger:: Will Robinson + +.. note:: + + F Sharp is a note, right? + + +.. admonition:: See also + + A customized admonition. + Read more at `docutils `_ diff --git a/tests/fixtures/test_rst_bibtex.html b/tests/fixtures/test_rst_bibtex.html new file mode 100644 index 0000000..4305801 --- /dev/null +++ b/tests/fixtures/test_rst_bibtex.html @@ -0,0 +1,2 @@ +
    @article{the_impact_of_pygments_docutils_config_and_html5,
    +  year = {2022},
    diff --git a/tests/fixtures/test_rst_bibtex.rst b/tests/fixtures/test_rst_bibtex.rst new file mode 100644 index 0000000..a7642f4 --- /dev/null +++ b/tests/fixtures/test_rst_bibtex.rst @@ -0,0 +1,4 @@ +.. code:: bibtex + + @article{the_impact_of_pygments_docutils_config_and_html5, + year = {2022}, diff --git a/tests/fixtures/test_rst_caption.html b/tests/fixtures/test_rst_caption.html index dc1ce6c..0eba267 100644 --- a/tests/fixtures/test_rst_caption.html +++ b/tests/fixtures/test_rst_caption.html @@ -1,50 +1,43 @@ ------- - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + +
    Multiplication
    12345

    1

    2

    3

    4

    5

    12345

    1

    2

    3

    4

    5

    246810

    2

    4

    6

    8

    10

    3691215

    3

    6

    9

    12

    15

    48121620

    4

    8

    12

    16

    20

    510152025

    5

    10

    15

    20

    25

    diff --git a/tests/fixtures/test_rst_contents.html b/tests/fixtures/test_rst_contents.html new file mode 100644 index 0000000..48a5556 --- /dev/null +++ b/tests/fixtures/test_rst_contents.html @@ -0,0 +1,22 @@ + +
    +

    Features

    +
      +
    • Eats cheese

    • +
    +
    +
    +

    Installation

    +

    Requirements

    +
      +
    • Teeth

    • +
    • Good taste

    • +
    +

    Let’s eat some cheese together!

    +
    diff --git a/tests/fixtures/test_rst_contents.rst b/tests/fixtures/test_rst_contents.rst new file mode 100644 index 0000000..13d77a5 --- /dev/null +++ b/tests/fixtures/test_rst_contents.rst @@ -0,0 +1,19 @@ +.. Tests that using a Table of Contents directive renders correctly + +.. contents:: + +Features +======== + +* Eats cheese + +Installation +============ + +Requirements +------------ + +* Teeth +* Good taste + +Let's eat some cheese together! diff --git a/tests/fixtures/test_rst_docinfo.html b/tests/fixtures/test_rst_docinfo.html index 7718e63..4d514b4 100644 --- a/tests/fixtures/test_rst_docinfo.html +++ b/tests/fixtures/test_rst_docinfo.html @@ -1,19 +1,18 @@ - --- - - - - - - - - - - - -
    Project:pg_query – Pythonic wrapper around libpg_query
    Created:mer 02 ago 2017 14:49:24 CEST
    Author:Lele Gaifax <lele@metapensiero.it>
    License:GNU General Public License version 3 or later
    Copyright:© 2017, 2018 Lele Gaifax
    -
    +
    +
    Project:
    +

    pg_query – Pythonic wrapper around libpg_query

    +
    +
    Created:
    +

    mer 02 ago 2017 14:49:24 CEST

    +
    +
    Author:
    +

    Lele Gaifax <lele@metapensiero.it>

    +
    License:
    +

    GNU General Public License version 3 or later

    +
    + + +
    +

    pg_query

    -
    + diff --git a/tests/fixtures/test_rst_linkify.html b/tests/fixtures/test_rst_linkify.html index b1864dd..a151f25 100644 --- a/tests/fixtures/test_rst_linkify.html +++ b/tests/fixtures/test_rst_linkify.html @@ -7,7 +7,7 @@

    It requires a spatial databases compatible with GeoDjango. PostgreSQL 9.x and PostGIS 2.x are recommended for development and production, since these support all the GeoDjango features.

    -
    +

    Status

    multigtfs is ready for your GTFS project.

    Point releases (0.4.1 to 0.4.2) should be safe, only adding features or fixing @@ -21,28 +21,28 @@

    Status

    release.

    All valid GTFS feeds are supported for import and export. This includes feeds with extra columns not yet included in the GTFS spec, and feeds that -omit calendar.txt in favor of calendar_dates.txt (such as the TriMet +omit calendar.txt in favor of calendar_dates.txt (such as the TriMet archive feeds). If you find a feed that doesn’t work, file a bug!

    See the issues list for more details on bugs and feature requests.

    -
    -
    + +

    Example project

    Check out the example project.

    -
    - +
    +
    Code:
    +

    https://github.com/tulsawebdevs/django-multi-gtfs

    +
    +
    Issues:
    +

    https://github.com/tulsawebdevs/django-multi-gtfs/issues

    +
    +
    Dev Docs:
    +

    http://multigtfs.readthedocs.org/

    +
    +
    IRC:
    +

    irc://irc.freenode.net/tulsawebdevs

    +
    +
    + diff --git a/tests/fixtures/test_rst_math.html b/tests/fixtures/test_rst_math.html new file mode 100644 index 0000000..8dd9844 --- /dev/null +++ b/tests/fixtures/test_rst_math.html @@ -0,0 +1,8 @@ +

    A math directive:

    +
    +\begin{equation*} +\alpha _t(i) = P(O_1, O_2, \ldots O_t, q_t = S_i \lambda ) +\end{equation*} +
    +

    A :math: role:

    +

    The area of a circle is \(A_\text{c} = (\pi/4) d^2\).

    diff --git a/tests/fixtures/test_rst_math.rst b/tests/fixtures/test_rst_math.rst new file mode 100644 index 0000000..b4e6db7 --- /dev/null +++ b/tests/fixtures/test_rst_math.rst @@ -0,0 +1,14 @@ +.. Sample from https://docutils.sourceforge.io/docs/ref/rst/directives.html#math + +A ``math`` directive: + +.. math:: + + α_t(i) = P(O_1, O_2, … O_t, q_t = S_i λ) + + +.. Sample from https://docutils.sourceforge.io/docs/ref/rst/roles.html#math + +A ``:math:`` role: + +The area of a circle is :math:`A_\text{c} = (\pi/4) d^2`. diff --git a/tests/fixtures/test_rst_tables.html b/tests/fixtures/test_rst_tables.html new file mode 100644 index 0000000..88cad8b --- /dev/null +++ b/tests/fixtures/test_rst_tables.html @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + +

    Header row, column 1 +(header rows optional)

    Header 2

    Header 3

    Header 4

    body row 1, column 1

    column 2

    column 3

    column 4

    body row 2

    Cells may span columns.

    body row 3

    Cells may +span rows.

      +
    • Table cells

    • +
    • contain

    • +
    • body elements.

    • +
    +

    body row 4

    + + + + + + + + + + + + + + + + + + + +

    title1

    title2

    col1

    col2

    mutirow

    cell1

    cell2

    singlerow

    cell3

    diff --git a/tests/fixtures/test_rst_tables.rst b/tests/fixtures/test_rst_tables.rst new file mode 100644 index 0000000..52ceaa2 --- /dev/null +++ b/tests/fixtures/test_rst_tables.rst @@ -0,0 +1,28 @@ +.. Example from https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#tables + ++------------------------+------------+----------+----------+ +| Header row, column 1 | Header 2 | Header 3 | Header 4 | +| (header rows optional) | | | | ++========================+============+==========+==========+ +| body row 1, column 1 | column 2 | column 3 | column 4 | ++------------------------+------------+----------+----------+ +| body row 2 | Cells may span columns. | ++------------------------+------------+---------------------+ +| body row 3 | Cells may | - Table cells | ++------------------------+ span rows. | - contain | +| body row 4 | | - body elements. | ++------------------------+------------+---------------------+ + +.. Example from #182 + ++---------------+---------------+ +| title1 | title2 | ++===============+===============+ +| col1 | col2 | ++---------------+---------------+ +| mutirow | cell1 | +| +---------------+ +| | cell2 | ++---------------+---------------+ +| singlerow | cell3 | ++---------------+---------------+ diff --git a/tests/test_integration_distutils.py b/tests/test_integration_distutils.py deleted file mode 100644 index 55a857e..0000000 --- a/tests/test_integration_distutils.py +++ /dev/null @@ -1,99 +0,0 @@ -import distutils.dist -import unittest.mock - -import pytest -import setuptools.dist - -import readme_renderer.integration.distutils - - -def test_valid_rst(): - dist = distutils.dist.Distribution(attrs=dict( - long_description="Hello, I am some text.")) - checker = readme_renderer.integration.distutils.Check(dist) - checker.warn = unittest.mock.Mock() - - checker.check_restructuredtext() - - checker.warn.assert_not_called() - - -def test_invalid_rst(): - dist = distutils.dist.Distribution(attrs=dict( - long_description="Hello, I am some `totally borked< text.")) - checker = readme_renderer.integration.distutils.Check(dist) - checker.warn = unittest.mock.Mock() - checker.announce = unittest.mock.Mock() - - checker.check_restructuredtext() - - # Should warn once for the syntax error, and finally to warn that the - # overall syntax is invalid - checker.warn.assert_called_once_with(unittest.mock.ANY) - message = checker.warn.call_args[0][0] - assert 'invalid markup' in message - assert 'line 1: Warning:' in message - assert 'start-string without end-string' in message - - # Should not have announced that it was valid. - checker.announce.assert_not_called() - - -def test_malicious_rst(): - description = """ -.. raw:: html - -""" - dist = distutils.dist.Distribution(attrs=dict( - long_description=description)) - checker = readme_renderer.integration.distutils.Check(dist) - checker.warn = unittest.mock.Mock() - checker.announce = unittest.mock.Mock() - - checker.check_restructuredtext() - - # Should warn once for the syntax error, and finally to warn that the - # overall syntax is invalid - checker.warn.assert_called_once_with(unittest.mock.ANY) - message = checker.warn.call_args[0][0] - assert 'directive disabled' in message - - # Should not have announced that it was valid. - checker.announce.assert_not_called() - - -@pytest.mark.filterwarnings('ignore:::distutils.dist') -def test_markdown(): - dist = setuptools.dist.Distribution(attrs=dict( - long_description="Hello, I am some text.", - long_description_content_type="text/markdown")) - checker = readme_renderer.integration.distutils.Check(dist) - checker.warn = unittest.mock.Mock() - - checker.check_restructuredtext() - - checker.warn.assert_called() - assert 'content type' in checker.warn.call_args[0][0] - - -def test_invalid_missing(): - dist = distutils.dist.Distribution(attrs=dict()) - checker = readme_renderer.integration.distutils.Check(dist) - checker.warn = unittest.mock.Mock() - - checker.check_restructuredtext() - - checker.warn.assert_called_once_with(unittest.mock.ANY) - assert 'missing' in checker.warn.call_args[0][0] - - -def test_invalid_empty(): - dist = distutils.dist.Distribution(attrs=dict( - long_description="")) - checker = readme_renderer.integration.distutils.Check(dist) - checker.warn = unittest.mock.Mock() - - checker.check_restructuredtext() - - checker.warn.assert_called_once_with(unittest.mock.ANY) - assert 'missing' in checker.warn.call_args[0][0] diff --git a/tests/test_markdown.py b/tests/test_markdown.py index 492bc81..8d529a7 100755 --- a/tests/test_markdown.py +++ b/tests/test_markdown.py @@ -1,36 +1,25 @@ -import io -import glob -import os +from pathlib import Path import pytest from readme_renderer.markdown import render, variants -MD_FIXTURES = [ - (fn, os.path.splitext(fn)[0] + ".html", variant) - for variant in variants - for fn in glob.iglob( - os.path.join( - os.path.dirname(__file__), - "fixtures", - "test_" + variant + "*.md" - ) - ) -] - - @pytest.mark.parametrize( ("md_filename", "html_filename", "variant"), - MD_FIXTURES, + [ + (pytest.param(fn, fn.with_suffix(".html"), variant, id=fn.name)) + for variant in variants + for fn in Path(__file__).parent.glob(f"fixtures/test_{variant}*.md") + ], ) def test_md_fixtures(md_filename, html_filename, variant): # Get our Markup - with io.open(md_filename, encoding='utf-8') as f: + with open(md_filename, encoding='utf-8') as f: md_markup = f.read() # Get our expected - with io.open(html_filename, encoding="utf-8") as f: + with open(html_filename, encoding="utf-8") as f: expected = f.read() assert render(md_markup, variant=variant) == expected diff --git a/tests/test_rst.py b/tests/test_rst.py index 64e552c..f1caf8d 100755 --- a/tests/test_rst.py +++ b/tests/test_rst.py @@ -1,6 +1,5 @@ import io -import glob -import os.path +from pathlib import Path import pytest @@ -10,19 +9,17 @@ @pytest.mark.parametrize( ("rst_filename", "html_filename"), [ - (fn, os.path.splitext(fn)[0] + ".html") - for fn in glob.glob( - os.path.join(os.path.dirname(__file__), "fixtures", "test_*.rst") - ) + (pytest.param(fn, fn.with_suffix(".html"), id=fn.name)) + for fn in Path(__file__).parent.glob("fixtures/test_*.rst") ], ) def test_rst_fixtures(rst_filename, html_filename): # Get our Markup - with io.open(rst_filename, encoding='utf-8') as f: + with open(rst_filename, encoding='utf-8') as f: rst_markup = f.read() # Get our expected - with io.open(html_filename, encoding="utf-8") as f: + with open(html_filename, encoding="utf-8") as f: expected = f.read() out = render(rst_markup) @@ -53,3 +50,33 @@ def test_rst_raw(): """, stream=warnings) is None assert '"raw" directive disabled' in warnings.getvalue() + + +def test_rst_empty_file(): + warnings = io.StringIO() + assert render("", stream=warnings) is None + + assert "No content rendered from RST source." in warnings.getvalue() + + +def test_rst_header_only(): + warnings = io.StringIO() + assert render(""" +Header +====== +""", stream=warnings) is None + + assert "No content rendered from RST source." in warnings.getvalue() + + +def test_header_and_malformed_emits_docutils_warning_only(): + warnings = io.StringIO() + assert render(""" +Header +====== + +====== +""", stream=warnings) is None + + assert len(warnings.getvalue().splitlines()) == 1 + assert "No content rendered from RST source." not in warnings.getvalue() diff --git a/tests/test_txt.py b/tests/test_txt.py index b3a5274..9f4f1ac 100644 --- a/tests/test_txt.py +++ b/tests/test_txt.py @@ -1,6 +1,4 @@ -import io -import glob -import os.path +from pathlib import Path import pytest @@ -10,19 +8,17 @@ @pytest.mark.parametrize( ("txt_filename", "html_filename"), [ - (fn, os.path.splitext(fn)[0] + ".html") - for fn in glob.glob( - os.path.join(os.path.dirname(__file__), "fixtures", "test_*.txt") - ) + (pytest.param(fn, fn.with_suffix(".html"), id=fn.name)) + for fn in Path(__file__).parent.glob("fixtures/test_*.txt") ], ) def test_txt_fixtures(txt_filename, html_filename): # Get our Markup - with io.open(txt_filename, encoding='utf-8') as f: + with open(txt_filename, encoding='utf-8') as f: txt_markup = f.read() # Get our expected - with io.open(html_filename, encoding="utf-8") as f: + with open(html_filename, encoding="utf-8") as f: expected = f.read() out = render(txt_markup) diff --git a/tox.ini b/tox.ini index d131239..7e77737 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,24 @@ [tox] -envlist = py36,py37,py38,py39,pep8,packaging,noextra +envlist = py37,py38,py39,py310,pep8,packaging,noextra,mypy +isolated_build = True [testenv] deps = pytest + pytest-cov commands = - pytest --strict {posargs} + pytest --strict-markers --cov {posargs} extras = md +[testenv:mypy] +basepython = python3 +deps = + mypy + types-bleach + types-docutils + types-Pygments +commands = mypy readme_renderer + [testenv:pep8] basepython = python3 deps =