dash icon indicating copy to clipboard operation
dash copied to clipboard

[BUG] Dash 2.1.0 incompatible with Apache wsgi

Open pfbuxton opened this issue 3 years ago • 4 comments

In summary I am using Apache as the web-server, but the importing of app causes the error: 'Read-only: can only be set in the Dash constructor or during init_app()'

This seems to be similar to https://github.com/plotly/dash/issues/1907. All works fine with dash 2.0.0, but breaks with 2.1.0.

Detailed explanation:

Following https://dash.plotly.com/urls we have:

- app.py
- pages
   |-- __init__.py
   |-- page1.py
   |-- page2.py

We also have the modwsgi python file, /var/www/path_to_site/site_name.wsgi, which Apache is set-up to use, this is what is causing the error:

import sys
sys.path.insert(0, "/var/www/path_to_site/")
from site_name.app import server as application

and the Apache settings /etc/httpd/sites-enabled/site_name.conf

<VirtualHost *:80>
    ServerName server_name

    WSGIDaemonProcess site_name home=/var/www/path_to_site processes=4 threads=12 python-home=/var/www/path_to_site/venv

    WSGIScriptAlias /st40_phys_viewer /var/www/path_to_site/site_name.wsgi

    <Directory /var/www/path_to_site>
        WSGIProcessGroup site_name
        WSGIApplicationGroup %{GLOBAL}
        Order allow,deny
        Allow from all
    </Directory>

    <Directory /var/www/path_to_site/assets>
        Order allow,deny
        Allow from all
    </Directory>
</VirtualHost>

pfbuxton avatar Feb 15 '22 09:02 pfbuxton

I have found a workaround... but it's not ideal.

Modify the following file: /PATH_TO_YOUR_PYTHON_ENVIROMENT/site-packages/dash/_utils.py lines 157-158 Original:

    def __setitem__(self, key, val):
        if key in self.__dict__.get("_read_only", {}):
            raise AttributeError(self._read_only[key], key)

        final_msg = self.__dict__.get("_final")
        if final_msg and key not in self:
            raise AttributeError(final_msg, key)

        return super().__setitem__(key, val)

Modified:

    def __setitem__(self, key, val):
        #if key in self.__dict__.get("_read_only", {}):
        #    raise AttributeError(self._read_only[key], key)

        final_msg = self.__dict__.get("_final")
        if final_msg and key not in self:
            raise AttributeError(final_msg, key)

        return super().__setitem__(key, val)

other suggestions https://github.com/plotly/jupyter-dash/issues/75 are to do this:

del app.config._read_only["requests_pathname_prefix"]

however, for me this did not work.

pfbuxton avatar Feb 15 '22 20:02 pfbuxton

@pfbuxton are you able to share the full traceback you're seeing? These attributes are read-only for a reason: if they're modified late without being extremely careful about it the app can break in some very confusing ways.

Obviously jupyter-dash needed to do this anyway - and https://github.com/plotly/jupyter-dash/pull/76 fixes it to work with Dash 2.1 - but before we go about extending that pattern elsewhere it would be nice to understand what's going on in your case.

alexcjohnson avatar Feb 15 '22 22:02 alexcjohnson

Hi @alexcjohnson,

Sorry for the delay. I needed to create a virtual OS to do some of the testing. We are using CentOS 7.5.

Attached are the relevant files (both settings and errors): FILES.zip

To start the Apache example you can do: sudo /sbin/service httpd restart

Here is the error log, /etc/httpd/logs/error_log:

