Skip to content

Commit ba5cfdf

Browse files
authored
Merge pull request #1 from samuelcolvin/prettier-print
prettier print and colorizing
2 parents c61c373 + 10dd33a commit ba5cfdf

File tree

10 files changed

+641
-77
lines changed

10 files changed

+641
-77
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ htmlcov/
1313
benchmarks/*.json
1414
docs/_build/
1515
old-version/
16+
*.swp

devtools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
# flake8: noqa
2+
from .ansi import *
23
from .debug import *
4+
from .prettier import *
35
from .version import VERSION

devtools/ansi.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import re
2+
import sys
3+
from enum import IntEnum
4+
from typing import Any
5+
6+
_ansi_template = '\033[{}m'
7+
_ansi_re = re.compile('\033\[((?:\d|;)*)([a-zA-Z])')
8+
9+
__all__ = ['sformat', 'sprint']
10+
11+
12+
def isatty(stream=None):
13+
stream = stream or sys.stdout
14+
try:
15+
return stream.isatty()
16+
except Exception:
17+
return False
18+
19+
20+
def strip_ansi(value):
21+
return _ansi_re.sub('', value)
22+
23+
24+
class Style(IntEnum):
25+
reset = 0
26+
27+
bold = 1
28+
not_bold = 22
29+
30+
dim = 2
31+
not_dim = 22
32+
33+
italic = 3
34+
not_italic = 23
35+
36+
underline = 4
37+
not_underline = 24
38+
39+
blink = 5
40+
not_blink = 25
41+
42+
reverse = 7
43+
not_reverse = 27
44+
45+
strike_through = 9
46+
not_strike_through = 29
47+
48+
# foreground colours
49+
black = 30
50+
red = 31
51+
green = 32
52+
yellow = 33
53+
blue = 34
54+
magenta = 35
55+
cyan = 36
56+
white = 37
57+
58+
# background colours
59+
bg_black = 40
60+
bg_red = 41
61+
bg_green = 42
62+
bg_yellow = 43
63+
bg_blue = 44
64+
bg_magenta = 45
65+
bg_cyan = 46
66+
bg_white = 47
67+
68+
# this is a meta value used for the "Style" instance which is the "style" function
69+
function = -1
70+
71+
def __call__(self, input: Any, *styles, reset: bool=True, apply: bool=True):
72+
"""
73+
Styles text with ANSI styles and returns the new string.
74+
75+
By default the styling is cleared at the end of the string, this can be prevented with``reset=False``.
76+
77+
Examples::
78+
79+
print(sformat('Hello World!', sformat.green))
80+
print(sformat('ATTENTION!', sformat.bg_magenta))
81+
print(sformat('Some things', sformat.reverse, sformat.bold))
82+
83+
:param input: the object to style with ansi codes.
84+
:param *styles: zero or more styles to apply to the text, should be either style instances or strings
85+
matching style names.
86+
:param reset: if False the ansi reset code is not appended to the end of the string
87+
:param: apply: if False no ansi codes are applied
88+
"""
89+
text = str(input)
90+
if not apply:
91+
return text
92+
codes = []
93+
for s in styles:
94+
# raw ints are allowed
95+
if not isinstance(s, self.__class__) and not isinstance(s, int):
96+
try:
97+
s = self.styles[s]
98+
except KeyError:
99+
raise ValueError('invalid style "{}"'.format(s))
100+
codes.append(str(s.value))
101+
102+
if codes:
103+
r = _ansi_template.format(';'.join(codes)) + text
104+
else:
105+
r = text
106+
107+
if reset:
108+
r += _ansi_template.format(self.reset)
109+
return r
110+
111+
@property
112+
def styles(self):
113+
return self.__class__.__members__
114+
115+
def __repr__(self):
116+
if self == self.function:
117+
return '<pseudo function sformat(text, *styles)>'
118+
else:
119+
return super().__repr__()
120+
121+
def __str__(self):
122+
if self == self.function:
123+
return repr(self)
124+
else:
125+
return super().__str__()
126+
127+
128+
sformat = Style(-1)
129+
130+
131+
class StylePrint:
132+
"""
133+
Annoyingly enums do not allow inheritance, this is a lazy design mistake, this is an ugly work around
134+
for that mistake.
135+
"""
136+
def __call__(self, input, *styles, reset=True, flush=True, file=None, **print_kwargs):
137+
text = sformat(input, *styles, reset=reset, apply=isatty(file))
138+
print(text, flush=flush, file=file, **print_kwargs)
139+
140+
def __getattr__(self, item):
141+
return getattr(sformat, item)
142+
143+
def __repr__(self):
144+
return '<pseudo function sprint(text, *styles)>'
145+
146+
147+
sprint = StylePrint()

devtools/debug.py

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
from textwrap import dedent
88
from typing import Generator, List, Optional, Tuple
99

10+
from .ansi import isatty, sformat
11+
from .prettier import PrettyFormat
12+
1013
__all__ = ['Debug', 'debug']
1114
CWD = Path('.').resolve()
15+
pformat = PrettyFormat()
1216

1317

1418
def env_true(var_name, alt='FALSE'):
@@ -26,20 +30,21 @@ def __init__(self, value, *, name=None, **extra):
2630
self.extra.append(('len', len(value)))
2731
self.extra += [(k, v) for k, v in extra.items() if v is not None]
2832

29-
def value_str(self):
30-
if isinstance(self.value, str):
31-
return '"{}"'.format(self.value)
32-
return str(self.value)
33+
def str(self, colours=False) -> str:
34+
s = ''
35+
if self.name:
36+
s = sformat(self.name, sformat.blue, apply=colours) + ': '
37+
s += pformat(self.value, indent=2)
38+
suffix = (
39+
' ({0.value.__class__.__name__}) {1}'
40+
.format(self, ' '.join('{}={}'.format(k, v) for k, v in self.extra))
41+
.rstrip(' ') # trailing space if extra is empty
42+
)
43+
s += sformat(suffix, sformat.dim, apply=colours)
44+
return s
3345

3446
def __str__(self) -> str:
35-
template = '{value} ({self.value.__class__.__name__}) {extra}'
36-
if self.name:
37-
template = '{self.name} = ' + template
38-
return template.format(
39-
self=self,
40-
value=self.value_str(),
41-
extra=' '.join('{}={}'.format(k, v) for k, v in self.extra)
42-
).rstrip(' ') # trailing space if extra is empty
47+
return self.str()
4348

4449

4550
class DebugOutput:
@@ -55,13 +60,19 @@ def __init__(self, *, filename: str, lineno: int, frame: str, arguments: List[De
5560
self.frame = frame
5661
self.arguments = arguments
5762

63+
def str(self, colours=False) -> str:
64+
if colours:
65+
prefix = '{}:{} {}\n '.format(
66+
sformat(self.filename, sformat.magenta),
67+
sformat(self.lineno, sformat.green),
68+
sformat(self.frame, sformat.green, sformat.italic)
69+
)
70+
else:
71+
prefix = '{0.filename}:{0.lineno} {0.frame}\n '.format(self)
72+
return prefix + '\n '.join(a.str(colours) for a in self.arguments)
73+
5874
def __str__(self) -> str:
59-
template = '{s.filename}:{s.lineno} {s.frame}'
60-
# turns out single line output is ugly
61-
# if len(self.arguments) == 1:
62-
# return (template + ': {a}').format(s=self, a=self.arguments[0])
63-
# else:
64-
return (template + '\n {a}').format(s=self, a='\n '.join(str(a) for a in self.arguments))
75+
return self.str()
6576

6677
def __repr__(self) -> str:
6778
arguments = ' '.join(str(a) for a in self.arguments)
@@ -78,21 +89,30 @@ class Debug:
7889

7990
def __init__(self, *,
8091
warnings: Optional[bool]=None,
92+
colours: Optional[bool]=None,
8193
frame_context_length: int=50):
82-
if warnings is None:
83-
self._warnings = env_true('PY_DEVTOOLS_WARNINGS', 'TRUE')
84-
else:
85-
self._warnings = warnings
94+
self._warnings = self._env_bool(warnings, 'PY_DEVTOOLS_WARNINGS')
95+
self._colours = self._env_bool(colours, 'PY_DEVTOOLS_COLOURS')
8696
# 50 lines should be enough to make sure we always get the entire function definition
8797
self._frame_context_length = frame_context_length
8898

89-
def __call__(self, *args, **kwargs):
90-
print(self._process(args, kwargs, r'debug *\('), flush=True)
99+
@classmethod
100+
def _env_bool(cls, value, env_name, env_default='TRUE'):
101+
if value is None:
102+
return env_true(env_name, env_default)
103+
else:
104+
return value
105+
106+
def __call__(self, *args, file_=None, flush_=True, **kwargs) -> None:
107+
d_out = self._process(args, kwargs, r'debug *\(')
108+
colours_possible = isatty(file_)
109+
s = d_out.str(self._colours and colours_possible)
110+
print(s, file=file_, flush=flush_)
91111

92-
def format(self, *args, **kwargs):
112+
def format(self, *args, **kwargs) -> DebugOutput:
93113
return self._process(args, kwargs, r'debug.format *\(')
94114

95-
def _process(self, args, kwargs, func_regex):
115+
def _process(self, args, kwargs, func_regex) -> DebugOutput:
96116
curframe = inspect.currentframe()
97117
frames = inspect.getouterframes(curframe, context=self._frame_context_length)
98118
# BEWARE: this must be call by a method which in turn is called "directly" for the frame to be correct

0 commit comments

Comments
 (0)