Skip to content

Commit 331214b

Browse files
ziegenbergssbarnea
andauthored
Add type hinting (#143)
Co-authored-by: Sorin Sbarnea <[email protected]>
1 parent 7515540 commit 331214b

File tree

6 files changed

+153
-116
lines changed

6 files changed

+153
-116
lines changed

.pre-commit-config.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,19 @@ repos:
3737
additional_dependencies:
3838
- ansible-base
3939
- testinfra
40+
- repo: https://github.com/pre-commit/mirrors-mypy
41+
rev: v0.910-1
42+
hooks:
43+
- id: mypy
44+
# empty args needed in order to match mypy cli behavior
45+
args: ["--strict"]
46+
additional_dependencies:
47+
- pytest
48+
- types-setuptools
49+
- types-pkg_resources
50+
- types-mock
51+
exclude: >
52+
(?x)^(
53+
docs/.*|
54+
setup.py
55+
)$

ansi2html/converter.py

Lines changed: 78 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,17 @@
2020
# along with this program. If not, see
2121
# <http://www.gnu.org/licenses/>.
2222

23-
import io
2423
import optparse
2524
import re
2625
import sys
26+
from typing import Dict, Iterator, List, Optional, Set, Tuple, Union
2727

2828
import pkg_resources
2929

3030
try:
3131
from collections import OrderedDict
3232
except ImportError:
33-
from ordereddict import OrderedDict
33+
from ordereddict import OrderedDict # type: ignore
3434

3535
from ansi2html.style import SCHEME, get_styles
3636

@@ -115,22 +115,22 @@
115115

116116

117117
class _State:
118-
def __init__(self):
118+
def __init__(self) -> None:
119119
self.inside_span = False
120120
self.reset()
121121

122-
def reset(self):
123-
self.intensity = ANSI_INTENSITY_NORMAL
124-
self.style = ANSI_STYLE_NORMAL
125-
self.blink = ANSI_BLINK_OFF
126-
self.underline = ANSI_UNDERLINE_OFF
127-
self.crossedout = ANSI_CROSSED_OUT_OFF
128-
self.visibility = ANSI_VISIBILITY_ON
129-
self.foreground = (ANSI_FOREGROUND_DEFAULT, None)
130-
self.background = (ANSI_BACKGROUND_DEFAULT, None)
131-
self.negative = ANSI_NEGATIVE_OFF
132-
133-
def adjust(self, ansi_code, parameter=None):
122+
def reset(self) -> None:
123+
self.intensity: int = ANSI_INTENSITY_NORMAL
124+
self.style: int = ANSI_STYLE_NORMAL
125+
self.blink: int = ANSI_BLINK_OFF
126+
self.underline: int = ANSI_UNDERLINE_OFF
127+
self.crossedout: int = ANSI_CROSSED_OUT_OFF
128+
self.visibility: int = ANSI_VISIBILITY_ON
129+
self.foreground: Tuple[int, Optional[int]] = (ANSI_FOREGROUND_DEFAULT, None)
130+
self.background: Tuple[int, Optional[int]] = (ANSI_BACKGROUND_DEFAULT, None)
131+
self.negative: int = ANSI_NEGATIVE_OFF
132+
133+
def adjust(self, ansi_code: int, parameter: Optional[int] = None) -> None:
134134
if ansi_code in (
135135
ANSI_INTENSITY_INCREASED,
136136
ANSI_INTENSITY_REDUCED,
@@ -174,17 +174,21 @@ def adjust(self, ansi_code, parameter=None):
174174
elif ansi_code in (ANSI_NEGATIVE_ON, ANSI_NEGATIVE_OFF):
175175
self.negative = ansi_code
176176

177-
def to_css_classes(self):
178-
css_classes = []
177+
def to_css_classes(self) -> List[str]:
178+
css_classes: List[str] = []
179179

180-
def append_unless_default(output, value, default):
180+
def append_unless_default(output: List[str], value: int, default: int) -> None:
181181
if value != default:
182182
css_class = "ansi%d" % value
183183
output.append(css_class)
184184

185185
def append_color_unless_default(
186-
output, color, default, negative, neg_css_class
187-
):
186+
output: List[str],
187+
color: Tuple[int, Optional[int]],
188+
default: int,
189+
negative: bool,
190+
neg_css_class: str,
191+
) -> None:
188192
value, parameter = color
189193
if value != default:
190194
prefix = "inv" if negative else "ansi"
@@ -222,17 +226,17 @@ def append_color_unless_default(
222226

223227

224228
class OSC_Link:
225-
def __init__(self, url, text):
229+
def __init__(self, url: str, text: str) -> None:
226230
self.url = url
227231
self.text = text
228232

229233

230-
def map_vt100_box_code(char):
234+
def map_vt100_box_code(char: str) -> str:
231235
char_hex = hex(ord(char))
232236
return VT100_BOX_CODES[char_hex] if char_hex in VT100_BOX_CODES else char
233237

234238

235-
def _needs_extra_newline(text):
239+
def _needs_extra_newline(text: str) -> bool:
236240
if not text or text.endswith("\n"):
237241
return False
238242
return True
@@ -254,18 +258,18 @@ class Ansi2HTMLConverter:
254258

255259
def __init__(
256260
self,
257-
latex=False,
258-
inline=False,
259-
dark_bg=True,
260-
line_wrap=True,
261-
font_size="normal",
262-
linkify=False,
263-
escaped=True,
264-
markup_lines=False,
265-
output_encoding="utf-8",
266-
scheme="ansi2html",
267-
title="",
268-
):
261+
latex: bool = False,
262+
inline: bool = False,
263+
dark_bg: bool = True,
264+
line_wrap: bool = True,
265+
font_size: str = "normal",
266+
linkify: bool = False,
267+
escaped: bool = True,
268+
markup_lines: bool = False,
269+
output_encoding: str = "utf-8",
270+
scheme: str = "ansi2html",
271+
title: str = "",
272+
) -> None:
269273

270274
self.latex = latex
271275
self.inline = inline
@@ -278,7 +282,7 @@ def __init__(
278282
self.output_encoding = output_encoding
279283
self.scheme = scheme
280284
self.title = title
281-
self._attrs = None
285+
self._attrs: Optional[Dict[str, Union[bool, str, Set[str]]]] = None
282286
self.hyperref = False
283287

284288
if inline:
@@ -298,27 +302,27 @@ def __init__(
298302
)
299303
self.osc_link_re = re.compile("\033\\]8;;(.*?)\007(.*?)\033\\]8;;\007")
300304

301-
def do_linkify(self, line):
305+
def do_linkify(self, line: str) -> str:
302306
if not isinstance(line, str):
303307
return line # If line is an object, e.g. OSC_Link, it
304308
# will be expanded to a string later
305309
if self.latex:
306310
return self.url_matcher.sub(r"\\url{\1}", line)
307311
return self.url_matcher.sub(r'<a href="\1">\1</a>', line)
308312

309-
def handle_osc_links(self, part):
313+
def handle_osc_links(self, part: OSC_Link) -> str:
310314
if self.latex:
311315
self.hyperref = True
312316
return """\\href{%s}{%s}""" % (part.url, part.text)
313317
return """<a href="%s">%s</a>""" % (part.url, part.text)
314318

315-
def apply_regex(self, ansi):
316-
styles_used = set()
317-
parts = self._apply_regex(ansi, styles_used)
318-
parts = self._collapse_cursor(parts)
319-
parts = list(parts)
319+
def apply_regex(self, ansi: str) -> Tuple[str, Set[str]]:
320+
styles_used: Set[str] = set()
321+
all_parts = self._apply_regex(ansi, styles_used)
322+
no_cursor_parts = self._collapse_cursor(all_parts)
323+
no_cursor_parts = list(no_cursor_parts)
320324

321-
def _check_links(parts):
325+
def _check_links(parts: List[Union[str, OSC_Link]]) -> Iterator[str]:
322326
for part in parts:
323327
if isinstance(part, str):
324328
if self.linkify:
@@ -330,7 +334,7 @@ def _check_links(parts):
330334
else:
331335
yield part
332336

333-
parts = list(_check_links(parts))
337+
parts = list(_check_links(no_cursor_parts))
334338
combined = "".join(parts)
335339
if self.markup_lines and not self.latex:
336340
combined = "\n".join(
@@ -341,7 +345,9 @@ def _check_links(parts):
341345
)
342346
return combined, styles_used
343347

344-
def _apply_regex(self, ansi, styles_used):
348+
def _apply_regex(
349+
self, ansi: str, styles_used: Set[str]
350+
) -> Iterator[Union[str, OSC_Link, CursorMoveUp]]:
345351
if self.escaped:
346352
if (
347353
self.latex
@@ -358,7 +364,7 @@ def _apply_regex(self, ansi, styles_used):
358364
for pattern, special in specials.items():
359365
ansi = ansi.replace(pattern, special)
360366

361-
def _vt100_box_drawing():
367+
def _vt100_box_drawing() -> Iterator[str]:
362368
last_end = 0 # the index of the last end of a code we've seen
363369
box_drawing_mode = False
364370
for match in self.vt100_box_codes_prog.finditer(ansi):
@@ -374,7 +380,7 @@ def _vt100_box_drawing():
374380

375381
ansi = "".join(_vt100_box_drawing())
376382

377-
def _osc_link(ansi):
383+
def _osc_link(ansi: str) -> Iterator[Union[str, OSC_Link]]:
378384
last_end = 0
379385
for match in self.osc_link_re.finditer(ansi):
380386
trailer = ansi[last_end : match.start()]
@@ -397,20 +403,23 @@ def _osc_link(ansi):
397403
else:
398404
yield "</span>"
399405

400-
def _handle_ansi_code(self, ansi, styles_used, state):
406+
def _handle_ansi_code(
407+
self, ansi: str, styles_used: Set[str], state: _State
408+
) -> Iterator[Union[str, CursorMoveUp]]:
401409
last_end = 0 # the index of the last end of a code we've seen
402410
for match in self.ansi_codes_prog.finditer(ansi):
403411
yield ansi[last_end : match.start()]
404412
last_end = match.end()
405413

414+
params: Union[str, List[int]]
406415
params, command = match.groups()
407416

408417
if command not in "mMA":
409418
continue
410419

411420
# Special cursor-moving code. The only supported one.
412421
if command == "A":
413-
yield CursorMoveUp
422+
yield CursorMoveUp # type: ignore
414423
continue
415424

416425
try:
@@ -452,7 +461,7 @@ def _handle_ansi_code(self, ansi, styles_used, state):
452461

453462
if v in (ANSI_FOREGROUND_256, ANSI_BACKGROUND_256):
454463
try:
455-
parameter = params[i + 2]
464+
parameter: Optional[int] = params[i + 2]
456465
except IndexError:
457466
continue
458467
skip_after_index = i + 2
@@ -495,22 +504,24 @@ def _handle_ansi_code(self, ansi, styles_used, state):
495504
state.inside_span = True
496505
yield ansi[last_end:]
497506

498-
def _collapse_cursor(self, parts):
507+
def _collapse_cursor(
508+
self, parts: Iterator[Union[str, OSC_Link, CursorMoveUp]]
509+
) -> List[Union[str, OSC_Link]]:
499510
"""Act on any CursorMoveUp commands by deleting preceding tokens"""
500511

501-
final_parts = []
512+
final_parts: List[Union[str, OSC_Link]] = []
502513
for part in parts:
503514

504515
# Throw out empty string tokens ("")
505516
if not part:
506517
continue
507518

508519
# Go back, deleting every token in the last 'line'
509-
if part == CursorMoveUp:
520+
if isinstance(part, CursorMoveUp):
510521
if final_parts:
511522
final_parts.pop()
512523

513-
while final_parts and "\n" not in final_parts[-1]:
524+
while final_parts and "\n" not in final_parts[-1]: # type: ignore
514525
final_parts.pop()
515526

516527
continue
@@ -520,7 +531,9 @@ def _collapse_cursor(self, parts):
520531

521532
return final_parts
522533

523-
def prepare(self, ansi="", ensure_trailing_newline=False):
534+
def prepare(
535+
self, ansi: str = "", ensure_trailing_newline: bool = False
536+
) -> Dict[str, Union[bool, str, Set[str]]]:
524537
"""Load the contents of 'ansi' into this object"""
525538

526539
body, styles = self.apply_regex(ansi)
@@ -538,29 +551,31 @@ def prepare(self, ansi="", ensure_trailing_newline=False):
538551

539552
return self._attrs
540553

541-
def attrs(self):
554+
def attrs(self) -> Dict[str, Union[bool, str, Set[str]]]:
542555
"""Prepare attributes for the template"""
543556
if not self._attrs:
544557
raise Exception("Method .prepare not yet called.")
545558
return self._attrs
546559

547-
def convert(self, ansi, full=True, ensure_trailing_newline=False):
560+
def convert(
561+
self, ansi: str, full: bool = True, ensure_trailing_newline: bool = False
562+
) -> str:
548563
r"""
549564
:param ansi: ANSI sequence to convert.
550565
:param full: Whether to include the full HTML document or only the body.
551566
:param ensure_trailing_newline: Ensures that ``\n`` character is present at the end of the output.
552567
"""
553568
attrs = self.prepare(ansi, ensure_trailing_newline=ensure_trailing_newline)
554569
if not full:
555-
return attrs["body"]
570+
return attrs["body"] # type: ignore
556571
if self.latex:
557572
_template = _latex_template
558573
else:
559574
_template = _html_template
560575
all_styles = get_styles(self.dark_bg, self.line_wrap, self.scheme)
561576
backgrounds = all_styles[:6]
562577
used_styles = filter(
563-
lambda e: e.klass.lstrip(".") in attrs["styles"], all_styles
578+
lambda e: e.klass.lstrip(".") in attrs["styles"], all_styles # type: ignore
564579
)
565580

566581
return _template % {
@@ -572,15 +587,15 @@ def convert(self, ansi, full=True, ensure_trailing_newline=False):
572587
"hyperref": "\\usepackage{hyperref}" if self.hyperref else "",
573588
}
574589

575-
def produce_headers(self):
590+
def produce_headers(self) -> str:
576591
return '<style type="text/css">\n%(style)s\n</style>\n' % {
577592
"style": "\n".join(
578593
map(str, get_styles(self.dark_bg, self.line_wrap, self.scheme))
579594
)
580595
}
581596

582597

583-
def main():
598+
def main() -> None:
584599
"""
585600
$ ls --color=always | ansi2html > directories.html
586601
$ sudo tail /var/log/messages | ccze -A | ansi2html > logs.html
@@ -718,16 +733,7 @@ def main():
718733
title=opts.output_title,
719734
)
720735

721-
try:
722-
sys.stdin = io.TextIOWrapper(sys.stdin.detach(), opts.input_encoding, "replace")
723-
except io.UnsupportedOperation:
724-
# This only fails in the test suite...
725-
pass
726-
727-
def _read(input_bytes):
728-
return input_bytes
729-
730-
def _print(output_unicode, end="\n"):
736+
def _print(output_unicode: str, end: str = "\n") -> None:
731737
if hasattr(sys.stdout, "buffer"):
732738
output_bytes = (output_unicode + end).encode(opts.output_encoding)
733739
sys.stdout.buffer.write(output_bytes)

0 commit comments

Comments
 (0)