scratchattach 2.1.15b0__py3-none-any.whl → 3.0.0b1__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 (87) hide show
  1. cli/__about__.py +1 -0
  2. cli/__init__.py +26 -0
  3. cli/cmd/__init__.py +4 -0
  4. cli/cmd/group.py +127 -0
  5. cli/cmd/login.py +60 -0
  6. cli/cmd/profile.py +7 -0
  7. cli/cmd/sessions.py +5 -0
  8. cli/context.py +142 -0
  9. cli/db.py +66 -0
  10. cli/namespace.py +14 -0
  11. {scratchattach/cloud → cloud}/_base.py +112 -87
  12. {scratchattach/cloud → cloud}/cloud.py +16 -16
  13. {scratchattach/editor → editor}/__init__.py +2 -1
  14. {scratchattach/editor → editor}/asset.py +26 -14
  15. {scratchattach/editor → editor}/backpack_json.py +3 -5
  16. {scratchattach/editor → editor}/base.py +2 -4
  17. {scratchattach/editor → editor}/block.py +27 -22
  18. {scratchattach/editor → editor}/blockshape.py +1 -1
  19. {scratchattach/editor → editor}/build_defaulting.py +2 -2
  20. editor/commons.py +145 -0
  21. {scratchattach/editor → editor}/field.py +1 -1
  22. {scratchattach/editor → editor}/inputs.py +6 -3
  23. {scratchattach/editor → editor}/meta.py +10 -7
  24. {scratchattach/editor → editor}/monitor.py +10 -8
  25. {scratchattach/editor → editor}/mutation.py +68 -11
  26. {scratchattach/editor → editor}/pallete.py +1 -3
  27. {scratchattach/editor → editor}/prim.py +4 -0
  28. {scratchattach/editor → editor}/project.py +118 -16
  29. {scratchattach/editor → editor}/sprite.py +25 -15
  30. {scratchattach/editor → editor}/vlb.py +2 -2
  31. {scratchattach/eventhandlers → eventhandlers}/_base.py +1 -0
  32. {scratchattach/eventhandlers → eventhandlers}/cloud_events.py +26 -6
  33. {scratchattach/eventhandlers → eventhandlers}/cloud_recorder.py +4 -4
  34. {scratchattach/eventhandlers → eventhandlers}/cloud_requests.py +139 -54
  35. {scratchattach/eventhandlers → eventhandlers}/cloud_server.py +6 -3
  36. {scratchattach/eventhandlers → eventhandlers}/cloud_storage.py +1 -2
  37. eventhandlers/filterbot.py +163 -0
  38. other/other_apis.py +598 -0
  39. {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/METADATA +7 -11
  40. scratchattach-3.0.0b1.dist-info/RECORD +79 -0
  41. {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/WHEEL +1 -1
  42. scratchattach-3.0.0b1.dist-info/entry_points.txt +2 -0
  43. scratchattach-3.0.0b1.dist-info/top_level.txt +7 -0
  44. {scratchattach/site → site}/_base.py +32 -5
  45. site/activity.py +426 -0
  46. {scratchattach/site → site}/alert.py +4 -5
  47. {scratchattach/site → site}/backpack_asset.py +2 -1
  48. {scratchattach/site → site}/classroom.py +80 -73
  49. {scratchattach/site → site}/cloud_activity.py +43 -29
  50. {scratchattach/site → site}/comment.py +86 -100
  51. {scratchattach/site → site}/forum.py +8 -4
  52. site/placeholder.py +132 -0
  53. {scratchattach/site → site}/project.py +228 -122
  54. {scratchattach/site → site}/session.py +156 -71
  55. {scratchattach/site → site}/studio.py +139 -46
  56. site/typed_dicts.py +151 -0
  57. {scratchattach/site → site}/user.py +511 -215
  58. {scratchattach/utils → utils}/commons.py +12 -4
  59. {scratchattach/utils → utils}/encoder.py +7 -4
  60. {scratchattach/utils → utils}/enums.py +1 -0
  61. {scratchattach/utils → utils}/exceptions.py +36 -2
  62. utils/optional_async.py +154 -0
  63. utils/requests.py +306 -0
  64. scratchattach/__init__.py +0 -29
  65. scratchattach/editor/commons.py +0 -273
  66. scratchattach/eventhandlers/filterbot.py +0 -161
  67. scratchattach/other/other_apis.py +0 -284
  68. scratchattach/site/activity.py +0 -382
  69. scratchattach/utils/requests.py +0 -93
  70. scratchattach-2.1.15b0.dist-info/RECORD +0 -66
  71. scratchattach-2.1.15b0.dist-info/top_level.txt +0 -1
  72. {scratchattach/cloud → cloud}/__init__.py +0 -0
  73. {scratchattach/editor → editor}/code_translation/__init__.py +0 -0
  74. {scratchattach/editor → editor}/code_translation/parse.py +0 -0
  75. {scratchattach/editor → editor}/comment.py +0 -0
  76. {scratchattach/editor → editor}/extension.py +0 -0
  77. {scratchattach/editor → editor}/twconfig.py +0 -0
  78. {scratchattach/eventhandlers → eventhandlers}/__init__.py +0 -0
  79. {scratchattach/eventhandlers → eventhandlers}/combine.py +0 -0
  80. {scratchattach/eventhandlers → eventhandlers}/message_events.py +0 -0
  81. {scratchattach/other → other}/__init__.py +0 -0
  82. {scratchattach/other → other}/project_json_capabilities.py +0 -0
  83. {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
  84. {scratchattach/site → site}/__init__.py +0 -0
  85. {scratchattach/site → site}/browser_cookie3_stub.py +0 -0
  86. {scratchattach/site → site}/browser_cookies.py +0 -0
  87. {scratchattach/utils → utils}/__init__.py +0 -0
@@ -68,8 +68,8 @@ class Block(base.SpriteSubComponent):
68
68
 
69
69
  def add_input(self, name: str, _input: inputs.Input) -> Self:
70
70
  """
71
- Add an input to the block.
72
- """ # not sure what else to say
71
+ Attach an input object to the block.
72
+ """
73
73
  self.inputs[name] = _input
74
74
  for val in (_input.value, _input.obscurer):
75
75
  if isinstance(val, Block):
@@ -78,15 +78,15 @@ class Block(base.SpriteSubComponent):
78
78
 
79
79
  def add_field(self, name: str, _field: field.Field) -> Self:
80
80
  """
81
- Add a field to the block.
82
- """ # not sure what else to sa
81
+ Attach a field object to the block.
82
+ """
83
83
  self.fields[name] = _field
84
84
  return self
85
85
 
86
86
  def set_mutation(self, _mutation: mutation.Mutation) -> Self:
87
87
  """
88
- Attach a mutation object and call mutation.link_arguments()
89
- """ # this comment explains *what* this does, not *why*
88
+ Attach a mutation object and link it, also asking the mutation to link its own subcomponents.
89
+ """
90
90
  self.mutation = _mutation
91
91
  _mutation.block = self
92
92
  _mutation.link_arguments()
@@ -113,14 +113,15 @@ class Block(base.SpriteSubComponent):
113
113
  @property
114
114
  def target(self):
115
115
  """
116
- Alias for sprite
117
- """ # remove this?
116
+ Alias for .sprite
117
+ """
118
+ # should this be deprecated?
118
119
  return self.sprite
119
120
 
120
121
  @property
121
122
  def block_shape(self) -> blockshape.BlockShape:
122
123
  """
123
- Search for the blockshape stored in blockshape.py
124
+ Search for & return the associated blockshape stored in blockshape.py
124
125
  :return: The block's block shape (by opcode)
125
126
  """
126
127
  _shape = blockshape.BlockShapes.find(self.opcode, "opcode")
@@ -133,7 +134,7 @@ class Block(base.SpriteSubComponent):
133
134
  @property
134
135
  def can_next(self):
135
136
  """
136
- :return: Whether the block *can* have a next block (basically checks if it's not a cap block, also considering the behaviour of control_stop)
137
+ :return: Whether the block *can* have a next block (checks if it's not a cap block, but also considering the special behaviour of control_stop)
137
138
  """
138
139
  _shape = self.block_shape
139
140
  if _shape.is_cap is not blockshape.MUTATION_DEPENDENT:
@@ -141,7 +142,8 @@ class Block(base.SpriteSubComponent):
141
142
  else:
142
143
  if self.mutation is None:
143
144
  # If there's no mutation, let's just assume yes
144
- warnings.warn(f"{self} has no mutation! Assuming we can add block ;-;")
145
+ # add filterwarnings?
146
+ warnings.warn(f"{self} has no mutation! Assuming we can add blocks!")
145
147
  return True
146
148
 
147
149
  return self.mutation.has_next
@@ -150,19 +152,20 @@ class Block(base.SpriteSubComponent):
150
152
  def id(self) -> str:
151
153
  """
152
154
  Work out the id of this block by searching through the sprite dictionary
155
+ If one cannot be found, generate a new one and return that.
153
156
  """
154
157
  if self._id:
155
- return self.id
158
+ return self._id
156
159
  # warnings.warn(f"Using block IDs can cause consistency issues and is not recommended")
157
160
  # This property is used when converting comments to JSON (we don't want random warning when exporting a project)
158
161
  for _block_id, _block in self.sprite.blocks.items():
159
162
  if _block is self:
160
163
  self._id = _block_id
161
- return self.id
164
+ return self._id
162
165
 
163
166
  # Let's just automatically assign ourselves an id
164
167
  self.sprite.add_block(self)
165
- return self.id
168
+ return self._id
166
169
 
167
170
  @id.setter
168
171
  def id(self, value: str) -> None:
@@ -191,8 +194,9 @@ class Block(base.SpriteSubComponent):
191
194
  @property
192
195
  def relatives(self) -> list[Block]:
193
196
  """
194
- :return: A list of blocks which are related to this block (e.g. parent, next, inputs)
197
+ :return: A list of blocks which are related to this block (i.e. parent & next)
195
198
  """
199
+ # TODO: consider adding input blocks?
196
200
  _ret = []
197
201
 
198
202
  def yield_block(_block: Block | None):
@@ -209,6 +213,7 @@ class Block(base.SpriteSubComponent):
209
213
  """
210
214
  :return: A list of blocks that are inside of this block, **NOT INCLUDING THE ATTACHED BLOCK**
211
215
  """
216
+ # does this include procedure definitions' inner block?
212
217
  _children = []
213
218
  for _input in self.inputs.values():
214
219
  if isinstance(_input.value, Block) or isinstance(_input.value, prim.Prim):
@@ -223,7 +228,8 @@ class Block(base.SpriteSubComponent):
223
228
  """
224
229
  Recursive getter method to get all previous blocks in the blockchain (until hitting a top-level block)
225
230
  """
226
- if self.parent is None: # todo: use is_top_level?
231
+ # TODO: check if this hits the recursion limit
232
+ if self.parent is None: # TODO: use is_top_level?
227
233
  return [self]
228
234
 
229
235
  return [self] + self.parent.previous_chain
@@ -233,6 +239,7 @@ class Block(base.SpriteSubComponent):
233
239
  """
234
240
  Recursive getter method to get all next blocks in the blockchain (until hitting a bottom-levell block)
235
241
  """
242
+ # TODO: check if this hits the recursion limit
236
243
  if self.next is None:
237
244
  return [self]
238
245
 
@@ -248,8 +255,7 @@ class Block(base.SpriteSubComponent):
248
255
  @property
249
256
  def top_level_block(self):
250
257
  """
251
- Get the first block in the block stack that this block is part of
252
- same as the old stack_parent property from sbedtior v1
258
+ Get the first (top level) block in the block stack that this block is part of
253
259
  """
254
260
  return self.previous_chain[-1]
255
261
 
@@ -266,7 +272,7 @@ class Block(base.SpriteSubComponent):
266
272
  Useful for showing a block stack in the console, using pprint
267
273
  :return: A tree-like nested list structure representing the stack of blocks, including inputs, starting at this block
268
274
  """
269
- _tree = [self]
275
+ _tree: list[Block | prim.Prim | list[Block]] = [self]
270
276
  for child in self.children:
271
277
  if isinstance(child, prim.Prim):
272
278
  _tree.append(child)
@@ -281,7 +287,7 @@ class Block(base.SpriteSubComponent):
281
287
  @property
282
288
  def category(self):
283
289
  """
284
- Works out what category of block this is using the opcode. Does not perform validation
290
+ Works out what category of block this is as a string, using the opcode. Does not perform validation
285
291
  """
286
292
  return self.opcode.split('_')[0]
287
293
 
@@ -526,7 +532,7 @@ class Block(base.SpriteSubComponent):
526
532
  def duplicate_single_block(self) -> Block:
527
533
  return self.attach_block(self.dcopy())
528
534
 
529
- def attach_chain(self, *chain: Iterable[Block]) -> Block:
535
+ def attach_chain(self, *chain: Block) -> Block:
530
536
  attaching_block = self
531
537
  for _block in chain:
532
538
  attaching_block = attaching_block.attach_block(_block)
@@ -576,4 +582,3 @@ class Block(base.SpriteSubComponent):
576
582
  """
577
583
  for _block in self.attached_chain:
578
584
  _block.delete_single_block()
579
-
@@ -20,7 +20,7 @@ class _MutationDependent(commons.Singleton):
20
20
  raise TypeError("Need mutation data to work out attribute value.")
21
21
 
22
22
 
23
- MUTATION_DEPENDENT: Final[Literal[_MutationDependent]] = _MutationDependent.INSTANCE
23
+ MUTATION_DEPENDENT: Final[Literal[_MutationDependent.INSTANCE]] = _MutationDependent.INSTANCE
24
24
  """Value used when mutation data is required to work out the attribute value"""
25
25
 
26
26
 
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
9
9
  from . import sprite, block, prim, comment
10
10
  from . import commons
11
11
 
12
-
12
+ # TODO: should deque be used here?
13
13
  class _SetSprite(commons.Singleton):
14
14
  INSTANCE = 0
15
15
  def __repr__(self):
@@ -43,7 +43,7 @@ def add_block(_block: block.Block | prim.Prim) -> block.Block | prim.Prim:
43
43
  return current_sprite().add_block(_block)
44
44
 
45
45
 
46
- def add_chain(*chain: Iterable[block.Block, prim.Prim]) -> block.Block | prim.Prim:
46
+ def add_chain(*chain: Iterable[block.Block | prim.Prim]) -> block.Block | prim.Prim:
47
47
  return current_sprite().add_chain(*chain)
48
48
 
49
49
 
editor/commons.py ADDED
@@ -0,0 +1,145 @@
1
+ """
2
+ Shared functions used by the editor module
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import random
8
+ import string
9
+ from typing_extensions import Optional, Final, Any, TYPE_CHECKING, Union, Self, TypeVar
10
+ from enum import EnumMeta, Enum
11
+
12
+ if TYPE_CHECKING:
13
+ from . import sprite, build_defaulting
14
+
15
+ SpriteInput = Union[sprite.Sprite, build_defaulting._SetSprite]
16
+ else:
17
+ SpriteInput = Any
18
+
19
+ T = TypeVar('T')
20
+ from scratchattach.utils import exceptions
21
+
22
+ DIGITS: Final[tuple[str, ...]] = tuple("0123456789")
23
+
24
+ # Strangely enough, it seems like something in string.punctuation causes issues. Not sure why
25
+ ID_CHARS: Final[str] = string.ascii_letters + string.digits # + string.punctuation
26
+
27
+
28
+ def is_valid_json(_str: Any) -> bool:
29
+ """
30
+ Try to load a json string, if it fails, return False, else return true.
31
+ """
32
+ try:
33
+ json.loads(_str)
34
+ return True
35
+ except (ValueError, TypeError):
36
+ return False
37
+
38
+
39
+ def noneless_update(obj: dict, update: dict) -> None:
40
+ """
41
+ equivalent to dict.update, except and values of None are not assigned
42
+ """
43
+ for key, value in update.items():
44
+ if value is not None:
45
+ obj[key] = value
46
+
47
+
48
+ def remove_nones(obj: dict) -> None:
49
+ """
50
+ Removes all None values from a dict.
51
+ :param obj: Dictionary to remove all None values.
52
+ """
53
+ nones = []
54
+ for key, value in obj.items():
55
+ if value is None:
56
+ nones.append(key)
57
+ for key in nones:
58
+ del obj[key]
59
+
60
+
61
+ def safe_get(lst: list | tuple, _i: int, default: Optional[Any] = None) -> Any:
62
+ """
63
+ Like dict.get() but for lists
64
+ """
65
+ if len(lst) <= _i:
66
+ return default
67
+ else:
68
+ return lst[_i]
69
+
70
+
71
+ def trim_final_nones(lst: list[T]) -> list[T]:
72
+ """
73
+ Removes the last None values from a list until a non-None value is hit.
74
+ :param lst: list which will **not** be modified.
75
+ """
76
+ i = len(lst)
77
+ for item in lst[::-1]:
78
+ if item is not None:
79
+ break
80
+ i -= 1
81
+ return lst[:i]
82
+
83
+
84
+ def dumps_ifnn(obj: Any) -> Optional[str]:
85
+ """
86
+ Return json.dumps(obj) if the object is not None
87
+ """
88
+ if obj is None:
89
+ return None
90
+ else:
91
+ return json.dumps(obj)
92
+
93
+
94
+ def gen_id() -> str:
95
+ """
96
+ Generate an id for scratch blocks/variables/lists/broadcasts
97
+
98
+ The old 'naïve' method but that chances of a repeat are so miniscule
99
+ Have to check if whitespace chars break it
100
+ May later add checking within sprites so that we don't need such long ids (we can save space this way)
101
+ If this is implemented, we would need to be careful when merging sprites/adding blocks etc
102
+ """
103
+ return ''.join(random.choices(ID_CHARS, k=20))
104
+
105
+
106
+ def sanitize_fn(filename: str):
107
+ """
108
+ Removes illegal chars from a filename
109
+ :return: Sanitized filename
110
+ """
111
+ # Maybe could import a slugify module, but it's a bit overkill
112
+ ret = ''
113
+ for char in filename:
114
+ if char in string.ascii_letters + string.digits + "-_":
115
+ ret += char
116
+ else:
117
+ ret += '_'
118
+ return ret
119
+
120
+
121
+ def get_folder_name(name: str) -> str | None:
122
+ """
123
+ Get the name of the folder if this is a turbowarp-style costume name
124
+ """
125
+ if name.startswith('//'):
126
+ return None
127
+
128
+ if '//' in name:
129
+ return name.split('//')[0]
130
+ else:
131
+ return None
132
+
133
+
134
+ def get_name_nofldr(name: str) -> str:
135
+ """
136
+ Get the sprite/asset name without the folder name
137
+ """
138
+ fldr = get_folder_name(name)
139
+ if fldr is None:
140
+ return name
141
+ else:
142
+ return name[len(fldr) + 2:]
143
+
144
+ Singleton = Enum
145
+
@@ -93,7 +93,7 @@ class Field(base.BlockSubComponent):
93
93
  _value, _id = data
94
94
  return Field(_value, _id)
95
95
 
96
- def to_json(self) -> dict:
96
+ def to_json(self):
97
97
  return commons.trim_final_nones([
98
98
  self.value_str, self.value_id
99
99
  ])
@@ -11,7 +11,7 @@ from dataclasses import dataclass
11
11
  @dataclass
12
12
  class ShadowStatus:
13
13
  """
14
- Dataclass representing a possible shadow value and giving it a name
14
+ Dataclass representing a possible shadow value for a block and giving it a name
15
15
  """
16
16
  idx: int
17
17
  name: str
@@ -33,8 +33,7 @@ class ShadowStatuses:
33
33
  if status.idx == idx:
34
34
  return status
35
35
 
36
- if not 1 <= idx <= 3:
37
- raise ValueError(f"Invalid ShadowStatus idx={idx}")
36
+ raise ValueError(f"Invalid ShadowStatus idx={idx}")
38
37
 
39
38
 
40
39
  class Input(base.BlockSubComponent):
@@ -111,6 +110,10 @@ class Input(base.BlockSubComponent):
111
110
  return data
112
111
 
113
112
  def link_using_block(self):
113
+ """
114
+ Link the Input object to any menu blocks, obscurer blocks, sprites, and links any of its subcomponents
115
+ """
116
+
114
117
  # Link to value
115
118
  if self._id is not None:
116
119
  new_value = self.sprite.find_block(self._id, "id")
@@ -9,13 +9,16 @@ from typing import Optional
9
9
 
10
10
  @dataclass
11
11
  class PlatformMeta(base.JSONSerializable):
12
+ """
13
+ Represents a TurboWarp platform meta object
14
+ """
12
15
  name: str = None
13
16
  url: str = field(repr=True, default=None)
14
17
 
15
18
  def __bool__(self):
16
19
  return self.name is not None or self.url is not None
17
20
 
18
- def to_json(self) -> dict:
21
+ def to_json(self):
19
22
  _json = {"name": self.name, "url": self.url}
20
23
  commons.remove_nones(_json)
21
24
  return _json
@@ -32,7 +35,7 @@ DEFAULT_VM = "0.1.0"
32
35
  DEFAULT_AGENT = "scratchattach.editor by https://scratch.mit.edu/users/timmccool/"
33
36
  DEFAULT_PLATFORM = PlatformMeta("scratchattach", "https://github.com/timMcCool/scratchattach/")
34
37
 
35
- EDIT_META = True
38
+ EDIT_META = False
36
39
  META_SET_PLATFORM = False
37
40
 
38
41
 
@@ -42,7 +45,7 @@ def set_meta_platform(true_false: bool = None):
42
45
  """
43
46
  global META_SET_PLATFORM
44
47
  if true_false is None:
45
- true_false = bool(1 - true_false)
48
+ true_false = bool(1 - META_SET_PLATFORM)
46
49
  META_SET_PLATFORM = true_false
47
50
 
48
51
 
@@ -76,12 +79,12 @@ class Meta(base.JSONSerializable):
76
79
  def vm_is_valid(self):
77
80
  """
78
81
  Check whether the vm value is valid using a regex
79
- Thanks to TurboWarp for this pattern ↓↓↓↓, I just copied it
82
+ regex pattern from TurboWarp ↓↓↓↓
80
83
  """
81
84
  return re.match("^([0-9]+\\.[0-9]+\\.[0-9]+)($|-)", self.vm) is not None
82
85
 
83
86
  def to_json(self):
84
- _json = {
87
+ _json: dict[str, str | dict[str, str]] = {
85
88
  "semver": self.semver,
86
89
  "vm": self.vm,
87
90
  "agent": self.agent
@@ -92,9 +95,9 @@ class Meta(base.JSONSerializable):
92
95
  return _json
93
96
 
94
97
  @staticmethod
95
- def from_json(data):
98
+ def from_json(data: dict[str, str | dict[str, str]] | None):
96
99
  if data is None:
97
- data = ""
100
+ data = {"semver": "3.0.0"}
98
101
 
99
102
  semver = data["semver"]
100
103
  vm = data.get("vm")
@@ -1,15 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
- import warnings
4
3
  from typing import Optional, TYPE_CHECKING
5
4
 
6
5
  from typing_extensions import deprecated
6
+ import warnings
7
7
 
8
8
  if TYPE_CHECKING:
9
- from .block import Block
10
9
  from . import project
11
10
 
12
- from . import base
11
+ from . import base, block
13
12
 
14
13
 
15
14
  class Monitor(base.ProjectSubcomponent):
@@ -35,6 +34,9 @@ class Monitor(base.ProjectSubcomponent):
35
34
  """
36
35
  assert isinstance(reporter, base.SpriteSubComponent) or reporter is None
37
36
 
37
+ if sprite_name is None and hasattr(reporter, 'sprite'):
38
+ sprite_name = None if reporter.sprite.is_stage else reporter.sprite.name
39
+
38
40
  self.reporter_id = reporter_id
39
41
  """
40
42
  ID referencing the VLB being referenced. Replaced with None during project instantiation, where the reporter attribute is updated
@@ -142,16 +144,16 @@ class Monitor(base.ProjectSubcomponent):
142
144
  self.reporter_id = None
143
145
 
144
146
  # todo: consider reimplementing this
145
- @deprecated("This method does not work correctly (This may be fixed in the future)")
146
147
  @staticmethod
147
- def from_reporter(reporter: "Block", _id: str = None, mode: str = "default",
148
+ @deprecated("This method does not work correctly (This may be fixed in the future)")
149
+ def from_reporter(reporter: block.Block, _id: str = None, mode: str = "default",
148
150
  opcode: str = None, sprite_name: str = None, value=0, width: int | float = 0,
149
151
  height: int | float = 0,
150
152
  x: int | float = 5, y: int | float = 5, visible: bool = False, slider_min: int | float = 0,
151
153
  slider_max: int | float = 100, is_discrete: bool = True, params: dict = None):
152
- if "reporter" not in reporter.stack_type:
154
+ if not reporter.block_shape.is_reporter:
153
155
  warnings.warn(f"{reporter} is not a reporter block; the monitor will return '0'")
154
- elif "(menu)" in reporter.stack_type:
156
+ elif reporter.block_shape.is_menu:
155
157
  warnings.warn(f"{reporter} is a menu block; the monitor will return '0'")
156
158
  # Maybe add note that length of list doesn't work fsr?? idk
157
159
  if _id is None:
@@ -168,7 +170,7 @@ class Monitor(base.ProjectSubcomponent):
168
170
  params[field.id] = field.value, field.value_id
169
171
 
170
172
  return Monitor(
171
- _id,
173
+ reporter,
172
174
  mode,
173
175
  opcode,
174
176
 
@@ -1,3 +1,7 @@
1
+ """
2
+ Contains the mutation class and handlers for Arguments/ArgumentTypes/ArgSettings, and utilities for handling proc codes
3
+ """
4
+
1
5
  from __future__ import annotations
2
6
 
3
7
  import json
@@ -18,6 +22,7 @@ class ArgumentType(base.Base):
18
22
  proc_str: str
19
23
 
20
24
  def __eq__(self, other):
25
+ # noinspection PyProtectedMember
21
26
  if isinstance(other, enums._EnumWrapper):
22
27
  other = other.value
23
28
 
@@ -40,6 +45,9 @@ class ArgumentType(base.Base):
40
45
 
41
46
  @dataclass
42
47
  class ArgSettings(base.Base):
48
+ """
49
+ Contains whether the ids, names, and defaults of arguments in a mutation are None or not - i.e. the configuration of the arguments
50
+ """
43
51
  ids: bool
44
52
  names: bool
45
53
  defaults: bool
@@ -65,25 +73,37 @@ class ArgSettings(base.Base):
65
73
  class Argument(base.MutationSubComponent):
66
74
  name: str
67
75
  default: str = ''
76
+ _type: Optional[ArgumentType] = None
68
77
 
69
78
  _id: str = None
70
79
  """
71
80
  Argument ID: Will be used to replace other parameters during block instantiation.
72
81
  """
73
82
 
83
+ def __post_init__(self):
84
+ super().__init__()
85
+
74
86
  @property
75
87
  def index(self):
76
88
  return self.mutation.arguments.index(self)
77
89
 
78
90
  @property
79
- def type(self) -> None | ArgumentType:
80
- i = 0
81
- goal = self.index
82
- for token in parse_proc_code(self.mutation.proc_code):
83
- if isinstance(token, ArgumentType):
84
- if i == goal:
85
- return token
86
- i += 1
91
+ def type(self) -> Optional[ArgumentType]:
92
+ if not self._type:
93
+ if not self.mutation:
94
+ raise ValueError(f"Cannot infer 'type' of {self} when there is no mutation attached. "
95
+ f"Consider providing a type manually.")
96
+
97
+ i = 0
98
+ goal = self.index
99
+ for token in parse_proc_code(self.mutation.proc_code):
100
+ if isinstance(token, ArgumentType):
101
+ if i == goal:
102
+ self._type = token
103
+ break
104
+ i += 1
105
+
106
+ return self._type
87
107
 
88
108
  @staticmethod
89
109
  def from_json(data: dict | list | Any):
@@ -97,12 +117,13 @@ class Argument(base.MutationSubComponent):
97
117
  self._id = self.block.new_id
98
118
 
99
119
 
120
+ # noinspection PyProtectedMember
100
121
  class ArgTypes(enums._EnumWrapper):
101
122
  BOOLEAN = ArgumentType("boolean", "%b")
102
123
  NUMBER_OR_TEXT = ArgumentType("number or text", "%s")
103
124
 
104
125
 
105
- def parse_proc_code(_proc_code: str) -> list[str, ArgumentType] | None:
126
+ def parse_proc_code(_proc_code: str) -> Optional[list[str | ArgumentType]]:
106
127
  """
107
128
  Parse a proccode (part of a mutation) into argument types and strings
108
129
  """
@@ -120,6 +141,10 @@ def parse_proc_code(_proc_code: str) -> list[str, ArgumentType] | None:
120
141
  token = token[:-1]
121
142
  # Clip the % sign off the token
122
143
 
144
+ if token.endswith(' '):
145
+ # A space is required before params, but this should not be part of the parsed output
146
+ token = token[:-1]
147
+
123
148
  if token != '':
124
149
  # Make sure not to append an empty token
125
150
  tokens.append(token)
@@ -142,6 +167,37 @@ def parse_proc_code(_proc_code: str) -> list[str, ArgumentType] | None:
142
167
 
143
168
  return tokens
144
169
 
170
+ def construct_proccode(*components: ArgumentType | ArgTypes | Argument | str) -> str:
171
+ """
172
+ Create a proccode from strings/ArgumentType enum members/Argument instances
173
+
174
+ :param components: list of strings/Arguments/ArgumentType instances
175
+ :return: A proccode, e.g. 'move %s steps' or 'say %s for %n seconds'
176
+ """
177
+
178
+ result = ""
179
+
180
+ for comp in components:
181
+ if isinstance(comp, ArgumentType):
182
+ result += comp.proc_str
183
+
184
+ elif isinstance(comp, ArgTypes):
185
+ new: ArgumentType = comp.value
186
+ result += new.proc_str
187
+
188
+ elif isinstance(comp, Argument):
189
+ result += comp.type.proc_str
190
+
191
+ elif isinstance(comp, str):
192
+ result += comp
193
+
194
+ else:
195
+ raise TypeError(f"Unsupported component type: {type(comp)}")
196
+
197
+ result += ' '
198
+
199
+ return result
200
+
145
201
 
146
202
  class Mutation(base.BlockSubComponent):
147
203
  def __init__(self, _tag_name: str = "mutation", _children: Optional[list] = None, _proc_code: Optional[str] = None,
@@ -212,7 +268,7 @@ class Mutation(base.BlockSubComponent):
212
268
  bool(commons.safe_get(self.argument_defaults, 0)))
213
269
 
214
270
  @property
215
- def parsed_proc_code(self) -> list[str, ArgumentType] | None:
271
+ def parsed_proc_code(self) -> list[str | ArgumentType] | None:
216
272
  """
217
273
  Parse the proc code into arguments & strings
218
274
  """
@@ -272,7 +328,7 @@ class Mutation(base.BlockSubComponent):
272
328
  _arg_name = get(_argument_names, i)
273
329
  _arg_default = get(_argument_defaults, i)
274
330
 
275
- _arguments.append(Argument(_arg_name, _arg_default, _arg_id))
331
+ _arguments.append(Argument(_arg_name, _arg_default, _id=_arg_id))
276
332
 
277
333
  return Mutation(_tag_name, _children, _proc_code, _is_warp, _arguments, _has_next, _argument_settings)
278
334
 
@@ -322,3 +378,4 @@ class Mutation(base.BlockSubComponent):
322
378
  for _argument in self.arguments:
323
379
  _argument.mutation = self
324
380
  _argument.link_using_mutation()
381
+
@@ -1,8 +1,6 @@
1
1
  """
2
2
  Collection of block information, stating input/field names and opcodes
3
- New version of sbuild.py
4
-
5
- May want to completely change this later
3
+ Not ready for use
6
4
  """
7
5
  from __future__ import annotations
8
6
 
@@ -1,3 +1,7 @@
1
+ """
2
+ The PrimType(s) and Prim classes
3
+ """
4
+
1
5
  from __future__ import annotations
2
6
 
3
7
  import warnings