Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-prometheus-client for 
openSUSE:Factory checked in at 2021-11-17 01:14:16
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-prometheus-client (Old)
 and      /work/SRC/openSUSE:Factory/.python-prometheus-client.new.1890 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-prometheus-client"

Wed Nov 17 01:14:16 2021 rev:2 rq:931776 version:0.12.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-prometheus-client/python-prometheus-client.changes
        2021-08-28 22:29:11.461979249 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-prometheus-client.new.1890/python-prometheus-client.changes
      2021-11-17 01:15:26.338191206 +0100
@@ -1,0 +2,11 @@
+Sat Nov 13 22:50:42 UTC 2021 - Michael Str??der <[email protected]>
+
+- Update to upstream 0.12.0 release
+  * [FEATURE] Exemplar support (excludes multiprocess) #669
+  * [ENHANCEMENT] Add support for Python 3.10 #706
+  * [ENHANCEMENT] Restricted Registry will handle metrics added after 
restricting #675, #680
+  * [ENHANCEMENT] Raise a more helpful error if a metric is not observable #666
+  * [BUGFIX] Fix instance_ip_grouping_key not working on MacOS #687
+  * [BUGFIX] Fix assertion error from favicion.ico with Python 2.7 #715
+
+-------------------------------------------------------------------

Old:
----
  v0.11.0.tar.gz

New:
----
  v0.12.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-prometheus-client.spec ++++++
--- /var/tmp/diff_new_pack.NsPkfV/_old  2021-11-17 01:15:26.982191237 +0100
+++ /var/tmp/diff_new_pack.NsPkfV/_new  2021-11-17 01:15:26.982191237 +0100
@@ -19,7 +19,7 @@
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 %bcond_without python2
 Name:           python-prometheus-client
-Version:        0.11.0
+Version:        0.12.0
 Release:        0
 Summary:        Python client for the Prometheus monitoring system
 License:        Apache-2.0

++++++ v0.11.0.tar.gz -> v0.12.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/client_python-0.11.0/.circleci/config.yml 
new/client_python-0.12.0/.circleci/config.yml
--- old/client_python-0.11.0/.circleci/config.yml       2021-06-01 
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/.circleci/config.yml       2021-10-29 
19:27:14.000000000 +0200
@@ -75,6 +75,7 @@
                  - "3.7"
                  - "3.8"
                  - "3.9"
+                 - "3.10"
       - test_nooptionals:
           matrix:
             parameters:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/client_python-0.11.0/README.md 
