scratchattach 2.1.13__py3-none-any.whl → 2.1.14__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 +8 -4
  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 +82 -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 +37 -17
  42. scratchattach/site/session.py +169 -79
  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.14.dist-info}/METADATA +8 -3
  50. scratchattach-2.1.14.dist-info/RECORD +66 -0
  51. {scratchattach-2.1.13.dist-info → scratchattach-2.1.14.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.14.dist-info}/licenses/LICENSE +0 -0
  55. {scratchattach-2.1.13.dist-info → scratchattach-2.1.14.dist-info}/top_level.txt +0 -0
@@ -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,50 @@ 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
237
271
 
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
272
+
273
+ if TYPE_CHECKING:
274
+ Singleton = Enum
275
+ else:
276
+ class Singleton(metaclass=SingletonMeta):
277
+
278
+ def __new__(cls, val=None):
279
+ if cls is Singleton:
280
+ raise TypeError("Singleton cannot be created directly.")
281
+ if hasattr(cls, "INSTANCE"):
282
+ return getattr(cls, "INSTANCE")
283
+ if val == 0:
284
+ return super().__new__(cls)
285
+ raise TypeError("Has no instance.")
286
+
287
+ def __init__(self, *args, **kwds):
288
+ pass
289
+
290
+ def __repr__(self):
291
+ return self.__class__.__name__
292
+
293
+ def __str__(self):
294
+ return self.__class__.__name__
295
+
296
+ def __format__(self, format_spec):
297
+ return str.__format__(str(self), format_spec)
298
+
299
+ def __hash__(self):
300
+ return hash(self.__class__)
301
+
302
+ def __reduce_ex__(self, proto):
303
+ return self.__class__, ()
304
+
305
+ def __deepcopy__(self, memo):
306
+ return self
@@ -1,14 +1,21 @@
1
+ """
2
+ Enum & dataclass representing extension categories
3
+ """
4
+
1
5
  from __future__ import annotations
2
6
 
3
7
 
4
8
  from dataclasses import dataclass
5
9
 
6
10
  from . import base
7
- from ..utils import enums
11
+ from scratchattach.utils import enums
8
12
 
9
13
 
10
- @dataclass(init=True, repr=True)
14
+ @dataclass
11
15
  class Extension(base.JSONSerializable):
16
+ """
17
+ Represents an extension in the Scratch block pallete - e.g. video sensing
18
+ """
12
19
  code: str
13
20
  name: str = None
14
21
 
@@ -40,4 +47,4 @@ class Extensions(enums._EnumWrapper):
40
47
  TRANSLATE = Extension("translate", "Translate Extension")
41
48
  VIDEOSENSING = Extension("videoSensing", "Video Sensing Extension")
42
49
  WEDO2 = Extension("wedo2", "LEGO Education WeDo 2.0 Extension")
43
- COREEXAMPLE = Extension("coreExample", "CoreEx Extension")
50
+ COREEXAMPLE = Extension("coreExample", "CoreEx Extension") # hidden extension!
@@ -38,6 +38,9 @@ class Field(base.BlockSubComponent):
38
38
 
39
39
  @property
40
40
  def value_id(self):
41
+ """
42
+ Get the id of the value associated with this field (if applicable) - when value is var/list/broadcast
43
+ """
41
44
  if self.id is not None:
42
45
  return self.id
43
46
  else:
@@ -48,6 +51,9 @@ class Field(base.BlockSubComponent):
48
51
 
49
52
  @property
50
53
  def value_str(self):
54
+ """
55
+ Convert the associated value to a string - if this is a VLB, return the VLB name
56
+ """
51
57
  if not isinstance(self.value, base.NamedIDComponent):
52
58
  return self.value
53
59
  else:
@@ -55,6 +61,9 @@ class Field(base.BlockSubComponent):
55
61
 
56
62
  @property
57
63
  def name(self) -> str:
64
+ """
65
+ Fetch the name of this field using the associated block
66
+ """
58
67
  for _name, _field in self.block.fields.items():
59
68
  if _field is self:
60
69
  return _name
@@ -8,8 +8,11 @@ from . import base, commons, prim
8
8
  from dataclasses import dataclass
9
9
 
10
10
 
11
- @dataclass(init=True)
11
+ @dataclass
12
12
  class ShadowStatus:
13
+ """
14
+ Dataclass representing a possible shadow value and giving it a name
15
+ """
13
16
  idx: int
14
17
  name: str
15
18
 
@@ -7,7 +7,7 @@ from . import base, commons
7
7
  from typing import Optional
8
8
 
9
9
 
