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.
Files changed (55) hide show
  1. scratchattach/cloud/_base.py +12 -8
  2. scratchattach/cloud/cloud.py +19 -7
  3. scratchattach/editor/asset.py +59 -5
  4. scratchattach/editor/base.py +82 -31
  5. scratchattach/editor/block.py +86 -15
  6. scratchattach/editor/blockshape.py +10 -6
  7. scratchattach/editor/build_defaulting.py +6 -2
  8. scratchattach/editor/code_translation/__init__.py +0 -0
  9. scratchattach/editor/code_translation/parse.py +177 -0
  10. scratchattach/editor/comment.py +6 -0
  11. scratchattach/editor/commons.py +49 -19
  12. scratchattach/editor/extension.py +10 -3
  13. scratchattach/editor/field.py +9 -0
  14. scratchattach/editor/inputs.py +4 -1
  15. scratchattach/editor/meta.py +11 -3
  16. scratchattach/editor/monitor.py +46 -38
  17. scratchattach/editor/mutation.py +11 -4
  18. scratchattach/editor/pallete.py +24 -25
  19. scratchattach/editor/prim.py +2 -2
  20. scratchattach/editor/project.py +9 -3
  21. scratchattach/editor/sprite.py +19 -6
  22. scratchattach/editor/twconfig.py +2 -1
  23. scratchattach/editor/vlb.py +1 -1
  24. scratchattach/eventhandlers/_base.py +2 -2
  25. scratchattach/eventhandlers/cloud_events.py +2 -2
  26. scratchattach/eventhandlers/cloud_requests.py +3 -3
  27. scratchattach/eventhandlers/cloud_server.py +3 -3
  28. scratchattach/eventhandlers/message_events.py +1 -1
  29. scratchattach/other/other_apis.py +4 -4
  30. scratchattach/other/project_json_capabilities.py +3 -3
  31. scratchattach/site/_base.py +13 -12
  32. scratchattach/site/activity.py +11 -43
  33. scratchattach/site/alert.py +227 -0
  34. scratchattach/site/backpack_asset.py +2 -2
  35. scratchattach/site/browser_cookie3_stub.py +17 -0
  36. scratchattach/site/browser_cookies.py +27 -21
  37. scratchattach/site/classroom.py +51 -34
  38. scratchattach/site/cloud_activity.py +4 -4
  39. scratchattach/site/comment.py +30 -8
  40. scratchattach/site/forum.py +101 -69
  41. scratchattach/site/project.py +42 -21
  42. scratchattach/site/session.py +170 -80
  43. scratchattach/site/studio.py +4 -4
  44. scratchattach/site/user.py +179 -64
  45. scratchattach/utils/commons.py +35 -23
  46. scratchattach/utils/enums.py +44 -5
  47. scratchattach/utils/exceptions.py +10 -0
  48. scratchattach/utils/requests.py +57 -31
  49. {scratchattach-2.1.13.dist-info → scratchattach-2.1.15b0.dist-info}/METADATA +8 -3
  50. scratchattach-2.1.15b0.dist-info/RECORD +66 -0
  51. {scratchattach-2.1.13.dist-info → scratchattach-2.1.15b0.dist-info}/WHEEL +1 -1
  52. scratchattach/editor/sbuild.py +0 -2837
  53. scratchattach-2.1.13.dist-info/RECORD +0 -63
  54. {scratchattach-2.1.13.dist-info → scratchattach-2.1.15b0.dist-info}/licenses/LICENSE +0 -0
  55. {scratchattach-2.1.13.dist-info → scratchattach-2.1.15b0.dist-info}/top_level.txt +0 -0
@@ -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 ..utils import exceptions
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: sprite.Sprite = build_defaulting.SPRITE_DEFAULT):
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 next block. Will be used later during project instantiation to find the next block object
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
- return blockshape.BlockShapes.UNDEFINED
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 | None:
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
- return _block_id
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
- if self.parent is None:
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
- # Both previous and attached chains start with self
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 pallet.py
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 ..utils.enums import _EnumWrapper
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
- A class that describes the shape of a block; e.g. is it a stack, c-mouth, cap, hat reporter, boolean or menu block?
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))
@@ -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:
@@ -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
- from ..utils import exceptions
12
+ if TYPE_CHECKING:
13
+ from . import sprite, build_defaulting
12
14
 
13
- DIGITS: Final[tuple[str]] = tuple("0123456789")
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
- # The old 'naïve' method but that chances of a repeat are so miniscule
194
- # Have to check if whitespace chars break it
195
- # May later add checking within sprites so that we don't need such long ids (we can save space this way)
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
- class Singleton(object):
236
- _instance: Singleton
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