pyscript-programming-language 1.13.4__py3-none-any.whl → 1.15.0.dev1__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.
Files changed (56) hide show
  1. pyscript/__init__.py +2 -1
  2. pyscript/__init__.pyi +1 -0
  3. pyscript/__main__.py +87 -48
  4. pyscript/core/analyzer.py +31 -40
  5. pyscript/core/buffer.py +6 -10
  6. pyscript/core/cache.py +4 -3
  7. pyscript/core/checks.py +5 -21
  8. pyscript/core/constants.py +2 -2
  9. pyscript/core/context.py +7 -10
  10. pyscript/core/editor/__init__.py +27 -11
  11. pyscript/core/editor/bases.py +16 -16
  12. pyscript/core/editor/gui.py +110 -28
  13. pyscript/core/editor/terminal.py +17 -4
  14. pyscript/core/exceptions.py +8 -13
  15. pyscript/core/handlers.py +34 -32
  16. pyscript/core/highlight.py +23 -3
  17. pyscript/core/interpreter.py +58 -65
  18. pyscript/core/lexer.py +51 -52
  19. pyscript/core/mapping.py +11 -8
  20. pyscript/core/nodes.py +415 -203
  21. pyscript/core/parser.py +236 -246
  22. pyscript/core/position.py +86 -81
  23. pyscript/core/pysbuiltins.py +41 -81
  24. pyscript/core/pystypes.py +66 -13
  25. pyscript/core/results.py +2 -2
  26. pyscript/core/runner.py +5 -5
  27. pyscript/core/shell.py +50 -73
  28. pyscript/core/symtab.py +14 -15
  29. pyscript/core/token.py +3 -4
  30. pyscript/core/utils/ansi.py +97 -50
  31. pyscript/core/utils/debug.py +18 -5
  32. pyscript/core/utils/decorators.py +8 -8
  33. pyscript/core/utils/generic.py +19 -3
  34. pyscript/core/utils/module.py +6 -3
  35. pyscript/core/utils/similarity.py +8 -6
  36. pyscript/core/utils/string.py +8 -13
  37. pyscript/core/version.py +2 -2
  38. pyscript/lib/ansi.pys +1 -14
  39. pyscript/lib/ast/ast_dump.py +20 -32
  40. pyscript/lib/ast/ast_literal_eval.py +5 -12
  41. pyscript/lib/ast/ast_unparse.py +10 -10
  42. pyscript/lib/ast/ast_walk.py +20 -26
  43. pyscript/lib/brainfuck.pys +10 -8
  44. pyscript/lib/dual/__init__.pys +17 -0
  45. pyscript/lib/dual/py_dual.py +246 -0
  46. pyscript/lib/getch/__init__.pys +6 -1
  47. pyscript/lib/getch/py_getch.py +92 -11
  48. pyscript/lib/history.pys +10 -8
  49. pyscript/lib/inspect.pys +1 -1
  50. pyscript/lib/parser.pys +6 -4
  51. {pyscript_programming_language-1.13.4.dist-info → pyscript_programming_language-1.15.0.dev1.dist-info}/METADATA +32 -29
  52. pyscript_programming_language-1.15.0.dev1.dist-info/RECORD +81 -0
  53. pyscript/site-packages/anniversary.pys +0 -4
  54. pyscript_programming_language-1.13.4.dist-info/RECORD +0 -80
  55. {pyscript_programming_language-1.13.4.dist-info → pyscript_programming_language-1.15.0.dev1.dist-info}/WHEEL +0 -0
  56. {pyscript_programming_language-1.13.4.dist-info → pyscript_programming_language-1.15.0.dev1.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,31 @@
1
1
  """
2
2
  All Editor Application Implementations for PyScript.
3
+
4
+ Not all editors are loaded (lazy import).
3
5
  """
4
6
 
5
- from . import (
6
- bases,
7
- gui,
8
- terminal
9
- )
10
-
11
- __all__ = (
12
- 'bases',
13
- 'gui',
14
- 'terminal'
15
- )
7
+ from ..utils.path import normpath
8
+
9
+ from importlib import import_module as _import
10
+ from os.path import dirname
11
+ from pkgutil import iter_modules
12
+
13
+ __all__ = tuple(frozenset(
14
+ name for _, name, _ in
15
+ iter_modules((dirname(normpath(__file__)),))
16
+ ))
17
+
18
+ def __getattr__(name: str):
19
+ g = globals()
20
+ if name in g:
21
+ return g[name]
22
+ elif name in __all__:
23
+ module = _import(f'{__name__}.{name}')
24
+ g[name] = module
25
+ return module
26
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
27
+
28
+ def __dir__() -> list[str]:
29
+ return sorted(tuple(globals().keys()) + __all__)
30
+
31
+ del normpath, iter_modules, dirname
@@ -3,24 +3,25 @@ from ..buffer import PysFileBuffer
3
3
  from ..cache import pys_sys
4
4
  from ..constants import CONFIGURATIONS_PATH
5
5
  from ..utils.decorators import typecheck, inheritable
6
- from ..utils.generic import setimuattr
6
+ from ..utils.generic import delimuattr
7
7
  from ..utils.string import normstr
8
8
 
9
9
  from json import dump, load
10
+ from os.path import basename
10
11
  from typing import Any
11
12
 
12
- import os
13
-
14
13
  class PysEditor(Pys):
15
14
 
16
15
  @typecheck
17
16
  def __init__(self, file: PysFileBuffer, colored: bool = True) -> None:
18
17
  self.file = file
18
+ self.basename = basename(self.file.name)
19
19
  self.colored = bool(colored)
20
- self.basename = os.path.basename(self.file.name)
21
20
  self.used = False
22
21
  self.modified = False
23
22
 
23
+ self.load_configuration()
24
+
24
25
  def __init_subclass__(cls, **kwargs) -> None:
25
26
  super().__init_subclass__(**kwargs)
26
27
  inheritable(cls)
@@ -29,20 +30,17 @@ class PysEditor(Pys):
29
30
  try:
30
31
  with open(CONFIGURATIONS_PATH, 'r', encoding=pys_sys.encoding) as file:
31
32
  result = load(file)
32
- if not isinstance(result, dict):
33
+ if not isinstance(result, dict):
34
+ raise ValueError
35
+ for key in result:
36
+ if not isinstance(key, str):
33
37
  raise ValueError
34
- for key in result:
35
- if not isinstance(key, str):
36
- raise ValueError
37
- self.configurations = result
38
+ self.configurations = result
38
39
  except:
39
40
  self.configurations = {}
40
41
 
41
42
  def get_configuration(self, configuration: str, default: Any) -> Any:
42
- if configuration in self.configurations:
43
- return self.configurations[configuration]
44
- self.configurations[configuration] = default
45
- return default
43
+ return self.configurations.setdefault(configuration, default)
46
44
 
47
45
  def set_configuration(self, configuration: str, value: Any) -> None:
48
46
  self.configurations[configuration] = value
@@ -55,11 +53,12 @@ class PysEditor(Pys):
55
53
  pass
56
54
 
57
55
  def save(self, text) -> None:
56
+ if not self.modified:
57
+ return
58
+
58
59
  try:
59
- text = normstr(text)
60
60
  with open(self.file.name, 'w', encoding=pys_sys.encoding) as file:
61
- file.write(text)
62
- setimuattr(self.file, 'text', text)
61
+ file.write(normstr(text))
63
62
  except:
64
63
  pass
65
64
  else:
@@ -68,4 +67,5 @@ class PysEditor(Pys):
68
67
  def run(self) -> None:
69
68
  if self.used:
70
69
  raise RuntimeError("one application object can only be used once")
70
+ delimuattr(self.file, 'text')
71
71
  self.used = True
@@ -1,13 +1,22 @@
1
1
  from .bases import PysEditor
2
2
  from ..buffer import PysFileBuffer
3
3
  from ..constants import ICON_PATH
4
- from ..highlight import PygmentsPyScriptStyle, PygmentsPyScriptLexer
4
+ from ..highlight import PYGMENTS, PygmentsPyScriptStyle, PygmentsPyScriptLexer
5
5
  from ..utils.generic import boundary
6
6
  from ..version import __version__
7
7
 
8
8
  try:
9
- from tkinter import Tk, Scrollbar, Text, messagebox
10
- from tkinter.font import Font
9
+ from tkinter import Tk, Scrollbar, Text, Canvas, messagebox
10
+ from tkinter.font import families, Font
11
+
12
+ MONO_FALLBACKS = (
13
+ 'Fira Code',
14
+ 'Cascadia Code', 'SF Mono', 'Ubuntu Mono', 'Liberation Mono',
15
+ 'Consolas', 'Menlo', 'DejaVu Sans Mono',
16
+ 'Courier New', 'Lucida Console', 'Monaco', 'Andale Mono',
17
+ 'Courier', 'Terminal',
18
+ 'monospace'
19
+ )
11
20
 
12
21
  class PysGUIEditor(PysEditor, Tk):
13
22
 
@@ -15,7 +24,8 @@ try:
15
24
  PysEditor.__init__(self, file, colored)
16
25
  Tk.__init__(self)
17
26
 
18
- self.load_configuration()
27
+ def update_tab_size():
28
+ self.text.configure(tabs=(self.font.measure(' '),))
19
29
 
20
30
  def on_save(event=None):
21
31
  text = self.text.get('1.0', 'end')
@@ -26,46 +36,43 @@ try:
26
36
  def on_close():
27
37
  if self.modified:
28
38
  answer = messagebox.askyesnocancel('Unsaved changes', 'File has been modified. Save before exit?')
29
- if answer is True:
39
+ if answer is None:
40
+ return
41
+ elif answer:
30
42
  on_save()
31
- self.save_configuration()
32
- self.destroy()
33
- elif answer is False:
34
- self.save_configuration()
35
- self.destroy()
36
- else:
37
- self.save_configuration()
38
- self.destroy()
43
+ self.save_configuration()
44
+ self.destroy()
39
45
 
40
46
  def on_configure(event):
41
47
  if event.widget == self:
42
48
  zoom = self.wm_state() == 'zoomed'
43
49
  self.set_configuration('zoom', zoom)
44
50
  if not zoom:
45
- self.set_configuration(
46
- 'gui-geometry',
47
- f'{event.width}x{event.height}+{event.x}+{event.y}'
48
- )
51
+ self.set_configuration('gui-geometry', f'{event.width}x{event.height}+{event.x}+{event.y}')
49
52
 
50
53
  def on_modified(event=None):
51
- text = self.text
52
- if text.edit_modified():
54
+ if self.text.edit_modified():
53
55
  self.modified = True
54
56
  update()
55
- text.edit_modified(False)
57
+ refresh_line_numbers()
58
+ self.text.edit_modified(False)
56
59
 
57
60
  def on_change_font(value):
58
61
  def wrapper(event=None):
59
62
  font = self.font
60
63
  size = boundary(font.cget('size') + value, 1, 127)
61
64
  font.config(size=size)
65
+ update_tab_size()
66
+ refresh_line_numbers()
62
67
  self.set_configuration('gui-size-font', size)
63
68
  return 'break'
64
69
  return wrapper
65
70
 
66
71
  def on_toggle_wrap(event=None):
67
- wrap = self.text.cget('wrap') != 'char'
68
- self.text.configure(wrap='char' if wrap else 'none')
72
+ text = self.text
73
+ wrap = text.cget('wrap') != 'char'
74
+ text.configure(wrap='char' if wrap else 'none')
75
+ refresh_line_numbers()
69
76
  self.set_configuration('wrap', wrap)
70
77
  return 'break'
71
78
 
@@ -83,13 +90,72 @@ try:
83
90
 
84
91
  def on_enter(event=None):
85
92
  text = self.text
93
+ if text.tag_ranges('sel'):
94
+ text.delete('sel.first', 'sel.last')
95
+
86
96
  line = text.get('insert linestart', 'insert lineend')
87
97
  text.insert('insert', '\n' + line[:len(line) - len(line.lstrip())])
98
+ text.see('insert')
88
99
  return 'break'
89
100
 
101
+ def draw_line_numbers():
102
+ self.line_job = None
103
+ text = self.text
104
+ canvas = self.line_numbers
105
+ font = self.font
106
+
107
+ bold = (font.cget('family'), font.cget('size'), 'bold')
108
+ digits = len(text.index('end-1c').split('.')[0])
109
+ padding = font.measure(' ')
110
+ width = font.measure('0' * digits) + padding * 2
111
+
112
+ canvas.delete('all')
113
+ canvas.configure(width=width, bg='#1f1f1f')
114
+
115
+ current = text.index('insert').split('.')[0]
116
+ index = text.index('@0,0')
117
+ previous = None
118
+
119
+ while True:
120
+ info = text.dlineinfo(index)
121
+ if info is None:
122
+ break
123
+
124
+ number = index.split('.')[0]
125
+ if number == previous:
126
+ break
127
+
128
+ canvas.create_text(
129
+ width - padding, info[1],
130
+ anchor='ne',
131
+ text=number,
132
+ font=bold if number == current else font,
133
+ fill='#ffffff'
134
+ )
135
+
136
+ previous = number
137
+ index = text.index(f'{index} +1 lines')
138
+
139
+ def refresh_line_numbers(event=None):
140
+ if self.line_job is None:
141
+ self.line_job = self.after_idle(draw_line_numbers)
142
+
143
+ def on_scroll(first, last):
144
+ self.scrollbar.set(first, last)
145
+ refresh_line_numbers()
146
+
147
+ installed = frozenset(families())
148
+ for font_name in MONO_FALLBACKS:
149
+ if font_name in installed:
150
+ break
151
+ else:
152
+ font_name = 'monospace'
153
+
90
154
  self.lexer = PygmentsPyScriptLexer()
91
- self.font = Font(family='Consolas', size=boundary(self.get_configuration('gui-size-font', 10), 1, 127))
155
+ self.font = Font(family=font_name, size=boundary(self.get_configuration('gui-size-font', 10), 1, 127))
156
+ self.line_job = None
92
157
  self.scrollbar = Scrollbar(self)
158
+ self.line_numbers = Canvas(self, width=0, highlightthickness=0, bd=0)
93
159
  self.text = Text(
94
160
  self,
95
161
  undo=True,
@@ -99,12 +165,16 @@ try:
99
165
  bg='#000000',
100
166
  fg='#ffffff',
101
167
  insertbackground='white',
102
- yscrollcommand=self.scrollbar.set
168
+ yscrollcommand=on_scroll
103
169
  )
104
170
 
171
+ update_tab_size()
172
+
105
173
  self.scrollbar.pack_configure(side='right', fill='y')
106
174
  self.scrollbar.configure(command=self.text.yview)
107
175
 
176
+ self.line_numbers.pack_configure(side='left', fill='y')
177
+ self.text.pack_configure(side='left', expand=True, fill='both')
108
178
  self.text.pack_configure(side='left', expand=True, fill='both')
109
179
 
110
180
  self.text.insert('1.0', self.file.text)
@@ -114,6 +184,9 @@ try:
114
184
  self.text.bind('<Return>', on_enter)
115
185
  self.text.bind('<Prior>', on_page_up)
116
186
  self.text.bind('<Next>', on_page_down)
187
+ self.text.bind('<KeyRelease>', refresh_line_numbers)
188
+ self.text.bind('<ButtonRelease-1>', refresh_line_numbers)
189
+ self.text.bind('<Configure>', refresh_line_numbers)
117
190
 
118
191
  self.bind('<Configure>', on_configure)
119
192
  self.bind_all('<Control-S>', on_save)
@@ -140,18 +213,20 @@ try:
140
213
  )
141
214
  )
142
215
 
143
- self.wm_minsize(300, 250)
216
+ self.wm_minsize(250, 200)
144
217
  self.wm_protocol('WM_DELETE_WINDOW', on_close)
145
218
 
146
- if colored:
219
+ if PYGMENTS and colored:
220
+ self.update_job = None
221
+
147
222
  tag_configure = self.text.tag_configure
148
223
  for token, style in PygmentsPyScriptStyle.list_styles():
149
224
  color = style['color']
150
225
  if color:
151
226
  tag_configure(str(token), foreground=f'#{color}')
152
227
 
153
- def update():
154
- self.title(f'PyScript {__version__} - {self.basename}{"*" if self.modified else ""}')
228
+ def refresh_highlight():
229
+ self.update_job = None
155
230
 
156
231
  index = '1.0'
157
232
 
@@ -171,11 +246,18 @@ try:
171
246
  text_tag_add(to_string(type), index, end_index)
172
247
  index = end_index
173
248
 
249
+ def update():
250
+ self.title(f'PyScript {__version__} - {self.basename}{"*" if self.modified else ""}')
251
+ if self.update_job is not None:
252
+ self.after_cancel(self.update_job)
253
+ self.update_job = self.after(500, refresh_highlight)
254
+
174
255
  else:
175
256
  def update():
176
257
  self.title(f'PyScript {__version__} - {self.basename}{"*" if self.modified else ""}')
177
258
 
178
259
  update()
260
+ refresh_line_numbers()
179
261
 
180
262
  def run(self) -> None:
181
263
  PysEditor.run(self)
@@ -22,8 +22,6 @@ try:
22
22
  def __init__(self, file: PysFileBuffer, colored: bool = True) -> None:
23
23
  PysEditor.__init__(self, file, colored)
24
24
 
25
- self.load_configuration()
26
-
27
25
  self.show_exit_window = False
28
26
 
29
27
  on_edit = Condition(lambda: not self.show_exit_window)
@@ -47,6 +45,9 @@ try:
47
45
  'style': style_from_pygments_cls(PygmentsPyScriptStyle)
48
46
  })
49
47
 
48
+ text_other_keyword.update({
49
+ 'scrollbar': True
50
+ })
50
51
  close_window_keyword.update({
51
52
  'content': FormattedTextControl(
52
53
  " File has been modified. Save before exit? \n"
@@ -64,6 +65,13 @@ try:
64
65
  title = file
65
66
  return ANSI(f'\x1b[7m{title}{" " * max(0, columns - len(title))}\x1b[0m')
66
67
 
68
+ def create_line_prefix(line_number, wrap_count):
69
+ width = len(str(self.text.document.line_count))
70
+ number = str(line_number + 1) if wrap_count == 0 else ''
71
+ is_current = line_number == self.text.document.cursor_position_row
72
+ style = 'bg:#ffffff fg:#000000' + (' bold' if is_current else '')
73
+ return [(style, f' {number:>{width}} '), ('', ' ')]
74
+
67
75
  else:
68
76
  app_other_keyword.update({
69
77
  'color_depth': ColorDepth.DEPTH_1_BIT
@@ -84,11 +92,16 @@ try:
84
92
  title = file
85
93
  return title + ' ' * max(0, columns - len(title))
86
94
 
95
+ def create_line_prefix(line_number, wrap_count):
96
+ width = len(str(self.text.document.line_count))
97
+ number = str(line_number + 1) if wrap_count == 0 else ''
98
+ return f' {number:>{width}} | '
99
+
87
100
  self.text = TextArea(
88
101
  multiline=True,
89
- scrollbar=True,
90
102
  wrap_lines=on_wrap,
91
103
  input_processors=[TabsProcessor(tabstop=4)],
104
+ get_line_prefix=create_line_prefix,
92
105
  **text_other_keyword
93
106
  )
94
107
 
@@ -188,7 +201,7 @@ try:
188
201
  )
189
202
 
190
203
  self.text.text = self.file.text
191
- self.text.buffer.on_text_changed += on_change
204
+ self.text.buffer.on_text_changed.add_handler(on_change)
192
205
 
193
206
  Application.__init__(
194
207
  self,
@@ -1,7 +1,5 @@
1
1
  from .bases import Pys
2
2
  from .constants import NO_COLOR
3
- from .utils.decorators import immutable
4
- from .utils.generic import setimuattr
5
3
  from .utils.string import indent
6
4
 
7
5
  from typing import TYPE_CHECKING, Optional
@@ -12,7 +10,6 @@ if TYPE_CHECKING:
12
10
  from .position import PysPosition
13
11
  from .results import PysRunTimeResult
14
12
 
15
- @immutable
16
13
  class PysTraceback(Pys):
17
14
 
18
15
  __slots__ = ('exception', 'context', 'position', 'primary', 'implicit')
@@ -26,25 +23,23 @@ class PysTraceback(Pys):
26
23
  implicit: bool = False
27
24
  ) -> None:
28
25
 
29
- setimuattr(self, 'exception', exception)
30
- setimuattr(self, 'context', context)
31
- setimuattr(self, 'position', position)
32
- setimuattr(self, 'primary', primary)
33
- setimuattr(self, 'implicit', implicit)
26
+ self.exception = exception
27
+ self.context = context
28
+ self.position = position
29
+ self.primary = primary
30
+ self.implicit = implicit
34
31
 
35
32
  def __repr__(self) -> str:
36
33
  return f'<traceback of {self.exception!r}>'
37
34
 
38
35
  def string_traceback(self) -> str:
39
- # circular import problem solved
40
- from .mapping import GET_ACOLOR
41
- from .position import format_error_arrow
42
-
43
36
  context = self.context
44
37
  position = self.position
45
38
  colored = not (context.flags & NO_COLOR)
46
39
 
47
40
  if colored:
41
+ # circular import problem solved
42
+ from .mapping import GET_ACOLOR
48
43
  reset = GET_ACOLOR('reset')
49
44
  magenta = GET_ACOLOR('magenta')
50
45
  bmagenta = GET_ACOLOR('bold-magenta')
@@ -63,7 +58,7 @@ class PysTraceback(Pys):
63
58
  f' File {magenta}"{position.file.name}"{reset}' +
64
59
  ('' if is_positionless else f', line {magenta}{position.start_line}{reset}') +
65
60
  ('' if context_name is None else f', in {magenta}{context_name}{reset}') +
66
- ('' if is_positionless else f'\n{indent(format_error_arrow(position, colored), 4)}')
61
+ ('' if is_positionless else f'\n{indent(position.format_error_arrow(colored), 4)}')
67
62
  )
68
63
 
69
64
  position = context.parent_entry_position
pyscript/core/handlers.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from .cache import pys_sys
2
- from .constants import ENV_PYSCRIPT_NO_GIL
2
+ from .constants import ENV_PYSCRIPT_WITH_GIL
3
3
  from .context import PysContext
4
4
  from .position import PysPosition
5
5
  from .pystypes import PysFunction
@@ -10,36 +10,17 @@ from typing import Any
10
10
 
11
11
  wrapper_function = (MethodType, classmethod, staticmethod)
12
12
 
13
- if is_environ(ENV_PYSCRIPT_NO_GIL):
14
-
15
- def handle_call(object: Any, context: PysContext, position: PysPosition) -> None:
16
- ins = isinstance
17
-
18
- if ins(object, PysFunction):
19
- code = object.__code__
20
- code.context = context
21
- code.position = position
22
-
23
- elif ins(object, wrapper_function):
24
- handle_call(object.__func__, context, position)
25
-
26
- elif ins(object, type):
27
- gt = getattr
28
-
29
- method = gt(object, '__new__', None)
30
- if method is not None:
31
- handle_call(method, context, position)
32
-
33
- method = gt(object, '__init__', None)
34
- if method is not None:
35
- handle_call(method, context, position)
36
-
37
- GIL = pys_sys.gil = False
38
-
39
- else:
40
- from threading import RLock
41
- gil_lock = RLock()
42
-
13
+ GIL = pys_sys.gil = False
14
+ if is_environ(ENV_PYSCRIPT_WITH_GIL):
15
+ try:
16
+ from threading import RLock
17
+ gil_lock = RLock()
18
+ except:
19
+ pass
20
+ else:
21
+ GIL = pys_sys.gil = True
22
+
23
+ if GIL:
43
24
  def handle_call(object: Any, context: PysContext, position: PysPosition) -> None:
44
25
  with gil_lock:
45
26
  ins = isinstance
@@ -63,4 +44,25 @@ else:
63
44
  if method is not None:
64
45
  handle_call(method, context, position)
65
46
 
66
- GIL = pys_sys.gil = True
47
+ else:
48
+ def handle_call(object: Any, context: PysContext, position: PysPosition) -> None:
49
+ ins = isinstance
50
+
51
+ if ins(object, PysFunction):
52
+ code = object.__code__
53
+ code.context = context
54
+ code.position = position
55
+
56
+ elif ins(object, wrapper_function):
57
+ handle_call(object.__func__, context, position)
58
+
59
+ elif ins(object, type):
60
+ gt = getattr
61
+
62
+ method = gt(object, '__new__', None)
63
+ if method is not None:
64
+ handle_call(method, context, position)
65
+
66
+ method = gt(object, '__init__', None)
67
+ if method is not None:
68
+ handle_call(method, context, position)
@@ -9,10 +9,11 @@ from .pysbuiltins import pys_builtins
9
9
  from .token import TOKENS
10
10
  from .utils.ansi import acolor
11
11
  from .utils.decorators import typecheck
12
+ from .utils.generic import dfrozen
12
13
  from .utils.string import normstr
13
14
 
14
15
  from html import escape as html_escape
15
- from typing import Callable, Optional
16
+ from typing import Callable, Iterator, Optional
16
17
 
17
18
  BUILTIN_TYPES = frozenset(
18
19
  name
@@ -31,10 +32,11 @@ try:
31
32
  from pygments.lexer import RegexLexer, include, bygroups
32
33
  from pygments.style import Style
33
34
  from pygments.token import (
34
- Comment, Error, Escape, Generic, Keyword, Name, Number, Operator, Punctuation, String, Whitespace
35
+ _TokenType, Comment, Error, Escape, Generic, Keyword, Name, Number, Operator, Punctuation, String, Whitespace
35
36
  )
36
37
  from pygments.unistring import xid_start, xid_continue
37
38
 
39
+ _brackets_tokens = dfrozen({index: getattr(Punctuation, f'Brackets{index}') for index in range(3)})
38
40
  _set_constant_keywords = frozenset(CONSTANT_KEYWORDS)
39
41
  _keywords = '|'.join(KEYWORDS)
40
42
  _unicode_name = f'[{xid_start}][{xid_continue}]*'
@@ -112,6 +114,10 @@ try:
112
114
  Comment.Single: HIGHLIGHT_MAP['comment'],
113
115
  Error: HIGHLIGHT_MAP['invalid'],
114
116
  Generic.Prompt: HIGHLIGHT_MAP['keyword-other'],
117
+
118
+ Punctuation.Brackets0: HIGHLIGHT_MAP['brackets-0'],
119
+ Punctuation.Brackets1: HIGHLIGHT_MAP['brackets-1'],
120
+ Punctuation.Brackets2: HIGHLIGHT_MAP['brackets-2']
115
121
  }
116
122
 
117
123
  class PygmentsPyScriptLexer(Pys, RegexLexer):
@@ -324,6 +330,20 @@ try:
324
330
 
325
331
  }
326
332
 
333
+ def get_tokens_unprocessed(self, *args, **kwargs) -> Iterator[tuple[int, _TokenType, str]]:
334
+ brackets_stack = []
335
+
336
+ for index, token, value in super().get_tokens_unprocessed(*args, **kwargs):
337
+ if token is Punctuation and len(value) == 1 and is_bracket(ttype := ord(value)):
338
+ if is_left_bracket(ttype):
339
+ token = _brackets_tokens[len(brackets_stack) % 3]
340
+ brackets_stack.append(ttype)
341
+ elif not (brackets_stack and BRACKETS_MAP[brackets_stack.pop()] == ttype):
342
+ token = Error
343
+ else:
344
+ token = _brackets_tokens[len(brackets_stack) % 3]
345
+ yield index, token, value
346
+
327
347
  class PygmentsPyScriptShellLexer(PygmentsPyScriptLexer):
328
348
 
329
349
  """
@@ -521,8 +541,8 @@ def pys_highlight(
521
541
  type_format = 'comment'
522
542
 
523
543
  elif is_bracket(ttype):
524
- type_format = f'brackets-{len(brackets_stack) % max_bracket_level}'
525
544
  if is_left_bracket(ttype):
545
+ type_format = f'brackets-{len(brackets_stack) % max_bracket_level}'
526
546
  brackets_stack.append(ttype)
527
547
  elif not (brackets_stack and BRACKETS_MAP[brackets_stack.pop()] == ttype):
528
548
  type_format = 'invalid'