10
- @dataclass(init=True, repr=True)
10
+ @dataclass
11
11
  class PlatformMeta(base.JSONSerializable):
12
12
  name: str = None
13
13
  url: str = field(repr=True, default=None)
@@ -36,8 +36,13 @@ EDIT_META = True
36
36
  META_SET_PLATFORM = False
37
37
 
38
38
 
39
- def set_meta_platform(true_false: bool = False):
39
+ def set_meta_platform(true_false: bool = None):
40
+ """
41
+ toggle whether to set the meta platform by default (or specify a value)
42
+ """
40
43
  global META_SET_PLATFORM
44
+ if true_false is None:
45
+ true_false = bool(1 - true_false)
41
46
  META_SET_PLATFORM = true_false
42
47
 
43
48
 
@@ -69,7 +74,10 @@ class Meta(base.JSONSerializable):
69
74
 
70
75
  @property
71
76
  def vm_is_valid(self):
72
- # Thanks to TurboWarp for this pattern ↓↓↓↓, I just copied it
77
+ """
78
+ Check whether the vm value is valid using a regex
79
+ Thanks to TurboWarp for this pattern ↓↓↓↓, I just copied it
80
+ """
73
81
  return re.match("^([0-9]+\\.[0-9]+\\.[0-9]+)($|-)", self.vm) is not None
74
82
 
75
83
  def to_json(self):
@@ -1,8 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import warnings
3
4
  from typing import Optional, TYPE_CHECKING
4
5
 
6
+ from typing_extensions import deprecated
7
+
5
8
  if TYPE_CHECKING:
9
+ from .block import Block
6
10
  from . import project
7
11
 
8
12
  from . import base
@@ -26,6 +30,8 @@ class Monitor(base.ProjectSubcomponent):
26
30
  """
27
31
  Represents a variable/list monitor
28
32
  https://en.scratch-wiki.info/wiki/Scratch_File_Format#Monitors
33
+
34
+ Instantiating these yourself and attaching these to projects can lead to interesting results!
29
35
  """
30
36
  assert isinstance(reporter, base.SpriteSubComponent) or reporter is None
31
37
 
@@ -135,41 +141,43 @@ class Monitor(base.ProjectSubcomponent):
135
141
  self.reporter = new_vlb
136
142
  self.reporter_id = None
137
143
 
138
- # @staticmethod
139
- # def from_reporter(reporter: Block, _id: str = None, mode: str = "default",
140
- # opcode: str = None, sprite_name: str = None, value=0, width: int | float = 0,
141
- # height: int | float = 0,
142
- # x: int | float = 5, y: int | float = 5, visible: bool = False, slider_min: int | float = 0,
143
- # slider_max: int | float = 100, is_discrete: bool = True, params: dict = None):
144
- # if "reporter" not in reporter.stack_type:
145
- # warnings.warn(f"{reporter} is not a reporter block; the monitor will return '0'")
146
- # elif "(menu)" in reporter.stack_type:
147
- # warnings.warn(f"{reporter} is a menu block; the monitor will return '0'")
148
- # # Maybe add note that length of list doesn't work fsr?? idk
149
- # if _id is None:
150
- # _id = reporter.opcode
151
- # if opcode is None:
152
- # opcode = reporter.opcode # .replace('_', ' ')
153
-
154
- # if params is None:
155
- # params = {}
156
- # for field in reporter.fields:
157
- # if field.value_id is None:
158
- # params[field.id] = field.value
159
- # else:
160
- # params[field.id] = field.value, field.value_id
161
-
162
- # return Monitor(
163
- # _id,
164
- # mode,
165
- # opcode,
166
-
167
- # params,
168
- # sprite_name,
169
- # value,
170
-
171
- # width, height,
172
- # x, y,
173
- # visible,
174
- # slider_min, slider_max, is_discrete
175
- # )
144
+ # todo: consider reimplementing this
145
+ @deprecated("This method does not work correctly (This may be fixed in the future)")
146
+ @staticmethod
147
+ def from_reporter(reporter: "Block", _id: str = None, mode: str = "default",
148
+ opcode: str = None, sprite_name: str = None, value=0, width: int | float = 0,
149
+ height: int | float = 0,
150
+ x: int | float = 5, y: int | float = 5, visible: bool = False, slider_min: int | float = 0,
151
+ slider_max: int | float = 100, is_discrete: bool = True, params: dict = None):
152
+ if "reporter" not in reporter.stack_type:
153
+ warnings.warn(f"{reporter} is not a reporter block; the monitor will return '0'")
154
+ elif "(menu)" in reporter.stack_type:
155
+ warnings.warn(f"{reporter} is a menu block; the monitor will return '0'")
156
+ # Maybe add note that length of list doesn't work fsr?? idk
157
+ if _id is None:
158
+ _id = reporter.opcode
159
+ if opcode is None:
160
+ opcode = reporter.opcode # .replace('_', ' ')
161
+
162
+ if params is None:
163
+ params = {}
164
+ for field in reporter.fields:
165
+ if field.value_id is None:
166
+ params[field.id] = field.value
167
+ else:
168
+ params[field.id] = field.value, field.value_id
169
+
170
+ return Monitor(
171
+ _id,
172
+ mode,
173
+ opcode,
174
+
175
+ params,
176
+ sprite_name,
177
+ value,
178
+
179
+ width, height,
180
+ x, y,
181
+ visible,
182
+ slider_min, slider_max, is_discrete
183
+ )
@@ -6,13 +6,13 @@ from dataclasses import dataclass
6
6
  from typing import Optional, TYPE_CHECKING, Iterable, Any