[Mon Jul 11 14:44:33.473341 2022] [wsgi:error] [pid 20737] hello1
[Mon Jul 11 14:44:33.473416 2022] [wsgi:error] [pid 20737] /var/www/example/venv/bin/python
[Mon Jul 11 14:44:33.722478 2022] [wsgi:error] [pid 20737] hello2
[Mon Jul 11 14:44:34.996686 2022] [wsgi:error] [pid 20737] [remote 10.99.1.36:51432] mod_wsgi (pid=20737): Failed to exec Python script file '/var/www/example/example_wsgi.wsgi'.
[Mon Jul 11 14:44:34.996737 2022] [wsgi:error] [pid 20737] [remote 10.99.1.36:51432] mod_wsgi (pid=20737): Exception occurred processing WSGI script '/var/www/example/example_wsgi.wsgi'.
[Mon Jul 11 14:44:34.997070 2022] [wsgi:error] [pid 20737] [remote 10.99.1.36:51432] Traceback (most recent call last):
[Mon Jul 11 14:44:34.997097 2022] [wsgi:error] [pid 20737] [remote 10.99.1.36:51432]   File "/var/www/example/example_wsgi.wsgi", line 9, in <module>
[Mon Jul 11 14:44:34.997100 2022] [wsgi:error] [pid 20737] [remote 10.99.1.36:51432]     from minimal_example import app
[Mon Jul 11 14:44:34.997113 2022] [wsgi:error] [pid 20737] [remote 10.99.1.36:51432]   File "/var/www/example/minimal_example.py", line 12, in <module>
[Mon Jul 11 14:44:34.997116 2022] [wsgi:error] [pid 20737] [remote 10.99.1.36:51432]     requests_pathname_prefix = '/st40_phys_viewer/'
[Mon Jul 11 14:44:34.997120 2022] [wsgi:error] [pid 20737] [remote 10.99.1.36:51432]   File "/var/www/example/venv/lib64/python3.6/site-packages/dash/_utils.py", line 124, in update
[Mon Jul 11 14:44:34.997123 2022] [wsgi:error] [pid 20737] [remote 10.99.1.36:51432]     self[k] = v
[Mon Jul 11 14:44:34.997127 2022] [wsgi:error] [pid 20737] [remote 10.99.1.36:51432]   File "/var/www/example/venv/lib64/python3.6/site-packages/dash/_utils.py", line 113, in __setitem__
[Mon Jul 11 14:44:34.997129 2022] [wsgi:error] [pid 20737] [remote 10.99.1.36:51432]     raise AttributeError(self._read_only[key], key)
[Mon Jul 11 14:44:34.997143 2022] [wsgi:error] [pid 20737] [remote 10.99.1.36:51432] AttributeError: ('Read-only: can only be set in the Dash constructor or during init_app()', 'routes_pathname_prefix')

Thanks, Peter

pfbuxton avatar Jul 11 '22 14:07 pfbuxton

I see, thanks. Is there a reason this can't all be moved into the constructor though? Also if you're setting routes_pathname_prefix and requests_pathname_prefix to the same thing ('/example/'), I don't believe there's any purpose setting url_base_pathname to something else. And while we're at it, a few of the other settings you're using here are now the defaults as of Dash 2.0: compress=False and serve_locally=True. And app.enable_dev_tools(debug=False) I believe is a noop. So I'd think you can rewrite this block in minimal_example.py:

app = Dash(__name__, url_base_pathname='/', compress=False)
app.config.update(dict(
    routes_pathname_prefix = '/example/', 
    requests_pathname_prefix = '/example/'
))
app.enable_dev_tools(debug=False)

# Only use local css and javascripts
app.css.config.serve_locally = True
app.scripts.config.serve_locally = True

app.config.suppress_callback_exceptions = True
app.title = 'example'

as:

app = Dash(
    __name__,
    url_base_pathname='/example/',
    suppress_callback_exceptions=True,
    title='example'
)

Does that work?

alexcjohnson avatar Jul 11 '22 15:07 alexcjohnson

Hi @alexcjohnson,

Your suggestion partially fixed this issue.

With my apache settings (i.e. the /etc/httpd/sites-enabled/site_name.conf file) I needed to have routes_pathname_prefix, and requests_pathname_prefix set using app.config.update. If they were all set during the initial construction Dash complained that they weren't the same as url_base_pathname!

Anyway the solution is to change the scope in the wsgi file form:

    WSGIScriptAlias /st40_phys_viewer /var/www/path_to_site/site_name.wsgi

to:

    WSGIScriptAlias / /var/www/path_to_site/site_name.wsgi

pfbuxton avatar Mar 23 '23 20:03 pfbuxton