diff --git a/.travis.yml b/.travis.yml index d529d9a8..19c7b825 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,15 +17,6 @@ jobs: directories: - $HOME/.cache/pre-commit - - - python: 2.7 - env: TOXENV=py27 - - - - python: 2.7 - env: TOXENV=py27-ansi2html - - - python: 3.6 env: TOXENV=py36 @@ -45,14 +36,6 @@ jobs: sudo: required env: TOXENV=py37-ansi2html - - - python: pypy - env: TOXENV=pypy - - - - python: pypy - env: TOXENV=pypy-ansi2html - - python: pypy3 env: TOXENV=pypy3 diff --git a/README.rst b/README.rst index 02dad39b..c4ccb1f1 100644 --- a/README.rst +++ b/README.rst @@ -28,7 +28,7 @@ Requirements You will need the following prerequisites in order to use pytest-html: -- Python 2.7, 3.6, PyPy, or PyPy3 +- Python 3.6+ or PyPy3 Installation ------------ diff --git a/appveyor.yml b/appveyor.yml index 3b3b413f..66236325 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,11 +9,7 @@ environment: - TOXENV: py36-pytest29 PYTHON_HOME: C:\Python36 - TOXENV: flake8 - PYTHON_HOME: C:\Python27 - - TOXENV: py27-pytest30 - PYTHON_HOME: C:\Python27 - - TOXENV: py27-pytest29 - PYTHON_HOME: C:\Python27 + PYTHON_HOME: C:\Python37 install: - '%PYTHON_HOME%\Scripts\pip --version' - '%PYTHON_HOME%\Scripts\pip install tox' diff --git a/pytest_html/plugin.py b/pytest_html/plugin.py index 49578cba..7e14df5d 100644 --- a/pytest_html/plugin.py +++ b/pytest_html/plugin.py @@ -2,8 +2,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -from __future__ import absolute_import - from base64 import b64encode, b64decode from collections import OrderedDict from os.path import isfile @@ -11,12 +9,13 @@ import json import os import pkg_resources -import sys import time import bisect import warnings import re +from html import escape + try: from ansi2html import Ansi2HTMLConverter, style @@ -30,16 +29,6 @@ from . import extras from . import __version__, __pypi_url__ -PY3 = sys.version_info[0] == 3 - -# Python 2.X and 3.X compatibility -if PY3: - basestring = str - from html import escape -else: - from codecs import open - from cgi import escape - def pytest_addhooks(pluginmanager): from . import hooks @@ -95,10 +84,10 @@ def pytest_unconfigure(config): def data_uri(content, mime_type="text/plain", charset="utf-8"): data = b64encode(content.encode(charset)).decode("ascii") - return "data:{0};charset={1};base64,{2}".format(mime_type, charset, data) + return f"data:{mime_type};charset={charset};base64,{data}" -class HTMLReport(object): +class HTMLReport: def __init__(self, logfile, config): logfile = os.path.expanduser(os.path.expandvars(logfile)) self.logfile = os.path.abspath(logfile) @@ -136,7 +125,7 @@ def __init__(self, outcome, report, logfile, config): cells = [ html.td(self.outcome, class_="col-result"), html.td(self.test_id, class_="col-name"), - html.td("{0:.2f}".format(self.time), class_="col-duration"), + html.td(f"{self.time:.2f}", class_="col-duration"), html.td(self.links_html, class_="col-links"), ] @@ -181,7 +170,7 @@ def create_asset( if not os.path.exists(os.path.dirname(asset_path)): os.makedirs(os.path.dirname(asset_path)) - relative_path = "{0}/{1}".format("assets", asset_file_name) + relative_path = f"assets/{asset_file_name}" kwargs = {"encoding": "utf-8"} if "b" not in mode else {} with open(asset_path, mode, **kwargs) as f: @@ -209,13 +198,10 @@ def append_extra_html(self, extra, extra_index, test_index): ) html_div = html.a(html.img(src=content), href=content) elif self.self_contained: - src = "data:{0};base64,{1}".format(extra.get("mime_type"), content) + src = "data:{};base64,{}".format(extra.get("mime_type"), content) html_div = html.img(src=src) else: - if PY3: - content = content.encode("utf-8") - - content = b64decode(content) + content = b64decode(content.encode("utf-8")) href = src = self.create_asset( content, extra_index, test_index, extra.get("extension"), "wb" ) @@ -276,7 +262,7 @@ def append_log_html(self, report, additional_html): for section in report.sections: header, content = map(escape, section) - log.append(" {0} ".format(header).center(80, "-")) + log.append(f" {header:-^80} ") log.append(html.br()) if ANSI: converter = Ansi2HTMLConverter(inline=False, escaped=False) @@ -296,7 +282,7 @@ def _appendrow(self, outcome, report): self.results.insert(index, result) tbody = html.tbody( result.row_table, - class_="{0} results-table-row".format(result.outcome.lower()), + class_="{} results-table-row".format(result.outcome.lower()), ) if result.row_extra is not None: tbody.append(result.row_extra) @@ -345,9 +331,7 @@ def _generate_report(self, session): self.style_css = pkg_resources.resource_string( __name__, os.path.join("resources", "style.css") - ) - if PY3: - self.style_css = self.style_css.decode("utf-8") + ).decode("utf-8") if ANSI: ansi_css = [ @@ -362,12 +346,12 @@ def _generate_report(self, session): for path in self.config.getoption("css"): self.style_css += "\n/******************************" self.style_css += "\n * CUSTOM CSS" - self.style_css += "\n * {}".format(path) + self.style_css += f"\n * {path}" self.style_css += "\n ******************************/\n\n" with open(path, "r") as f: self.style_css += f.read() - css_href = "{0}/{1}".format("assets", "style.css") + css_href = "assets/style.css" html_css = html.link(href=css_href, rel="stylesheet", type="text/css") if self.self_contained: html_css = html.style(raw(self.style_css)) @@ -401,12 +385,12 @@ def generate_checkbox(self): name="filter_checkbox", class_="filter", hidden="true", - **checkbox_kwargs + **checkbox_kwargs, ) def generate_summary_item(self): self.summary_item = html.span( - "{0} {1}".format(self.total, self.label), class_=self.class_html + f"{self.total} {self.label}", class_=self.class_html ) outcomes = [ @@ -422,9 +406,7 @@ def generate_summary_item(self): outcomes.append(Outcome("rerun", self.rerun)) summary = [ - html.p( - "{0} tests ran in {1:.2f} seconds. ".format(numtests, suite_time_delta) - ), + html.p(f"{numtests} tests ran in {suite_time_delta:.2f} seconds. "), html.p( "(Un)check the boxes to filter the results.", class_="filter", @@ -472,19 +454,17 @@ def generate_summary_item(self): main_js = pkg_resources.resource_string( __name__, os.path.join("resources", "main.js") - ) - if PY3: - main_js = main_js.decode("utf-8") + ).decode("utf-8") body = html.body( html.script(raw(main_js)), html.h1(os.path.basename(self.logfile)), html.p( - "Report generated on {0} at {1} by ".format( + "Report generated on {} at {} by ".format( generated.strftime("%d-%b-%Y"), generated.strftime("%H:%M:%S") ), html.a("pytest-html", href=__pypi_url__), - " v{0}".format(__version__), + f" v{__version__}", ), onLoad="init()", ) @@ -501,12 +481,11 @@ def generate_summary_item(self): doc = html.html(head, body) - unicode_doc = u"\n{0}".format(doc.unicode(indent=2)) - if PY3: - # Fix encoding issues, e.g. with surrogates - unicode_doc = unicode_doc.encode("utf-8", errors="xmlcharrefreplace") - unicode_doc = unicode_doc.decode("utf-8") - return unicode_doc + unicode_doc = "\n{}".format(doc.unicode(indent=2)) + + # Fix encoding issues, e.g. with surrogates + unicode_doc = unicode_doc.encode("utf-8", errors="xmlcharrefreplace") + return unicode_doc.decode("utf-8") def _generate_environment(self, config): if not hasattr(config, "_metadata") or config._metadata is None: @@ -522,10 +501,10 @@ def _generate_environment(self, config): for key in keys: value = metadata[key] - if isinstance(value, basestring) and value.startswith("http"): + if isinstance(value, str) and value.startswith("http"): value = html.a(value, href=value, target="_blank") elif isinstance(value, (list, tuple, set)): - value = ", ".join((str(i) for i in value)) + value = ", ".join(str(i) for i in value) rows.append(html.tr(html.td(key), html.td(value))) environment.append(html.table(rows, id="environment")) @@ -569,6 +548,4 @@ def pytest_sessionfinish(self, session): self._save_report(report_content) def pytest_terminal_summary(self, terminalreporter): - terminalreporter.write_sep( - "-", "generated html file: file://{0}".format(self.logfile) - ) + terminalreporter.write_sep("-", f"generated html file: file://{self.logfile}") diff --git a/setup.py b/setup.py index 1f4922c3..97f40083 100644 --- a/setup.py +++ b/setup.py @@ -12,9 +12,10 @@ package_data={"pytest_html": ["resources/*"]}, entry_points={"pytest11": ["html = pytest_html.plugin"]}, setup_requires=["setuptools_scm"], - install_requires=["pytest>=3.0", "pytest-metadata"], + install_requires=["pytest>=5.0", "pytest-metadata"], license="Mozilla Public License 2.0 (MPL 2.0)", keywords="py.test pytest html report", + python_requires=">=3.6", classifiers=[ "Development Status :: 5 - Production/Stable", "Framework :: Pytest", @@ -27,8 +28,9 @@ "Topic :: Software Development :: Testing", "Topic :: Utilities", "Programming Language :: Python", - "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3 :: Only", ], ) diff --git a/testing/test_pytest_html.py b/testing/test_pytest_html.py index c7309a40..1fe847e6 100644 --- a/testing/test_pytest_html.py +++ b/testing/test_pytest_html.py @@ -3,17 +3,14 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. from base64 import b64encode -from distutils.version import LooseVersion import json import os -import sys import pkg_resources import random import re import pytest -PY3 = sys.version_info[0] == 3 pytest_plugins = ("pytester",) @@ -30,20 +27,19 @@ def read_html(path): def assert_results_by_outcome(html, test_outcome, test_outcome_number, label=None): # Asserts if the test number of this outcome in the summary is correct - regex_summary = r"(\d)+ {0}".format(label or test_outcome) + regex_summary = r"(\d)+ {}".format(label or test_outcome) assert int(re.search(regex_summary, html).group(1)) == test_outcome_number # Asserts if the generated checkbox of this outcome is correct regex_checkbox = ( - '{report_name}" assert report_title in html def test_report_title_addopts_env_var(self, testdir, monkeypatch): @@ -232,17 +224,15 @@ def test_report_title_addopts_env_var(self, testdir, monkeypatch): monkeypatch.setenv(report_location, report_name) testdir.makefile( ".ini", - pytest=""" + pytest=f""" [pytest] - addopts = --html ${0} - """.format( - report_location - ), + addopts = --html ${report_location} + """, ) testdir.makepyfile("def test_pass(): pass") result = testdir.runpytest() assert result.ret == 0 - report_title = "

{0}

".format(report_name) + report_title = f"

{report_name}

" assert report_title in read_html(report_name) def test_resources_inline_css(self, testdir): @@ -253,8 +243,7 @@ def test_resources_inline_css(self, testdir): content = pkg_resources.resource_string( "pytest_html", os.path.join("resources", "style.css") ) - if PY3: - content = content.decode("utf-8") + content = content.decode("utf-8") assert content assert content in html @@ -266,8 +255,7 @@ def test_resources(self, testdir): content = pkg_resources.resource_string( "pytest_html", os.path.join("resources", "main.js") ) - if PY3: - content = content.decode("utf-8") + content = content.decode("utf-8") assert content assert content in html regex_css_link = '{0}')] - """.format( - content - ) + report.extra = [extras.html('
{content}
')] + """ ) testdir.makepyfile("def test_pass(): pass") result, html = run(testdir) @@ -340,7 +322,7 @@ def pytest_runtest_makereport(item, call): ) def test_extra_text(self, testdir, content, encoded): testdir.makeconftest( - """ + f""" import pytest @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): @@ -348,22 +330,20 @@ def pytest_runtest_makereport(item, call): report = outcome.get_result() if report.when == 'call': from pytest_html import extras - report.extra = [extras.text({0})] - """.format( - content - ) + report.extra = [extras.text({content})] + """ ) testdir.makepyfile("def test_pass(): pass") result, html = run(testdir, "report.html", "--self-contained-html") assert result.ret == 0 - href = "data:text/plain;charset=utf-8;base64,{0}".format(encoded) - link = 'Text'.format(href) + href = f"data:text/plain;charset=utf-8;base64,{encoded}" + link = f'Text' assert link in html def test_extra_json(self, testdir): content = {str(random.random()): str(random.random())} testdir.makeconftest( - """ + f""" import pytest @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): @@ -371,27 +351,22 @@ def pytest_runtest_makereport(item, call): report = outcome.get_result() if report.when == 'call': from pytest_html import extras - report.extra = [extras.json({0})] - """.format( - content - ) + report.extra = [extras.json({content})] + """ ) testdir.makepyfile("def test_pass(): pass") result, html = run(testdir, "report.html", "--self-contained-html") assert result.ret == 0 content_str = json.dumps(content) - if PY3: - data = b64encode(content_str.encode("utf-8")).decode("ascii") - else: - data = b64encode(content_str) - href = "data:application/json;charset=utf-8;base64,{0}".format(data) - link = 'JSON'.format(href) + data = b64encode(content_str.encode("utf-8")).decode("ascii") + href = f"data:application/json;charset=utf-8;base64,{data}" + link = f'JSON' assert link in html def test_extra_url(self, testdir): content = str(random.random()) testdir.makeconftest( - """ + f""" import pytest @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): @@ -399,15 +374,13 @@ def pytest_runtest_makereport(item, call): report = outcome.get_result() if report.when == 'call': from pytest_html import extras - report.extra = [extras.url('{0}')] - """.format( - content - ) + report.extra = [extras.url('{content}')] + """ ) testdir.makepyfile("def test_pass(): pass") result, html = run(testdir) assert result.ret == 0 - link = 'URL'.format(content) + link = f'URL' assert link in html @pytest.mark.parametrize( @@ -422,7 +395,7 @@ def pytest_runtest_makereport(item, call): def test_extra_image(self, testdir, mime_type, extension): content = str(random.random()) testdir.makeconftest( - """ + f""" import pytest @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): @@ -430,16 +403,14 @@ def pytest_runtest_makereport(item, call): report = outcome.get_result() if report.when == 'call': from pytest_html import extras - report.extra = [extras.{0}('{1}')] - """.format( - extension, content - ) + report.extra = [extras.{extension}('{content}')] + """ ) testdir.makepyfile("def test_pass(): pass") result, html = run(testdir, "report.html", "--self-contained-html") assert result.ret == 0 - src = "data:{0};base64,{1}".format(mime_type, content) - assert ''.format(src) in html + src = f"data:{mime_type};base64,{content}" + assert f'' in html def test_extra_image_windows(self, mocker, testdir): mock_isfile = mocker.patch("pytest_html.plugin.isfile") @@ -452,7 +423,7 @@ def test_extra_image_windows(self, mocker, testdir): ) def test_extra_text_separated(self, testdir, content): testdir.makeconftest( - """ + f""" import pytest @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): @@ -460,16 +431,14 @@ def pytest_runtest_makereport(item, call): report = outcome.get_result() if report.when == 'call': from pytest_html import extras - report.extra = [extras.text({0})] - """.format( - content - ) + report.extra = [extras.text({content})] + """ ) testdir.makepyfile("def test_pass(): pass") result, html = run(testdir) assert result.ret == 0 src = "assets/test_extra_text_separated.py__test_pass_0_0.txt" - link = ''.format(src) + link = f'' assert link in html assert os.path.exists(src) @@ -478,9 +447,9 @@ def pytest_runtest_makereport(item, call): [("png", "image"), ("png", "png"), ("svg", "svg"), ("jpg", "jpg")], ) def test_extra_image_separated(self, testdir, file_extension, extra_type): - content = b64encode("foo".encode("utf-8")).decode("ascii") + content = b64encode(b"foo").decode("ascii") testdir.makeconftest( - """ + f""" import pytest @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): @@ -488,18 +457,14 @@ def pytest_runtest_makereport(item, call): report = outcome.get_result() if report.when == 'call': from pytest_html import extras - report.extra = [extras.{0}('{1}')] - """.format( - extra_type, content - ) + report.extra = [extras.{extra_type}('{content}')] + """ ) testdir.makepyfile("def test_pass(): pass") result, html = run(testdir) assert result.ret == 0 - src = "assets/test_extra_image_separated.py__test_pass_0_0.{}".format( - file_extension - ) - link = ''.format(src) + src = f"assets/test_extra_image_separated.py__test_pass_0_0.{file_extension}" + link = f'' assert link in html assert os.path.exists(src) @@ -508,9 +473,9 @@ def pytest_runtest_makereport(item, call): [("png", "image"), ("png", "png"), ("svg", "svg"), ("jpg", "jpg")], ) def test_extra_image_separated_rerun(self, testdir, file_extension, extra_type): - content = b64encode("foo".encode("utf-8")).decode("ascii") + content = b64encode(b"foo").decode("ascii") testdir.makeconftest( - """ + f""" import pytest @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): @@ -518,10 +483,8 @@ def pytest_runtest_makereport(item, call): report = outcome.get_result() if report.when == 'call': from pytest_html import extras - report.extra = [extras.{0}('{1}')] - """.format( - extra_type, content - ) + report.extra = [extras.{extra_type}('{content}')] + """ ) testdir.makepyfile( """ @@ -534,8 +497,8 @@ def test_fail(): for i in range(1, 4): asset_name = "test_extra_image_separated_rerun.py__test_fail" - src = "assets/{}_0_{}.{}".format(asset_name, i, file_extension) - link = ''.format(src) + src = f"assets/{asset_name}_0_{i}.{file_extension}" + link = f'' assert result.ret assert link in html assert os.path.exists(src) @@ -544,7 +507,7 @@ def test_fail(): def test_extra_image_non_b64(self, testdir, src_type): content = src_type testdir.makeconftest( - """ + f""" import pytest @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): @@ -552,10 +515,8 @@ def pytest_runtest_makereport(item, call): report = outcome.get_result() if report.when == 'call': from pytest_html import extras - report.extra = [extras.image('{0}')] - """.format( - content - ) + report.extra = [extras.image('{content}')] + """ ) testdir.makepyfile("def test_pass(): pass") if src_type == "image.png": @@ -580,17 +541,15 @@ def pytest_runtest_makereport(item, call): # This will get truncated test_name = "test_{}".format("a" * 300) testdir.makepyfile( - """ - def {0}(): + f""" + def {test_name}(): assert False - """.format( - test_name - ) + """ ) result, html = run(testdir) - file_name = "test_very_long_test_name.py__{}_0_0.png".format(test_name)[-255:] + file_name = f"test_very_long_test_name.py__{test_name}_0_0.png"[-255:] src = "assets/" + file_name - link = ''.format(src) + link = f'' assert result.ret assert link in html assert os.path.exists(src) @@ -633,12 +592,10 @@ def pytest_configure(config): def test_environment(self, testdir): content = str(random.random()) testdir.makeconftest( - """ + f""" def pytest_configure(config): - config._metadata['content'] = '{0}' - """.format( - content - ) + config._metadata['content'] = '{content}' + """ ) testdir.makepyfile("def test_pass(): pass") result, html = run(testdir) @@ -649,13 +606,11 @@ def pytest_configure(config): def test_environment_xdist(self, testdir): content = str(random.random()) testdir.makeconftest( - """ + f""" def pytest_configure(config): for i in range(2): - config._metadata['content'] = '{0}' - """.format( - content - ) + config._metadata['content'] = '{content}' + """ ) testdir.makepyfile("def test_pass(): pass") result, html = run(testdir, "report.html", "-n", "1") @@ -666,13 +621,11 @@ def pytest_configure(config): def test_environment_xdist_reruns(self, testdir): content = str(random.random()) testdir.makeconftest( - """ + f""" def pytest_configure(config): for i in range(2): - config._metadata['content'] = '{0}' - """.format( - content - ) + config._metadata['content'] = '{content}' + """ ) testdir.makepyfile("def test_fail(): assert False") result, html = run(testdir, "report.html", "-n", "1", "--reruns", "1") @@ -683,16 +636,14 @@ def pytest_configure(config): def test_environment_list_value(self, testdir): content = tuple(str(random.random()) for i in range(10)) content += tuple(random.random() for i in range(10)) - expected_content = ", ".join((str(i) for i in content)) - expected_html_re = r"content\n\s+{}".format(expected_content) + expected_content = ", ".join(str(i) for i in content) + expected_html_re = fr"content\n\s+{expected_content}" testdir.makeconftest( - """ + f""" def pytest_configure(config): for i in range(2): - config._metadata['content'] = {0} - """.format( - content - ) + config._metadata['content'] = {content} + """ ) testdir.makepyfile("def test_pass(): pass") result, html = run(testdir) @@ -714,12 +665,6 @@ def pytest_configure(config): assert "Environment" in html assert len(re.findall("ZZZ.+AAA", html, re.DOTALL)) == 1 - @pytest.mark.xfail( - sys.version_info < (3, 2) - and LooseVersion(pytest.__version__) >= LooseVersion("2.8.0"), - reason="Fails on earlier versions of Python and pytest", - run=False, - ) def test_xdist_crashing_slave(self, testdir): """https://github.com/pytest-dev/pytest-html/issues/21""" testdir.makepyfile( @@ -779,17 +724,15 @@ def test_ansi(): @pytest.mark.parametrize("content", [("'foo'"), ("u'\u0081'")]) def test_utf8_longrepr(self, testdir, content): testdir.makeconftest( - """ + f""" import pytest @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == 'call': - report.longrepr = 'utf8 longrepr: ' + {0} - """.format( - content - ) + report.longrepr = 'utf8 longrepr: ' + {content} + """ ) testdir.makepyfile( """ @@ -821,7 +764,7 @@ def test_css(self, testdir, colors): css = {} cssargs = [] for color in colors: - style = "* {{color: {}}}".format(color) + style = f"* {{color: {color}}}" path = testdir.makefile(".css", **{color: style}) css[color] = {"style": style, "path": path} cssargs.extend(["--css", path]) diff --git a/tox.ini b/tox.ini index 97e4c015..6a9cc242 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py{27,36,37,py,py3}{,-ansi2html}, linting +envlist = py{36,37,py3}{,-ansi2html}, linting [testenv] setenv = PYTHONDONTWRITEBYTECODE=1 @@ -12,7 +12,7 @@ deps = pytest-xdist pytest-rerunfailures pytest-mock - py{27,36,py,py3}-ansi2html: ansi2html + py{36,37,py3}-ansi2html: ansi2html commands = pytest -v -r a {posargs} [testenv:linting]