neoprint 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
neoprint/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ from . import text_object
2
+ from .config import config
3
+ from .console import bprint
4
+ from .console import console
5
+ from .control import setup
6
+ from .debugger import debugger
7
+ from .format import format
8
+ from .format import format_list
9
+ from .frame_info import FrameInfo
10
+ from .progress import Progress
11
+ from .progress import ProgressItem
12
+ from .progress import Spinner
13
+ from .progress import progress
14
+ from .progress import spinner
15
+ from .scope import scope
16
+ from .show import debug
17
+ from .show import divider
18
+ from .show import error
19
+ from .show import exception
20
+ from .show import expand
21
+ from .show import expand2
22
+ from .show import index
23
+ from .show import info
24
+ from .show import show
25
+ from .show import show as print
26
+ from .show import success
27
+ from .show import vshow
28
+ from .show import warning
29
+ from .text_object import Markdown
30
+ from .text_object import RichObject
31
+ from .text_object import Text
32
+
33
+ __version__ = '0.1.0'
neoprint/config.py ADDED
@@ -0,0 +1,135 @@
1
+ import os
2
+ import sys
3
+ import typing as tp
4
+ from sys import excepthook as _default_excepthook
5
+
6
+ from rich.traceback import Traceback
7
+
8
+ from .console import console
9
+ from .console import rich_console
10
+ from .debugger import debugger
11
+
12
+
13
+ class _Config:
14
+ clear_unfinished_stream: bool
15
+ console_width: int
16
+ debug_output: bool
17
+ disable_varnames: bool
18
+ # if source code is obfuscated, disable parsing varnames.
19
+ # this is usually used with `pyportable-crypto` library.
20
+ legacy_windows: bool
21
+ # if true, decrease console color effect.
22
+ multiline_indent: int
23
+ path_style: tp.Literal['filename', 'relpath'] = 'filename'
24
+ # 'relpath' (default): show relative path.
25
+ # for external libraries, will show `[lib_name]/relpath:lineno`
26
+ # 'filename': show only filename.
27
+ # for external libraries, will show `[lib_name]/filename:lineno`
28
+ rich_traceback: bool
29
+ show_funcname: bool
30
+ show_source: bool
31
+ # attach source file path and line number info prefixed to the log -
32
+ # messages.
33
+ # True example:
34
+ # 'main.py:10 >> hello world'
35
+ # False example:
36
+ # 'hello world'
37
+ show_traceback_locals: bool
38
+ show_varnames: bool
39
+ # show both variable names and values. (magic reflection)
40
+ # example:
41
+ # a, b = 1, 2
42
+ # logger.log(a, b, a + b)
43
+ # # enabled: 'main.py:11 >> a = 1; b = 2; a + b = 3'
44
+ # # disabled: 'main.py:11 >> 1, 2, 3'
45
+ show_verbosity_tag: bool
46
+ # example: print(':v8', 'some error happens')
47
+ # enabled: (red text) '[ERROR] some error happens'
48
+ # disabled: (red text) 'some error happens'
49
+ sourcemap_alignment: tp.Literal['left', 'right'] = 'left'
50
+ subthreaded: bool
51
+ # run lk logger in separate thread.
52
+
53
+ _preset_conf: tp.Dict[str, tp.Union[bool, int, str]] = {
54
+ 'clear_unfinished_stream': False,
55
+ 'console_width': console.width,
56
+ 'debug_output': False,
57
+ 'disable_varnames': os.getenv('NEOPRINT_DISABLE_VARNAMES') == '1',
58
+ 'legacy_windows': os.getenv('NEOPRINT_LEGACY_WINDOWS') == '1',
59
+ 'multiline_indent': 2,
60
+ 'path_style': 'relpath',
61
+ 'rich_traceback': True,
62
+ 'show_funcname': False,
63
+ 'show_source': True,
64
+ 'show_traceback_locals': False,
65
+ 'show_varnames': False,
66
+ 'show_verbosity_tag': False,
67
+ 'sourcemap_alignment': 'left',
68
+ 'subthreaded': False,
69
+ }
70
+
71
+ def __init__(self) -> None:
72
+ for k, v in self._preset_conf.items():
73
+ setattr(self, k, v)
74
+ assert self.rich_traceback
75
+ sys.excepthook = self._custom_excepthook
76
+
77
+ def __call__(self, **kwargs) -> None:
78
+ for k, v in kwargs.items():
79
+ assert hasattr(self, k), k
80
+ self._apply(k, v)
81
+
82
+ def reset(self) -> None:
83
+ for k, v in self._preset_conf.items():
84
+ if getattr(self, k, None) != v: # if None type, always skip it.
85
+ # assert v is not None
86
+ self._apply(k, v)
87
+
88
+ def _apply(self, key: str, val: tp.Union[bool, int, str]) -> None:
89
+ setattr(self, key, val)
90
+ if key == 'console_width':
91
+ assert isinstance(val, int)
92
+ console.width = val
93
+ elif key == 'debug_output':
94
+ assert isinstance(val, bool)
95
+ debugger.enabled = val
96
+ elif key == 'disable_varnames':
97
+ os.environ['NEOPRINT_DISABLE_VARNAMES'] = '1' if val else '0'
98
+ elif key == 'legacy_windows':
99
+ os.environ['NEOPRINT_LEGACY_WINDOWS'] = '1' if val else '0'
100
+ elif key == 'rich_traceback':
101
+ # assert isinstance(val, bool)
102
+ if val:
103
+ sys.excepthook = self._custom_excepthook
104
+ else:
105
+ sys.excepthook = _default_excepthook
106
+
107
+ def _custom_excepthook(self, type_, value, traceback) -> None:
108
+ # print(':r', '[red dim]drain out message queue[/]')
109
+ # from .logger import logger
110
+ # if hasattr(logger, '_stop_running'):
111
+ # logger._stop_running() # noqa
112
+ if type_ is KeyboardInterrupt:
113
+ # fmt: off
114
+ from .show import show
115
+ show(':v7', 'KeyboardInterrupt')
116
+ sys.exit(0)
117
+ # fmt: on
118
+ else:
119
+ # https://rich.readthedocs.io/en/stable/traceback.html
120
+ # dprint(getattr(self, 'show_traceback_locals'))
121
+ rich_console.print(
122
+ Traceback.from_exception(
123
+ type_,
124
+ value,
125
+ traceback,
126
+ show_locals=self.show_traceback_locals,
127
+ locals_hide_dunder=True,
128
+ locals_hide_sunder=True,
129
+ # word_wrap=True,
130
+ ),
131
+ soft_wrap=False, # fixed line wrap problem.
132
+ )
133
+
134
+
135
+ config = _Config()
neoprint/console.py ADDED
@@ -0,0 +1,81 @@
1
+ import builtins
2
+ import os
3
+ import sys
4
+ import typing as tp
5
+ from rich.console import Console as RichConsole
6
+ from .debugger import debugger
7
+
8
+ _stdout = sys.stdout
9
+
10
+
11
+ class Console:
12
+ def __init__(self) -> None:
13
+ self.width = self.get_console_width()
14
+
15
+ def get_console_width(self) -> int:
16
+ if hasattr(sys.stdout, 'columns'):
17
+ columns = sys.stdout.columns
18
+ if columns:
19
+ return int(columns) # type: ignore
20
+ if hasattr(os, 'get_terminal_size'):
21
+ try:
22
+ size = os.get_terminal_size()
23
+ if size.columns > 0:
24
+ return int(size.columns)
25
+ except OSError:
26
+ pass
27
+ fallback = os.environ.get('COLUMNS', '')
28
+ if fallback.isdigit():
29
+ return int(fallback)
30
+ return 80
31
+
32
+ def print(self, text: str, end: str = '\n', flush: bool = False) -> None:
33
+ if debugger.enabled:
34
+ debugger.output.append(text)
35
+ _stdout.write(text + end)
36
+ if flush:
37
+ _stdout.flush()
38
+
39
+
40
+ class _LegacyRichConsole(RichConsole):
41
+ def __init__(self) -> None:
42
+ # https://github.com/Textualize/rich/issues/2622
43
+ super().__init__(
44
+ color_system='standard' if os.name == 'nt' else 'auto',
45
+ legacy_windows=True,
46
+ )
47
+
48
+ def capture_output(self, *args, **kwargs) -> str:
49
+ # https://chatgpt.com/share/6a16a585-0e00-8320-97ee-5fc2b572690e
50
+ with self.capture() as cap:
51
+ self.print(*args, **kwargs)
52
+ return cap.get()
53
+
54
+
55
+ class _ModernRichConsole(_LegacyRichConsole):
56
+ def __init__(self) -> None:
57
+ RichConsole.__init__(
58
+ self,
59
+ color_system='standard' if os.name == 'nt' else 'auto',
60
+ legacy_windows=False, # make sure ansi color is used
61
+ )
62
+
63
+
64
+ console = Console()
65
+ # CONSOLE_WIDTH = console.width
66
+ rich_console = _ModernRichConsole()
67
+ legacy_rich_console = _LegacyRichConsole()
68
+
69
+ std_print = tp.cast(tp.Callable, builtins.print)
70
+ con_print = console.print
71
+ # con_error = partial(console.print_exception, word_wrap=True)
72
+ dbg_print = debugger.print
73
+ # non_print = NothingPrinter()
74
+
75
+ # alias
76
+ bprint = std_print
77
+ cprint = con_print
78
+ dprint = dbg_print
79
+
80
+ # default alias
81
+ # print = con_print
neoprint/control.py ADDED
@@ -0,0 +1,23 @@
1
+ import builtins
2
+
3
+ from .console import bprint
4
+ from .frame_info import get_last_frame
5
+ from .show import show
6
+
7
+ effected_packages = set()
8
+
9
+
10
+ def setup(scope: str = 'this_package') -> None:
11
+ assert scope == 'this_package', f'scope `{scope}` is not supported'
12
+ frame = get_last_frame()
13
+ effected_packages.add(frame.package_name)
14
+ if getattr(builtins, 'print') is bprint:
15
+ setattr(builtins, 'print', _variable_print)
16
+
17
+
18
+ def _variable_print(*args, **kwargs) -> None:
19
+ frame = get_last_frame()
20
+ if frame.package_name in effected_packages:
21
+ show(*args, _frame=frame, **kwargs)
22
+ else:
23
+ bprint(*args, **kwargs)
neoprint/debugger.py ADDED
@@ -0,0 +1,18 @@
1
+ from inspect import currentframe
2
+ from rich.pretty import pprint
3
+ from .frame_info import FrameInfo
4
+
5
+
6
+ class _Debugger:
7
+ def __init__(self) -> None:
8
+ self.enabled = False
9
+ self.output = []
10
+
11
+ def print(self, *args) -> None:
12
+ frame = FrameInfo(currentframe().f_back) # type: ignore
13
+ pprint(
14
+ ('[debug]:{}:{}'.format(frame.file_name, frame.line_number), *args)
15
+ )
16
+
17
+
18
+ debugger = _Debugger()
neoprint/format.py ADDED
@@ -0,0 +1,224 @@
1
+ import typing as tp
2
+ from inspect import currentframe
3
+
4
+ from . import text_object as to
5
+ from .config import config
6
+ from .console import dprint # noqa
7
+ from .frame_info import FrameInfo
8
+ from .markup import Mark
9
+ from .markup import markup_analyzer
10
+
11
+
12
+ class T: # Typehint
13
+ Args = tp.Tuple[tp.Any, ...]
14
+ FlushScheme = int
15
+ # 0: no flush
16
+ # 1: instant flush
17
+ # 2: instant flush and drain
18
+ # 3: wait for flush
19
+ Marks = tp.Dict[str, tp.Any]
20
+ Markup = str
21
+ MarkupPos = int # -1, 0, 1
22
+
23
+
24
+ def format_list(
25
+ *args,
26
+ markup: tp.Optional[T.Markup] = None,
27
+ _elevate_parent_level: int = 0,
28
+ _frame: tp.Optional[FrameInfo] = None,
29
+ _mark_position: int = 0,
30
+ ) -> tp.List[to.TextObject]:
31
+ """
32
+ frame relationship:
33
+ this_frame: frame to this function.
34
+ foreign_frame: frame to first call of foreign function.
35
+ for example:
36
+ 1.
37
+ # aaa.py
38
+ import neoprint as np # ln1
39
+ np.format_list(...) # ln2
40
+ # foreign_frame is `aaa.py:2`
41
+ 2.
42
+ # neoprint/show.py
43
+ def show(...):
44
+ x = format_list(
45
+ ..., _frame=FrameInfo(currentframe().f_back)
46
+ )
47
+ # foreign_frame is the one who is calling `show(...)`.
48
+ target_frame: frame to the most proper place.
49
+ if ':p' markup is not used, target_frame is foreign_frame.
50
+ if ':p' markup is set, target_frame = `foreign_frame.f_back_to_:p`.
51
+
52
+ how does caller make foreign_frame:
53
+ caller can set _frame directly, or set _elevate_parent_level.
54
+ the following are same:
55
+ def foo():
56
+ format_list(..., _frame=FrameInfo(currentframe().f_back))
57
+ def bar():
58
+ format_list(..., _elevate_parent_level=1)
59
+ if both params are set, _frame will be used.
60
+ """
61
+ if _frame is None:
62
+ this_frame = FrameInfo(currentframe()) # type: ignore
63
+ foreign_frame = this_frame.get_parent(1 + _elevate_parent_level)
64
+ else:
65
+ foreign_frame = _frame
66
+ target_frame = foreign_frame
67
+
68
+ if markup is None:
69
+ args, markpos, markup = extract_markup_from_arguments(args)
70
+ else:
71
+ markpos = _mark_position
72
+ # dprint(args, markup, args[-1], markup_analyzer.is_valid_markup(args[-1]))
73
+ marks = markup_analyzer.analyze(markup, foreign_frame)
74
+
75
+ if marks['p']:
76
+ target_frame = foreign_frame.get_parent(marks['p'])
77
+ assert target_frame
78
+
79
+ # --------------------------------------------------------------------------
80
+
81
+ result = []
82
+
83
+ # head part
84
+ head_parts = get_head_parts(target_frame)
85
+ result.extend(head_parts)
86
+
87
+ # --------------------------------------------------------------------------
88
+
89
+ # body part
90
+ before_body_parts = []
91
+ body_parts = []
92
+
93
+ if marks['i']:
94
+ before_body_parts.append(to.Index(marks['i']))
95
+ before_body_parts.append(to.Space())
96
+
97
+ if marks['n']:
98
+ varnames = foreign_frame.varnames
99
+ if markpos:
100
+ varnames = varnames[1:] if markpos == 1 else varnames[:-1]
101
+ # assert len(varnames) == len(args), (varnames, args)
102
+ if len(varnames) != len(args):
103
+ # this may because user has modified the source code after call.
104
+ # we should refresh the AST to get new varnames.
105
+ foreign_frame.refresh()
106
+ varnames = foreign_frame.varnames
107
+ assert len(varnames) == len(args), (varnames, args)
108
+ else:
109
+ varnames = (None,) * len(args)
110
+
111
+ for name, arg in zip(varnames, args):
112
+ if name is None:
113
+ body_parts.append(to.Text(arg))
114
+ else:
115
+ body_parts.append(to.NamedVariable(name, arg))
116
+ body_parts.append(to.InBodySeparator())
117
+ body_parts.append(to.Space())
118
+ body_parts = body_parts[:-2]
119
+
120
+ if marks['l'] or marks['r']:
121
+ # there are three cases:
122
+ # l1 and r*: expand object
123
+ # l2 or r2: special expand object
124
+ # l0 and r1: bbcode object
125
+ if marks['l'] == Mark.EXPAND_FORMAT:
126
+ body_parts = [
127
+ to.ExpandedObject(x)
128
+ if to.ExpandedObject.check_expandable(x)
129
+ else x
130
+ for x in body_parts
131
+ ]
132
+ elif (
133
+ marks['l'] == Mark.SPECIAL_EXPAND_FORMAT
134
+ or marks['r'] == Mark.RICH_OBJECT
135
+ ):
136
+ body_parts = [
137
+ to.SpecialExpandedObject(x)
138
+ if to.SpecialExpandedObject.check_expandable(x)
139
+ else to.ExpandedObject(x)
140
+ if to.ExpandedObject.check_expandable(x)
141
+ else x
142
+ for x in body_parts
143
+ ]
144
+ elif marks['r'] == Mark.RICH_FORMAT:
145
+ ... # TODO
146
+ else:
147
+ raise Exception('unreachable case')
148
+ body_parts = [
149
+ to.ExpandedObjectGroup(body_parts, head_parts + before_body_parts)
150
+ ]
151
+
152
+ if marks['d']:
153
+ body_parts = [
154
+ to.DividerLine(
155
+ body_parts,
156
+ head_parts + before_body_parts,
157
+ bold=marks['d'] == Mark.THICK_DIVIDER_LINE,
158
+ )
159
+ ]
160
+
161
+ if marks['v']:
162
+ global_color, global_style = marks['v']
163
+ for part in body_parts:
164
+ if part.editable:
165
+ # dprint(part, global_color, global_style)
166
+ part.color = global_color
167
+ part.style = global_style
168
+
169
+ result.extend(before_body_parts)
170
+ result.extend(body_parts)
171
+
172
+ return result
173
+
174
+
175
+ def format(*args, markup: tp.Optional[str] = None) -> str:
176
+ if markup is None:
177
+ args, markpos, markup = extract_markup_from_arguments(args)
178
+ else:
179
+ markpos, markup = 0, markup
180
+ result = format_list(
181
+ *args, markup=markup, _elevate_parent_level=1, _mark_position=markpos
182
+ )
183
+ return ''.join(p.render(color_code_scheme='none') for p in result)
184
+
185
+
186
+ # ------------------------------------------------------------------------------
187
+
188
+
189
+ def extract_markup_from_arguments(
190
+ args: T.Args,
191
+ ) -> tp.Tuple[T.Args, int, T.Markup]:
192
+ if (
193
+ len(args) > 0
194
+ and isinstance(args[0], str)
195
+ and args[0].startswith(':')
196
+ and markup_analyzer.is_valid_markup(args[0])
197
+ ):
198
+ return args[1:], 1, args[0]
199
+ elif (
200
+ len(args) > 1
201
+ and isinstance(args[-1], str)
202
+ and args[-1].startswith(':')
203
+ and markup_analyzer.is_valid_markup(args[-1])
204
+ ):
205
+ return args[:-1], -1, args[-1]
206
+ else:
207
+ return args, 0, ''
208
+
209
+
210
+ def get_head_parts(frame: FrameInfo) -> tp.List[to.TextObject]:
211
+ out = []
212
+ if config.show_source:
213
+ out.append(to.Source(frame))
214
+ if config.show_funcname:
215
+ if out:
216
+ out.append(to.Space())
217
+ out.append(to.FuncnameSeparator())
218
+ out.append(to.Space())
219
+ out.append(...) # TODO
220
+ if out:
221
+ out.append(to.Space())
222
+ out.append(to.BodySeparator())
223
+ out.append(to.Space())
224
+ return out
neoprint/frame_info.py ADDED
@@ -0,0 +1,100 @@
1
+ import inspect
2
+ import typing as tp
3
+ from textwrap import dedent
4
+ from types import FrameType
5
+
6
+ from . import sourcemap
7
+
8
+
9
+ class FrameInfo:
10
+ file_name: str
11
+ file_path: str
12
+ function_name: str
13
+ line_number: int
14
+ package_name: str
15
+ _frame: FrameType
16
+ _varnames: tp.Optional[tp.Tuple[tp.Optional[str], ...]]
17
+
18
+ @classmethod
19
+ def this_place(cls) -> 'FrameInfo':
20
+ return cls(inspect.currentframe().f_back) # type: ignore
21
+
22
+ @classmethod
23
+ def parent_place(cls) -> 'FrameInfo':
24
+ return cls(inspect.currentframe().f_back.f_back) # type: ignore
25
+
26
+ def __init__(self, frame: FrameType) -> None:
27
+ self._frame = frame
28
+ self.refresh()
29
+
30
+ def __str__(self) -> str:
31
+ return self.info
32
+
33
+ @property
34
+ def id(self) -> str:
35
+ return f'{self.file_path}:{self.line_number}'
36
+
37
+ @property
38
+ def indentation(self) -> int:
39
+ # https://stackoverflow.com/a/39172552
40
+ if x := inspect.getframeinfo(self._frame).code_context:
41
+ ctx = x[0]
42
+ return len(ctx) - len(ctx.lstrip())
43
+ return 0
44
+
45
+ @property
46
+ def info(self) -> str:
47
+ return dedent(
48
+ f"""
49
+ <FrameInfo object
50
+ filepath: {self.file_path}
51
+ lineno: {self.line_number}
52
+ funcname: {self.function_name}
53
+ >
54
+ """
55
+ ).rstrip()
56
+
57
+ @property
58
+ def parent(self) -> 'FrameInfo':
59
+ return self.get_parent(1)
60
+
61
+ @property
62
+ def varnames(self) -> tp.Sequence[tp.Optional[str]]:
63
+ if self._varnames is None:
64
+ self._varnames = tuple(self.collect_varnames())
65
+ return self._varnames
66
+
67
+ def collect_varnames(self) -> tp.Sequence[tp.Optional[str]]:
68
+ return sourcemap.get_varnames(self.file_path, self.line_number)
69
+
70
+ def get_parent(self, traceback_level: int = 1) -> 'FrameInfo':
71
+ frame = self._frame
72
+ for _ in range(traceback_level):
73
+ frame = frame.f_back # type: ignore
74
+ return FrameInfo(frame) # type: ignore
75
+
76
+ def refresh(self) -> None:
77
+ frame = self._frame
78
+ self.function_name = frame.f_code.co_name
79
+ self.package_name = frame.f_globals['__name__'].split('.', 1)[0]
80
+ self.file_path = (
81
+ frame.f_globals.get('__file__', frame.f_code.co_filename)
82
+ # note:
83
+ # - path may be "<string>", "<unknown>" etc.
84
+ # - path may be "<ipython-input-10-5abb16185f48>" in ipython
85
+ # environment.
86
+ # - `co_filename` may be a relative python in python 3.8.
87
+ # - path may not exist.
88
+ ).replace('\\', '/')
89
+ self.file_name = (
90
+ self.file_path
91
+ if self.file_path[0] == '<'
92
+ else self.file_path.rsplit('/', 1)[-1]
93
+ )
94
+ self.line_number = frame.f_lineno
95
+ self._varnames = None
96
+
97
+
98
+ def get_last_frame() -> FrameInfo:
99
+ frame = inspect.currentframe().f_back.f_back # type: ignore
100
+ return FrameInfo(frame) # type: ignore