Repository: plotly/dash

Issue:
[BUG] + in version string breaks fingerprint system
**Describe your context**
-  replace the result of `pip list | grep dash` below
```
dash                                  1.5.1                           
dash-core-components                  1.4.0                           
dash-daq                              0.2.2                           
dash-html-components                  1.0.1                           
dash-renderer                         1.2.0                           
dash-table                            4.5.0  
```

**Describe the bug**

When going from `dash==1.4` to `dash==1.5`, we experienced a breaking change in the custom Dash components we use.

It took some hours to debug, but the reason was found to be related to the new "fingerprint" system in Dash. In our project, we use the [setuptools_scm](https://github.com/pypa/setuptools_scm) package (by the Python Packaging Authority) in order to have a versioning system that automatically is linked to the git repo tags. This makes continuous deployment to e.g. Pypi easy and robust wrt. keeping versions consistent.

I.e. instead of
```python
__version__ = package['version']
```
in the component package, we use something like
```
__version__ = get_distribution(__name__).version
```
This worked until `dash==1.5`, then it broke on non-release-versions due to automatic tags of the type
`1.0.0.dev5+af4304c.d20191103`, where the tag includes a `+`. See [the default tag formats](https://github.com/pypa/setuptools_scm#default-versioning-scheme).

Changing the line above to
```
__version__ = get_distribution(__name__).version.replace("+", ".")
```
is one workaround that gets the third party components to also work on `dash==1.5`

**Expected behavior**

`setuptools_scm` provided versions to work also in `dash>=1.5`.

**Suggested solution**

Change [this line](https://github.com/plotly/dash/blob/40b5357f262ac207f94ac980e6cb928d94df65b7/dash/fingerprint.py#L12) in Dash's `build_fingerprint` to also replace `+` with `_`?


Discussion:
Thanks @anders-kiaer 

@Marc-Andre-Rivet we could go for full symmetry with `cache_regex`:

```py
cache_regex = re.compile(r"^v[\w-]+m[0-9a-fA-F]+$")  # unchanged
version_clean = re.compile(r"[^\w-]")


def build_fingerprint(path, version, hash_value):
    path_parts = path.split("/")
    filename, extension = path_parts[-1].split(".", 1)

    return "{}.v{}m{}.{}".format(
        "/".join(path_parts[:-1] + [filename]),
        re.sub(version_clean, "_", str(version)),
        hash_value,
        extension,
    )
```

unless you can think of a scenario where we end up with matching cleaned strings for different versions... then I guess we'd need to somehow escape these chars rather than just setting them all to `_`.
The metadata should be the last of them..

major.minor.patch: https://semver.org/#spec-item-2
pre-release: https://semver.org/#spec-item-9
metadata: https://semver.org/#spec-item-10

@alexcjohnson 👍 for the suggested change above. In theory it would be possible to generate the same result string by appending the metadata to the pre-release string with `.` instead of `+` -- we could transform `+` instead two consecutive `_` to be safe, `_` not being allowed in either of the pre-release or metadata strings, same for consecutive `.`.

The paranoid version becomes:
```
str(version).replace(".", "_").replace("+", "__")
```
which is what I'm going for.

Will also need to update https://github.com/plotly/dash/blob/dev/@plotly/webpack-dash-dynamic-import/src/index.js#L8 - neither the DataTable or DCC use metadata in their release versions, so only the plugin needs to have a patch release. We can update usage across the board in the next minor.


Resolving patch:
diff --git a/dash/fingerprint.py b/dash/fingerprint.py
--- a/dash/fingerprint.py
+++ b/dash/fingerprint.py
@@ -1,7 +1,7 @@
 import re
 
 cache_regex = re.compile(r"^v[\w-]+m[0-9a-fA-F]+$")
-
+version_clean = re.compile(r"[^\w-]")
 
 def build_fingerprint(path, version, hash_value):
     path_parts = path.split("/")
@@ -9,7 +9,7 @@ def build_fingerprint(path, version, hash_value):
 
     return "{}.v{}m{}.{}".format(
         "/".join(path_parts[:-1] + [filename]),
-        str(version).replace(".", "_"),
+        re.sub(version_clean, "_", str(version)),
         hash_value,
         extension,
     )


Test patch:
diff --git a/tests/integration/devtools/test_props_check.py b/tests/integration/devtools/test_props_check.py
--- a/tests/integration/devtools/test_props_check.py
+++ b/tests/integration/devtools/test_props_check.py
@@ -216,7 +216,7 @@
 
 
 def test_dvpc001_prop_check_errors_with_path(dash_duo):
-    app = dash.Dash(__name__)
+    app = dash.Dash(__name__, eager_loading=True)
 
     app.layout = html.Div([html.Div(id="content"), dcc.Location(id="location")])
 
diff --git a/tests/unit/test_fingerprint.py b/tests/unit/test_fingerprint.py
--- a/tests/unit/test_fingerprint.py
+++ b/tests/unit/test_fingerprint.py
@@ -17,6 +17,12 @@
         "version": "1.1.1-alpha.1",
         "hash": "1234567890abcdef",
     },
+    {
+        "path": "react@16.8.6.min.js",
+        "fingerprint": "react@16.v1_1_1-alpha_x_y_y_X_Y_Z_1_2_3_metadata_xx_yy_zz_XX_YY_ZZ_11_22_33_mmm1234567890abcdefABCDEF.8.6.min.js",
+        "version": "1.1.1-alpha.x.y.y.X.Y.Z.1.2.3+metadata.xx.yy.zz.XX.YY.ZZ.11.22.33.mm",
+        "hash": "1234567890abcdefABCDEF",
+    },
     {"path": "dash.plotly.js", "fingerprint": "dash.v1m1.plotly.js"},
     {"path": "dash.plotly.j_s", "fingerprint": "dash.v1m1.plotly.j_s"},
     {"path": "dash.plotly.css", "fingerprint": "dash.v1m1.plotly.css"},