7
7
 
8
8
  from . import base, commons
9
- from ..utils import enums
9
+ from scratchattach.utils import enums
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  from . import block
13
13
 
14
14
 
15
- @dataclass(init=True)
15
+ @dataclass
16
16
  class ArgumentType(base.Base):
17
17
  type: str
18
18
  proc_str: str
@@ -38,7 +38,7 @@ class ArgumentType(base.Base):
38
38
  return None
39
39
 
40
40
 
41
- @dataclass(init=True, repr=True)
41
+ @dataclass
42
42
  class ArgSettings(base.Base):
43
43
  ids: bool
44
44
  names: bool
@@ -61,7 +61,7 @@ class ArgSettings(base.Base):
61
61
  return int(self) > int(other)
62
62
 
63
63
 
64
- @dataclass(init=True, repr=True)
64
+ @dataclass
65
65
  class Argument(base.MutationSubComponent):
66
66
  name: str
67
67
  default: str = ''
@@ -103,6 +103,10 @@ class ArgTypes(enums._EnumWrapper):
103
103
 
104
104
 
105
105
  def parse_proc_code(_proc_code: str) -> list[str, ArgumentType] | None:
106
+ """
107
+ Parse a proccode (part of a mutation) into argument types and strings
108
+ """
109
+
106
110
  if _proc_code is None:
107
111
  return None
108
112
  token = ''
@@ -209,6 +213,9 @@ class Mutation(base.BlockSubComponent):
209
213
 
210
214
  @property
211
215
  def parsed_proc_code(self) -> list[str, ArgumentType] | None:
216
+ """
217
+ Parse the proc code into arguments & strings
218
+ """
212
219
  return parse_proc_code(self.proc_code)
213
220
 
214
221
  @staticmethod
