Repository: plotly/dash

Issue:
Can't override external CSS without !important
Not really a bug, just something I noticed that we should be aware of.

In the new `assets` directory approach to including CSS, [external stylesheets always appear beneath local stylesheets.](https://github.com/plotly/dash/blob/7361db52690810891c5f68501eda1d71043cb777/dash/dash.py#L333-L336)

There doesn't seem to be a way to switch this (at least that I'm aware of), so a user can't override CSS from external libraries without !important since external scripts will always be imported last and thus take precedence. I think this is a very common use case (e.g. import bootstrap, then add your own CSS to customize a small part of bootstrap) and users shouldn't have to manually add !important to all their stylesheets or change the specificity of selectors to get it to work.




Discussion:
@T4rk1n 
I'll change the assets to go after external.
> I'll change the assets to go after external.

Yeah, that seems like it would be the right default behaviour.
And if the user really needs to customize it, then they can override the index
Is that possible right now? I tried but it looks like external and local css are imported as one chunk without an option to order them: https://github.com/plotly/dash/blob/7361db52690810891c5f68501eda1d71043cb777/dash/dash.py#L438
No, it's not possible to reorder right now, I want to change the generate_html methods to be separate from the resources collection and give raw resources list to interpolate_index instead.

Resolving patch:
diff --git a/dash/dash.py b/dash/dash.py
--- a/dash/dash.py
+++ b/dash/dash.py
@@ -331,9 +331,8 @@ def _relative_url_path(relative_package_path='', namespace=''):
         return srcs
 
     def _generate_css_dist_html(self):
-        links = self._collect_and_register_resources(
-            self.css.get_all_css()
-        ) + self._external_stylesheets
+        links = self._external_stylesheets + \
+                self._collect_and_register_resources(self.css.get_all_css())
 
         return '\n'.join([
             _format_tag('link', link, opened=True)
@@ -353,13 +352,11 @@ def _generate_scripts_html(self):
         srcs = self._collect_and_register_resources(
             self.scripts._resources._filter_resources(
                 dash_renderer._js_dist_dependencies
-            ) +
-            self.scripts.get_all_scripts() +
-            self.scripts._resources._filter_resources(
-                dash_renderer._js_dist
-            )
-        )
-        srcs = srcs[:-1] + self._external_scripts + [srcs[-1]]
+            )) + self._external_scripts + self._collect_and_register_resources(
+                self.scripts.get_all_scripts() +
+                self.scripts._resources._filter_resources(
+                    dash_renderer._js_dist
+                ))
 
         return '\n'.join([
             _format_tag('script', src)
diff --git a/dash/version.py b/dash/version.py
--- a/dash/version.py
+++ b/dash/version.py
@@ -1 +1 @@
-__version__ = '0.25.0'
+__version__ = '0.25.1'


Test patch:
diff --git a/tests/assets/load_first.js b/tests/assets/load_first.js
--- a/tests/assets/load_first.js
+++ b/tests/assets/load_first.js
@@ -1 +1,7 @@
-window.tested = ['load_first'];
\ No newline at end of file
+window.tested = ['load_first'];
+var ramdaTest = document.getElementById('ramda-test');
+if (ramdaTest) {
+    ramdaTest.innerHTML = R.join(' ', R.concat(['hello'], ['world']).map(function(x) {
+        return _.capitalize(x);
+    }));
+}
diff --git a/tests/assets/reset.css b/tests/assets/reset.css
--- a/tests/assets/reset.css
+++ b/tests/assets/reset.css
@@ -1 +1,2 @@
 body {margin: 0;}
+button {height: 18px}
diff --git a/tests/test_integration.py b/tests/test_integration.py
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -452,11 +452,17 @@ def test_external_files_init(self):
             'https://www.google-analytics.com/analytics.js',
             {'src': 'https://cdn.polyfill.io/v2/polyfill.min.js'},
             {
-                'src': 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.core.js',
-                'integrity': 'sha256-Qqd/EfdABZUcAxjOkMi8eGEivtdTkh3b65xCZL4qAQA=',
+                'src': 'https://cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js',
+                'integrity': 'sha256-YN22NHB7zs5+LjcHWgk3zL0s+CRnzCQzDOFnndmUamY=',
+                'crossorigin': 'anonymous'
+            },
+            {
+                'src': 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js',
+                'integrity': 'sha256-VKITM616rVzV+MI3kZMNUDoY5uTsuSl1ZvEeZhNoJVk=',
                 'crossorigin': 'anonymous'
             }
         ]
+
         css_files = [
             'https://codepen.io/chriddyp/pen/bWLwgP.css',
             {
@@ -467,8 +473,30 @@ def test_external_files_init(self):
             }
         ]
 
-        app = dash.Dash(
-            external_scripts=js_files, external_stylesheets=css_files)
+        app = dash.Dash(__name__,
+                        external_scripts=js_files,
+                        external_stylesheets=css_files)
+
+        app.index_string = '''
+        <!DOCTYPE html>
+        <html>
+            <head>
+                {%metas%}
+                <title>{%title%}</title>
+                {%css%}
+            </head>
+            <body>
+                <div id="tested"></div>
+                <div id="ramda-test"></div>
+                <button type="button" id="btn">Btn</button>
+                {%app_entry%}
+                <footer>
+                    {%config%}
+                    {%scripts%}
+                </footer>
+            </body>
+        </html>
+        '''
 
         app.layout = html.Div()
 
@@ -482,3 +510,13 @@ def test_external_files_init(self):
                 (("//script[@src='{}']", x) for x in js_urls),
                 (("//link[@href='{}']", x) for x in css_urls)):
             self.driver.find_element_by_xpath(fmt.format(url))
+
+        # Ensure the button style was overloaded by reset (set to 38px in codepen)
+        btn = self.driver.find_element_by_id('btn')
+        btn_height = btn.value_of_css_property('height')
+
+        self.assertEqual('18px', btn_height)
+
+        # ensure ramda was loaded before the assets so they can use it.
+        lo_test = self.driver.find_element_by_id('ramda-test')
+        self.assertEqual('Hello World', lo_test.text)