new/client_python-0.12.0/README.md
--- old/client_python-0.11.0/README.md  2021-06-01 19:19:33.000000000 +0200
+++ new/client_python-0.12.0/README.md  2021-10-29 19:27:14.000000000 +0200
@@ -231,6 +231,27 @@
 c.labels('post', '/submit')
 ```
 
+### Exemplars
+
+Exemplars can be added to counter and histogram metrics. Exemplars can be
+specified by passing a dict of label value pairs to be exposed as the exemplar.
+For example with a counter:
+
+```python
+from prometheus_client import Counter
+c = Counter('my_requests_total', 'HTTP Failures', ['method', 'endpoint'])
+c.labels('get', '/').inc(exemplar={'trace_id': 'abc123'})
+c.labels('post', '/submit').inc(1.0, {'trace_id': 'def456'})
+```
+
+And with a histogram:
+
+```python
+from prometheus_client import Histogram
+h = Histogram('request_latency_seconds', 'Description of histogram')
+h.observe(4.7, {'trace_id': 'abc123'})
+```
+
 ### Process Collector
 
 The Python client automatically exports metrics about process CPU usage, RAM,
@@ -510,6 +531,7 @@
 - Info and Enum metrics do not work
 - The pushgateway cannot be used
 - Gauges cannot use the `pid` label
+- Exemplars are not supported
 
 There's several steps to getting this working:
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/client_python-0.11.0/prometheus_client/exposition.py 
new/client_python-0.12.0/prometheus_client/exposition.py
--- old/client_python-0.11.0/prometheus_client/exposition.py    2021-06-01 
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/prometheus_client/exposition.py    2021-10-29 
19:27:14.000000000 +0200
@@ -34,7 +34,6 @@
 CONTENT_TYPE_LATEST = str('text/plain; version=0.0.4; charset=utf-8')
 """Content type of the latest text format"""
 PYTHON27_OR_OLDER = sys.version_info < (3, )
-PYTHON26_OR_OLDER = sys.version_info < (2, 7)
 PYTHON376_OR_NEWER = sys.version_info > (3, 7, 5)
 
 
@@ -115,8 +114,8 @@
         params = parse_qs(environ.get('QUERY_STRING', ''))
         if environ['PATH_INFO'] == '/favicon.ico':
             # Serve empty response for browsers
-            status = '200 OK'
-            header = ('', '')
+            status = str('200 OK')
+            header = (str(''), str(''))
             output = b''
         else:
             # Bake output
@@ -445,7 +444,7 @@
     gateway_url = urlparse(gateway)
     # See https://bugs.python.org/issue27657 for details on urlparse in 
py>=3.7.6.
     if not gateway_url.scheme or (
-            (PYTHON376_OR_NEWER or PYTHON26_OR_OLDER)
+            PYTHON376_OR_NEWER
             and gateway_url.scheme not in ['http', 'https']
     ):
         gateway = 'http://{0}'.format(gateway)
@@ -481,7 +480,16 @@
 def instance_ip_grouping_key():
     """Grouping key with instance set to the IP Address of this host."""
     with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as s:
-        s.connect(('localhost', 0))
+        if sys.platform == 'darwin':
+            # This check is done this way only on MacOS devices
+            # it is done this way because the localhost method does
+            # not work.
+            # This method was adapted from this StackOverflow answer:
+            # https://stackoverflow.com/a/28950776
+            s.connect(('10.255.255.255', 1))
+        else:
+            s.connect(('localhost', 0))
+
         return {'instance': s.getsockname()[0]}
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/client_python-0.11.0/prometheus_client/metrics.py 
new/client_python-0.12.0/prometheus_client/metrics.py
--- old/client_python-0.11.0/prometheus_client/metrics.py       2021-06-01 
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/prometheus_client/metrics.py       2021-10-29 
19:27:14.000000000 +0200
@@ -10,6 +10,7 @@
     RESERVED_METRIC_LABEL_NAME_RE,
 )
 from .registry import REGISTRY
+from .samples import Exemplar
 from .utils import floatToGoString, INF
 
 if sys.version_info > (3,):
@@ -36,18 +37,32 @@
     return full_name
 
 
+def _validate_labelname(l):
+    if not METRIC_LABEL_NAME_RE.match(l):
+        raise ValueError('Invalid label metric name: ' + l)
+    if RESERVED_METRIC_LABEL_NAME_RE.match(l):
+        raise ValueError('Reserved label metric name: ' + l)
+
+
 def _validate_labelnames(cls, labelnames):
     labelnames = tuple(labelnames)
     for l in labelnames:
-        if not METRIC_LABEL_NAME_RE.match(l):
-            raise ValueError('Invalid label metric name: ' + l)
-        if RESERVED_METRIC_LABEL_NAME_RE.match(l):
-            raise ValueError('Reserved label metric name: ' + l)
+        _validate_labelname(l)
         if l in cls._reserved_labelnames:
             raise ValueError('Reserved label metric name: ' + l)
     return labelnames
 
 
+def _validate_exemplar(exemplar):
+    runes = 0
+    for k, v in exemplar.items():
+        _validate_labelname(k)
+        runes += len(k)
+        runes += len(v)
+    if runes > 128:
+        raise ValueError('Exemplar labels have %d UTF-8 characters, exceeding 
the limit of 128')
+
+
 class MetricWrapperBase(object):
     _type = None
     _reserved_labelnames = ()
@@ -76,8 +91,8 @@
 
     def collect(self):
         metric = self._get_metric()
-        for suffix, labels, value in self._samples():
-            metric.add_sample(self._name + suffix, labels, value)
+        for suffix, labels, value, timestamp, exemplar in self._samples():
+            metric.add_sample(self._name + suffix, labels, value, timestamp, 
exemplar)
         return [metric]
 
     def __str__(self):
@@ -202,8 +217,8 @@
             metrics = self._metrics.copy()
         for labels, metric in metrics.items():
             series_labels = list(zip(self._labelnames, labels))
-            for suffix, sample_labels, value in metric._samples():
-                yield (suffix, dict(series_labels + 
list(sample_labels.items())), value)
+            for suffix, sample_labels, value, timestamp, exemplar in 
metric._samples():
+                yield (suffix, dict(series_labels + 
list(sample_labels.items())), value, timestamp, exemplar)
 
     def _child_samples(self):  # pragma: no cover
         raise NotImplementedError('_child_samples() must be implemented by %r' 
% self)
@@ -256,11 +271,15 @@
                                         self._labelvalues)
         self._created = time.time()
 
-    def inc(self, amount=1):
+    def inc(self, amount=1, exemplar=None):
         """Increment counter by the given amount."""
+        self._raise_if_not_observable()
         if amount < 0:
             raise ValueError('Counters can only be incremented by non-negative 
amounts.')
         self._value.inc(amount)
+        if exemplar:
+            _validate_exemplar(exemplar)
+            self._value.set_exemplar(Exemplar(exemplar, amount, time.time()))
 
     def count_exceptions(self, exception=Exception):
         """Count exceptions in a block of code or function.
@@ -274,8 +293,8 @@
 
     def _child_samples(self):
         return (
-            ('_total', {}, self._value.get()),
-            ('_created', {}, self._created),
+            ('_total', {}, self._value.get(), None, 
self._value.get_exemplar()),
+            ('_created', {}, self._created, None, None),
         )
 
 
@@ -353,14 +372,17 @@
 
     def inc(self, amount=1):
         """Increment gauge by the given amount."""
+        self._raise_if_not_observable()
         self._value.inc(amount)
 
     def dec(self, amount=1):
         """Decrement gauge by the given amount."""
+        self._raise_if_not_observable()
         self._value.inc(-amount)
 
     def set(self, value):
         """Set gauge to the given value."""
+        self._raise_if_not_observable()
         self._value.set(float(value))
 
     def set_to_current_time(self):
@@ -392,13 +414,15 @@
         multiple threads. All other methods of the Gauge become NOOPs.
         """
 
+        self._raise_if_not_observable()
+
         def samples(self):
-            return (('', {}, float(f())),)
+            return (('', {}, float(f()), None, None),)
 
         self._child_samples = create_bound_method(samples, self)
 
     def _child_samples(self):
-        return (('', {}, self._value.get()),)
+        return (('', {}, self._value.get(), None, None),)
 
 
 class Summary(MetricWrapperBase):
@@ -450,6 +474,7 @@
         
https://prometheus.io/docs/practices/histograms/#count-and-sum-of-observations
         for details.
         """
+        self._raise_if_not_observable()
         self._count.inc(1)
         self._sum.inc(amount)
 
@@ -463,9 +488,10 @@
 
     def _child_samples(self):
         return (
-            ('_count', {}, self._count.get()),
-            ('_sum', {}, self._sum.get()),
-            ('_created', {}, self._created))
+            ('_count', {}, self._count.get(), None, None),
+            ('_sum', {}, self._sum.get(), None, None),
+            ('_created', {}, self._created, None, None),
+        )
 
 
 class Histogram(MetricWrapperBase):
@@ -557,7 +583,7 @@
                 self._labelvalues + (floatToGoString(b),))
             )
 
-    def observe(self, amount):
+    def observe(self, amount, exemplar=None):
         """Observe the given amount.
 
         The amount is usually positive or zero. Negative values are
@@ -567,10 +593,14 @@
         
https://prometheus.io/docs/practices/histograms/#count-and-sum-of-observations
         for details.
         """
+        self._raise_if_not_observable()
         self._sum.inc(amount)
         for i, bound in enumerate(self._upper_bounds):
             if amount <= bound:
                 self._buckets[i].inc(1)
+                if exemplar:
+                    _validate_exemplar(exemplar)
+                    self._buckets[i].set_exemplar(Exemplar(exemplar, amount, 
time.time()))
                 break
 
     def time(self):
@@ -585,11 +615,11 @@
         acc = 0
         for i, bound in enumerate(self._upper_bounds):
             acc += self._buckets[i].get()
-            samples.append(('_bucket', {'le': floatToGoString(bound)}, acc))
-        samples.append(('_count', {}, acc))
+            samples.append(('_bucket', {'le': floatToGoString(bound)}, acc, 
None, self._buckets[i].get_exemplar()))
+        samples.append(('_count', {}, acc, None, None))
         if self._upper_bounds[0] >= 0:
-            samples.append(('_sum', {}, self._sum.get()))
-        samples.append(('_created', {}, self._created))
+            samples.append(('_sum', {}, self._sum.get(), None, None))
+        samples.append(('_created', {}, self._created, None, None))
         return tuple(samples)
 
 
@@ -626,7 +656,7 @@
 
     def _child_samples(self):
         with self._lock:
-            return (('_info', self._value, 1.0,),)
+            return (('_info', self._value, 1.0, None, None),)
 
 
 class Enum(MetricWrapperBase):
@@ -684,7 +714,7 @@
     def _child_samples(self):
         with self._lock:
             return [
-                ('', {self._name: s}, 1 if i == self._value else 0,)
+                ('', {self._name: s}, 1 if i == self._value else 0, None, None)
                 for i, s
                 in enumerate(self._states)
             ]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/client_python-0.11.0/prometheus_client/metrics_core.py 
new/client_python-0.12.0/prometheus_client/metrics_core.py
--- old/client_python-0.11.0/prometheus_client/metrics_core.py  2021-06-01 
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/prometheus_client/metrics_core.py  2021-10-29 
19:27:14.000000000 +0200
@@ -58,6 +58,15 @@
             self.samples,
         )
 
+    def _restricted_metric(self, names):
+        """Build a snapshot of a metric with samples restricted to a given set 
of names."""
+        samples = [s for s in self.samples if s[0] in names]
+        if samples:
+            m = Metric(self.name, self.documentation, self.type)
+            m.samples = samples
+            return m
+        return None
+
 
 class UnknownMetricFamily(Metric):
     """A single unknown metric and its samples.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/client_python-0.11.0/prometheus_client/openmetrics/exposition.py 
new/client_python-0.12.0/prometheus_client/openmetrics/exposition.py
--- old/client_python-0.11.0/prometheus_client/openmetrics/exposition.py        
2021-06-01 19:19:33.000000000 +0200
+++ new/client_python-0.12.0/prometheus_client/openmetrics/exposition.py        
2021-10-29 19:27:14.000000000 +0200
@@ -8,6 +8,14 @@
 """Content type of the latest OpenMetrics text format"""
 
 
+def _is_valid_exemplar_metric(metric, sample):
+    if metric.type == 'counter' and sample.name.endswith('_total'):
+        return True
+    if metric.type in ('histogram', 'gaugehistogram') and 
sample.name.endswith('_bucket'):
+        return True
+    return False
+
+
 def generate_latest(registry):
     '''Returns the metrics from the registry in latest text format as a 
string.'''
     output = []
@@ -28,8 +36,8 @@
                 else:
                     labelstr = ''
                 if s.exemplar:
-                    if metric.type not in ('histogram', 'gaugehistogram') or 
not s.name.endswith('_bucket'):
-                        raise ValueError("Metric {0} has exemplars, but is not 
a histogram bucket".format(metric.name))
+                    if not _is_valid_exemplar_metric(metric, s):
+                        raise ValueError("Metric {0} has exemplars, but is not 
a histogram bucket or counter".format(metric.name))
                     labels = '{{{0}}}'.format(','.join(
                         ['{0}="{1}"'.format(
                             k, v.replace('\\', r'\\').replace('\n', 
r'\n').replace('"', r'\"'))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/client_python-0.11.0/prometheus_client/registry.py 
new/client_python-0.12.0/prometheus_client/registry.py
--- old/client_python-0.11.0/prometheus_client/registry.py      2021-06-01 
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/prometheus_client/registry.py      2021-10-29 
19:27:14.000000000 +0200
@@ -94,28 +94,7 @@
 
         Experimental."""
         names = set(names)
-        collectors = set()
-        metrics = []
-        with self._lock:
-            if 'target_info' in names and self._target_info:
-                metrics.append(self._target_info_metric())
-                names.remove('target_info')
-            for name in names:
-                if name in self._names_to_collectors:
-                    collectors.add(self._names_to_collectors[name])
-        for collector in collectors:
-            for metric in collector.collect():
-                samples = [s for s in metric.samples if s[0] in names]
-                if samples:
-                    m = Metric(metric.name, metric.documentation, metric.type)
-                    m.samples = samples
-                    metrics.append(m)
-
-        class RestrictedRegistry(object):
-            def collect(self):
-                return metrics
-
-        return RestrictedRegistry()
+        return RestrictedRegistry(names, self)
 
     def set_target_info(self, labels):
         with self._lock:
@@ -150,4 +129,27 @@
         return None
 
 
+class RestrictedRegistry(object):
+    def __init__(self, names, registry):
+        self._name_set = set(names)
+        self._registry = registry
+
+    def collect(self):
+        collectors = set()
+        target_info_metric = None
+        with self._registry._lock:
+            if 'target_info' in self._name_set and self._registry._target_info:
+                target_info_metric = self._registry._target_info_metric()
+            for name in self._name_set:
+                if name != 'target_info' and name in 
self._registry._names_to_collectors:
+                    collectors.add(self._registry._names_to_collectors[name])
+        if target_info_metric:
+            yield target_info_metric
+        for collector in collectors:
+            for metric in collector.collect():
+                m = metric._restricted_metric(self._name_set)
+                if m:
+                    yield m
+
+
 REGISTRY = CollectorRegistry(auto_describe=True)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/client_python-0.11.0/prometheus_client/values.py 
new/client_python-0.12.0/prometheus_client/values.py
--- old/client_python-0.11.0/prometheus_client/values.py        2021-06-01 
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/prometheus_client/values.py        2021-10-29 
19:27:14.000000000 +0200
@@ -14,6 +14,7 @@
 
     def __init__(self, typ, metric_name, name, labelnames, labelvalues, 
**kwargs):
         self._value = 0.0
+        self._exemplar = None
         self._lock = Lock()
 
     def inc(self, amount):
@@ -24,10 +25,18 @@
         with self._lock:
             self._value = value
 
+    def set_exemplar(self, exemplar):
+        with self._lock:
+            self._exemplar = exemplar
+
     def get(self):
         with self._lock:
             return self._value
 
+    def get_exemplar(self):
+        with self._lock:
+            return self._exemplar
+
 
 def MultiProcessValue(process_identifier=os.getpid):
     """Returns a MmapedValue class based on a process_identifier function.
@@ -100,11 +109,19 @@
                 self._value = value
                 self._file.write_value(self._key, self._value)
 
+        def set_exemplar(self, exemplar):
+            # TODO: Implement exemplars for multiprocess mode.
+            return
+
         def get(self):
             with lock:
                 self.__check_for_pid_change()
                 return self._value
 
+        def get_exemplar(self):
+            # TODO: Implement exemplars for multiprocess mode.
+            return None
+
     return MmapedValue
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/client_python-0.11.0/setup.py 
new/client_python-0.12.0/setup.py
--- old/client_python-0.11.0/setup.py   2021-06-01 19:19:33.000000000 +0200
+++ new/client_python-0.12.0/setup.py   2021-10-29 19:27:14.000000000 +0200
@@ -1,18 +1,14 @@
 from os import path
-import sys
 
 from setuptools import setup
 
-if sys.version_info >= (2, 7):
-    with open(path.join(path.abspath(path.dirname(__file__)), 'README.md')) as 
f:
-        long_description = f.read()
-else:  # Assuming we don't run setup in order to publish under python 2.6
-    long_description = "NA"
+with open(path.join(path.abspath(path.dirname(__file__)), 'README.md')) as f:
+    long_description = f.read()
 
 
 setup(
     name="prometheus_client",
-    version="0.11.0",
+    version="0.12.0",
     author="Brian Brazil",
     author_email="[email protected]",
     description="Python client for the Prometheus monitoring system.",
@@ -47,6 +43,7 @@
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
         "Programming Language :: Python :: Implementation :: CPython",
         "Programming Language :: Python :: Implementation :: PyPy",
         "Topic :: System :: Monitoring",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/client_python-0.11.0/tests/openmetrics/test_exposition.py 
new/client_python-0.12.0/tests/openmetrics/test_exposition.py
--- old/client_python-0.11.0/tests/openmetrics/test_exposition.py       
2021-06-01 19:19:33.000000000 +0200
+++ new/client_python-0.12.0/tests/openmetrics/test_exposition.py       
2021-10-29 19:27:14.000000000 +0200
@@ -1,7 +1,7 @@
 from __future__ import unicode_literals
 
-import sys
 import time
+import unittest
 
 from prometheus_client import (
     CollectorRegistry, Counter, Enum, Gauge, Histogram, Info, Metric, Summary,
@@ -11,12 +11,6 @@
 )
 from prometheus_client.openmetrics.exposition import generate_latest
 
-if sys.version_info < (2, 7):
-    # We need the skip decorators from unittest2 on Python 2.6.
-    import unittest2 as unittest
-else:
-    import unittest
-
 
 class TestGenerateText(unittest.TestCase):
     def setUp(self):
@@ -70,7 +64,6 @@
 # EOF
 """, generate_latest(self.registry))
 
-    @unittest.skipIf(sys.version_info < (2, 7), "Test requires Python 2.7+.")
     def test_histogram(self):
         s = Histogram('hh', 'A histogram', registry=self.registry)
         s.observe(0.05)
@@ -114,29 +107,36 @@
 """, generate_latest(self.registry))
 
     def test_histogram_exemplar(self):
-        class MyCollector(object):
-            def collect(self):
-                metric = Metric("hh", "help", 'histogram')
-                # This is not sane, but it covers all the cases.
-                metric.add_sample("hh_bucket", {"le": "1"}, 0, None, 
Exemplar({'a': 'b'}, 0.5))
-                metric.add_sample("hh_bucket", {"le": "2"}, 0, None, 
Exemplar({'le': '7'}, 0.5, 12))
-                metric.add_sample("hh_bucket", {"le": "3"}, 0, 123, 
Exemplar({'a': 'b'}, 2.5, 12))
-                metric.add_sample("hh_bucket", {"le": "4"}, 0, None, 
Exemplar({'a': '\n"\\'}, 3.5))
-                metric.add_sample("hh_bucket", {"le": "+Inf"}, 0, None, None)
-                yield metric
-
-        self.registry.register(MyCollector())
-        self.assertEqual(b"""# HELP hh help
+        s = Histogram('hh', 'A histogram', buckets=[1, 2, 3, 4], 
registry=self.registry)
+        s.observe(0.5, {'a': 'b'})
+        s.observe(1.5, {'le': '7'})
+        s.observe(2.5, {'a': 'b'})
+        s.observe(3.5, {'a': '\n"\\'})
+        print(generate_latest(self.registry))
+        self.assertEqual(b"""# HELP hh A histogram
 # TYPE hh histogram
-hh_bucket{le="1"} 0.0 # {a="b"} 0.5
-hh_bucket{le="2"} 0.0 # {le="7"} 0.5 12
-hh_bucket{le="3"} 0.0 123 # {a="b"} 2.5 12
-hh_bucket{le="4"} 0.0 # {a="\\n\\"\\\\"} 3.5
-hh_bucket{le="+Inf"} 0.0
+hh_bucket{le="1.0"} 1.0 # {a="b"} 0.5 123.456
+hh_bucket{le="2.0"} 2.0 # {le="7"} 1.5 123.456
+hh_bucket{le="3.0"} 3.0 # {a="b"} 2.5 123.456
+hh_bucket{le="4.0"} 4.0 # {a="\\n\\"\\\\"} 3.5 123.456
+hh_bucket{le="+Inf"} 4.0
+hh_count 4.0
+hh_sum 8.0
+hh_created 123.456
+# EOF
+""", generate_latest(self.registry))
+
+    def test_counter_exemplar(self):
+        c = Counter('cc', 'A counter', registry=self.registry)
+        c.inc(exemplar={'a': 'b'})
+        self.assertEqual(b"""# HELP cc A counter
+# TYPE cc counter
+cc_total 1.0 # {a="b"} 1.0 123.456
+cc_created 123.456
 # EOF
 """, generate_latest(self.registry))
 
-    def test_nonhistogram_exemplar(self):
+    def test_untyped_exemplar(self):
         class MyCollector(object):
             def collect(self):
                 metric = Metric("hh", "help", 'untyped')
@@ -148,7 +148,7 @@
         with self.assertRaises(ValueError):
             generate_latest(self.registry)
 
-    def test_nonhistogram_bucket_exemplar(self):
+    def test_histogram_non_bucket_exemplar(self):
         class MyCollector(object):
             def collect(self):
                 metric = Metric("hh", "help", 'histogram')
@@ -157,6 +157,18 @@
                 yield metric
 
         self.registry.register(MyCollector())
+        with self.assertRaises(ValueError):
+            generate_latest(self.registry)
+
+    def test_counter_non_total_exemplar(self):
+        class MyCollector(object):
+            def collect(self):
+                metric = Metric("cc", "A counter", 'counter')
+                metric.add_sample("cc_total", {}, 1, None, None)
+                metric.add_sample("cc_created", {}, 123.456, None, 
Exemplar({'a': 'b'}, 1.0, 123.456))
+                yield metric
+
+        self.registry.register(MyCollector())
         with self.assertRaises(ValueError):
             generate_latest(self.registry)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/client_python-0.11.0/tests/openmetrics/test_parser.py 
new/client_python-0.12.0/tests/openmetrics/test_parser.py
--- old/client_python-0.11.0/tests/openmetrics/test_parser.py   2021-06-01 
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/tests/openmetrics/test_parser.py   2021-10-29 
19:27:14.000000000 +0200
@@ -2,6 +2,7 @@
 
 import math
 import sys
+import unittest
 
 from prometheus_client.core import (
     CollectorRegistry, CounterMetricFamily, Exemplar,
@@ -12,12 +13,6 @@
 from prometheus_client.openmetrics.exposition import generate_latest
 from prometheus_client.openmetrics.parser import text_string_to_metric_families
 
-if sys.version_info < (2, 7):
-    # We need the skip decorators from unittest2 on Python 2.6.
-    import unittest2 as unittest
-else:
-    import unittest
-
 
 class TestParse(unittest.TestCase):
 
@@ -549,7 +544,6 @@
                     mock2.assert_not_called()
                     mock3.assert_called_once_with('1')
 
-    @unittest.skipIf(sys.version_info < (2, 7), "Test requires Python 2.7+.")
     def test_roundtrip(self):
         text = """# HELP go_gc_duration_seconds A summary of the GC invocation 
durations.
 # TYPE go_gc_duration_seconds summary
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/client_python-0.11.0/tests/test_asgi.py 
new/client_python-0.12.0/tests/test_asgi.py
--- old/client_python-0.11.0/tests/test_asgi.py 2021-06-01 19:19:33.000000000 
+0200
+++ new/client_python-0.12.0/tests/test_asgi.py 2021-10-29 19:27:14.000000000 
+0200
@@ -1,16 +1,10 @@
 from __future__ import absolute_import, unicode_literals
 
-import sys
-from unittest import TestCase
+from unittest import skipUnless, TestCase
 
 from prometheus_client import CollectorRegistry, Counter
 from prometheus_client.exposition import CONTENT_TYPE_LATEST
 
-if sys.version_info < (2, 7):
-    from unittest2 import skipUnless
-else:
-    from unittest import skipUnless
-
 try:
     # Python >3.5 only
     import asyncio
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/client_python-0.11.0/tests/test_core.py 
new/client_python-0.12.0/tests/test_core.py
--- old/client_python-0.11.0/tests/test_core.py 2021-06-01 19:19:33.000000000 
+0200
+++ new/client_python-0.12.0/tests/test_core.py 2021-10-29 19:27:14.000000000 
+0200
@@ -1,6 +1,8 @@
+# coding=utf-8
 from __future__ import unicode_literals
 
 from concurrent.futures import ThreadPoolExecutor
+import sys
 import time
 
 import pytest
@@ -19,6 +21,21 @@
     import unittest
 
 
+def assert_not_observable(fn, *args, **kwargs):
+    """
+    Assert that a function call falls with a ValueError exception containing
+    'missing label values'
+    """
+
+    try:
+        fn(*args, **kwargs)
+    except ValueError as e:
+        assert 'missing label values' in str(e)
+        return
+
+    assert False, "Did not raise a 'missing label values' exception"
+
+
 class TestCounter(unittest.TestCase):
     def setUp(self):
         self.registry = CollectorRegistry()
@@ -30,13 +47,12 @@
         self.assertEqual(1, self.registry.get_sample_value('c_total'))
         self.counter.inc(7)
         self.assertEqual(8, self.registry.get_sample_value('c_total'))
-    
+
     def test_repr(self):
         self.assertEqual(repr(self.counter), 
"prometheus_client.metrics.Counter(c)")
 
     def test_negative_increment_raises(self):
         self.assertRaises(ValueError, self.counter.inc, -1)
-    
 
     def test_function_decorator(self):
         @self.counter.count_exceptions(ValueError)
@@ -76,18 +92,43 @@
 
     def test_count_exceptions_not_observable(self):
         counter = Counter('counter', 'help', labelnames=('label',), 
registry=self.registry)
+        assert_not_observable(counter.count_exceptions)
 
-        try:
-            counter.count_exceptions()
-        except ValueError as e:
-            self.assertIn('missing label values', str(e))
+    def test_inc_not_observable(self):
+        """.inc() must fail if the counter is not observable."""
+
+        counter = Counter('counter', 'help', labelnames=('label',), 
registry=self.registry)
+        assert_not_observable(counter.inc)
+
+    def test_exemplar_invalid_label_name(self):
+        self.assertRaises(ValueError, self.counter.inc, exemplar={':o)': 
'smile'})
+        self.assertRaises(ValueError, self.counter.inc, exemplar={'1': 
'number'})
+
+    def test_exemplar_unicode(self):
+        # 128 characters should not raise, even using characters larger than 1 
byte.
+        self.counter.inc(exemplar={
+            'abcdefghijklmnopqrstuvwxyz': '26+16 characters',
+            'x123456': '7+15 characters',
+            'zyxwvutsrqponmlkjihgfedcba': '26+16 characters',
+            'unicode': '7+15 chars    ???',
+        })
+
+    def test_exemplar_too_long(self):
+        # 129 characters should fail.
+        self.assertRaises(ValueError, self.counter.inc, exemplar={
+            'abcdefghijklmnopqrstuvwxyz': '26+16 characters',
+            'x1234567': '8+15 characters',
+            'zyxwvutsrqponmlkjihgfedcba': '26+16 characters',
+            'y123456': '7+15 characters',
+        })
 
 
 class TestGauge(unittest.TestCase):
     def setUp(self):
         self.registry = CollectorRegistry()
         self.gauge = Gauge('g', 'help', registry=self.registry)
-    
+        self.gauge_with_label = Gauge('g2', 'help', labelnames=("label1",), 
registry=self.registry)
+
     def test_repr(self):
         self.assertEqual(repr(self.gauge), 
"prometheus_client.metrics.Gauge(g)")
 
@@ -100,6 +141,21 @@
         self.gauge.set(9)
         self.assertEqual(9, self.registry.get_sample_value('g'))
 
+    def test_inc_not_observable(self):
+        """.inc() must fail if the gauge is not observable."""
+
+        assert_not_observable(self.gauge_with_label.inc)
+
+    def test_dec_not_observable(self):
+        """.dec() must fail if the gauge is not observable."""
+
+        assert_not_observable(self.gauge_with_label.dec)
+
+    def test_set_not_observable(self):
+        """.set() must fail if the gauge is not observable."""
+
+        assert_not_observable(self.gauge_with_label.set, 1)
+
     def test_inprogress_function_decorator(self):
         self.assertEqual(0, self.registry.get_sample_value('g'))
 
@@ -127,6 +183,11 @@
         x['a'] = None
         self.assertEqual(1, self.registry.get_sample_value('g'))
 
+    def test_set_function_not_observable(self):
+        """.set_function() must fail if the gauge is not observable."""
+
+        assert_not_observable(self.gauge_with_label.set_function, lambda: 1)
+
     def test_time_function_decorator(self):
         self.assertEqual(0, self.registry.get_sample_value('g'))
 
@@ -167,25 +228,18 @@
 
     def test_track_in_progress_not_observable(self):
         g = Gauge('test', 'help', labelnames=('label',), 
registry=self.registry)
-
-        try:
-            g.track_inprogress()
-        except ValueError as e:
-            self.assertIn('missing label values', str(e))
+        assert_not_observable(g.track_inprogress)
 
     def test_timer_not_observable(self):
         g = Gauge('test', 'help', labelnames=('label',), 
registry=self.registry)
-
-        try:
-            g.time()
-        except ValueError as e:
-            self.assertIn('missing label values', str(e))
+        assert_not_observable(g.time)
 
 
 class TestSummary(unittest.TestCase):
     def setUp(self):
         self.registry = CollectorRegistry()
         self.summary = Summary('s', 'help', registry=self.registry)
+        self.summary_with_labels = Summary('s_with_labels', 'help', 
labelnames=("label1",), registry=self.registry)
 
     def test_repr(self):
         self.assertEqual(repr(self.summary), 
"prometheus_client.metrics.Summary(s)")
@@ -197,6 +251,10 @@
         self.assertEqual(1, self.registry.get_sample_value('s_count'))
         self.assertEqual(10, self.registry.get_sample_value('s_sum'))
 
+    def test_summary_not_observable(self):
+        """.observe() must fail if the Summary is not observable."""
+        assert_not_observable(self.summary_with_labels.observe, 1)
+
     def test_function_decorator(self):
         self.assertEqual(0, self.registry.get_sample_value('s_count'))
 
@@ -267,10 +325,7 @@
     def test_timer_not_observable(self):
         s = Summary('test', 'help', labelnames=('label',), 
registry=self.registry)
 
-        try:
-            s.time()
-        except ValueError as e:
-            self.assertIn('missing label values', str(e))
+        assert_not_observable(s.time)
 
 
 class TestHistogram(unittest.TestCase):
@@ -315,6 +370,10 @@
         self.assertEqual(3, self.registry.get_sample_value('h_count'))
         self.assertEqual(float("inf"), self.registry.get_sample_value('h_sum'))
 
+    def test_histogram_not_observable(self):
+        """.observe() must fail if the Summary is not observable."""
+        assert_not_observable(self.labels.observe, 1)
+
     def test_setting_buckets(self):
         h = Histogram('h', 'help', registry=None, buckets=[0, 1, 2])
         self.assertEqual([0.0, 1.0, 2.0, float("inf")], h._upper_bounds)
@@ -380,6 +439,19 @@
         self.assertEqual(1, self.registry.get_sample_value('h_count'))
         self.assertEqual(1, self.registry.get_sample_value('h_bucket', {'le': 
'+Inf'}))
 
+    def test_exemplar_invalid_label_name(self):
+        self.assertRaises(ValueError, self.histogram.observe, 3.0, 
exemplar={':o)': 'smile'})
+        self.assertRaises(ValueError, self.histogram.observe, 3.0, 
exemplar={'1': 'number'})
+
+    def test_exemplar_too_long(self):
+        # 129 characters in total should fail.
+        self.assertRaises(ValueError, self.histogram.observe, 1.0, exemplar={
+            'abcdefghijklmnopqrstuvwxyz': '26+16 characters',
+            'x1234567': '8+15 characters',
+            'zyxwvutsrqponmlkjihgfedcba': '26+16 characters',
+            'y123456': '7+15 characters',
+        })
+
 
 class TestInfo(unittest.TestCase):
     def setUp(self):
@@ -761,7 +833,19 @@
 
         m = Metric('s', 'help', 'summary')
         m.samples = [Sample('s_sum', {}, 7)]
-        self.assertEqual([m], 
registry.restricted_registry(['s_sum']).collect())
+        self.assertEqual([m], 
list(registry.restricted_registry(['s_sum']).collect()))
+
+    def test_restricted_registry_adds_new_metrics(self):
+        registry = CollectorRegistry()
+        Counter('c_total', 'help', registry=registry)
+
+        restricted_registry = registry.restricted_registry(['s_sum'])
+
+        Summary('s', 'help', registry=registry).observe(7)
+        m = Metric('s', 'help', 'summary')
+        m.samples = [Sample('s_sum', {}, 7)]
+
+        self.assertEqual([m], list(restricted_registry.collect()))
 
     def test_target_info_injected(self):
         registry = CollectorRegistry(target_info={'foo': 'bar'})
@@ -785,11 +869,38 @@
 
         m = Metric('s', 'help', 'summary')
         m.samples = [Sample('s_sum', {}, 7)]
-        self.assertEqual([m], 
registry.restricted_registry(['s_sum']).collect())
+        self.assertEqual([m], 
list(registry.restricted_registry(['s_sum']).collect()))
+
+        m = Metric('target', 'Target metadata', 'info')
+        m.samples = [Sample('target_info', {'foo': 'bar'}, 1)]
+        self.assertEqual([m], 
list(registry.restricted_registry(['target_info']).collect()))
+
+    @unittest.skipIf(sys.version_info < (3, 3), "Test requires Python 3.3+.")
+    def test_restricted_registry_does_not_call_extra(self):
+        from unittest.mock import MagicMock
+        registry = CollectorRegistry()
+        mock_collector = MagicMock()
+        mock_collector.describe.return_value = [Metric('foo', 'help', 
'summary')]
+        registry.register(mock_collector)
+        Summary('s', 'help', registry=registry).observe(7)
+
+        m = Metric('s', 'help', 'summary')
+        m.samples = [Sample('s_sum', {}, 7)]
+        self.assertEqual([m], 
list(registry.restricted_registry(['s_sum']).collect()))
+        mock_collector.collect.assert_not_called()
+
+    def test_restricted_registry_does_not_yield_while_locked(self):
+        registry = CollectorRegistry(target_info={'foo': 'bar'})
+        Summary('s', 'help', registry=registry).observe(7)
+
+        m = Metric('s', 'help', 'summary')
+        m.samples = [Sample('s_sum', {}, 7)]
+        self.assertEqual([m], 
list(registry.restricted_registry(['s_sum']).collect()))
 
         m = Metric('target', 'Target metadata', 'info')
         m.samples = [Sample('target_info', {'foo': 'bar'}, 1)]
-        self.assertEqual([m], 
registry.restricted_registry(['target_info']).collect())
+        for _ in registry.restricted_registry(['target_info', 
's_sum']).collect():
+            self.assertFalse(registry._lock.locked())
 
 
 if __name__ == '__main__':
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/client_python-0.11.0/tests/test_exposition.py 
new/client_python-0.12.0/tests/test_exposition.py
--- old/client_python-0.11.0/tests/test_exposition.py   2021-06-01 
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/tests/test_exposition.py   2021-10-29 
19:27:14.000000000 +0200
@@ -1,8 +1,8 @@
 from __future__ import unicode_literals
 
-import sys
 import threading
 import time
+import unittest
 
 import pytest
 
@@ -17,12 +17,6 @@
     passthrough_redirect_handler,
 )
 
-if sys.version_info < (2, 7):
-    # We need the skip decorators from unittest2 on Python 2.6.
-    import unittest2 as unittest
-else:
-    import unittest
-
 try:
     from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
 except ImportError:
@@ -98,7 +92,6 @@
 ss_created{a="c",b="d"} 123.456
 """, generate_latest(self.registry))
 
-    @unittest.skipIf(sys.version_info < (2, 7), "Test requires Python 2.7+.")
     def test_histogram(self):
         s = Histogram('hh', 'A histogram', registry=self.registry)
         s.observe(0.05)
@@ -376,10 +369,6 @@
         # ensure the redirect took place at the expected redirect location.
         self.assertEqual(self.requests[1][0].path, "/" + self.redirect_flag)
 
-    @unittest.skipIf(
-        sys.platform == "darwin",
-        "instance_ip_grouping_key() does not work on macOS."
-    )
     def test_instance_ip_grouping_key(self):
         self.assertTrue('' != instance_ip_grouping_key()['instance'])
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/client_python-0.11.0/tests/test_gc_collector.py 
new/client_python-0.12.0/tests/test_gc_collector.py
--- old/client_python-0.11.0/tests/test_gc_collector.py 2021-06-01 
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/tests/test_gc_collector.py 2021-10-29 
19:27:14.000000000 +0200
@@ -3,12 +3,7 @@
 import gc
 import platform
 import sys
-
-if sys.version_info < (2, 7):
-    # We need the skip decorators from unittest2 on Python 2.6.
-    import unittest2 as unittest
-else:
-    import unittest
+import unittest
 
 from prometheus_client import CollectorRegistry, GCCollector
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/client_python-0.11.0/tests/test_multiprocess.py 
new/client_python-0.12.0/tests/test_multiprocess.py
--- old/client_python-0.11.0/tests/test_multiprocess.py 2021-06-01 
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/tests/test_multiprocess.py 2021-10-29 
19:27:14.000000000 +0200
@@ -3,8 +3,8 @@
 import glob
 import os
 import shutil
-import sys
 import tempfile
+import unittest
 import warnings
 
 from prometheus_client import mmap_dict, values
@@ -18,12 +18,6 @@
     get_value_class, MultiProcessValue, MutexValue,
 )
 
-if sys.version_info < (2, 7):
-    # We need the skip decorators from unittest2 on Python 2.6.
-    import unittest2 as unittest
-else:
-    import unittest
-
 
 class TestMultiProcessDeprecation(unittest.TestCase):
     def setUp(self):
@@ -196,7 +190,6 @@
         c3 = Counter('c3', 'c3', registry=None)
         self.assertEqual(files(), ['counter_0.db', 'counter_1.db'])
 
-    @unittest.skipIf(sys.version_info < (2, 7), "Test requires Python 2.7+.")
     def test_collect(self):
         pid = 0
         values.ValueClass = MultiProcessValue(lambda: pid)
@@ -257,7 +250,6 @@
 
         self.assertEqual(metrics['h'].samples, expected_histogram)
 
-    @unittest.skipIf(sys.version_info < (2, 7), "Test requires Python 2.7+.")
     def test_merge_no_accumulate(self):
         pid = 0
         values.ValueClass = MultiProcessValue(lambda: pid)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/client_python-0.11.0/tests/test_parser.py 
new/client_python-0.12.0/tests/test_parser.py
--- old/client_python-0.11.0/tests/test_parser.py       2021-06-01 
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/tests/test_parser.py       2021-10-29 
19:27:14.000000000 +0200
@@ -1,7 +1,7 @@
 from __future__ import unicode_literals
 
 import math
-import sys
+import unittest
 
 from prometheus_client.core import (
     CollectorRegistry, CounterMetricFamily, GaugeMetricFamily,
@@ -10,12 +10,6 @@
 from prometheus_client.exposition import generate_latest
 from prometheus_client.parser import text_string_to_metric_families
 
-if sys.version_info < (2, 7):
-    # We need the skip decorators from unittest2 on Python 2.6.
-    import unittest2 as unittest
-else:
-    import unittest
-
 
 class TestParse(unittest.TestCase):
     def assertEqualMetrics(self, first, second, msg=None):
@@ -289,7 +283,6 @@
         b.add_metric([], 88, timestamp=1234566)
         self.assertEqualMetrics([a, b], list(families))
 
-    @unittest.skipIf(sys.version_info < (2, 7), "Test requires Python 2.7+.")
     def test_roundtrip(self):
         text = """# HELP go_gc_duration_seconds A summary of the GC invocation 
durations.
 # TYPE go_gc_duration_seconds summary
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/client_python-0.11.0/tests/test_twisted.py 
new/client_python-0.12.0/tests/test_twisted.py
--- old/client_python-0.11.0/tests/test_twisted.py      2021-06-01 
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/tests/test_twisted.py      2021-10-29 
19:27:14.000000000 +0200
@@ -1,14 +1,9 @@
 from __future__ import absolute_import, unicode_literals
 
-import sys
+from unittest import skipUnless
 
 from prometheus_client import CollectorRegistry, Counter, generate_latest
 
-if sys.version_info < (2, 7):
-    from unittest2 import skipUnless
-else:
-    from unittest import skipUnless
-
 try:
     from twisted.internet import reactor
     from twisted.trial.unittest import TestCase
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/client_python-0.11.0/tests/test_wsgi.py 
new/client_python-0.12.0/tests/test_wsgi.py
--- old/client_python-0.11.0/tests/test_wsgi.py 2021-06-01 19:19:33.000000000 
+0200
+++ new/client_python-0.12.0/tests/test_wsgi.py 2021-10-29 19:27:14.000000000 
+0200
@@ -22,17 +22,6 @@
         self.captured_status = status
         self.captured_headers = header
 
-    def assertIn(self, item, iterable):
-        try:
-            super().assertIn(item, iterable)
-        except:  # Python < 2.7
-            self.assertTrue(
-                item in iterable,
-                msg="{item} not found in {iterable}".format(
-                    item=item, iterable=iterable
-                )
-            )
-
     def validate_metrics(self, metric_name, help_text, increments):
         """
         WSGI app serves the metrics from the provided registry.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/client_python-0.11.0/tox.ini 
new/client_python-0.12.0/tox.ini
--- old/client_python-0.11.0/tox.ini    2021-06-01 19:19:33.000000000 +0200
+++ new/client_python-0.12.0/tox.ini    2021-10-29 19:27:14.000000000 +0200
@@ -1,5 +1,5 @@
 [tox]
-envlist = 
coverage-clean,py2.7,py3.4,py3.5,py3.6,py3.7,py3.8,py3.9,pypy2.7,pypy3.7,{py2.7,py3.9}-nooptionals,coverage-report,flake8,isort
+envlist = 
coverage-clean,py2.7,py3.4,py3.5,py3.6,py3.7,py3.8,py3.9,py3.10,pypy2.7,pypy3.7,{py2.7,py3.9}-nooptionals,coverage-report,flake8,isort
 
 
 [base]

Reply via email to