Repository: prometheus/client_python

Issue:
Using None as a label value breaks exposure of all metrics
If a metric is added and provided a label value of `None`, the response generated for scraping raises an AttributeError, preventing any attribute from being scraped, not just the misconfigured one.

Sample code:

```
from prometheus_client import Info, generate_latest

info_metric = Info("metric", "description")
info_metric.info({"label": None})
_ = generate_latest()
```

Output:

```
Traceback (most recent call last):
  File "/home/mol/git/tango/test_metrics.py", line 7, in <module>
    _ = generate_latest()
  File "/home/mol/miniconda3/lib/python3.10/site-packages/prometheus_client/exposition.py", line 282, in generate_latest
    output.append(sample_line(s))
  File "/home/mol/miniconda3/lib/python3.10/site-packages/prometheus_client/exposition.py", line 239, in sample_line
    ['{}="{}"'.format(
  File "/home/mol/miniconda3/lib/python3.10/site-packages/prometheus_client/exposition.py", line 240, in <listcomp>
    k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
AttributeError: ("'NoneType' object has no attribute 'replace'", Metric(metric, description, info, , [Sample(name='metric_info', labels={'label': None}, value=1.0, timestamp=None, exemplar=None)]))
```

The misconfigured metric not getting exposed is fine -- `None` is not a valid label value. But the other metrics not getting exposed either is painful. There's several easy ways to prevent this, any of those would do:

* detect label values of `None` (or non-strings) and raise a ValueError when they are set
* cast label value to string (`"None"` in this case) before parsing them as a string (use `str(k)` instead of `k` in the traceback below)
* filter out misconfigured metrics when generating the HTTP response

Since error checking is light in prometheus_client and duck-typing is a thing, one of the last two options seem most attractive?

Discussion:

Solution patch:
diff --git a/prometheus_client/metrics.py b/prometheus_client/metrics.py
index 91cd9ec..af51211 100644
--- a/prometheus_client/metrics.py
+++ b/prometheus_client/metrics.py
@@ -705,6 +705,8 @@ class Info(MetricWrapperBase):
         if self._labelname_set.intersection(val.keys()):
             raise ValueError('Overlapping labels for Info metric, metric: {} child: {}'.format(
                 self._labelnames, val))
+        if any(i is None for i in val.values()):
+            raise ValueError('Label value cannot be None')
         with self._lock:
             self._value = dict(val)
 


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index 30f9e0a..8a54a02 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -534,6 +534,7 @@ class TestInfo(unittest.TestCase):
 
     def test_labels(self):
         self.assertRaises(ValueError, self.labels.labels('a').info, {'l': ''})
+        self.assertRaises(ValueError, self.labels.labels('a').info, {'il': None})
 
         self.labels.labels('a').info({'foo': 'bar'})
         self.assertEqual(1, self.registry.get_sample_value('il_info', {'l': 'a', 'foo': 'bar'}))