scratchattach 2.1.14__py3-none-any.whl → 3.0.0b0__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/__init__.py +14 -6
- scratchattach/__main__.py +93 -0
- {scratchattach-2.1.14.dist-info → scratchattach-3.0.0b0.dist-info}/METADATA +7 -11
- scratchattach-3.0.0b0.dist-info/RECORD +8 -0
- {scratchattach-2.1.14.dist-info → scratchattach-3.0.0b0.dist-info}/WHEEL +1 -1
- scratchattach-3.0.0b0.dist-info/entry_points.txt +2 -0
- scratchattach/cloud/__init__.py +0 -2
- scratchattach/cloud/_base.py +0 -458
- scratchattach/cloud/cloud.py +0 -183
- scratchattach/editor/__init__.py +0 -21
- scratchattach/editor/asset.py +0 -253
- scratchattach/editor/backpack_json.py +0 -117
- scratchattach/editor/base.py +0 -193
- scratchattach/editor/block.py +0 -579
- scratchattach/editor/blockshape.py +0 -357
- scratchattach/editor/build_defaulting.py +0 -51
- scratchattach/editor/code_translation/__init__.py +0 -0
- scratchattach/editor/code_translation/parse.py +0 -177
- scratchattach/editor/comment.py +0 -80
- scratchattach/editor/commons.py +0 -306
- scratchattach/editor/extension.py +0 -50
- scratchattach/editor/field.py +0 -99
- scratchattach/editor/inputs.py +0 -135
- scratchattach/editor/meta.py +0 -114
- scratchattach/editor/monitor.py +0 -183
- scratchattach/editor/mutation.py +0 -324
- scratchattach/editor/pallete.py +0 -90
- scratchattach/editor/prim.py +0 -170
- scratchattach/editor/project.py +0 -279
- scratchattach/editor/sprite.py +0 -599
- scratchattach/editor/twconfig.py +0 -114
- scratchattach/editor/vlb.py +0 -134
- scratchattach/eventhandlers/__init__.py +0 -0
- scratchattach/eventhandlers/_base.py +0 -100
- scratchattach/eventhandlers/cloud_events.py +0 -110
- scratchattach/eventhandlers/cloud_recorder.py +0 -26
- scratchattach/eventhandlers/cloud_requests.py +0 -459
- scratchattach/eventhandlers/cloud_server.py +0 -246
- scratchattach/eventhandlers/cloud_storage.py +0 -136
- scratchattach/eventhandlers/combine.py +0 -30
- scratchattach/eventhandlers/filterbot.py +0 -161
- scratchattach/eventhandlers/message_events.py +0 -42
- scratchattach/other/__init__.py +0 -0
- scratchattach/other/other_apis.py +0 -284
- scratchattach/other/project_json_capabilities.py +0 -475
- scratchattach/site/__init__.py +0 -0
- scratchattach/site/_base.py +0 -66
- scratchattach/site/activity.py +0 -382
- scratchattach/site/alert.py +0 -227
- scratchattach/site/backpack_asset.py +0 -118
- scratchattach/site/browser_cookie3_stub.py +0 -17
- scratchattach/site/browser_cookies.py +0 -61
- scratchattach/site/classroom.py +0 -447
- scratchattach/site/cloud_activity.py +0 -107
- scratchattach/site/comment.py +0 -242
- scratchattach/site/forum.py +0 -432
- scratchattach/site/project.py +0 -825
- scratchattach/site/session.py +0 -1238
- scratchattach/site/studio.py +0 -611
- scratchattach/site/user.py +0 -956
- scratchattach/utils/__init__.py +0 -0
- scratchattach/utils/commons.py +0 -255
- scratchattach/utils/encoder.py +0 -158
- scratchattach/utils/enums.py +0 -236
- scratchattach/utils/exceptions.py +0 -243
- scratchattach/utils/requests.py +0 -93
- scratchattach-2.1.14.dist-info/RECORD +0 -66
- {scratchattach-2.1.14.dist-info → scratchattach-3.0.0b0.dist-info}/licenses/LICENSE +0 -0
- {scratchattach-2.1.14.dist-info → scratchattach-3.0.0b0.dist-info}/top_level.txt +0 -0
scratchattach/editor/commons.py
DELETED
|
@@ -1,306 +0,0 @@
|
|
|
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 import Optional, Final, Any, TYPE_CHECKING, Union
|
|
10
|
-
from enum import Enum, EnumMeta
|
|
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
|
-
from scratchattach.utils import exceptions
|
|
20
|
-
|
|
21
|
-
DIGITS: Final[tuple[str, ...]] = tuple("0123456789")
|
|
22
|
-
|
|
23
|
-
ID_CHARS: Final[str] = string.ascii_letters + string.digits # + string.punctuation
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# Strangely enough, it seems like something in string.punctuation causes issues. Not sure why
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def _read_json_number(_str: str) -> float | int:
|
|
30
|
-
ret = ''
|
|
31
|
-
|
|
32
|
-
minus = _str[0] == '-'
|
|
33
|
-
if minus:
|
|
34
|
-
ret += '-'
|
|
35
|
-
_str = _str[1:]
|
|
36
|
-
|
|
37
|
-
def read_fraction(sub: str):
|
|
38
|
-
sub_ret = ''
|
|
39
|
-
if sub[0] == '.':
|
|
40
|
-
sub_ret += '.'
|
|
41
|
-
sub = sub[1:]
|
|
42
|
-
while sub[0] in DIGITS:
|
|
43
|
-
sub_ret += sub[0]
|
|
44
|
-
sub = sub[1:]
|
|
45
|
-
|
|
46
|
-
return sub_ret, sub
|
|
47
|
-
|
|
48
|
-
def read_exponent(sub: str):
|
|
49
|
-
sub_ret = ''
|
|
50
|
-
if sub[0].lower() == 'e':
|
|
51
|
-
sub_ret += sub[0]
|
|
52
|
-
sub = sub[1:]
|
|
53
|
-
|
|
54
|
-
if sub[0] in "-+":
|
|
55
|
-
sub_ret += sub[0]
|
|
56
|
-
sub = sub[1:]
|
|
57
|
-
|
|
58
|
-
if sub[0] not in DIGITS:
|
|
59
|
-
raise exceptions.UnclosedJSONError(f"Invalid exponent {sub}")
|
|
60
|
-
|
|
61
|
-
while sub[0] in DIGITS:
|
|
62
|
-
sub_ret += sub[0]
|
|
63
|
-
sub = sub[1:]
|
|
64
|
-
|
|
65
|
-
return sub_ret
|
|
66
|
-
|
|
67
|
-
if _str[0] == '0':
|
|
68
|
-
ret += '0'
|
|
69
|
-
_str = _str[1:]
|
|
70
|
-
|
|
71
|
-
elif _str[0] in DIGITS[1:9]:
|
|
72
|
-
while _str[0] in DIGITS:
|
|
73
|
-
ret += _str[0]
|
|
74
|
-
_str = _str[1:]
|
|
75
|
-
|
|
76
|
-
frac, _str = read_fraction(_str)
|
|
77
|
-
ret += frac
|
|
78
|
-
|
|
79
|
-
ret += read_exponent(_str)
|
|
80
|
-
|
|
81
|
-
return json.loads(ret)
|
|
82
|
-
|
|
83
|
-
# todo: consider if this should be moved to util.commons instead of editor.commons
|
|
84
|
-
# note: this is currently unused code
|
|
85
|
-
def consume_json(_str: str, i: int = 0) -> str | float | int | dict | list | bool | None:
|
|
86
|
-
"""
|
|
87
|
-
*'gobble up some JSON until we hit something not quite so tasty'*
|
|
88
|
-
|
|
89
|
-
Reads a JSON string and stops at the natural end (i.e. when brackets close, or when quotes end, etc.)
|
|
90
|
-
"""
|
|
91
|
-
# Named by ChatGPT
|
|
92
|
-
section = ''.join(_str[i:])
|
|
93
|
-
if section.startswith("true"):
|
|
94
|
-
return True
|
|
95
|
-
elif section.startswith("false"):
|
|
96
|
-
return False
|
|
97
|
-
elif section.startswith("null"):
|
|
98
|
-
return None
|
|
99
|
-
elif section[0] in "0123456789.-":
|
|
100
|
-
return _read_json_number(section)
|
|
101
|
-
|
|
102
|
-
depth = 0
|
|
103
|
-
json_text = ''
|
|
104
|
-
out_string = True
|
|
105
|
-
|
|
106
|
-
for char in section:
|
|
107
|
-
json_text += char
|
|
108
|
-
|
|
109
|
-
if char == '"':
|
|
110
|
-
if len(json_text) > 1:
|
|
111
|
-
unescaped = json_text[-2] != '\\'
|
|
112
|
-
else:
|
|
113
|
-
unescaped = True
|
|
114
|
-
if unescaped:
|
|
115
|
-
out_string ^= True
|
|
116
|
-
if out_string:
|
|
117
|
-
depth -= 1
|
|
118
|
-
else:
|
|
119
|
-
depth += 1
|
|
120
|
-
|
|
121
|
-
if out_string:
|
|
122
|
-
if char in "[{":
|
|
123
|
-
depth += 1
|
|
124
|
-
elif char in "}]":
|
|
125
|
-
depth -= 1
|
|
126
|
-
|
|
127
|
-
if depth == 0 and json_text.strip():
|
|
128
|
-
return json.loads(json_text.strip())
|
|
129
|
-
|
|
130
|
-
raise exceptions.UnclosedJSONError(f"Unclosed JSON string, read {json_text}")
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def is_partial_json(_str: str, i: int = 0) -> bool:
|
|
134
|
-
try:
|
|
135
|
-
consume_json(_str, i)
|
|
136
|
-
return True
|
|
137
|
-
|
|
138
|
-
except exceptions.UnclosedJSONError:
|
|
139
|
-
return False
|
|
140
|
-
|
|
141
|
-
except ValueError:
|
|
142
|
-
return False
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
"""
|
|
149
|
-
try:
|
|
150
|
-
json.loads(_str)
|
|
151
|
-
return True
|
|
152
|
-
except (ValueError, TypeError):
|
|
153
|
-
return False
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def noneless_update(obj: dict, update: dict) -> None:
|
|
157
|
-
"""
|
|
158
|
-
equivalent to dict.update, except and values of None are not assigned
|
|
159
|
-
"""
|
|
160
|
-
for key, value in update.items():
|
|
161
|
-
if value is not None:
|
|
162
|
-
obj[key] = value
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def remove_nones(obj: dict) -> None:
|
|
166
|
-
"""
|
|
167
|
-
Removes all None values from a dict.
|
|
168
|
-
:param obj: Dictionary to remove all None values.
|
|
169
|
-
"""
|
|
170
|
-
nones = []
|
|
171
|
-
for key, value in obj.items():
|
|
172
|
-
if value is None:
|
|
173
|
-
nones.append(key)
|
|
174
|
-
for key in nones:
|
|
175
|
-
del obj[key]
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def safe_get(lst: list | tuple, _i: int, default: Optional[Any] = None) -> Any:
|
|
179
|
-
"""
|
|
180
|
-
Like dict.get() but for lists
|
|
181
|
-
"""
|
|
182
|
-
if len(lst) <= _i:
|
|
183
|
-
return default
|
|
184
|
-
else:
|
|
185
|
-
return lst[_i]
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def trim_final_nones(lst: list) -> list:
|
|
189
|
-
"""
|
|
190
|
-
Removes the last None values from a list until a non-None value is hit.
|
|
191
|
-
:param lst: list which will **not** be modified.
|
|
192
|
-
"""
|
|
193
|
-
i = len(lst)
|
|
194
|
-
for item in lst[::-1]:
|
|
195
|
-
if item is not None:
|
|
196
|
-
break
|
|
197
|
-
i -= 1
|
|
198
|
-
return lst[:i]
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def dumps_ifnn(obj: Any) -> Optional[str]:
|
|
202
|
-
"""
|
|
203
|
-
Return json.dumps(obj) if the object is not None
|
|
204
|
-
"""
|
|
205
|
-
if obj is None:
|
|
206
|
-
return None
|
|
207
|
-
else:
|
|
208
|
-
return json.dumps(obj)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def gen_id() -> str:
|
|
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
|
-
"""
|
|
219
|
-
return ''.join(random.choices(ID_CHARS, k=20))
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def sanitize_fn(filename: str):
|
|
223
|
-
"""
|
|
224
|
-
Removes illegal chars from a filename
|
|
225
|
-
:return: Sanitized filename
|
|
226
|
-
"""
|
|
227
|
-
# Maybe could import a slugify module, but it's a bit overkill
|
|
228
|
-
ret = ''
|
|
229
|
-
for char in filename:
|
|
230
|
-
if char in string.ascii_letters + string.digits + "-_":
|
|
231
|
-
ret += char
|
|
232
|
-
else:
|
|
233
|
-
ret += '_'
|
|
234
|
-
return ret
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
"""
|
|
241
|
-
if name.startswith('//'):
|
|
242
|
-
return None
|
|
243
|
-
|
|
244
|
-
if '//' in name:
|
|
245
|
-
return name.split('//')[0]
|
|
246
|
-
else:
|
|
247
|
-
return None
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
def get_name_nofldr(name: str) -> str:
|
|
251
|
-
"""
|
|
252
|
-
Get the sprite/asset name without the folder name
|
|
253
|
-
"""
|
|
254
|
-
fldr = get_folder_name(name)
|
|
255
|
-
if fldr is None:
|
|
256
|
-
return name
|
|
257
|
-
else:
|
|
258
|
-
return name[len(fldr) + 2:]
|
|
259
|
-
|
|
260
|
-
# Parent enum class
|
|
261
|
-
class SingletonMeta(EnumMeta):
|
|
262
|
-
|
|
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
|
-
|
|
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,50 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Enum & dataclass representing extension categories
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
from dataclasses import dataclass
|
|
9
|
-
|
|
10
|
-
from . import base
|
|
11
|
-
from scratchattach.utils import enums
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@dataclass
|
|
15
|
-
class Extension(base.JSONSerializable):
|
|
16
|
-
"""
|
|
17
|
-
Represents an extension in the Scratch block pallete - e.g. video sensing
|
|
18
|
-
"""
|
|
19
|
-
code: str
|
|
20
|
-
name: str = None
|
|
21
|
-
|
|
22
|
-
def __eq__(self, other):
|
|
23
|
-
return self.code == other.code
|
|
24
|
-
|
|
25
|
-
@staticmethod
|
|
26
|
-
def from_json(data: str):
|
|
27
|
-
assert isinstance(data, str)
|
|
28
|
-
_extension = Extensions.find(data, "code")
|
|
29
|
-
if _extension is None:
|
|
30
|
-
_extension = Extension(data)
|
|
31
|
-
|
|
32
|
-
return _extension
|
|
33
|
-
|
|
34
|
-
def to_json(self) -> str:
|
|
35
|
-
return self.code
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class Extensions(enums._EnumWrapper):
|
|
39
|
-
BOOST = Extension("boost", "LEGO BOOST Extension")
|
|
40
|
-
EV3 = Extension("ev3", "LEGO MINDSTORMS EV3 Extension")
|
|
41
|
-
GDXFOR = Extension("gdxfor", "Go Direct Force & Acceleration Extension")
|
|
42
|
-
MAKEYMAKEY = Extension("makeymakey", "Makey Makey Extension")
|
|
43
|
-
MICROBIT = Extension("microbit", "micro:bit Extension")
|
|
44
|
-
MUSIC = Extension("music", "Music Extension")
|
|
45
|
-
PEN = Extension("pen", "Pen Extension")
|
|
46
|
-
TEXT2SPEECH = Extension("text2speech", "Text to Speech Extension")
|
|
47
|
-
TRANSLATE = Extension("translate", "Translate Extension")
|
|
48
|
-
VIDEOSENSING = Extension("videoSensing", "Video Sensing Extension")
|
|
49
|
-
WEDO2 = Extension("wedo2", "LEGO Education WeDo 2.0 Extension")
|
|
50
|
-
COREEXAMPLE = Extension("coreExample", "CoreEx Extension") # hidden extension!
|
scratchattach/editor/field.py
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Optional, TYPE_CHECKING, Final
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if TYPE_CHECKING:
|
|
7
|
-
from . import block, vlb
|
|
8
|
-
|
|
9
|
-
from . import base, commons
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class Types:
|
|
13
|
-
VARIABLE: Final[str] = "variable"
|
|
14
|
-
LIST: Final[str] = "list"
|
|
15
|
-
BROADCAST: Final[str] = "broadcast"
|
|
16
|
-
DEFAULT: Final[str] = "default"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class Field(base.BlockSubComponent):
|
|
20
|
-
def __init__(self, _value: str | vlb.Variable | vlb.List | vlb.Broadcast, _id: Optional[str] = None, *, _block: Optional[block.Block] = None):
|
|
21
|
-
"""
|
|
22
|
-
A field for a scratch block
|
|
23
|
-
https://en.scratch-wiki.info/wiki/Scratch_File_Format#Blocks:~:text=it.%5B9%5D-,fields,element%2C%20which%20is%20the%20ID%20of%20the%20field%27s%20value.%5B10%5D,-shadow
|
|
24
|
-
"""
|
|
25
|
-
self.value = _value
|
|
26
|
-
self.id = _id
|
|
27
|
-
"""
|
|
28
|
-
ID of associated VLB. Will be used to get VLB object during sprite initialisation, where it will be replaced with 'None'
|
|
29
|
-
"""
|
|
30
|
-
super().__init__(_block)
|
|
31
|
-
|
|
32
|
-
def __repr__(self):
|
|
33
|
-
if self.id is not None:
|
|
34
|
-
# This shouldn't occur after sprite initialisation
|
|
35
|
-
return f"<Field {self.value!r} : {self.id!r}>"
|
|
36
|
-
else:
|
|
37
|
-
return f"<Field {self.value!r}>"
|
|
38
|
-
|
|
39
|
-
@property
|
|
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
|
-
"""
|
|
44
|
-
if self.id is not None:
|
|
45
|
-
return self.id
|
|
46
|
-
else:
|
|
47
|
-
if hasattr(self.value, "id"):
|
|
48
|
-
return self.value.id
|
|
49
|
-
else:
|
|
50
|
-
return None
|
|
51
|
-
|
|
52
|
-
@property
|
|
53
|
-
def value_str(self):
|
|
54
|
-
"""
|
|
55
|
-
Convert the associated value to a string - if this is a VLB, return the VLB name
|
|
56
|
-
"""
|
|
57
|
-
if not isinstance(self.value, base.NamedIDComponent):
|
|
58
|
-
return self.value
|
|
59
|
-
else:
|
|
60
|
-
return self.value.name
|
|
61
|
-
|
|
62
|
-
@property
|
|
63
|
-
def name(self) -> str:
|
|
64
|
-
"""
|
|
65
|
-
Fetch the name of this field using the associated block
|
|
66
|
-
"""
|
|
67
|
-
for _name, _field in self.block.fields.items():
|
|
68
|
-
if _field is self:
|
|
69
|
-
return _name
|
|
70
|
-
|
|
71
|
-
@property
|
|
72
|
-
def type(self):
|
|
73
|
-
"""
|
|
74
|
-
Infer the type of value that this field holds
|
|
75
|
-
:return: A string (from field.Types) as a name of the type
|
|
76
|
-
"""
|
|
77
|
-
if "variable" in self.name.lower():
|
|
78
|
-
return Types.VARIABLE
|
|
79
|
-
elif "list" in self.name.lower():
|
|
80
|
-
return Types.LIST
|
|
81
|
-
elif "broadcast" in self.name.lower():
|
|
82
|
-
return Types.BROADCAST
|
|
83
|
-
else:
|
|
84
|
-
return Types.DEFAULT
|
|
85
|
-
|
|
86
|
-
@staticmethod
|
|
87
|
-
def from_json(data: list[str, str | None]):
|
|
88
|
-
# Sometimes you may have a stray field with no id. Not sure why
|
|
89
|
-
while len(data) < 2:
|
|
90
|
-
data.append(None)
|
|
91
|
-
data = data[:2]
|
|
92
|
-
|
|
93
|
-
_value, _id = data
|
|
94
|
-
return Field(_value, _id)
|
|
95
|
-
|
|
96
|
-
def to_json(self) -> dict:
|
|
97
|
-
return commons.trim_final_nones([
|
|
98
|
-
self.value_str, self.value_id
|
|
99
|
-
])
|
scratchattach/editor/inputs.py
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import warnings
|
|
4
|
-
from typing import Optional, Final
|
|
5
|
-
|
|
6
|
-
from . import block
|
|
7
|
-
from . import base, commons, prim
|
|
8
|
-
from dataclasses import dataclass
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@dataclass
|
|
12
|
-
class ShadowStatus:
|
|
13
|
-
"""
|
|
14
|
-
Dataclass representing a possible shadow value and giving it a name
|
|
15
|
-
"""
|
|
16
|
-
idx: int
|
|
17
|
-
name: str
|
|
18
|
-
|
|
19
|
-
def __repr__(self):
|
|
20
|
-
return f"<ShadowStatus {self.name!r} ({self.idx})>"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class ShadowStatuses:
|
|
24
|
-
# Not an enum so you don't need to do .value
|
|
25
|
-
# Uh why?
|
|
26
|
-
HAS_SHADOW: Final[ShadowStatus] = ShadowStatus(1, "has shadow")
|
|
27
|
-
NO_SHADOW: Final[ShadowStatus] = ShadowStatus(2, "no shadow")
|
|
28
|
-
OBSCURED: Final[ShadowStatus] = ShadowStatus(3, "obscured")
|
|
29
|
-
|
|
30
|
-
@classmethod
|
|
31
|
-
def find(cls, idx: int) -> ShadowStatus:
|
|
32
|
-
for status in (cls.HAS_SHADOW, cls.NO_SHADOW, cls.OBSCURED):
|
|
33
|
-
if status.idx == idx:
|
|
34
|
-
return status
|
|
35
|
-
|
|
36
|
-
if not 1 <= idx <= 3:
|
|
37
|
-
raise ValueError(f"Invalid ShadowStatus idx={idx}")
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class Input(base.BlockSubComponent):
|
|
41
|
-
def __init__(self, _shadow: ShadowStatus | None = ShadowStatuses.HAS_SHADOW, _value: Optional[prim.Prim | block.Block | str] = None, _id: Optional[str] = None,
|
|
42
|
-
_obscurer: Optional[prim.Prim | block.Block | str] = None, *, _obscurer_id: Optional[str] = None, _block: Optional[block.Block] = None):
|
|
43
|
-
"""
|
|
44
|
-
An input for a scratch block
|
|
45
|
-
https://en.scratch-wiki.info/wiki/Scratch_File_Format#Blocks:~:text=inputs,it.%5B9%5D
|
|
46
|
-
"""
|
|
47
|
-
super().__init__(_block)
|
|
48
|
-
|
|
49
|
-
# If the shadow is None, we'll have to work it out later
|
|
50
|
-
self.shadow = _shadow
|
|
51
|
-
|
|
52
|
-
self.value: prim.Prim | block.Block = _value
|
|
53
|
-
self.obscurer: prim.Prim | block.Block = _obscurer
|
|
54
|
-
|
|
55
|
-
self._id = _id
|
|
56
|
-
"""
|
|
57
|
-
ID referring to the input value. Upon project initialisation, this will be set to None and the value attribute will be set to the relevant object
|
|
58
|
-
"""
|
|
59
|
-
self._obscurer_id = _obscurer_id
|
|
60
|
-
"""
|
|
61
|
-
ID referring to the obscurer. Upon project initialisation, this will be set to None and the obscurer attribute will be set to the relevant block
|
|
62
|
-
"""
|
|
63
|
-
|
|
64
|
-
def __repr__(self):
|
|
65
|
-
if self._id is not None:
|
|
66
|
-
return f"<Input<id={self._id!r}>"
|
|
67
|
-
else:
|
|
68
|
-
return f"<Input {self.value!r}>"
|
|
69
|
-
|
|
70
|
-
@staticmethod
|
|
71
|
-
def from_json(data: list):
|
|
72
|
-
_shadow = ShadowStatuses.find(data[0])
|
|
73
|
-
|
|
74
|
-
_value, _id = None, None
|
|
75
|
-
if isinstance(data[1], list):
|
|
76
|
-
_value = prim.Prim.from_json(data[1])
|
|
77
|
-
else:
|
|
78
|
-
_id = data[1]
|
|
79
|
-
|
|
80
|
-
_obscurer_data = commons.safe_get(data, 2)
|
|
81
|
-
|
|
82
|
-
_obscurer, _obscurer_id = None, None
|
|
83
|
-
if isinstance(_obscurer_data, list):
|
|
84
|
-
_obscurer = prim.Prim.from_json(_obscurer_data)
|
|
85
|
-
else:
|
|
86
|
-
_obscurer_id = _obscurer_data
|
|
87
|
-
return Input(_shadow, _value, _id, _obscurer, _obscurer_id=_obscurer_id)
|
|
88
|
-
|
|
89
|
-
def to_json(self) -> list:
|
|
90
|
-
data = [self.shadow.idx]
|
|
91
|
-
|
|
92
|
-
def add_pblock(pblock: prim.Prim | block.Block | None):
|
|
93
|
-
"""
|
|
94
|
-
Adds a primitive or a block to the data in the right format
|
|
95
|
-
"""
|
|
96
|
-
if pblock is None:
|
|
97
|
-
return
|
|
98
|
-
|
|
99
|
-
if isinstance(pblock, prim.Prim):
|
|
100
|
-
data.append(pblock.to_json())
|
|
101
|
-
|
|
102
|
-
elif isinstance(pblock, block.Block):
|
|
103
|
-
data.append(pblock.id)
|
|
104
|
-
|
|
105
|
-
else:
|
|
106
|
-
warnings.warn(f"Bad prim/block {pblock!r} of type {type(pblock)}")
|
|
107
|
-
|
|
108
|
-
add_pblock(self.value)
|
|
109
|
-
add_pblock(self.obscurer)
|
|
110
|
-
|
|
111
|
-
return data
|
|
112
|
-
|
|
113
|
-
def link_using_block(self):
|
|
114
|
-
# Link to value
|
|
115
|
-
if self._id is not None:
|
|
116
|
-
new_value = self.sprite.find_block(self._id, "id")
|
|
117
|
-
if new_value is not None:
|
|
118
|
-
self.value = new_value
|
|
119
|
-
self._id = None
|
|
120
|
-
|
|
121
|
-
# Link to obscurer
|
|
122
|
-
if self._obscurer_id is not None:
|
|
123
|
-
new_block = self.sprite.find_block(self._obscurer_id, "id")
|
|
124
|
-
if new_block is not None:
|
|
125
|
-
self.obscurer = new_block
|
|
126
|
-
self._obscurer_id = None
|
|
127
|
-
|
|
128
|
-
# Link value to sprite
|
|
129
|
-
if isinstance(self.value, prim.Prim):
|
|
130
|
-
self.value.sprite = self.sprite
|
|
131
|
-
self.value.link_using_sprite()
|
|
132
|
-
|
|
133
|
-
# Link obscurer to sprite
|
|
134
|
-
if self.obscurer is not None:
|
|
135
|
-
self.obscurer.sprite = self.sprite
|
scratchattach/editor/meta.py
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
from dataclasses import dataclass, field
|
|
5
|
-
|
|
6
|
-
from . import base, commons
|
|
7
|
-
from typing import Optional
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@dataclass
|
|
11
|
-
class PlatformMeta(base.JSONSerializable):
|
|
12
|
-
name: str = None
|
|
13
|
-
url: str = field(repr=True, default=None)
|
|
14
|
-
|
|
15
|
-
def __bool__(self):
|
|
16
|
-
return self.name is not None or self.url is not None
|
|
17
|
-
|
|
18
|
-
def to_json(self) -> dict:
|
|
19
|
-
_json = {"name": self.name, "url": self.url}
|
|
20
|
-
commons.remove_nones(_json)
|
|
21
|
-
return _json
|
|
22
|
-
|
|
23
|
-
@staticmethod
|
|
24
|
-
def from_json(data: dict | None):
|
|
25
|
-
if data is None:
|
|
26
|
-
return PlatformMeta()
|
|
27
|
-
else:
|
|
28
|
-
return PlatformMeta(data.get("name"), data.get("url"))
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
DEFAULT_VM = "0.1.0"
|
|
32
|
-
DEFAULT_AGENT = "scratchattach.editor by https://scratch.mit.edu/users/timmccool/"
|
|
33
|
-
DEFAULT_PLATFORM = PlatformMeta("scratchattach", "https://github.com/timMcCool/scratchattach/")
|
|
34
|
-
|
|
35
|
-
EDIT_META = True
|
|
36
|
-
META_SET_PLATFORM = False
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
"""
|
|
43
|
-
global META_SET_PLATFORM
|
|
44
|
-
if true_false is None:
|
|
45
|
-
true_false = bool(1 - true_false)
|
|
46
|
-
META_SET_PLATFORM = true_false
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
class Meta(base.JSONSerializable):
|
|
50
|
-
def __init__(self, semver: str = "3.0.0", vm: str = DEFAULT_VM, agent: str = DEFAULT_AGENT,
|
|
51
|
-
platform: Optional[PlatformMeta] = None):
|
|
52
|
-
"""
|
|
53
|
-
Represents metadata of the project
|
|
54
|
-
https://en.scratch-wiki.info/wiki/Scratch_File_Format#Metadata
|
|
55
|
-
"""
|
|
56
|
-
if platform is None and META_SET_PLATFORM:
|
|
57
|
-
platform = DEFAULT_PLATFORM.dcopy()
|
|
58
|
-
|
|
59
|
-
self.semver = semver
|
|
60
|
-
self.vm = vm
|
|
61
|
-
self.agent = agent
|
|
62
|
-
self.platform = platform
|
|
63
|
-
|
|
64
|
-
if not self.vm_is_valid:
|
|
65
|
-
raise ValueError(
|
|
66
|
-
f"{vm!r} does not match pattern '^([0-9]+\\.[0-9]+\\.[0-9]+)($|-)' - maybe try '0.0.0'?")
|
|
67
|
-
|
|
68
|
-
def __repr__(self):
|
|
69
|
-
data = f"{self.semver} : {self.vm} : {self.agent}"
|
|
70
|
-
if self.platform:
|
|
71
|
-
data += f": {self.platform}"
|
|
72
|
-
|
|
73
|
-
return f"Meta<{data}>"
|
|
74
|
-
|
|
75
|
-
@property
|
|
76
|
-
def vm_is_valid(self):
|
|
77
|
-
"""
|
|
78
|
-
Check whether the vm value is valid using a regex
|
|
79
|
-
Thanks to TurboWarp for this pattern ↓↓↓↓, I just copied it
|
|
80
|
-
"""
|
|
81
|
-
return re.match("^([0-9]+\\.[0-9]+\\.[0-9]+)($|-)", self.vm) is not None
|
|
82
|
-
|
|
83
|
-
def to_json(self):
|
|
84
|
-
_json = {
|
|
85
|
-
"semver": self.semver,
|
|
86
|
-
"vm": self.vm,
|
|
87
|
-
"agent": self.agent
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if self.platform:
|
|
91
|
-
_json["platform"] = self.platform.to_json()
|
|
92
|
-
return _json
|
|
93
|
-
|
|
94
|
-
@staticmethod
|
|
95
|
-
def from_json(data):
|
|
96
|
-
if data is None:
|
|
97
|
-
data = ""
|
|
98
|
-
|
|
99
|
-
semver = data["semver"]
|
|
100
|
-
vm = data.get("vm")
|
|
101
|
-
agent = data.get("agent")
|
|
102
|
-
platform = PlatformMeta.from_json(data.get("platform"))
|
|
103
|
-
|
|
104
|
-
if EDIT_META or vm is None:
|
|
105
|
-
vm = DEFAULT_VM
|
|
106
|
-
|
|
107
|
-
if EDIT_META or agent is None:
|
|
108
|
-
agent = DEFAULT_AGENT
|
|
109
|
-
|
|
110
|
-
if EDIT_META:
|
|
111
|
-
if META_SET_PLATFORM and not platform:
|
|
112
|
-
platform = DEFAULT_PLATFORM.dcopy()
|
|
113
|
-
|
|
114
|
-
return Meta(semver, vm, agent, platform)
|