diff --git a/.pylintrc b/.pylintrc
index fba35186cc..fcf0968f10 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -57,7 +57,8 @@ confidence=
disable=fixme,
missing-docstring,
invalid-name,
- too-many-lines
+ too-many-lines,
+ old-style-class
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
diff --git a/dash/_utils.py b/dash/_utils.py
index 17dc247ebe..d91eab8a86 100644
--- a/dash/_utils.py
+++ b/dash/_utils.py
@@ -1,3 +1,24 @@
+import functools
+import hashlib
+import base64
+import pkgutil
+
+
+def memoize(func):
+ results = {}
+
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs):
+ key = hash((args, frozenset(kwargs.items())))
+ cached = results.get(key)
+ if cached:
+ return cached
+ results[key] = r = func(*args, **kwargs)
+ return r
+
+ return wrapper
+
+
def interpolate_str(template, **data):
s = template
for k, v in data.items():
@@ -37,6 +58,37 @@ def get_asset_path(
])
+def pluck(obj, *props, **additions):
+ return dict({k: v for k, v in obj.items() if k in props}, **additions)
+
+
+def first_key(data, *keys):
+ for key in keys:
+ value = data.get(key)
+ if value:
+ return key, value
+ return None, None
+
+
+def _integrity_hash(b):
+ h = hashlib.sha384(b)
+
+ return 'sha384-{}'.format(
+ base64.b64encode(h.digest()).decode()
+ )
+
+
+@memoize
+def integrity_hash_from_file(filename):
+ with open(filename, 'rb') as f:
+ return _integrity_hash(f.read())
+
+
+@memoize
+def integrity_hash_from_package(namespace, path):
+ return _integrity_hash(pkgutil.get_data(namespace, path))
+
+
class AttributeDict(dict):
"""
Dictionary subclass enabling attribute lookup/assignment of keys/values.
diff --git a/dash/dash.py b/dash/dash.py
index 0a68d70bce..41cd7a1c79 100644
--- a/dash/dash.py
+++ b/dash/dash.py
@@ -25,6 +25,7 @@
from ._utils import interpolate_str as _interpolate
from ._utils import format_tag as _format_tag
from ._utils import get_asset_path as _get_asset_path
+from ._utils import pluck as _pluck
from . import _configs
@@ -313,6 +314,7 @@ def _collect_and_register_resources(self, resources):
def _relative_url_path(relative_package_path='', namespace=''):
# track the registered packages
+ # FIXME it append a new path each time the index is served ?
if namespace in self.registered_paths:
self.registered_paths[namespace].append(relative_package_path)
else:
@@ -332,23 +334,34 @@ def _relative_url_path(relative_package_path='', namespace=''):
modified
)
+ def pack(src, **kwargs):
+ return dict(kwargs, src=src)
+
srcs = []
for resource in resources:
if 'relative_package_path' in resource:
if isinstance(resource['relative_package_path'], str):
- srcs.append(_relative_url_path(**resource))
+ srcs.append(
+ pack(
+ _relative_url_path(
+ resource['relative_package_path'],
+ resource['namespace']),
+ **resource)
+ )
else:
for rel_path in resource['relative_package_path']:
- srcs.append(_relative_url_path(
+ srcs.append(pack(_relative_url_path(
relative_package_path=rel_path,
- namespace=resource['namespace']
- ))
+ namespace=resource['namespace'],
+ ), **resource))
elif 'external_url' in resource:
if isinstance(resource['external_url'], str):
- srcs.append(resource['external_url'])
+ srcs.append(
+ pack(resource['external_url'], **resource)
+ )
else:
for url in resource['external_url']:
- srcs.append(url)
+ srcs.append(pack(url))
elif 'absolute_path' in resource:
raise Exception(
'Serving files from absolute_path isn\'t supported yet'
@@ -357,7 +370,7 @@ def _relative_url_path(relative_package_path='', namespace=''):
static_url = self.get_asset_url(resource['asset_path'])
# Add a bust query param
static_url += '?m={}'.format(resource['ts'])
- srcs.append(static_url)
+ srcs.append(pack(static_url, **resource))
return srcs
def _generate_css_dist_html(self):
@@ -365,7 +378,22 @@ def _generate_css_dist_html(self):
self._collect_and_register_resources(self.css.get_all_css())
return '\n'.join([
- _format_tag('link', link, opened=True)
+ _format_tag(
+ 'link',
+ _pluck(
+ link,
+ 'integrity',
+ 'crossorigin',
+ 'media',
+ 'charset',
+ 'rev',
+ 'target',
+ 'type',
+ href=link.get('src', link.get('href')),
+ rel='stylesheet'
+ ),
+ opened=True
+ )
if isinstance(link, dict)
else ''.format(link)
for link in links
@@ -392,7 +420,15 @@ def _generate_scripts_html(self):
))
return '\n'.join([
- _format_tag('script', src)
+ _format_tag('script', _pluck(
+ src,
+ 'src',
+ 'integrity',
+ 'crossorigin',
+ 'charset',
+ 'async',
+ 'defer',
+ 'type'))
if isinstance(src, dict)
else ''.format(src)
for src in srcs
@@ -492,7 +528,7 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument
checks = (
(_re_index_entry_id.search(index), '#react-entry-point'),
(_re_index_config_id.search(index), '#_dash-configs'),
- (_re_index_scripts_id.search(index), 'dash-renderer'),
+ # (_re_index_scripts_id.search(index), 'dash-renderer'),
)
missing = [missing for check, missing in checks if not check]
diff --git a/dash/resources.py b/dash/resources.py
index aa1ce871d9..d9a060ec0f 100644
--- a/dash/resources.py
+++ b/dash/resources.py
@@ -4,6 +4,27 @@
import os
from .development.base_component import Component
+from ._utils import \
+ first_key, integrity_hash_from_file, integrity_hash_from_package
+
+
+# pylint: disable=inconsistent-return-statements
+def find_unpkg(value, relative_package_paths):
+ # find the local file for a unpkg url.
+ # The structure of the _js_dist/_css_dist does not allow
+ # for easy translation between local and external dependencies.
+ v = value.replace('https://unpkg.com/', '')
+ s = v.split('/')
+ lib, version = s[0].split('@')
+ filename = s[-1]
+ ext = filename.split('.')[-1]
+
+ for i in relative_package_paths:
+ if (i == filename and i not in [
+ 'index.js', 'index.css', 'style.css', 'style.min.css',
+ 'styles.css', 'styles.min.css']) \
+ or (lib in i and version in i and i.endswith(ext)):
+ return i
# pylint: disable=old-style-class
@@ -16,14 +37,17 @@ def __init__(self, resource_name, layout):
def append_resource(self, resource):
self._resources.append(resource)
+ # pylint: disable=too-many-branches
def _filter_resources(self, all_resources, dev_bundles=False):
filtered_resources = []
for s in all_resources:
filtered_resource = {}
+ added = False
if 'namespace' in s:
filtered_resource['namespace'] = s['namespace']
if 'external_url' in s and not self.config.serve_locally:
filtered_resource['external_url'] = s['external_url']
+ filtered_resource['local_file'] = s['relative_package_path']
elif 'dev_package_path' in s and dev_bundles:
filtered_resource['relative_package_path'] = (
s['dev_package_path']
@@ -36,6 +60,9 @@ def _filter_resources(self, all_resources, dev_bundles=False):
filtered_resource['absolute_path'] = s['absolute_path']
elif 'asset_path' in s:
info = os.stat(s['filepath'])
+ filtered_resource['integrity'] = integrity_hash_from_file(
+ s['filepath'])
+ filtered_resource['crossorigin'] = 'anonymous'
filtered_resource['asset_path'] = s['asset_path']
filtered_resource['ts'] = info.st_mtime
elif self.config.serve_locally:
@@ -54,7 +81,41 @@ def _filter_resources(self, all_resources, dev_bundles=False):
)
)
- filtered_resources.append(filtered_resource)
+ if 'integrity' not in filtered_resource \
+ and 'namespace' in filtered_resource:
+ key, filename = first_key(
+ filtered_resource,
+ 'external_url',
+ 'dev_package_path',
+ 'relative_package_path'
+ )
+ if isinstance(filename, list):
+ # flatten these dependencies and add a hash for each.
+ for f in filename:
+ local_file = filtered_resource.get('local_file')
+ filename = find_unpkg(f, local_file) \
+ if local_file else f.split('/')[-1]
+ filtered_resources.append({
+ key: f,
+ 'integrity': integrity_hash_from_package(
+ filtered_resource['namespace'],
+ filename),
+ 'crossorigin': 'anonymous',
+ 'namespace': filtered_resource['namespace']
+ })
+ added = True
+ else:
+ filename = filtered_resource.get(
+ 'local_file', filename.split('/')[-1])
+ filtered_resource['integrity'] = \
+ integrity_hash_from_package(
+ filtered_resource['namespace'],
+ filename
+ )
+ filtered_resource['crossorigin'] = 'anonymous'
+
+ if not added:
+ filtered_resources.append(filtered_resource)
return filtered_resources