[Feature] Improve importing functions shared between block programs
Is your feature request related to a problem? Please describe. You can define functions in one block project and import them into another (just like Python), but there are some subtleties that make this nontrivial at the moment.
For the following, let's assume there is one "main.py" program and a "module.py" with useful functions.
Right now, if main does any multitasking (async), it assumes functions imported from module are async too. Likewise, if main is synchronous, it will just call imported functions and not await them.
The workaround for users to make such programs work together is to just make everything async. (This is trivially done by just adding another start block, but it is not intuitive)
Solving it through blocks We could make the block interface aware of multiple files/workspaces, and take all called functions into account when generating the code, as if it was a single project. Or keep it more or less as is, but regenerate all programs to use async if at least one of them does.
Solving it with Python The way blocks are integrated right now, the above approach is not really realistic, even if that is perhaps the "correct" approach. Another way to do it is to generate some extra Python code when an imported block is called.
If the currently open main block project is synchronous, it could call imported functions as follows:
from module import imported_function
# utility function, only included if user imports are used
def make_sync(called_func):
if called_func is a generator object:
return run_task(called_func)
return called_func
# currently the imported function caller block always gives:
imported_function(args)
# but we could generate:
make_sync(imported_function(args))
Likewise, if the main program uses multitasking, it could do:
async def the_main_start():
# builtin function calls remain unchanged
await wait(1000)
# currently the imported function caller block always gives:
await imported_function(args)
# but we could generate
(await imported_function(args)) if is_generator(imported_function) else imported_function(args)
# could also do the above in a utility
Additional context Example use case: I made a generic balance calculation function with blocks (synchronous, just math), but would like it to work whether the user's main program uses multitasking or not. If the library used Python, then it can already be solved in
Note that this does not change anything in Python programs, just how we auto-generate code for imported functions in block programs.
How about always generating 2 .py files for every module, a non-async version and an async version?
The async version could just be the same module name with _async suffix. Then an async main would import module_async while a non async main would import module. This way the generated source code stays readable and the binary .mpy files stay smaller. The unused generated files would just be ignored because they aren't imported.
Good idea. We might still need some way to handle imports from pure python modules though (without auto generated async variant).
Maybe this issue should just wait until after the cleaned up pull request for block integration, so we can tell block programs and python scripts properly apart.
For python modules, the imports can just be as is, without any special wrapping.
We might still need some way to handle imports from pure python modules though (without auto generated async variant)
Another solution which covers this but maybe also all the cases above, is to add a (optional) dropdown to the import block below, as e.g. below. So the block visually reads from module_name import [function ▼] function_name.
The dropdown could include (non-technical names of):
- def
- async def
- constant or object (related to a request by @dlech a while ago)
Then it can generate the code correctly. For example, if a constant/object is imported, then the generated caller code would just be name instead of name().
So in the case below, instead of changing the Python utility to an async def as a workaround just because the calling block program uses multitasking, you could say from color_tools import [function ▼] get_hue, and the block program would know that it shouldn't add await when calling it.