posit-dev / py-shiny

Shiny for Python
https://shiny.posit.co/py/
MIT License
1.1k stars 62 forks source link

Enable relative imports in Express apps #1464

Closed wch closed 2 weeks ago

wch commented 2 weeks ago

This PR makes it possible to do relative imports in a Shiny Express app, such as:

from . import utils

This is useful in the case of running Shiny Express apps in Shinylive on a web page where there are multiple Shiny apps running concurrently. This is important for the case where a page has multiple versions of an app, and each imports from a file with the same name, like utils.py -- without relative imports, only one copy of utils.py will be imported, and it will be shared across all of the concurrent apps.

In this sheet, this change has the effect of making all of the Error cells in the bottom row (in red) into OK cells (which would be green). https://docs.google.com/spreadsheets/d/1jIVoEDr234uo_ALOPiilSIoU-bj18L-R6YP_39ORXwM/edit#gid=0

image

I'm not sure yet, but it might be possible to make relative imports also work for Shiny Core apps.

wch commented 2 weeks ago

Regarding getting relative imports to work with Shiny Core, it looks like it is possible but it's a little odd: we would need to use the app's parent directory as the app_dir (this is equivalent to adding the parent directory to PYTHONPATH environment variable). This is a restriction that comes from the way uvicorn works.

Because the parent dir is on the Python search path, that means that if the directory containing app.py also contains utils.py, the relative import would work:

from . import utils  # OK

But the non-relative import would not:

import utils  # Error

This would be a surprising change of behavior.

One way around this would be to also add the application directory to the PYTHONPATH. However, the parent dir would still be on the search path, so there could be a conflict if the parent dir contains a utils.py or a utils/ subdir with __init__.py in it.

IMO, it is generally preferable to use relative imports to avoid naming conflicts. For example, a file named os.py would have a name conflict with the built-in os package, so what does import os do in that case?

References about relative imports with uvicorn:

wch commented 2 weeks ago

After merging, this is the updated spreadsheet. (Also note that I changed the colors to be more colorblind-friendly)

image