• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pyta-uoft / pyta / 21491946693

29 Jan 2026 07:30PM UTC coverage: 94.183% (+0.009%) from 94.174%
21491946693

Pull #1286

github

web-flow
Merge 5c3927db7 into 1fbc5d121
Pull Request #1286: Fix markdown formatting issue

6 of 6 new or added lines in 1 file covered. (100.0%)

3 existing lines in 2 files now uncovered.

3562 of 3782 relevant lines covered (94.18%)

17.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.97
/packages/python-ta/src/python_ta/reporters/html_reporter.py
1
import base64
20✔
2
import os
20✔
3
import socket
20✔
4
import sys
20✔
5
from html import unescape
20✔
6

7
from jinja2 import Environment, FileSystemLoader
20✔
8
from markdown_it import MarkdownIt
20✔
9
from pygments import highlight
20✔
10
from pygments.formatters import HtmlFormatter
20✔
11
from pygments.lexers import PythonLexer
20✔
12
from pylint.lint import PyLinter
20✔
13
from pylint.reporters.ureports.nodes import BaseLayout
20✔
14

15
from ..util.servers.one_shot_server import open_html_in_browser
20✔
16
from ..util.servers.persistent_server import PersistentHTMLServer
20✔
17
from .core import PythonTaReporter
20✔
18

19
TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates")
20✔
20

21

22
class HTMLReporter(PythonTaReporter):
20✔
23
    """Reporter that displays results in HTML form.
24

25
    By default, automatically opens the report in a web browser.
26
    """
27

28
    name = "pyta-html"
20✔
29

30
    _COLOURING = {
20✔
31
        "black": '<span class="black">',
32
        "black-line": '<span class="black line-num">',
33
        "bold": "<span>",
34
        "code-heading": "<span>",
35
        "style-heading": "<span>",
36
        "code-name": "<span>",
37
        "style-name": "<span>",
38
        "highlight": '<span class="highlight-pyta">',
39
        "grey": '<span class="grey">',
40
        "grey-line": '<span class="grey line-num">',
41
        "gbold": '<span class="gbold">',
42
        "gbold-line": '<span class="gbold line-num">',
43
        "reset": "</span>",
44
    }
45
    _PRE_LINE_NUM_SPACES = 0
20✔
46

47
    no_err_message = "No problems detected, good job!"
20✔
48
    no_snippet = "No code to display for this message."
20✔
49
    code_err_title = "Code Errors or Forbidden Usage (fix: high priority)"
20✔
50
    style_err_title = "Style or Convention Errors (fix: before submission)"
20✔
51
    OUTPUT_FILENAME = "pyta_report.html"
20✔
52
    port = None
20✔
53
    persistent_server = None
20✔
54

55
    def print_messages(self, level="all"):
20✔
56
        """Do nothing to print messages, since all are displayed in a single HTML file."""
57

58
    def display_messages(self, layout: BaseLayout) -> None:
20✔
59
        """Hook for displaying the messages of the reporter
60

61
        This will be called whenever the underlying messages
62
        needs to be displayed. For some reporters, it probably
63
        doesn't make sense to display messages as soon as they
64
        are available, so some mechanism of storing them could be used.
65
        This method can be implemented to display them after they've
66
        been aggregated.
67
        """
68
        md = MarkdownIt()
20✔
69
        grouped_messages = {
20✔
70
            path: self.group_messages(msgs) for path, msgs in self.gather_messages().items()
71
        }
72

73
        template_f = self.linter.config.pyta_template_file
20✔
74
        template_f = (
20✔
75
            template_f if template_f != "" else os.path.join(TEMPLATES_DIR, "template.html.jinja")
76
        )
77
        path = os.path.abspath(template_f)
20✔
78
        filename, file_parent_directory = os.path.basename(path), os.path.dirname(path)
20✔
79

80
        template = Environment(loader=FileSystemLoader(file_parent_directory)).get_template(
20✔
81
            filename
82
        )
83
        if not self.port:
20✔
84
            self.port = (
20✔
85
                _find_free_port()
86
                if self.linter.config.server_port == 0
87
                else self.linter.config.server_port
88
            )
89
        if not self.persistent_server:
20✔
90
            self.persistent_server = PersistentHTMLServer(self.port)
20✔
91

92
        # Embed resources so the output html can go anywhere, independent of assets.
93
        with open(os.path.join(TEMPLATES_DIR, "pyta_logo_markdown.png"), "rb+") as image_file:
20✔
94
            # Encode img binary to base64 (+33% size), decode to remove the "b'"
95
            pyta_logo_base64_encoded = base64.b64encode(image_file.read()).decode()
20✔
96
        pyta_logo_data_url = f"data:image/png;base64,{pyta_logo_base64_encoded}"
20✔
97

98
        # Use the same pyta_logo_url for the favicon
99
        favicon_data_url = pyta_logo_data_url
20✔
100

101
        # Render the jinja template
102
        rendered_template = template.render(
20✔
103
            date_time=self._generate_report_date_time(),
104
            port=self.port,
105
            reporter=self,
106
            grouped_messages=grouped_messages,
107
            os=os,
108
            enumerate=enumerate,
109
            md=md,
110
            pyta_logo_data_url=pyta_logo_data_url,
111
            favicon_data_url=favicon_data_url,
112
            unescape=unescape,
113
        )
114

115
        # If a filepath was specified, write to the file
116
        if self.out is not sys.stdout:
20✔
117
            self.writeln(rendered_template)
20✔
118
            self.out.flush()
20✔
119
        else:
120
            rendered_template = rendered_template.encode("utf8")
20✔
121
            if self.linter.config.watch:
20✔
UNCOV
122
                self.persistent_server.start_server_once(rendered_template)
×
123
            else:
124
                open_html_in_browser(rendered_template, self.port)
20✔
125
                print(
20✔
126
                    "[INFO] Your PythonTA report is being opened in your web browser.\n"
127
                    "       If it doesn't open, please add an output argument to python_ta.check_all\n"
128
                    "       as follows:\n\n"
129
                    "         check_all(..., output='pyta_report.html')\n\n"
130
                    "       This will cause PythonTA to save the report to a file, pyta_report.html,\n"
131
                    "       that you can open manually in a web browser.",
132
                    file=sys.stderr,
133
                )
134

135
    @classmethod
20✔
136
    def _colourify(cls, colour_class: str, text: str) -> str:
20✔
137
        """Return a colourized version of text, using colour_class."""
138
        colour = cls._COLOURING[colour_class]
20✔
139
        new_text = text.replace(" ", cls._SPACE)
20✔
140
        if "-line" not in colour_class:
20✔
141
            new_text = highlight(
20✔
142
                new_text,
143
                PythonLexer(),
144
                HtmlFormatter(nowrap=True, lineseparator="", classprefix="pygments-"),
145
            )
146

147
        return colour + new_text + cls._COLOURING["reset"]
20✔
148

149

150
def _find_free_port() -> int:
20✔
151
    """Find and return an available TCP port on localhost."""
152
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
20✔
153
        s.bind(("127.0.0.1", 0))
20✔
154
        return s.getsockname()[1]
20✔
155

156

157
def register(linter: PyLinter):
20✔
UNCOV
158
    linter.register_reporter(HTMLReporter)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc