scratchattach 2.1.13__py3-none-any.whl → 2.1.15b0__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.
- scratchattach/cloud/_base.py +12 -8
- scratchattach/cloud/cloud.py +19 -7
- scratchattach/editor/asset.py +59 -5
- scratchattach/editor/base.py +82 -31
- scratchattach/editor/block.py +86 -15
- scratchattach/editor/blockshape.py +10 -6
- scratchattach/editor/build_defaulting.py +6 -2
- scratchattach/editor/code_translation/__init__.py +0 -0
- scratchattach/editor/code_translation/parse.py +177 -0
- scratchattach/editor/comment.py +6 -0
- scratchattach/editor/commons.py +49 -19
- scratchattach/editor/extension.py +10 -3
- scratchattach/editor/field.py +9 -0
- scratchattach/editor/inputs.py +4 -1
- scratchattach/editor/meta.py +11 -3
- scratchattach/editor/monitor.py +46 -38
- scratchattach/editor/mutation.py +11 -4
- scratchattach/editor/pallete.py +24 -25
- scratchattach/editor/prim.py +2 -2
- scratchattach/editor/project.py +9 -3
- scratchattach/editor/sprite.py +19 -6
- scratchattach/editor/twconfig.py +2 -1
- scratchattach/editor/vlb.py +1 -1
- scratchattach/eventhandlers/_base.py +2 -2
- scratchattach/eventhandlers/cloud_events.py +2 -2
- scratchattach/eventhandlers/cloud_requests.py +3 -3
- scratchattach/eventhandlers/cloud_server.py +3 -3
- scratchattach/eventhandlers/message_events.py +1 -1
- scratchattach/other/other_apis.py +4 -4
- scratchattach/other/project_json_capabilities.py +3 -3
- scratchattach/site/_base.py +13 -12
- scratchattach/site/activity.py +11 -43
- scratchattach/site/alert.py +227 -0
- scratchattach/site/backpack_asset.py +2 -2
- scratchattach/site/browser_cookie3_stub.py +17 -0
- scratchattach/site/browser_cookies.py +27 -21
- scratchattach/site/classroom.py +51 -34
- scratchattach/site/cloud_activity.py +4 -4
- scratchattach/site/comment.py +30 -8
- scratchattach/site/forum.py +101 -69
- scratchattach/site/project.py +42 -21
- scratchattach/site/session.py +170 -80
- scratchattach/site/studio.py +4 -4
- scratchattach/site/user.py +179 -64
- scratchattach/utils/commons.py +35 -23
- scratchattach/utils/enums.py +44 -5
- scratchattach/utils/exceptions.py +10 -0
- scratchattach/utils/requests.py +57 -31
- {scratchattach-2.1.13.dist-info → scratchattach-2.1.15b0.dist-info}/METADATA +8 -3
- scratchattach-2.1.15b0.dist-info/RECORD +66 -0
- {scratchattach-2.1.13.dist-info → scratchattach-2.1.15b0.dist-info}/WHEEL +1 -1
- scratchattach/editor/sbuild.py +0 -2837
- scratchattach-2.1.13.dist-info/RECORD +0 -63
- {scratchattach-2.1.13.dist-info → scratchattach-2.1.15b0.dist-info}/licenses/LICENSE +0 -0
- {scratchattach-2.1.13.dist-info → scratchattach-2.1.15b0.dist-info}/top_level.txt +0 -0
scratchattach/editor/block.py
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import warnings
|
|
4
|
-
from typing import Optional, Iterable
|
|
4
|
+
from typing import Optional, Iterable, Union
|
|
5
5
|
from typing_extensions import Self
|
|
6
6
|
|
|
7
7
|
from . import base, sprite, mutation, field, inputs, commons, vlb, blockshape, prim, comment, build_defaulting
|
|
8
|
-
from
|
|
8
|
+
from scratchattach.utils import exceptions
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Block(base.SpriteSubComponent):
|
|
12
|
+
"""
|
|
13
|
+
Represents a block in the scratch editor, as a subcomponent of a sprite.
|
|
14
|
+
"""
|
|
15
|
+
_id: Optional[str] = None
|
|
12
16
|
def __init__(self, _opcode: str, _shadow: bool = False, _top_level: Optional[bool] = None,
|
|
13
17
|
_mutation: Optional[mutation.Mutation] = None, _fields: Optional[dict[str, field.Field]] = None,
|
|
14
18
|
_inputs: Optional[dict[str, inputs.Input]] = None, x: int = 0, y: int = 0, pos: Optional[tuple[int, int]] = None,
|
|
15
19
|
|
|
16
20
|
_next: Optional[Block] = None, _parent: Optional[Block] = None,
|
|
17
|
-
*, _next_id: Optional[str] = None, _parent_id: Optional[str] = None, _sprite:
|
|
21
|
+
*, _next_id: Optional[str] = None, _parent_id: Optional[str] = None, _sprite: commons.SpriteInput = build_defaulting.SPRITE_DEFAULT):
|
|
18
22
|
# Defaulting for args
|
|
19
23
|
if _fields is None:
|
|
20
24
|
_fields = {}
|
|
@@ -34,14 +38,11 @@ class Block(base.SpriteSubComponent):
|
|
|
34
38
|
self.fields = _fields
|
|
35
39
|
self.inputs = _inputs
|
|
36
40
|
|
|
41
|
+
# Temporarily stores id of next block. Will be used later during project instantiation to find the next block object
|
|
37
42
|
self._next_id = _next_id
|
|
38
|
-
|
|
39
|
-
Temporarily stores id of
|
|
40
|
-
"""
|
|
43
|
+
|
|
44
|
+
# Temporarily stores id of parent block. Will be used later during project instantiation to find the parent block object
|
|
41
45
|
self._parent_id = _parent_id
|
|
42
|
-
"""
|
|
43
|
-
Temporarily stores id of parent block. Will be used later during project instantiation to find the parent block object
|
|
44
|
-
"""
|
|
45
46
|
|
|
46
47
|
self.next = _next
|
|
47
48
|
self.parent = _parent
|
|
@@ -55,6 +56,9 @@ class Block(base.SpriteSubComponent):
|
|
|
55
56
|
return f"Block<{self.opcode!r}>"
|
|
56
57
|
|
|
57
58
|
def link_subcomponents(self):
|
|
59
|
+
"""
|
|
60
|
+
Iterate through subcomponents and assign the 'block' attribute
|
|
61
|
+
"""
|
|
58
62
|
if self.mutation:
|
|
59
63
|
self.mutation.block = self
|
|
60
64
|
|
|
@@ -63,6 +67,9 @@ class Block(base.SpriteSubComponent):
|
|
|
63
67
|
subcomponent.block = self
|
|
64
68
|
|
|
65
69
|
def add_input(self, name: str, _input: inputs.Input) -> Self:
|
|
70
|
+
"""
|
|
71
|
+
Add an input to the block.
|
|
72
|
+
""" # not sure what else to say
|
|
66
73
|
self.inputs[name] = _input
|
|
67
74
|
for val in (_input.value, _input.obscurer):
|
|
68
75
|
if isinstance(val, Block):
|
|
@@ -70,22 +77,34 @@ class Block(base.SpriteSubComponent):
|
|
|
70
77
|
return self
|
|
71
78
|
|
|
72
79
|
def add_field(self, name: str, _field: field.Field) -> Self:
|
|
80
|
+
"""
|
|
81
|
+
Add a field to the block.
|
|
82
|
+
""" # not sure what else to sa
|
|
73
83
|
self.fields[name] = _field
|
|
74
84
|
return self
|
|
75
85
|
|
|
76
86
|
def set_mutation(self, _mutation: mutation.Mutation) -> Self:
|
|
87
|
+
"""
|
|
88
|
+
Attach a mutation object and call mutation.link_arguments()
|
|
89
|
+
""" # this comment explains *what* this does, not *why*
|
|
77
90
|
self.mutation = _mutation
|
|
78
91
|
_mutation.block = self
|
|
79
92
|
_mutation.link_arguments()
|
|
80
93
|
return self
|
|
81
94
|
|
|
82
95
|
def set_comment(self, _comment: comment.Comment) -> Self:
|
|
96
|
+
"""
|
|
97
|
+
Attach a comment and add it to the sprite.
|
|
98
|
+
"""
|
|
83
99
|
_comment.block = self
|
|
84
100
|
self.sprite.add_comment(_comment)
|
|
85
101
|
|
|
86
102
|
return self
|
|
87
103
|
|
|
88
104
|
def check_toplevel(self):
|
|
105
|
+
"""
|
|
106
|
+
Edit the toplevel, x, and y attributes based on whether the parent attribute is None
|
|
107
|
+
"""
|
|
89
108
|
self.is_top_level = self.parent is None
|
|
90
109
|
|
|
91
110
|
if not self.is_top_level:
|
|
@@ -95,7 +114,7 @@ class Block(base.SpriteSubComponent):
|
|
|
95
114
|
def target(self):
|
|
96
115
|
"""
|
|
97
116
|
Alias for sprite
|
|
98
|
-
"""
|
|
117
|
+
""" # remove this?
|
|
99
118
|
return self.sprite
|
|
100
119
|
|
|
101
120
|
@property
|
|
@@ -107,7 +126,8 @@ class Block(base.SpriteSubComponent):
|
|
|
107
126
|
_shape = blockshape.BlockShapes.find(self.opcode, "opcode")
|
|
108
127
|
if _shape is None:
|
|
109
128
|
warnings.warn(f"No blockshape {self.opcode!r} exists! Defaulting to {blockshape.BlockShapes.UNDEFINED}")
|
|
110
|
-
|
|
129
|
+
_shape = blockshape.BlockShapes.UNDEFINED
|
|
130
|
+
assert isinstance(_shape, blockshape.BlockShape)
|
|
111
131
|
return _shape
|
|
112
132
|
|
|
113
133
|
@property
|
|
@@ -127,21 +147,32 @@ class Block(base.SpriteSubComponent):
|
|
|
127
147
|
return self.mutation.has_next
|
|
128
148
|
|
|
129
149
|
@property
|
|
130
|
-
def id(self) -> str
|
|
150
|
+
def id(self) -> str:
|
|
131
151
|
"""
|
|
132
152
|
Work out the id of this block by searching through the sprite dictionary
|
|
133
153
|
"""
|
|
154
|
+
if self._id:
|
|
155
|
+
return self.id
|
|
134
156
|
# warnings.warn(f"Using block IDs can cause consistency issues and is not recommended")
|
|
135
157
|
# This property is used when converting comments to JSON (we don't want random warning when exporting a project)
|
|
136
158
|
for _block_id, _block in self.sprite.blocks.items():
|
|
137
159
|
if _block is self:
|
|
138
|
-
|
|
160
|
+
self._id = _block_id
|
|
161
|
+
return self.id
|
|
139
162
|
|
|
140
163
|
# Let's just automatically assign ourselves an id
|
|
141
164
|
self.sprite.add_block(self)
|
|
165
|
+
return self.id
|
|
166
|
+
|
|
167
|
+
@id.setter
|
|
168
|
+
def id(self, value: str) -> None:
|
|
169
|
+
self._id = value
|
|
142
170
|
|
|
143
171
|
@property
|
|
144
172
|
def parent_id(self):
|
|
173
|
+
"""
|
|
174
|
+
Get the id of the parent block, if applicable
|
|
175
|
+
"""
|
|
145
176
|
if self.parent is not None:
|
|
146
177
|
return self.parent.id
|
|
147
178
|
else:
|
|
@@ -149,6 +180,9 @@ class Block(base.SpriteSubComponent):
|
|
|
149
180
|
|
|
150
181
|
@property
|
|
151
182
|
def next_id(self):
|
|
183
|
+
"""
|
|
184
|
+
Get the id of the next block, if applicable
|
|
185
|
+
"""
|
|
152
186
|
if self.next is not None:
|
|
153
187
|
return self.next.id
|
|
154
188
|
else:
|
|
@@ -186,13 +220,19 @@ class Block(base.SpriteSubComponent):
|
|
|
186
220
|
|
|
187
221
|
@property
|
|
188
222
|
def previous_chain(self):
|
|
189
|
-
|
|
223
|
+
"""
|
|
224
|
+
Recursive getter method to get all previous blocks in the blockchain (until hitting a top-level block)
|
|
225
|
+
"""
|
|
226
|
+
if self.parent is None: # todo: use is_top_level?
|
|
190
227
|
return [self]
|
|
191
228
|
|
|
192
229
|
return [self] + self.parent.previous_chain
|
|
193
230
|
|
|
194
231
|
@property
|
|
195
232
|
def attached_chain(self):
|
|
233
|
+
"""
|
|
234
|
+
Recursive getter method to get all next blocks in the blockchain (until hitting a bottom-levell block)
|
|
235
|
+
"""
|
|
196
236
|
if self.next is None:
|
|
197
237
|
return [self]
|
|
198
238
|
|
|
@@ -200,23 +240,30 @@ class Block(base.SpriteSubComponent):
|
|
|
200
240
|
|
|
201
241
|
@property
|
|
202
242
|
def complete_chain(self):
|
|
203
|
-
|
|
243
|
+
"""
|
|
244
|
+
Attach previous and attached chains from this block
|
|
245
|
+
"""
|
|
204
246
|
return self.previous_chain[:1:-1] + self.attached_chain
|
|
205
247
|
|
|
206
248
|
@property
|
|
207
249
|
def top_level_block(self):
|
|
208
250
|
"""
|
|
251
|
+
Get the first block in the block stack that this block is part of
|
|
209
252
|
same as the old stack_parent property from sbedtior v1
|
|
210
253
|
"""
|
|
211
254
|
return self.previous_chain[-1]
|
|
212
255
|
|
|
213
256
|
@property
|
|
214
257
|
def bottom_level_block(self):
|
|
258
|
+
"""
|
|
259
|
+
Get the last block in the block stack that this block is part of
|
|
260
|
+
"""
|
|
215
261
|
return self.attached_chain[-1]
|
|
216
262
|
|
|
217
263
|
@property
|
|
218
264
|
def stack_tree(self):
|
|
219
265
|
"""
|
|
266
|
+
Useful for showing a block stack in the console, using pprint
|
|
220
267
|
:return: A tree-like nested list structure representing the stack of blocks, including inputs, starting at this block
|
|
221
268
|
"""
|
|
222
269
|
_tree = [self]
|
|
@@ -254,6 +301,9 @@ class Block(base.SpriteSubComponent):
|
|
|
254
301
|
|
|
255
302
|
@property
|
|
256
303
|
def parent_input(self):
|
|
304
|
+
"""
|
|
305
|
+
Fetch an input that this block is placed inside of (if applicable)
|
|
306
|
+
"""
|
|
257
307
|
if not self.parent:
|
|
258
308
|
return None
|
|
259
309
|
|
|
@@ -268,6 +318,9 @@ class Block(base.SpriteSubComponent):
|
|
|
268
318
|
|
|
269
319
|
@property
|
|
270
320
|
def comment(self) -> comment.Comment | None:
|
|
321
|
+
"""
|
|
322
|
+
Fetch an associated comment (if applicable) by searching the associated sprite
|
|
323
|
+
"""
|
|
271
324
|
for _comment in self.sprite.comments:
|
|
272
325
|
if _comment.block is self:
|
|
273
326
|
return _comment
|
|
@@ -320,6 +373,9 @@ class Block(base.SpriteSubComponent):
|
|
|
320
373
|
|
|
321
374
|
@property
|
|
322
375
|
def is_turbowarp_block(self):
|
|
376
|
+
"""
|
|
377
|
+
Return whether this block is actually a turbowarp debugger/boolean block, based on mutation
|
|
378
|
+
"""
|
|
323
379
|
return self.turbowarp_block_opcode is not None
|
|
324
380
|
|
|
325
381
|
@staticmethod
|
|
@@ -356,6 +412,9 @@ class Block(base.SpriteSubComponent):
|
|
|
356
412
|
_parent_id=_parent_id)
|
|
357
413
|
|
|
358
414
|
def to_json(self) -> dict:
|
|
415
|
+
"""
|
|
416
|
+
Convert a block to the project.json format
|
|
417
|
+
"""
|
|
359
418
|
self.check_toplevel()
|
|
360
419
|
|
|
361
420
|
_json = {
|
|
@@ -387,6 +446,9 @@ class Block(base.SpriteSubComponent):
|
|
|
387
446
|
return _json
|
|
388
447
|
|
|
389
448
|
def link_using_sprite(self, link_subs: bool = True):
|
|
449
|
+
"""
|
|
450
|
+
Link this block to various other blocks once the sprite has been assigned
|
|
451
|
+
"""
|
|
390
452
|
if link_subs:
|
|
391
453
|
self.link_subcomponents()
|
|
392
454
|
|
|
@@ -443,6 +505,9 @@ class Block(base.SpriteSubComponent):
|
|
|
443
505
|
|
|
444
506
|
# Adding/removing block
|
|
445
507
|
def attach_block(self, new: Block) -> Block:
|
|
508
|
+
"""
|
|
509
|
+
Connect another block onto the boottom of this block (not necessarily bottom of chain)
|
|
510
|
+
"""
|
|
446
511
|
if not self.can_next:
|
|
447
512
|
raise exceptions.BadBlockShape(f"{self.block_shape} cannot be stacked onto")
|
|
448
513
|
elif new.block_shape.is_hat or not new.block_shape.is_stack:
|
|
@@ -474,6 +539,9 @@ class Block(base.SpriteSubComponent):
|
|
|
474
539
|
)
|
|
475
540
|
|
|
476
541
|
def slot_above(self, new: Block) -> Block:
|
|
542
|
+
"""
|
|
543
|
+
Place a single block directly above this block
|
|
544
|
+
"""
|
|
477
545
|
if not new.can_next:
|
|
478
546
|
raise exceptions.BadBlockShape(f"{new.block_shape} cannot be stacked onto")
|
|
479
547
|
|
|
@@ -503,6 +571,9 @@ class Block(base.SpriteSubComponent):
|
|
|
503
571
|
self.sprite.remove_block(self)
|
|
504
572
|
|
|
505
573
|
def delete_chain(self):
|
|
574
|
+
"""
|
|
575
|
+
Delete all blocks in the attached blockchain (and self)
|
|
576
|
+
"""
|
|
506
577
|
for _block in self.attached_chain:
|
|
507
578
|
_block.delete_single_block()
|
|
508
579
|
|
|
@@ -3,27 +3,31 @@ Enums stating the shape of a block from its opcode (i.e: stack, c-mouth, cap, ha
|
|
|
3
3
|
"""
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
-
# Perhaps this should be merged with
|
|
6
|
+
# Perhaps this should be merged with pallete.py
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
-
from typing import Final
|
|
8
|
+
from typing import Final, Literal
|
|
9
9
|
|
|
10
10
|
from . import commons
|
|
11
|
-
from
|
|
11
|
+
from scratchattach.utils.enums import _EnumWrapper
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class _MutationDependent(commons.Singleton):
|
|
15
|
+
"""
|
|
16
|
+
Singleton value that represents the uncertainty of a vablue because it depends on block mutation data.
|
|
17
|
+
"""
|
|
18
|
+
INSTANCE = 0
|
|
15
19
|
def __bool__(self):
|
|
16
20
|
raise TypeError("Need mutation data to work out attribute value.")
|
|
17
21
|
|
|
18
22
|
|
|
19
|
-
MUTATION_DEPENDENT: Final[_MutationDependent] = _MutationDependent
|
|
23
|
+
MUTATION_DEPENDENT: Final[Literal[_MutationDependent]] = _MutationDependent.INSTANCE
|
|
20
24
|
"""Value used when mutation data is required to work out the attribute value"""
|
|
21
25
|
|
|
22
26
|
|
|
23
27
|
@dataclass(init=True, repr=True)
|
|
24
28
|
class BlockShape:
|
|
25
29
|
"""
|
|
26
|
-
|
|
30
|
+
The shape of a block; e.g. is it a stack, c-mouth, cap, hat reporter, boolean or menu block?
|
|
27
31
|
"""
|
|
28
32
|
is_stack: bool | _MutationDependent = False # Most blocks - e.g. move [10] steps
|
|
29
33
|
is_c_mouth: bool | _MutationDependent = False # Has substack - e.g. repeat
|
|
@@ -262,7 +266,7 @@ class BlockShapes(_EnumWrapper):
|
|
|
262
266
|
MAKEYMAKEY_MENU_KEY = BlockShape(is_reporter=True, is_menu=True, opcode="makeymakey_menu_KEY")
|
|
263
267
|
MAKEYMAKEY_MENU_SEQUENCE = BlockShape(is_reporter=True, is_menu=True, opcode="makeymakey_menu_SEQUENCE")
|
|
264
268
|
|
|
265
|
-
MICROBIT_WHENBUTTONPRESSED = BlockShape(opcode="microbit_whenButtonPressed")
|
|
269
|
+
MICROBIT_WHENBUTTONPRESSED = BlockShape(opcode="microbit_whenButtonPressed") # todo: finish this
|
|
266
270
|
MICROBIT_ISBUTTONPRESSED = BlockShape(opcode="microbit_isButtonPressed")
|
|
267
271
|
MICROBIT_WHENGESTURE = BlockShape(opcode="microbit_whenGesture")
|
|
268
272
|
MICROBIT_DISPLAYSYMBOL = BlockShape(opcode="microbit_displaySymbol")
|
|
@@ -3,7 +3,7 @@ Module which stores the 'default' or 'current' selected Sprite/project (stored a
|
|
|
3
3
|
"""
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
-
from typing import Iterable, TYPE_CHECKING, Final
|
|
6
|
+
from typing import Iterable, TYPE_CHECKING, Final, Literal
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from . import sprite, block, prim, comment
|
|
@@ -11,11 +11,12 @@ from . import commons
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class _SetSprite(commons.Singleton):
|
|
14
|
+
INSTANCE = 0
|
|
14
15
|
def __repr__(self):
|
|
15
16
|
return f'<Reminder to default your sprite to {current_sprite()}>'
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
SPRITE_DEFAULT: Final[_SetSprite] = _SetSprite
|
|
19
|
+
SPRITE_DEFAULT: Final[Literal[_SetSprite.INSTANCE]] = _SetSprite.INSTANCE
|
|
19
20
|
|
|
20
21
|
_sprite_stack: list[sprite.Sprite] = []
|
|
21
22
|
|
|
@@ -25,6 +26,9 @@ def stack_add_sprite(_sprite: sprite.Sprite):
|
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
def current_sprite() -> sprite.Sprite | None:
|
|
29
|
+
"""
|
|
30
|
+
Retrieve the default sprite from the top of the sprite stack
|
|
31
|
+
"""
|
|
28
32
|
if len(_sprite_stack) == 0:
|
|
29
33
|
return None
|
|
30
34
|
return _sprite_stack[-1]
|
|
File without changes
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Union, Generic, TypeVar
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
from lark import Lark, Transformer, Tree, Token, v_args
|
|
8
|
+
from lark.reconstruct import Reconstructor
|
|
9
|
+
|
|
10
|
+
R = TypeVar("R")
|
|
11
|
+
class SupportsRead(ABC, Generic[R]):
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def read(self, size: int | None = -1) -> R:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
LANG_PATH = Path(__file__).parent / "language.lark"
|
|
17
|
+
|
|
18
|
+
lang = Lark(LANG_PATH.read_text(), maybe_placeholders=False)
|
|
19
|
+
reconstructor = Reconstructor(lang)
|
|
20
|
+
|
|
21
|
+
def parse(script: Union[str, bytes, SupportsRead[str], Path]) -> Tree:
|
|
22
|
+
if isinstance(script, Path):
|
|
23
|
+
script = script.read_text()
|
|
24
|
+
if isinstance(script, SupportsRead):
|
|
25
|
+
read_data = script.read()
|
|
26
|
+
assert isinstance(read_data, str)
|
|
27
|
+
script = read_data
|
|
28
|
+
if isinstance(script, bytes):
|
|
29
|
+
script = script.decode("utf-8")
|
|
30
|
+
return lang.parse(script)
|
|
31
|
+
|
|
32
|
+
def unparse(tree: Tree) -> str:
|
|
33
|
+
return reconstructor.reconstruct(tree)
|
|
34
|
+
|
|
35
|
+
class PrettyUnparser(Transformer):
|
|
36
|
+
INDENT_STRING = " "
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def _indent(cls, text):
|
|
40
|
+
if not text:
|
|
41
|
+
return ""
|
|
42
|
+
return "\n".join(cls.INDENT_STRING + line for line in text.splitlines())
|
|
43
|
+
|
|
44
|
+
def PARAM_NAME(self, token):
|
|
45
|
+
return token.value
|
|
46
|
+
|
|
47
|
+
def BLOCK_NAME(self, token):
|
|
48
|
+
return token.value
|
|
49
|
+
|
|
50
|
+
def EVENT(self, token):
|
|
51
|
+
return token.value
|
|
52
|
+
|
|
53
|
+
def CONTROL_BLOCK_NAME(self, token):
|
|
54
|
+
return token.value
|
|
55
|
+
|
|
56
|
+
def _PREPROC_INSTR_CONTENT(self, token):
|
|
57
|
+
return token.value
|
|
58
|
+
|
|
59
|
+
def _COMMMENT_CONTENT(self, token):
|
|
60
|
+
return token.value
|
|
61
|
+
|
|
62
|
+
@v_args(inline=True)
|
|
63
|
+
def hat(self, child):
|
|
64
|
+
return child
|
|
65
|
+
|
|
66
|
+
@v_args(inline=True)
|
|
67
|
+
def param(self, child):
|
|
68
|
+
return child
|
|
69
|
+
|
|
70
|
+
@v_args(inline=True)
|
|
71
|
+
def value_param(self, name):
|
|
72
|
+
return name
|
|
73
|
+
|
|
74
|
+
@v_args(inline=True)
|
|
75
|
+
def bool_param(self, name):
|
|
76
|
+
return f"<{name}>"
|
|
77
|
+
|
|
78
|
+
@v_args(inline=True)
|
|
79
|
+
def event_hat(self, event_name):
|
|
80
|
+
return f"when ({event_name})"
|
|
81
|
+
|
|
82
|
+
def block_hat(self, items):
|
|
83
|
+
name, *params = items
|
|
84
|
+
params_str = ", ".join(params)
|
|
85
|
+
return f"custom_block {name} ({params_str})"
|
|
86
|
+
|
|
87
|
+
@v_args(inline=True)
|
|
88
|
+
def PREPROC_INSTR(self, content):
|
|
89
|
+
return f"{content}"
|
|
90
|
+
|
|
91
|
+
@v_args(inline=True)
|
|
92
|
+
def COMMMENT(self, content):
|
|
93
|
+
return f"{content}"
|
|
94
|
+
|
|
95
|
+
def block(self, items):
|
|
96
|
+
params = []
|
|
97
|
+
inner_blocks = []
|
|
98
|
+
comments = []
|
|
99
|
+
for i in items[1:]:
|
|
100
|
+
if not isinstance(i, Tree):
|
|
101
|
+
continue
|
|
102
|
+
if str(i.data) == "block_content":
|
|
103
|
+
inner_blocks.extend(i.children)
|
|
104
|
+
if str(i.data) == "block_params":
|
|
105
|
+
params.extend(i.children)
|
|
106
|
+
if str(i.data) == "comments":
|
|
107
|
+
comments.extend(i.children)
|
|
108
|
+
block_name = items[0]
|
|
109
|
+
block_text = f"{block_name}({', '.join(params)})" if params or not inner_blocks else f"{block_name}"
|
|
110
|
+
if inner_blocks:
|
|
111
|
+
blocks_content = "\n".join(inner_blocks)
|
|
112
|
+
indented_content = self._indent(blocks_content)
|
|
113
|
+
block_text += f" {{\n{indented_content}\n}}"
|
|
114
|
+
if comments:
|
|
115
|
+
block_text += f" {' '.join(comments)}"
|
|
116
|
+
return block_text
|
|
117
|
+
|
|
118
|
+
def LITERAL_NUMBER(self, number: str):
|
|
119
|
+
return number
|
|
120
|
+
|
|
121
|
+
def expr(self, items):
|
|
122
|
+
text = items[0]
|
|
123
|
+
if len(items) > 1:
|
|
124
|
+
text += f" {' '.join(items[1].children)}"
|
|
125
|
+
return text
|
|
126
|
+
|
|
127
|
+
def low_expr1(self, items):
|
|
128
|
+
text = f"({items[0]})" if " " in items[0] else items[0]
|
|
129
|
+
if len(items) > 1:
|
|
130
|
+
text += f" {' '.join(items[1].children)}"
|
|
131
|
+
return text
|
|
132
|
+
|
|
133
|
+
@v_args(inline=True)
|
|
134
|
+
def low_expr2(self, item):
|
|
135
|
+
return item
|
|
136
|
+
|
|
137
|
+
def addition(self, items):
|
|
138
|
+
return items[0] + " + " + items[1]
|
|
139
|
+
|
|
140
|
+
def subtraction(self, items):
|
|
141
|
+
return items[0] + " - " + items[1]
|
|
142
|
+
|
|
143
|
+
def multiplication(self, items):
|
|
144
|
+
return items[0] + " * " + items[1]
|
|
145
|
+
|
|
146
|
+
def division(self, items):
|
|
147
|
+
return items[0] + " / " + items[1]
|
|
148
|
+
|
|
149
|
+
def top_level_block(self, items):
|
|
150
|
+
first_item = items[0]
|
|
151
|
+
if first_item.startswith("%%") or first_item.startswith("##"):
|
|
152
|
+
return first_item
|
|
153
|
+
|
|
154
|
+
hat, *blocks = items
|
|
155
|
+
blocks_content = "\n".join(blocks)
|
|
156
|
+
indented_content = self._indent(blocks_content)
|
|
157
|
+
return f"{hat} {{\n{indented_content}\n}}"
|
|
158
|
+
|
|
159
|
+
def start(self, items):
|
|
160
|
+
return "\n\n".join(items)
|
|
161
|
+
|
|
162
|
+
def pretty_unparse(tree: Tree):
|
|
163
|
+
return PrettyUnparser().transform(tree)
|
|
164
|
+
|
|
165
|
+
if __name__ == "__main__":
|
|
166
|
+
EXAMPLE_FILE = Path(__file__).parent / "example.txt"
|
|
167
|
+
tree = parse(EXAMPLE_FILE)
|
|
168
|
+
print(tree.pretty())
|
|
169
|
+
print()
|
|
170
|
+
print()
|
|
171
|
+
print(tree)
|
|
172
|
+
print()
|
|
173
|
+
print()
|
|
174
|
+
print(unparse(tree))
|
|
175
|
+
print()
|
|
176
|
+
print()
|
|
177
|
+
print(pretty_unparse(tree))
|
scratchattach/editor/comment.py
CHANGED
|
@@ -5,6 +5,9 @@ from typing import Optional
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class Comment(base.IDComponent):
|
|
8
|
+
"""
|
|
9
|
+
Represents a comment in the scratch editor.
|
|
10
|
+
"""
|
|
8
11
|
def __init__(self, _id: Optional[str] = None, _block: Optional[block.Block] = None, x: int = 0, y: int = 0, width: int = 200,
|
|
9
12
|
height: int = 200, minimized: bool = False, text: str = '', *, _block_id: Optional[str] = None,
|
|
10
13
|
_sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT, pos: Optional[tuple[int, int]] = None):
|
|
@@ -32,6 +35,9 @@ class Comment(base.IDComponent):
|
|
|
32
35
|
|
|
33
36
|
@property
|
|
34
37
|
def block_id(self):
|
|
38
|
+
"""
|
|
39
|
+
Retrieve the id of the associateed block (if applicable)
|
|
40
|
+
"""
|
|
35
41
|
if self.block is not None:
|
|
36
42
|
return self.block.id
|
|
37
43
|
elif self._block_id is not None:
|
scratchattach/editor/commons.py
CHANGED
|
@@ -6,11 +6,19 @@ from __future__ import annotations
|
|
|
6
6
|
import json
|
|
7
7
|
import random
|
|
8
8
|
import string
|
|
9
|
-
from typing import Optional, Final, Any
|
|
9
|
+
from typing import Optional, Final, Any, TYPE_CHECKING, Union
|
|
10
|
+
from enum import Enum, EnumMeta
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from . import sprite, build_defaulting
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
SpriteInput = Union[sprite.Sprite, build_defaulting._SetSprite]
|
|
16
|
+
else:
|
|
17
|
+
SpriteInput = Any
|
|
18
|
+
|
|
19
|
+
from scratchattach.utils import exceptions
|
|
20
|
+
|
|
21
|
+
DIGITS: Final[tuple[str, ...]] = tuple("0123456789")
|
|
14
22
|
|
|
15
23
|
ID_CHARS: Final[str] = string.ascii_letters + string.digits # + string.punctuation
|
|
16
24
|
|
|
@@ -72,7 +80,8 @@ def _read_json_number(_str: str) -> float | int:
|
|
|
72
80
|
|
|
73
81
|
return json.loads(ret)
|
|
74
82
|
|
|
75
|
-
|
|
83
|
+
# todo: consider if this should be moved to util.commons instead of editor.commons
|
|
84
|
+
# note: this is currently unused code
|
|
76
85
|
def consume_json(_str: str, i: int = 0) -> str | float | int | dict | list | bool | None:
|
|
77
86
|
"""
|
|
78
87
|
*'gobble up some JSON until we hit something not quite so tasty'*
|
|
@@ -134,16 +143,20 @@ def is_partial_json(_str: str, i: int = 0) -> bool:
|
|
|
134
143
|
|
|
135
144
|
|
|
136
145
|
def is_valid_json(_str: Any) -> bool:
|
|
146
|
+
"""
|
|
147
|
+
Try to load a json string, if it fails, return False, else return true.
|
|
148
|
+
"""
|
|
137
149
|
try:
|
|
138
150
|
json.loads(_str)
|
|
139
151
|
return True
|
|
140
|
-
except ValueError:
|
|
141
|
-
return False
|
|
142
|
-
except TypeError:
|
|
152
|
+
except (ValueError, TypeError):
|
|
143
153
|
return False
|
|
144
154
|
|
|
145
155
|
|
|
146
156
|
def noneless_update(obj: dict, update: dict) -> None:
|
|
157
|
+
"""
|
|
158
|
+
equivalent to dict.update, except and values of None are not assigned
|
|
159
|
+
"""
|
|
147
160
|
for key, value in update.items():
|
|
148
161
|
if value is not None:
|
|
149
162
|
obj[key] = value
|
|
@@ -163,6 +176,9 @@ def remove_nones(obj: dict) -> None:
|
|
|
163
176
|
|
|
164
177
|
|
|
165
178
|
def safe_get(lst: list | tuple, _i: int, default: Optional[Any] = None) -> Any:
|
|
179
|
+
"""
|
|
180
|
+
Like dict.get() but for lists
|
|
181
|
+
"""
|
|
166
182
|
if len(lst) <= _i:
|
|
167
183
|
return default
|
|
168
184
|
else:
|
|
@@ -182,7 +198,10 @@ def trim_final_nones(lst: list) -> list:
|
|
|
182
198
|
return lst[:i]
|
|
183
199
|
|
|
184
200
|
|
|
185
|
-
def dumps_ifnn(obj: Any) -> str:
|
|
201
|
+
def dumps_ifnn(obj: Any) -> Optional[str]:
|
|
202
|
+
"""
|
|
203
|
+
Return json.dumps(obj) if the object is not None
|
|
204
|
+
"""
|
|
186
205
|
if obj is None:
|
|
187
206
|
return None
|
|
188
207
|
else:
|
|
@@ -190,9 +209,13 @@ def dumps_ifnn(obj: Any) -> str:
|
|
|
190
209
|
|
|
191
210
|
|
|
192
211
|
def gen_id() -> str:
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
212
|
+
"""
|
|
213
|
+
Generate an id for scratch blocks/variables/lists/broadcasts
|
|
214
|
+
|
|
215
|
+
The old 'naïve' method but that chances of a repeat are so miniscule
|
|
216
|
+
Have to check if whitespace chars break it
|
|
217
|
+
May later add checking within sprites so that we don't need such long ids (we can save space this way)
|
|
218
|
+
"""
|
|
196
219
|
return ''.join(random.choices(ID_CHARS, k=20))
|
|
197
220
|
|
|
198
221
|
|
|
@@ -212,6 +235,9 @@ def sanitize_fn(filename: str):
|
|
|
212
235
|
|
|
213
236
|
|
|
214
237
|
def get_folder_name(name: str) -> str | None:
|
|
238
|
+
"""
|
|
239
|
+
Get the name of the folder if this is a turbowarp-style costume name
|
|
240
|
+
"""
|
|
215
241
|
if name.startswith('//'):
|
|
216
242
|
return None
|
|
217
243
|
|
|
@@ -231,13 +257,17 @@ def get_name_nofldr(name: str) -> str:
|
|
|
231
257
|
else:
|
|
232
258
|
return name[len(fldr) + 2:]
|
|
233
259
|
|
|
260
|
+
# Parent enum class
|
|
261
|
+
class SingletonMeta(EnumMeta):
|
|
234
262
|
|
|
235
|
-
|
|
236
|
-
|
|
263
|
+
def __call__(self, value=0, *args, **kwds):
|
|
264
|
+
if value != 0:
|
|
265
|
+
raise ValueError("Value must be 0.")
|
|
266
|
+
old_bases = self.__bases__
|
|
267
|
+
self.__bases__ = old_bases + (Enum,)
|
|
268
|
+
result = super().__call__(value, *args, **kwds)
|
|
269
|
+
self.__bases__ = old_bases
|
|
270
|
+
return result
|
|
271
|
+
|
|
272
|
+
Singleton = Enum
|
|
237
273
|
|
|
238
|
-
def __new__(cls, *args, **kwargs):
|
|
239
|
-
if hasattr(cls, "_instance"):
|
|
240
|
-
return cls._instance
|
|
241
|
-
else:
|
|
242
|
-
cls._instance = super(Singleton, cls).__new__(cls)
|
|
243
|
-
return cls._instance
|