Server code imports style that works with designer and uplink

What I’m trying to do:
I am trying to structure my imports so that code can run in the Anvil designer AND also via uplink without needing to make any changes to the code.

I have code in “Server Code” that import a couple modules and packages which also live in “Server Code”. Works fine when I run from the Anvil designer but I get import errors when running via uplink.

What I’ve tried and what’s not working:

As an example, I have server code structured like this:

Server Code
    runner.py
    mymod.py (contains minimal "calc1()" function)
    mypkg\
        mymod2.py (contains minimal "calc2()" function)

The runner.py file basically contains:

from . import mymod
from .mypkg import mymod2

@anvil.server.callable
def runtest(x):
  a = mymod.calc1(x)
  b = mymod2.calc2(x)

This works from the Anvil designer. But when I clone this repo (funcsharing) locally and run via uplink (v 0.6.0 of anvil-uplink), I get the import errors below. I’m running uplink on Windows 10 in a GitBash terminal, which has worked great with other apps.

$ python -m anvil.run_app_via_uplink funcsharing
Importing funcsharing
Importing funcsharing.mymod
Importing funcsharing.mypkg
Importing funcsharing.mymod2
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "C:\cache\repo\funcsharing\venv\Lib\site-packages\anvil\run_app_via_uplink.py", line 45, in <module>
    import_all(server_path, mod.__name__)
  File "C:\cache\repo\funcsharing\venv\Lib\site-packages\anvil\run_app_via_uplink.py", line 33, in import_all
    import_all(subpkg.__path__, mod.__package__ or mod.__name__)
  File "C:\cache\repo\funcsharing\venv\Lib\site-packages\anvil\run_app_via_uplink.py", line 29, in import_all
    submod = importlib.import_module(package_name+"."+f[:-3])
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Program Files\Python311\Lib\importlib\__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1206, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1178, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1142, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'funcsharing.mymod2'

I’ve read:

What is the best practice to follow to get import statements that work in the designer and with uplink?

1 Like

I took a moment and poked into the run_anvil_via_uplink.py script. It looks like the implementation that is handling subpackages (the code in elif) isn’t taking the parent directory into consideration as it recurses?

def import_all(paths, package_name):
    for path in paths:
        for f in os.listdir(path):
            fpath = os.path.join(path, f)
            if f.endswith(".py") and f != "__init__.py":
                print("Importing "+package_name+"."+f[:-3])
                submod = importlib.import_module(package_name+"."+f[:-3])
            elif os.path.isdir(fpath) and os.path.exists(os.path.join(fpath, "__init__.py")):
                print("Importing "+package_name+"."+f)
                subpkg = importlib.import_module(package_name+"."+f)
                import_all(subpkg.__path__, mod.__package__ or mod.__name__)

I’m going to eliminate my use of server packages for now and just use top-level server modules.

Is it possible that you are having problem because the folder structure is flattened by the server when the app runs?

Yeah, @stefano.menci, it does appear that run_anvil_via_uplink.py is flattening the imports (you can see my output above showing the script attempting to import funcsharing.mymod2 which is a sub-module of the mypkg package.

I guess my question isn’t “what is it doing”, but more “why is run_anvil_via_uplink.py flattening imports”? To my new-to-anvil eyes, it doesn’t make sense why imports are not treated consistently between standard Anvil hosting and apps run via run_anvil_via_uplink.py. This was likely done for a good reason and I am now curious why. :grin:

1 Like

My guess about why this happens is that, even though modules live under client_code and server_code on disk, Anvil effectively treats them as a single flat namespace at runtime. That lets you write imports like from ..module import name, instead of having to reference client_code or server_code explicitly (e.g. from ..client_code.module import name).

I don’t know if this is the actual reason behind the design, but the behavior shows up when running via uplink: the uplink runner doesn’t handle nested packages the same way the Anvil designer does, so submodules can get imported as if they were top-level. That makes running tests locally awkward.

I have used the same dirty trick of modifying __path__ to work around it, but my linter and PyCharm don’t know about that, so I usually end up with a try/except block to support both import styles.