@@ -6,43 +6,42 @@ May want to completely change this later
6
6
  """
7
7
  from __future__ import annotations
8
8
 
9
- from dataclasses import dataclass
9
+ from dataclasses import dataclass, field
10
+ from typing import Optional
10
11
 
11
12
  from . import prim
12
- from ..utils.enums import _EnumWrapper
13
+ from scratchattach.utils.enums import _EnumWrapper
13
14
 
14
15
 
15
- @dataclass(init=True, repr=True)
16
+ @dataclass
16
17
  class FieldUsage:
17
18
  name: str
18
- value_type: prim.PrimTypes = None
19
+ value_type: Optional[prim.PrimTypes] = None
19
20
 
20
21
 
21
- @dataclass(init=True, repr=True)
22
+ @dataclass
22
23
  class SpecialFieldUsage(FieldUsage):
23
24
  name: str
24
- attrs: list[str] = None
25
- if attrs is None:
26
- attrs = []
25
+ value_type: None = None # Order cannot be changed
26
+ attrs: list[str] = field(default_factory=list)
27
27
 
28
- value_type: None = None
29
28
 
30
29
 
31
- @dataclass(init=True, repr=True)
30
+ @dataclass
32
31
  class InputUsage:
33
32
  name: str
34
- value_type: prim.PrimTypes = None
35
- default_obscurer: BlockUsage = None
33
+ value_type: Optional[prim.PrimTypes] = None
34
+ default_obscurer: Optional[BlockUsage] = None
36
35
 
37
36
 
38
- @dataclass(init=True, repr=True)
37
+ @dataclass
39
38
  class BlockUsage:
40
39
  opcode: str
41
- fields: list[FieldUsage] = None
40
+ fields: Optional[list[FieldUsage]] = None
42
41
  if fields is None:
43
42
  fields = []
44
43
 
45
- inputs: list[InputUsage] = None
44
+ inputs: Optional[list[InputUsage]] = None
46
45
  if inputs is None:
47
46
  inputs = []
48
47
 
@@ -51,41 +50,41 @@ class BlockUsages(_EnumWrapper):
51
50
  # Special Enum blocks
52
51
  MATH_NUMBER = BlockUsage(
53
52
  "math_number",
54
- [SpecialFieldUsage("NUM", ["name", "value"])]
53
+ [SpecialFieldUsage("NUM", attrs=["name", "value"])]
55
54
  )
56
55
  MATH_POSITIVE_NUMBER = BlockUsage(
57
56
  "math_positive_number",
58
- [SpecialFieldUsage("NUM", ["name", "value"])]
57
+ [SpecialFieldUsage("NUM", attrs=["name", "value"])]
59
58
  )
60
59
  MATH_WHOLE_NUMBER = BlockUsage(
61
60
  "math_whole_number",
62
- [SpecialFieldUsage("NUM", ["name", "value"])]
61
+ [SpecialFieldUsage("NUM", attrs=["name", "value"])]
63
62
  )
64
63
  MATH_INTEGER = BlockUsage(
65
64
  "math_integer",
66
- [SpecialFieldUsage("NUM", ["name", "value"])]
65
+ [SpecialFieldUsage("NUM", attrs=["name", "value"])]
67
66
  )
68
67
  MATH_ANGLE = BlockUsage(
69
68
  "math_angle",
70
- [SpecialFieldUsage("NUM", ["name", "value"])]
69
+ [SpecialFieldUsage("NUM", attrs=["name", "value"])]
71
70
  )
72
71
  COLOUR_PICKER = BlockUsage(
73
72
  "colour_picker",
74
- [SpecialFieldUsage("COLOUR", ["name", "value"])]
73
+ [SpecialFieldUsage("COLOUR", attrs=["name", "value"])]
75
74
  )
76
75
  TEXT = BlockUsage(
77
76
  "text",
78
- [SpecialFieldUsage("TEXT", ["name", "value"])]
77
+ [SpecialFieldUsage("TEXT", attrs=["name", "value"])]
79
78
  )
80
79
  EVENT_BROADCAST_MENU = BlockUsage(
81
80
  "event_broadcast_menu",
82
- [SpecialFieldUsage("BROADCAST_OPTION", ["name", "id", "value", "variableType"])]
81
+ [SpecialFieldUsage("BROADCAST_OPTION", attrs=["name", "id", "value", "variableType"])]
83
82
  )
84
83
  DATA_VARIABLE = BlockUsage(
85
84
  "data_variable",
86
- [SpecialFieldUsage("VARIABLE", ["name", "id", "value", "variableType"])]
85
+ [SpecialFieldUsage("VARIABLE", attrs=["name", "id", "value", "variableType"])]
87
86
  )
88
87
  DATA_LISTCONTENTS = BlockUsage(
89
88
  "data_listcontents",
90
- [SpecialFieldUsage("LIST", ["name", "id", "value", "variableType"])]
89
+ [SpecialFieldUsage("LIST", attrs=["name", "id", "value", "variableType"])]
91
90
  )
@@ -5,10 +5,10 @@ from dataclasses import dataclass
5
5
  from typing import Optional, Callable, Final
6
6
 
7
7
  from . import base, sprite, vlb, commons, build_defaulting
8
- from ..utils import enums, exceptions
8
+ from scratchattach.utils import enums, exceptions
9
9
 
10
10
 
11
- @dataclass(init=True, repr=True)
11
+ @dataclass
12
12
  class PrimType(base.JSONSerializable):
13
13
  code: int
14
14
  name: str
@@ -8,12 +8,15 @@ from typing import Optional, Iterable, Generator, BinaryIO
8
8
  from zipfile import ZipFile
9
9
 
10
10
  from . import base, meta, extension, monitor, sprite, asset, vlb, twconfig, comment, commons
11
- from ..site import session
12
- from ..site.project import get_project
13
- from ..utils import exceptions
11
+ from scratchattach.site import session
12
+ from scratchattach.site.project import get_project
13
+ from scratchattach.utils import exceptions
14
14
 
15
15
 
16
16
  class Project(base.JSONExtractable):
17
+ """
18
+ sa.editor's equivalent of the ProjectBody. Represents the editor contents of a scratch project
19
+ """
17
20
  def __init__(self, _name: Optional[str] = None, _meta: Optional[meta.Meta] = None, _extensions: Iterable[extension.Extension] = (),
18
21
  _monitors: Iterable[monitor.Monitor] = (), _sprites: Iterable[sprite.Sprite] = (), *,
19
22
  _asset_data: Optional[list[asset.AssetFile]] = None, _session: Optional[session.Session] = None):
@@ -268,6 +271,9 @@ class Project(base.JSONExtractable):
268
271
  os.system(f"explorer.exe \"{fp}\"")
269
272
 
270
273
  def add_monitor(self, _monitor: monitor.Monitor) -> monitor.Monitor:
274
+ """
275
+ Bind a monitor to this project. Doing these manually can lead to interesting results.
276
+ """
271
277
  _monitor.project = self
272
278
  _monitor.reporter_id = self.new_id
273
279
  self.monitors.append(_monitor)
@@ -11,6 +11,8 @@ if TYPE_CHECKING:
11
11
  from . import asset
12
12
 
13
13
  class Sprite(base.ProjectSubcomponent, base.JSONExtractable):
14
+ _local_globals: list[base.NamedIDComponent]
15
+ asset_data: list[asset.AssetFile]
14
16
  def __init__(self, is_stage: bool = False, name: str = '', _current_costume: int = 1, _layer_order: Optional[int] = None,
15
17
  _volume: int = 100,
16
18
  _broadcasts: Optional[list[vlb.Broadcast]] = None,
@@ -120,6 +122,9 @@ class Sprite(base.ProjectSubcomponent, base.JSONExtractable):
120
122
  _comment.link_using_sprite()
121
123
 
122
124
  def add_local_global(self, _vlb: base.NamedIDComponent):
125
+ """
126
+ Add a global variable/list to this sprite (for when an overarching project/stage is not available)
127
+ """
123
128
  self._local_globals.append(_vlb)
124
129
  _vlb.sprite = self
125
130
 
@@ -128,11 +133,11 @@ class Sprite(base.ProjectSubcomponent, base.JSONExtractable):
128
133
  _variable.sprite = self
129
134
 
130
135
  def add_list(self, _list: vlb.List):
131
- self.variables.append(_list)
136
+ self.lists.append(_list)
132
137
  _list.sprite = self
133
138
 
134
139
  def add_broadcast(self, _broadcast: vlb.Broadcast):
135
- self.variables.append(_broadcast)
140
+ self.broadcasts.append(_broadcast)
136
141
  _broadcast.sprite = self
137
142
 
138
143
  def add_vlb(self, _vlb: base.NamedIDComponent):
@@ -151,18 +156,20 @@ class Sprite(base.ProjectSubcomponent, base.JSONExtractable):
151
156
  return _block
152
157
 
153
158
  _block.sprite = self
159
+ new_id = self.new_id
154
160
 
155
161
  if isinstance(_block, block.Block):
156
- self.blocks[self.new_id] = _block
162
+ self.blocks[new_id] = _block
163
+ _block.id = new_id
157
164
  _block.link_using_sprite()
158
165
 
159
166
  elif isinstance(_block, prim.Prim):
160
- self.prims[self.new_id] = _block
167
+ self.prims[new_id] = _block
161
168
  _block.link_using_sprite()
162
169
 
163
170
  return _block
164
171
 
165
- def add_chain(self, *chain: Iterable[block.Block | prim.Prim]) -> block.Block | prim.Prim:
172
+ def add_chain(self, *chain: block.Block | prim.Prim) -> block.Block | prim.Prim:
166
173
  """
167
174
  Adds a list of blocks to the sprite **AND RETURNS THE FIRST BLOCK**
168
175
  :param chain:
@@ -173,6 +180,8 @@ class Sprite(base.ProjectSubcomponent, base.JSONExtractable):
173
180
  _prev = self.add_block(chain[0])
174
181
 
175
182
  for _block in chain[1:]:
183
+ if not isinstance(_prev, block.Block) or not isinstance(_block, block.Block):
184
+ continue
176
185
  _prev = _prev.attach_block(_block)
177
186
 
178
187
  return chain[0]
@@ -206,7 +215,11 @@ class Sprite(base.ProjectSubcomponent, base.JSONExtractable):
206
215
  """
207
216
  :return: All vlbs associated with the sprite. No local globals are added
208
217
  """
209
- return self.variables + self.lists + self.broadcasts
218
+ vlbs: list[base.NamedIDComponent] = []
219
+ vlbs.extend(self.variables)
220
+ vlbs.extend(self.lists)
221
+ vlbs.extend(self.broadcasts)
222
+ return vlbs
210
223
 
211
224
  @property
212
225
  def assets(self) -> list[asset.Costume | asset.Sound]: