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

pyta-uoft / pyta / 27479146033

13 Jun 2026 09:07PM UTC coverage: 90.918% (-0.01%) from 90.929%
27479146033

Pull #1356

github

web-flow
Merge 691b90661 into 722e8c7db
Pull Request #1356: Modify PyTA report messages

17 of 18 new or added lines in 2 files covered. (94.44%)

6 existing lines in 3 files now uncovered.

3694 of 4063 relevant lines covered (90.92%)

17.67 hits per line

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

95.65
/packages/python-ta/src/python_ta/reporters/html_reporter.py
1
import base64
20✔
2
import os
20✔
3
import random
20✔
4
import socket
20✔
5
import sys
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.extended_markup import UNMAPPED_CODE_POINT
20✔
16
from ..util.servers.one_shot_server import open_html_in_browser
20✔
17
from ..util.servers.persistent_server import PersistentHTMLServer
20✔
18
from .core import PythonTaReporter
20✔
19

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

22

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

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

29
    name = "pyta-html"
20✔
30

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

48
    NO_ERR_EMOJIS = ["🎉", "🥳", "🌟", "👍", "👏", "😊", "🎊", "🙌", "🕺"]
20✔
49

50
    no_snippet = "No code to display for this message."
20✔
51
    code_err_title = "Code Errors or Forbidden Usage (fix: high priority)"
20✔
52
    style_err_title = "Style or Convention Errors (fix: before submission)"
20✔
53
    OUTPUT_FILENAME = "pyta_report.html"
20✔
54
    port = None
20✔
55
    persistent_server = None
20✔
56

57
    def no_err_message(self) -> str:
20✔
58
        """Return the no errors message with a random emoji."""
NEW
UNCOV
59
        return "No problems detected, good job! " + random.choice(self.NO_ERR_EMOJIS)
×
60

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

64
    def display_messages(self, layout: BaseLayout) -> None:
20✔
65
        """Hook for displaying the messages of the reporter
66

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

79
        template_f = self.linter.config.pyta_template_file
20✔
80
        template_f = (
20✔
81
            template_f if template_f != "" else os.path.join(TEMPLATES_DIR, "template.html.jinja")
82
        )
83
        path = os.path.abspath(template_f)
20✔
84
        filename, file_parent_directory = os.path.basename(path), os.path.dirname(path)
20✔
85

86
        template = Environment(loader=FileSystemLoader(file_parent_directory)).get_template(
20✔
87
            filename
88
        )
89
        if not self.port:
20✔
90
            self.port = (
20✔
91
                _find_free_port()
92
                if self.linter.config.server_port == 0
93
                else self.linter.config.server_port
94
            )
95
        if not self.persistent_server:
20✔
96
            self.persistent_server = PersistentHTMLServer(self.port)
20✔
97

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

104
        # Use the same pyta_logo_url for the favicon
105
        favicon_data_url = pyta_logo_data_url
20✔
106

107
        # Render the jinja template
108
        rendered_template = template.render(
20✔
109
            date_time=self._generate_report_date_time(),
110
            port=self.port,
111
            reporter=self,
112
            grouped_messages=grouped_messages,
113
            os=os,
114
            enumerate=enumerate,
115
            md=md,
116
            pyta_logo_data_url=pyta_logo_data_url,
117
            favicon_data_url=favicon_data_url,
118
            UNMAPPED_CODE_POINT=UNMAPPED_CODE_POINT,
119
        )
120

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

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

153
        return colour + new_text + cls._COLOURING["reset"]
20✔
154

155

156
def _find_free_port() -> int:
20✔
157
    """Find and return an available TCP port on localhost."""
158
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
20✔
159
        s.bind(("127.0.0.1", 0))
20✔
160
        return s.getsockname()[1]
20✔
161

162

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

© 2026 Coveralls, Inc