Repository: prometheus/client_python
Pull request #726 (2021-11-16 13:25:52)

Catch non-integer bucket/count values.
@roidelapluie 


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index b555a05..ced0080 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -68,9 +68,6 @@ workflows:
           matrix:
             parameters:
                python:
-                 - "2.7"
-                 - "3.4"
-                 - "3.5"
                  - "3.6"
                  - "3.7"
                  - "3.8"
@@ -80,11 +77,9 @@ workflows:
           matrix:
             parameters:
                python:
-                 - "2.7"
                  - "3.9"
       - test_pypy:
           matrix:
             parameters:
                python:
-                 - "2.7"
                  - "3.7"
diff --git a/README.md b/README.md
index efd74d9..9582830 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # Prometheus Python Client
 
-The official Python 2 and 3 client for [Prometheus](http://prometheus.io).
+The official Python client for [Prometheus](https://prometheus.io).
 
 ## Three Step Demo
 
diff --git a/prometheus_client/__init__.py b/prometheus_client/__init__.py
index cc8f841..f2aa98a 100644
--- a/prometheus_client/__init__.py
+++ b/prometheus_client/__init__.py
@@ -21,11 +21,7 @@ CONTENT_TYPE_LATEST = exposition.CONTENT_TYPE_LATEST
 generate_latest = exposition.generate_latest
 MetricsHandler = exposition.MetricsHandler
 make_wsgi_app = exposition.make_wsgi_app
-try:
-    # Python >3.5 only
-    make_asgi_app = exposition.make_asgi_app
-except:
-    pass
+make_asgi_app = exposition.make_asgi_app
 start_http_server = exposition.start_http_server
 start_wsgi_server = exposition.start_wsgi_server
 write_to_textfile = exposition.write_to_textfile
diff --git a/prometheus_client/bridge/graphite.py b/prometheus_client/bridge/graphite.py
index 35ef849..38bc941 100755
--- a/prometheus_client/bridge/graphite.py
+++ b/prometheus_client/bridge/graphite.py
@@ -1,5 +1,4 @@
 #!/usr/bin/python
-from __future__ import unicode_literals
 
 import logging
 import re
@@ -22,7 +21,7 @@ def _sanitize(s):
 
 class _RegularPush(threading.Thread):
     def __init__(self, pusher, interval, prefix):
-        super(_RegularPush, self).__init__()
+        super().__init__()
         self._pusher = pusher
         self._interval = interval
         self._prefix = prefix
@@ -41,11 +40,11 @@ class _RegularPush(threading.Thread):
                 time.sleep(wait_until - now)
             try:
                 self._pusher.push(prefix=self._prefix)
-            except IOError:
+            except OSError:
                 logging.exception("Push failed")
 
 
-class GraphiteBridge(object):
+class GraphiteBridge:
     def __init__(self, address, registry=REGISTRY, timeout_seconds=30, _timer=time.time, tags=False):
         self._address = address
         self._registry = registry
@@ -76,8 +75,7 @@ class GraphiteBridge(object):
                             for k, v in sorted(s.labels.items())])
                 else:
                     labelstr = ''
-                output.append('{0}{1}{2} {3} {4}\n'.format(
-                    prefixstr, _sanitize(s.name), labelstr, float(s.value), now))
+                output.append(f'{prefixstr}{_sanitize(s.name)}{labelstr} {float(s.value)} {now}\n')
 
         conn = socket.create_connection(self._address, self._timeout)
         conn.sendall(''.join(output).encode('ascii'))
diff --git a/prometheus_client/context_managers.py b/prometheus_client/context_managers.py
index 2b271a7..0704d1e 100644
--- a/prometheus_client/context_managers.py
+++ b/prometheus_client/context_managers.py
@@ -1,11 +1,9 @@
-from __future__ import unicode_literals
-
 from timeit import default_timer
 
 from .decorator import decorate
 
 
-class ExceptionCounter(object):
+class ExceptionCounter:
     def __init__(self, counter, exception):
         self._counter = counter
         self._exception = exception
@@ -25,7 +23,7 @@ class ExceptionCounter(object):
         return decorate(f, wrapped)
 
 
-class InprogressTracker(object):
+class InprogressTracker:
     def __init__(self, gauge):
         self._gauge = gauge
 
@@ -43,7 +41,7 @@ class InprogressTracker(object):
         return decorate(f, wrapped)
 
 
-class Timer(object):
+class Timer:
     def __init__(self, callback):
         self._callback = callback
 
diff --git a/prometheus_client/core.py b/prometheus_client/core.py
index 148573f..ad3a454 100644
--- a/prometheus_client/core.py
+++ b/prometheus_client/core.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
 from .metrics import Counter, Enum, Gauge, Histogram, Info, Summary
 from .metrics_core import (
     CounterMetricFamily, GaugeHistogramMetricFamily, GaugeMetricFamily,
diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py
index 0de9558..edb7ae7 100644
--- a/prometheus_client/exposition.py
+++ b/prometheus_client/exposition.py
@@ -1,39 +1,24 @@
-from __future__ import unicode_literals
-
 import base64
 from contextlib import closing
+from http.server import BaseHTTPRequestHandler
 import os
 import socket
+from socketserver import ThreadingMixIn
 import sys
 import threading
+from urllib.error import HTTPError
+from urllib.parse import parse_qs, quote_plus, urlparse
+from urllib.request import (
+    build_opener, HTTPHandler, HTTPRedirectHandler, Request,
+)
 from wsgiref.simple_server import make_server, WSGIRequestHandler, WSGIServer
 
 from .openmetrics import exposition as openmetrics
 from .registry import REGISTRY
 from .utils import floatToGoString
 
-try:
-    from urllib import quote_plus
-
-    from BaseHTTPServer import BaseHTTPRequestHandler
-    from SocketServer import ThreadingMixIn
-    from urllib2 import (
-        build_opener, HTTPError, HTTPHandler, HTTPRedirectHandler, Request,
-    )
-    from urlparse import parse_qs, urlparse
-except ImportError:
-    # Python 3
-    from http.server import BaseHTTPRequestHandler
-    from socketserver import ThreadingMixIn
-    from urllib.error import HTTPError
-    from urllib.parse import parse_qs, quote_plus, urlparse
-    from urllib.request import (
-        build_opener, HTTPHandler, HTTPRedirectHandler, Request,
-    )
-
-CONTENT_TYPE_LATEST = str('text/plain; version=0.0.4; charset=utf-8')
+CONTENT_TYPE_LATEST = 'text/plain; version=0.0.4; charset=utf-8'
 """Content type of the latest text format"""
-PYTHON27_OR_OLDER = sys.version_info < (3, )
 PYTHON376_OR_NEWER = sys.version_info > (3, 7, 5)
 
 
@@ -88,11 +73,7 @@ class _PrometheusRedirectHandler(HTTPRedirectHandler):
             unverifiable=True,
             data=req.data,
         )
-        if PYTHON27_OR_OLDER:
-            # the `method` attribute did not exist for Request in Python 2.7.
-            new_request.get_method = lambda: m
-        else:
-            new_request.method = m
+        new_request.method = m
         return new_request
 
 
@@ -102,7 +83,7 @@ def _bake_output(registry, accept_header, params):
     if 'name[]' in params:
         registry = registry.restricted_registry(params['name[]'])
     output = encoder(registry)
-    return str('200 OK'), (str('Content-Type'), content_type), output
+    return '200 OK', ('Content-Type', content_type), output
 
 
 def make_wsgi_app(registry=REGISTRY):
@@ -114,8 +95,8 @@ def make_wsgi_app(registry=REGISTRY):
         params = parse_qs(environ.get('QUERY_STRING', ''))
         if environ['PATH_INFO'] == '/favicon.ico':
             # Serve empty response for browsers
-            status = str('200 OK')
-            header = (str(''), str(''))
+            status = '200 OK'
+            header = ('', '')
             output = b''
         else:
             # Bake output
@@ -160,7 +141,7 @@ def generate_latest(registry=REGISTRY):
     def sample_line(line):
         if line.labels:
             labelstr = '{{{0}}}'.format(','.join(
-                ['{0}="{1}"'.format(
+                ['{}="{}"'.format(
                     k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
                     for k, v in sorted(line.labels.items())]))
         else:
@@ -168,9 +149,8 @@ def generate_latest(registry=REGISTRY):
         timestamp = ''
         if line.timestamp is not None:
             # Convert to milliseconds.
-            timestamp = ' {0:d}'.format(int(float(line.timestamp) * 1000))
-        return '{0}{1} {2}{3}\n'.format(
-            line.name, labelstr, floatToGoString(line.value), timestamp)
+            timestamp = f' {int(float(line.timestamp) * 1000):d}'
+        return f'{line.name}{labelstr} {floatToGoString(line.value)}{timestamp}\n'
 
     output = []
     for metric in registry.collect():
@@ -192,9 +172,9 @@ def generate_latest(registry=REGISTRY):
             elif mtype == 'unknown':
                 mtype = 'untyped'
 
-            output.append('# HELP {0} {1}\n'.format(
+            output.append('# HELP {} {}\n'.format(
                 mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
-            output.append('# TYPE {0} {1}\n'.format(mname, mtype))
+            output.append(f'# TYPE {mname} {mtype}\n')
 
             om_samples = {}
             for s in metric.samples:
@@ -210,9 +190,9 @@ def generate_latest(registry=REGISTRY):
             raise
 
         for suffix, lines in sorted(om_samples.items()):
-            output.append('# HELP {0}{1} {2}\n'.format(metric.name, suffix,
-                                                       metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
-            output.append('# TYPE {0}{1} gauge\n'.format(metric.name, suffix))
+            output.append('# HELP {}{} {}\n'.format(metric.name, suffix,
+                                                    metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
+            output.append(f'# TYPE {metric.name}{suffix} gauge\n')
             output.extend(lines)
     return ''.join(output).encode('utf-8')
 
@@ -267,25 +247,13 @@ def write_to_textfile(path, registry):
 
     This is intended for use with the Node exporter textfile collector.
     The path must end in .prom for the textfile collector to process it."""
-    tmppath = '%s.%s.%s' % (path, os.getpid(), threading.current_thread().ident)
+    tmppath = f'{path}.{os.getpid()}.{threading.current_thread().ident}'
     with open(tmppath, 'wb') as f:
         f.write(generate_latest(registry))
     
     # rename(2) is atomic but fails on Windows if the destination file exists
     if os.name == 'nt':
-        if sys.version_info <= (3, 3):
-            # Unable to guarantee atomic rename on Windows and Python<3.3
-            # Remove and rename instead (risks losing the file)
-            try:
-                os.remove(path)
-            except FileNotFoundError:
-                pass
-
-            os.rename(tmppath, path)
-        else:
-            # os.replace is introduced in Python 3.3 but there is some dispute whether
-            # it is a truly atomic file operation: https://bugs.python.org/issue8828
-            os.replace(tmppath, path)
+        os.replace(tmppath, path)
     else:
         os.rename(tmppath, path)
 
@@ -299,8 +267,7 @@ def _make_handler(url, method, timeout, headers, data, base_handler):
             request.add_header(k, v)
         resp = build_opener(base_handler).open(request, timeout=timeout)
         if resp.code >= 400:
-            raise IOError("error talking to pushgateway: {0} {1}".format(
-                resp.code, resp.msg))
+            raise OSError(f"error talking to pushgateway: {resp.code} {resp.msg}")
 
     return handle
 
@@ -337,7 +304,7 @@ def basic_auth_handler(url, method, timeout, headers, data, username=None, passw
         """Handler that implements HTTP Basic Auth.
         """
         if username is not None and password is not None:
-            auth_value = '{0}:{1}'.format(username, password).encode('utf-8')
+            auth_value = f'{username}:{password}'.encode()
             auth_token = base64.b64encode(auth_value)
             auth_header = b'Basic ' + auth_token
             headers.append(['Authorization', auth_header])
@@ -447,10 +414,10 @@ def _use_gateway(method, gateway, job, registry, grouping_key, timeout, handler)
             PYTHON376_OR_NEWER
             and gateway_url.scheme not in ['http', 'https']
     ):
-        gateway = 'http://{0}'.format(gateway)
+        gateway = f'http://{gateway}'
 
     gateway = gateway.rstrip('/')
-    url = '{0}/metrics/{1}/{2}'.format(gateway, *_escape_grouping_key("job", job))
+    url = '{}/metrics/{}/{}'.format(gateway, *_escape_grouping_key("job", job))
 
     data = b''
     if method != 'DELETE':
@@ -459,7 +426,7 @@ def _use_gateway(method, gateway, job, registry, grouping_key, timeout, handler)
     if grouping_key is None:
         grouping_key = {}
     url += ''.join(
-        '/{0}/{1}'.format(*_escape_grouping_key(str(k), str(v)))
+        '/{}/{}'.format(*_escape_grouping_key(str(k), str(v)))
         for k, v in sorted(grouping_key.items()))
 
     handler(
@@ -495,8 +462,4 @@ def instance_ip_grouping_key():
         return {'instance': s.getsockname()[0]}
 
 
-try:
-    # Python >3.5 only
-    from .asgi import make_asgi_app  # noqa
-except:
-    pass
+from .asgi import make_asgi_app  # noqa
diff --git a/prometheus_client/gc_collector.py b/prometheus_client/gc_collector.py
index 845ed75..0695670 100644
--- a/prometheus_client/gc_collector.py
+++ b/prometheus_client/gc_collector.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
 import gc
 import platform
 
@@ -7,7 +5,7 @@ from .metrics_core import CounterMetricFamily
 from .registry import REGISTRY
 
 
-class GCCollector(object):
+class GCCollector:
     """Collector for Garbage collection statistics."""
 
     def __init__(self, registry=REGISTRY):
diff --git a/prometheus_client/metrics.py b/prometheus_client/metrics.py
index 399830a..ea797e9 100644
--- a/prometheus_client/metrics.py
+++ b/prometheus_client/metrics.py
@@ -1,4 +1,3 @@
-import sys
 from threading import Lock
 import time
 import types
@@ -13,13 +12,6 @@ from .registry import REGISTRY
 from .samples import Exemplar
 from .utils import floatToGoString, INF
 
-if sys.version_info > (3,):
-    unicode = str
-    create_bound_method = types.MethodType
-else:
-    def create_bound_method(func, obj):
-        return types.MethodType(func, obj, obj.__class__)
-
 
 def _build_full_name(metric_type, name, namespace, subsystem, unit):
     full_name = ''
@@ -63,7 +55,7 @@ def _validate_exemplar(exemplar):
         raise ValueError('Exemplar labels have %d UTF-8 characters, exceeding the limit of 128')
 
 
-class MetricWrapperBase(object):
+class MetricWrapperBase:
     _type = None
     _reserved_labelnames = ()
 
@@ -96,11 +88,11 @@ class MetricWrapperBase(object):
         return [metric]
 
     def __str__(self):
-        return "{0}:{1}".format(self._type, self._name)
+        return f"{self._type}:{self._name}"
 
     def __repr__(self):
         metric_type = type(self)
-        return "{0}.{1}({2})".format(metric_type.__module__, metric_type.__name__, self._name)
+        return f"{metric_type.__module__}.{metric_type.__name__}({self._name})"
 
     def __init__(self,
                  name,
@@ -162,7 +154,7 @@ class MetricWrapperBase(object):
             raise ValueError('No label names were set when constructing %s' % self)
 
         if self._labelvalues:
-            raise ValueError('%s already has labels set (%s); can not chain calls to .labels()' % (
+            raise ValueError('{} already has labels set ({}); can not chain calls to .labels()'.format(
                 self,
                 dict(zip(self._labelnames, self._labelvalues))
             ))
@@ -173,11 +165,11 @@ class MetricWrapperBase(object):
         if labelkwargs:
             if sorted(labelkwargs) != sorted(self._labelnames):
                 raise ValueError('Incorrect label names')
-            labelvalues = tuple(unicode(labelkwargs[l]) for l in self._labelnames)
+            labelvalues = tuple(str(labelkwargs[l]) for l in self._labelnames)
         else:
             if len(labelvalues) != len(self._labelnames):
                 raise ValueError('Incorrect label count')
-            labelvalues = tuple(unicode(l) for l in labelvalues)
+            labelvalues = tuple(str(l) for l in labelvalues)
         with self._lock:
             if labelvalues not in self._metrics:
                 self._metrics[labelvalues] = self.__class__(
@@ -197,7 +189,7 @@ class MetricWrapperBase(object):
         """Remove the given labelset from the metric."""
         if len(labelvalues) != len(self._labelnames):
             raise ValueError('Incorrect label count (expected %d, got %s)' % (len(self._labelnames), labelvalues))
-        labelvalues = tuple(unicode(l) for l in labelvalues)
+        labelvalues = tuple(str(l) for l in labelvalues)
         with self._lock:
             del self._metrics[labelvalues]
 
@@ -352,7 +344,7 @@ class Gauge(MetricWrapperBase):
         self._multiprocess_mode = multiprocess_mode
         if multiprocess_mode not in self._MULTIPROC_MODES:
             raise ValueError('Invalid multiprocess mode: ' + multiprocess_mode)
-        super(Gauge, self).__init__(
+        super().__init__(
             name=name,
             documentation=documentation,
             labelnames=labelnames,
@@ -419,7 +411,7 @@ class Gauge(MetricWrapperBase):
         def samples(self):
             return (('', {}, float(f()), None, None),)
 
-        self._child_samples = create_bound_method(samples, self)
+        self._child_samples = types.MethodType(samples, self)
 
     def _child_samples(self):
         return (('', {}, self._value.get(), None, None),)
@@ -545,7 +537,7 @@ class Histogram(MetricWrapperBase):
                  buckets=DEFAULT_BUCKETS,
                  ):
         self._prepare_buckets(buckets)
-        super(Histogram, self).__init__(
+        super().__init__(
             name=name,
             documentation=documentation,
             labelnames=labelnames,
@@ -649,7 +641,7 @@ class Info(MetricWrapperBase):
     def info(self, val):
         """Set info metric."""
         if self._labelname_set.intersection(val.keys()):
-            raise ValueError('Overlapping labels for Info metric, metric: %s child: %s' % (
+            raise ValueError('Overlapping labels for Info metric, metric: {} child: {}'.format(
                 self._labelnames, val))
         with self._lock:
             self._value = dict(val)
@@ -685,7 +677,7 @@ class Enum(MetricWrapperBase):
                  _labelvalues=None,
                  states=None,
                  ):
-        super(Enum, self).__init__(
+        super().__init__(
             name=name,
             documentation=documentation,
             labelnames=labelnames,
@@ -696,9 +688,9 @@ class Enum(MetricWrapperBase):
             _labelvalues=_labelvalues,
         )
         if name in labelnames:
-            raise ValueError('Overlapping labels for Enum metric: %s' % (name,))
+            raise ValueError(f'Overlapping labels for Enum metric: {name}')
         if not states:
-            raise ValueError('No states provided for Enum metric: %s' % (name,))
+            raise ValueError(f'No states provided for Enum metric: {name}')
         self._kwargs['states'] = self._states = states
 
     def _metric_init(self):
diff --git a/prometheus_client/metrics_core.py b/prometheus_client/metrics_core.py
index 4ab2af1..a040878 100644
--- a/prometheus_client/metrics_core.py
+++ b/prometheus_client/metrics_core.py
@@ -11,7 +11,7 @@ METRIC_LABEL_NAME_RE = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$')
 RESERVED_METRIC_LABEL_NAME_RE = re.compile(r'^__.*$')
 
 
-class Metric(object):
+class Metric:
     """A single metric family and its samples.
 
     This is intended only for internal use by the instrumentation client.
@@ -50,7 +50,7 @@ class Metric(object):
                 and self.samples == other.samples)
 
     def __repr__(self):
-        return "Metric(%s, %s, %s, %s, %s)" % (
+        return "Metric({}, {}, {}, {}, {})".format(
             self.name,
             self.documentation,
             self.type,
diff --git a/prometheus_client/mmap_dict.py b/prometheus_client/mmap_dict.py
index 615d003..e8b2df9 100644
--- a/prometheus_client/mmap_dict.py
+++ b/prometheus_client/mmap_dict.py
@@ -45,7 +45,7 @@ def _read_all_values(data, used=0):
         pos += 8
 
 
-class MmapedDict(object):
+class MmapedDict:
     """A dict of doubles, backed by an mmapped file.
 
     The file starts with a 4 byte int, indicating how much of it is used.
@@ -94,7 +94,7 @@ class MmapedDict(object):
         encoded = key.encode('utf-8')
         # Pad to be 8-byte aligned.
         padded = encoded + (b' ' * (8 - (len(encoded) + 4) % 8))
-        value = struct.pack('i{0}sd'.format(len(padded)).encode(), len(encoded), padded, 0.0)
+        value = struct.pack(f'i{len(padded)}sd'.encode(), len(encoded), padded, 0.0)
         while self._used + len(value) > self._capacity:
             self._capacity *= 2
             self._f.truncate(self._capacity)
diff --git a/prometheus_client/multiprocess.py b/prometheus_client/multiprocess.py
index 03e3f4d..6c95374 100644
--- a/prometheus_client/multiprocess.py
+++ b/prometheus_client/multiprocess.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
 from collections import defaultdict
 import glob
 import json
@@ -19,7 +17,7 @@ except NameError:  # Python >= 2.5
 MP_METRIC_HELP = 'Multiprocess metric'
 
 
-class MultiProcessCollector(object):
+class MultiProcessCollector:
     """Collector for files for multi-process mode."""
 
     def __init__(self, registry, path=None):
@@ -97,7 +95,7 @@ class MultiProcessCollector(object):
             for s in metric.samples:
                 name, labels, value, timestamp, exemplar = s
                 if metric.type == 'gauge':
-                    without_pid_key = (name, tuple([l for l in labels if l[0] != 'pid']))
+                    without_pid_key = (name, tuple(l for l in labels if l[0] != 'pid'))
                     if metric._multiprocess_mode == 'min':
                         current = samples_setdefault(without_pid_key, value)
                         if value < current:
@@ -158,7 +156,7 @@ def mark_process_dead(pid, path=None):
     """Do bookkeeping for when one process dies in a multi-process setup."""
     if path is None:
         path = os.environ.get('PROMETHEUS_MULTIPROC_DIR', os.environ.get('prometheus_multiproc_dir'))
-    for f in glob.glob(os.path.join(path, 'gauge_livesum_{0}.db'.format(pid))):
+    for f in glob.glob(os.path.join(path, f'gauge_livesum_{pid}.db')):
         os.remove(f)
-    for f in glob.glob(os.path.join(path, 'gauge_liveall_{0}.db'.format(pid))):
+    for f in glob.glob(os.path.join(path, f'gauge_liveall_{pid}.db')):
         os.remove(f)
diff --git a/prometheus_client/openmetrics/exposition.py b/prometheus_client/openmetrics/exposition.py
index 56d00b5..d1c1f06 100644
--- a/prometheus_client/openmetrics/exposition.py
+++ b/prometheus_client/openmetrics/exposition.py
@@ -1,10 +1,9 @@
 #!/usr/bin/python
 
-from __future__ import unicode_literals
 
 from ..utils import floatToGoString
 
-CONTENT_TYPE_LATEST = str('application/openmetrics-text; version=0.0.1; charset=utf-8')
+CONTENT_TYPE_LATEST = 'application/openmetrics-text; version=0.0.1; charset=utf-8'
 """Content type of the latest OpenMetrics text format"""
 
 
@@ -22,34 +21,34 @@ def generate_latest(registry):
     for metric in registry.collect():
         try:
             mname = metric.name
-            output.append('# HELP {0} {1}\n'.format(
+            output.append('# HELP {} {}\n'.format(
                 mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')))
-            output.append('# TYPE {0} {1}\n'.format(mname, metric.type))
+            output.append(f'# TYPE {mname} {metric.type}\n')
             if metric.unit:
-                output.append('# UNIT {0} {1}\n'.format(mname, metric.unit))
+                output.append(f'# UNIT {mname} {metric.unit}\n')
             for s in metric.samples:
                 if s.labels:
                     labelstr = '{{{0}}}'.format(','.join(
-                        ['{0}="{1}"'.format(
+                        ['{}="{}"'.format(
                             k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
                             for k, v in sorted(s.labels.items())]))
                 else:
                     labelstr = ''
                 if s.exemplar:
                     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))
+                        raise ValueError(f"Metric {metric.name} has exemplars, but is not a histogram bucket or counter")
                     labels = '{{{0}}}'.format(','.join(
-                        ['{0}="{1}"'.format(
+                        ['{}="{}"'.format(
                             k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
                             for k, v in sorted(s.exemplar.labels.items())]))
                     if s.exemplar.timestamp is not None:
-                        exemplarstr = ' # {0} {1} {2}'.format(
+                        exemplarstr = ' # {} {} {}'.format(
                             labels,
                             floatToGoString(s.exemplar.value),
                             s.exemplar.timestamp,
                         )
                     else:
-                        exemplarstr = ' # {0} {1}'.format(
+                        exemplarstr = ' # {} {}'.format(
                             labels,
                             floatToGoString(s.exemplar.value),
                         )
@@ -57,8 +56,8 @@ def generate_latest(registry):
                     exemplarstr = ''
                 timestamp = ''
                 if s.timestamp is not None:
-                    timestamp = ' {0}'.format(s.timestamp)
-                output.append('{0}{1} {2}{3}{4}\n'.format(
+                    timestamp = f' {s.timestamp}'
+                output.append('{}{} {}{}{}\n'.format(
                     s.name,
                     labelstr,
                     floatToGoString(s.value),
diff --git a/prometheus_client/openmetrics/parser.py b/prometheus_client/openmetrics/parser.py
index 4a61939..9bcec18 100644
--- a/prometheus_client/openmetrics/parser.py
+++ b/prometheus_client/openmetrics/parser.py
@@ -1,6 +1,5 @@
 #!/usr/bin/python
 
-from __future__ import unicode_literals
 
 import math
 import re
@@ -21,11 +20,10 @@ def text_string_to_metric_families(text):
 
     See text_fd_to_metric_families.
     """
-    for metric_family in text_fd_to_metric_families(StringIO.StringIO(text)):
-        yield metric_family
+    yield from text_fd_to_metric_families(StringIO.StringIO(text))
 
 
-_CANONICAL_NUMBERS = set([float("inf")])
+_CANONICAL_NUMBERS = {float("inf")}
 
 
 def _isUncanonicalNumber(s):
@@ -83,7 +81,7 @@ def _unescape_help(text):
 def _parse_value(value):
     value = ''.join(value)
     if value != value.strip() or '_' in value:
-        raise ValueError("Invalid value: {0!r}".format(value))
+        raise ValueError(f"Invalid value: {value!r}")
     try:
         return int(value)
     except ValueError:
@@ -95,7 +93,7 @@ def _parse_timestamp(timestamp):
     if not timestamp:
         return None
     if timestamp != timestamp.strip() or '_' in timestamp:
-        raise ValueError("Invalid timestamp: {0!r}".format(timestamp))
+        raise ValueError(f"Invalid timestamp: {timestamp!r}")
     try:
         # Simple int.
         return Timestamp(int(timestamp), 0)
@@ -108,7 +106,7 @@ def _parse_timestamp(timestamp):
             # Float.
             ts = float(timestamp)
             if math.isnan(ts) or math.isinf(ts):
-                raise ValueError("Invalid timestamp: {0!r}".format(timestamp))
+                raise ValueError(f"Invalid timestamp: {timestamp!r}")
             return ts
 
 
@@ -359,7 +357,7 @@ def _parse_remaining_text(text):
     ts = _parse_timestamp(timestamp)
     exemplar = None
     if exemplar_labels is not None:
-        exemplar_length = sum([len(k) + len(v) for k, v in exemplar_labels.items()])
+        exemplar_length = sum(len(k) + len(v) for k, v in exemplar_labels.items())
         if exemplar_length > 128:
             raise ValueError("Exmplar labels are too long: " + text)
         exemplar = Exemplar(
@@ -562,10 +560,16 @@ def text_fd_to_metric_families(fd):
 
             if typ == 'stateset' and name not in sample.labels:
                 raise ValueError("Stateset missing label: " + line)
-            if (typ in ['histogram', 'gaugehistogram'] and name + '_bucket' == sample.name
+            if (name + '_bucket' == sample.name
                     and (sample.labels.get('le', "NaN") == "NaN"
                          or _isUncanonicalNumber(sample.labels['le']))):
                 raise ValueError("Invalid le label: " + line)
+            if (name + '_bucket' == sample.name
+                    and (not isinstance(sample.value, int) and not sample.value.is_integer())):
+                raise ValueError("Bucket value must be an integer: " + line)
+            if ((name + '_count' == sample.name or name + '_gcount' == sample.name)
+                    and (not isinstance(sample.value, int) and not sample.value.is_integer())):
+                raise ValueError("Count value must be an integer: " + line)
             if (typ == 'summary' and name == sample.name
                     and (not (0 <= float(sample.labels.get('quantile', -1)) <= 1)
                          or _isUncanonicalNumber(sample.labels['quantile']))):
diff --git a/prometheus_client/parser.py b/prometheus_client/parser.py
index b281a43..041c602 100644
--- a/prometheus_client/parser.py
+++ b/prometheus_client/parser.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
 import re
 
 from .metrics_core import Metric
@@ -17,8 +15,7 @@ def text_string_to_metric_families(text):
 
     See text_fd_to_metric_families.
     """
-    for metric_family in text_fd_to_metric_families(StringIO.StringIO(text)):
-        yield metric_family
+    yield from text_fd_to_metric_families(StringIO.StringIO(text))
 
 
 ESCAPE_SEQUENCES = {
diff --git a/prometheus_client/platform_collector.py b/prometheus_client/platform_collector.py
index 17f53cb..c57d9fe 100644
--- a/prometheus_client/platform_collector.py
+++ b/prometheus_client/platform_collector.py
@@ -1,13 +1,10 @@
-# -*- coding: utf-8
-from __future__ import unicode_literals
-
 import platform as pf
 
 from .metrics_core import GaugeMetricFamily
 from .registry import REGISTRY
 
 
-class PlatformCollector(object):
+class PlatformCollector:
     """Collector for python platform information"""
 
     def __init__(self, registry=REGISTRY, platform=None):
diff --git a/prometheus_client/process_collector.py b/prometheus_client/process_collector.py
index 12c08f6..e6271ba 100644
--- a/prometheus_client/process_collector.py
+++ b/prometheus_client/process_collector.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
 import os
 
 from .metrics_core import CounterMetricFamily, GaugeMetricFamily
@@ -14,7 +12,7 @@ except ImportError:
     _PAGESIZE = 4096
 
 
-class ProcessCollector(object):
+class ProcessCollector:
     """Collector for Standard Exports such as cpu and memory."""
 
     def __init__(self, namespace='', pid=lambda: 'self', proc='/proc', registry=REGISTRY):
@@ -37,7 +35,7 @@ class ProcessCollector(object):
         self._btime = 0
         try:
             self._btime = self._boot_time()
-        except IOError:
+        except OSError:
             pass
         if registry:
             registry.register(self)
@@ -73,7 +71,7 @@ class ProcessCollector(object):
                                       'Total user and system CPU time spent in seconds.',
                                       value=utime + stime)
             result.extend([vmem, rss, start_time, cpu])
-        except IOError:
+        except OSError:
             pass
 
         try:
@@ -88,7 +86,7 @@ class ProcessCollector(object):
                                          'Number of open file descriptors.',
                                          len(os.listdir(os.path.join(pid, 'fd'))))
             result.extend([open_fds, max_fds])
-        except (IOError, OSError):
+        except OSError:
             pass
 
         return result
diff --git a/prometheus_client/registry.py b/prometheus_client/registry.py
index 67cde45..fe435cd 100644
--- a/prometheus_client/registry.py
+++ b/prometheus_client/registry.py
@@ -4,7 +4,7 @@ from threading import Lock
 from .metrics_core import Metric
 
 
-class CollectorRegistry(object):
+class CollectorRegistry:
     """Metric collector registry.
 
     Collectors must have a no-argument method 'collect' that returns a list of
@@ -27,7 +27,7 @@ class CollectorRegistry(object):
             duplicates = set(self._names_to_collectors).intersection(names)
             if duplicates:
                 raise ValueError(
-                    'Duplicated timeseries in CollectorRegistry: {0}'.format(
+                    'Duplicated timeseries in CollectorRegistry: {}'.format(
                         duplicates))
             for name in names:
                 self._names_to_collectors[name] = collector
@@ -80,8 +80,7 @@ class CollectorRegistry(object):
         if ti:
             yield ti
         for collector in collectors:
-            for metric in collector.collect():
-                yield metric
+            yield from collector.collect()
 
     def restricted_registry(self, names):
         """Returns object that only collects some metrics.
@@ -129,7 +128,7 @@ class CollectorRegistry(object):
         return None
 
 
-class RestrictedRegistry(object):
+class RestrictedRegistry:
     def __init__(self, names, registry):
         self._name_set = set(names)
         self._registry = registry
diff --git a/prometheus_client/samples.py b/prometheus_client/samples.py
index 9ff8ead..da93cd5 100644
--- a/prometheus_client/samples.py
+++ b/prometheus_client/samples.py
@@ -1,22 +1,22 @@
 from collections import namedtuple
 
 
-class Timestamp(object):
+class Timestamp:
     """A nanosecond-resolution timestamp."""
 
     def __init__(self, sec, nsec):
         if nsec < 0 or nsec >= 1e9:
-            raise ValueError("Invalid value for nanoseconds in Timestamp: {0}".format(nsec))
+            raise ValueError(f"Invalid value for nanoseconds in Timestamp: {nsec}")
         if sec < 0:
             nsec = -nsec
         self.sec = int(sec)
         self.nsec = int(nsec)
 
     def __str__(self):
-        return "{0}.{1:09d}".format(self.sec, self.nsec)
+        return f"{self.sec}.{self.nsec:09d}"
 
     def __repr__(self):
-        return "Timestamp({0}, {1})".format(self.sec, self.nsec)
+        return f"Timestamp({self.sec}, {self.nsec})"
 
     def __float__(self):
         return float(self.sec) + float(self.nsec) / 1e9
diff --git a/prometheus_client/twisted/_exposition.py b/prometheus_client/twisted/_exposition.py
index 50cf50a..202a7d3 100644
--- a/prometheus_client/twisted/_exposition.py
+++ b/prometheus_client/twisted/_exposition.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, unicode_literals
-
 from twisted.internet import reactor
 from twisted.web.wsgi import WSGIResource
 
diff --git a/prometheus_client/utils.py b/prometheus_client/utils.py
index 1ff77c3..0d2b094 100644
--- a/prometheus_client/utils.py
+++ b/prometheus_client/utils.py
@@ -19,6 +19,6 @@ def floatToGoString(d):
         # Go switches to exponents sooner than Python.
         # We only need to care about positive values for le/quantile.
         if d > 0 and dot > 6:
-            mantissa = '{0}.{1}{2}'.format(s[0], s[1:dot], s[dot + 1:]).rstrip('0.')
-            return '{0}e+0{1}'.format(mantissa, dot - 1)
+            mantissa = f'{s[0]}.{s[1:dot]}{s[dot + 1:]}'.rstrip('0.')
+            return f'{mantissa}e+0{dot - 1}'
         return s
diff --git a/prometheus_client/values.py b/prometheus_client/values.py
index 842837c..03b203b 100644
--- a/prometheus_client/values.py
+++ b/prometheus_client/values.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
 import os
 from threading import Lock
 import warnings
@@ -7,7 +5,7 @@ import warnings
 from .mmap_dict import mmap_key, MmapedDict
 
 
-class MutexValue(object):
+class MutexValue:
     """A float protected by a mutex."""
 
     _multiprocess = False
@@ -54,7 +52,7 @@ def MultiProcessValue(process_identifier=os.getpid):
     # This avoids the need to also have mutexes in __MmapDict.
     lock = Lock()
 
-    class MmapedValue(object):
+    class MmapedValue:
         """A float protected by a mutex backed by a per-process mmaped file."""
 
         _multiprocess = True
@@ -79,7 +77,7 @@ def MultiProcessValue(process_identifier=os.getpid):
             if file_prefix not in files:
                 filename = os.path.join(
                     os.environ.get('PROMETHEUS_MULTIPROC_DIR'),
-                    '{0}_{1}.db'.format(file_prefix, pid['value']))
+                    '{}_{}.db'.format(file_prefix, pid['value']))
 
                 files[file_prefix] = MmapedDict(filename)
             self._file = files[file_prefix]
diff --git a/setup.py b/setup.py
index 95adc29..290e267 100644
--- a/setup.py
+++ b/setup.py
@@ -27,18 +27,14 @@ setup(
         'twisted': ['twisted'],
     },
     test_suite="tests",
-    python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
+    python_requires=">=3.6",
     classifiers=[
         "Development Status :: 4 - Beta",
         "Intended Audience :: Developers",
         "Intended Audience :: Information Technology",
         "Intended Audience :: System Administrators",
         "Programming Language :: Python",
-        "Programming Language :: Python :: 2",
-        "Programming Language :: Python :: 2.7",
         "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.4",
-        "Programming Language :: Python :: 3.5",
         "Programming Language :: Python :: 3.6",
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
@@ -49,5 +45,4 @@ setup(
         "Topic :: System :: Monitoring",
         "License :: OSI Approved :: Apache Software License",
     ],
-    options={'bdist_wheel': {'universal': '1'}},
 )
diff --git a/tox.ini b/tox.ini
index 2984518..9dddf7b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,38 +1,20 @@
 [tox]
-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
+envlist = coverage-clean,py3.6,py3.7,py3.8,py3.9,py3.10,pypy3.7,py3.9-nooptionals,coverage-report,flake8,isort
 
 
 [base]
 deps =
     coverage
     pytest
-; Python 3.4 fails if using 21.1.0.
-    attrs!=21.1.0
-
-[testenv:py2.7]
-deps =
-    {[base]deps}
-    futures
-
-[testenv:pypy2.7]
-deps =
-    {[base]deps}
-    futures
+    attrs
 
 [testenv]
 deps =
     {[base]deps}
-    {py2.7,py3.7,pypy2.7,pypy3.7}: twisted
+    {py3.7,pypy3.7}: twisted
     {py3.7,pypy3.7}: asgiref
 commands = coverage run --parallel -m pytest {posargs}
 
-; Ensure test suite passes if no optional dependencies are present.
-[testenv:py2.7-nooptionals]
-deps = 
-    {[base]deps}
-    futures
-commands = coverage run --parallel -m pytest {posargs}
-
 [testenv:py3.9-nooptionals]
 commands = coverage run --parallel -m pytest {posargs}
 


Test patch:
diff --git a/tests/openmetrics/test_exposition.py b/tests/openmetrics/test_exposition.py
index 87a382c..28a9083 100644
--- a/tests/openmetrics/test_exposition.py
+++ b/tests/openmetrics/test_exposition.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
 import time
 import unittest
 
@@ -24,7 +22,7 @@ class TestGenerateText(unittest.TestCase):
         time.time = self.old_time
 
     def custom_collector(self, metric_family):
-        class CustomCollector(object):
+        class CustomCollector:
             def collect(self):
                 return [metric_family]
 
@@ -137,7 +135,7 @@ cc_created 123.456
 """, generate_latest(self.registry))
 
     def test_untyped_exemplar(self):
-        class MyCollector(object):
+        class MyCollector:
             def collect(self):
                 metric = Metric("hh", "help", 'untyped')
                 # This is not sane, but it covers all the cases.
@@ -149,7 +147,7 @@ cc_created 123.456
             generate_latest(self.registry)
 
     def test_histogram_non_bucket_exemplar(self):
-        class MyCollector(object):
+        class MyCollector:
             def collect(self):
                 metric = Metric("hh", "help", 'histogram')
                 # This is not sane, but it covers all the cases.
@@ -161,7 +159,7 @@ cc_created 123.456
             generate_latest(self.registry)
 
     def test_counter_non_total_exemplar(self):
-        class MyCollector(object):
+        class MyCollector:
             def collect(self):
                 metric = Metric("cc", "A counter", 'counter')
                 metric.add_sample("cc_total", {}, 1, None, None)
@@ -236,14 +234,14 @@ cc_created{a="\\\\x\\n\\""} 123.456
 """, generate_latest(self.registry))
 
     def test_nonnumber(self):
-        class MyNumber(object):
+        class MyNumber:
             def __repr__(self):
                 return "MyNumber(123)"
 
             def __float__(self):
                 return 123.0
 
-        class MyCollector(object):
+        class MyCollector:
             def collect(self):
                 metric = Metric("nonnumber", "Non number", 'untyped')
                 metric.add_sample("nonnumber", {}, MyNumber())
@@ -254,7 +252,7 @@ cc_created{a="\\\\x\\n\\""} 123.456
                          generate_latest(self.registry))
 
     def test_timestamp(self):
-        class MyCollector(object):
+        class MyCollector:
             def collect(self):
                 metric = Metric("ts", "help", 'unknown')
                 metric.add_sample("ts", {"foo": "a"}, 0, 123.456)
diff --git a/tests/openmetrics/test_parser.py b/tests/openmetrics/test_parser.py
index 8fd2319..4b4aecd 100644
--- a/tests/openmetrics/test_parser.py
+++ b/tests/openmetrics/test_parser.py
@@ -1,7 +1,4 @@
-from __future__ import unicode_literals
-
 import math
-import sys
 import unittest
 
 from prometheus_client.core import (
@@ -115,6 +112,18 @@ a_bucket{le="+Inf"} 3
 a_count 3
 a_sum 2
 # EOF
+""")
+        self.assertEqual([HistogramMetricFamily("a", "help", sum_value=2, buckets=[("1.0", 0.0), ("+Inf", 3.0)])],
+                         list(families))
+
+    def test_simple_histogram_float_values(self):
+        families = text_string_to_metric_families("""# TYPE a histogram
+# HELP a help
+a_bucket{le="1.0"} 0.0
+a_bucket{le="+Inf"} 3.0
+a_count 3.0
+a_sum 2.0
+# EOF
 """)
         self.assertEqual([HistogramMetricFamily("a", "help", sum_value=2, buckets=[("1.0", 0.0), ("+Inf", 3.0)])],
                          list(families))
@@ -482,7 +491,6 @@ a_bucket{le="+Inf",foo="bar # "} 3 # {a="d",foo="bar # bar"} 4
         hfm.add_sample("a_bucket", {"le": "+Inf", "foo": "bar # "}, 3.0, None, Exemplar({"a": "d", "foo": "bar # bar"}, 4))
         self.assertEqual([hfm], list(families))
 
-    @unittest.skipIf(sys.version_info < (3, 3), "Test requires Python 3.3+.")
     def test_fallback_to_state_machine_label_parsing(self):
         from unittest.mock import patch
 
@@ -607,7 +615,7 @@ bar_bucket{a="c",le="+Inf"} 0.0
 """
         families = list(text_string_to_metric_families(text))
 
-        class TextCollector(object):
+        class TextCollector:
             def collect(self):
                 return families
 
@@ -763,15 +771,20 @@ bar_bucket{a="c",le="+Inf"} 0.0
             ('# TYPE a histogram\na_bucket{le="+Inf"} -1\n# EOF\n'),
             ('# TYPE a histogram\na_bucket{le="-1.0"} 1\na_bucket{le="+Inf"} 2\na_sum -1\n# EOF\n'),
             ('# TYPE a histogram\na_bucket{le="-1.0"} 1\na_bucket{le="+Inf"} 2\na_sum 1\n# EOF\n'),
+            ('# TYPE a histogram\na_bucket{le="+Inf"} 0.5\n# EOF\n'),
+            ('# TYPE a histogram\na_bucket{le="+Inf"} 0.5\na_count 0.5\na_sum 0\n# EOF\n'),
             ('# TYPE a gaugehistogram\na_bucket{le="+Inf"} NaN\n# EOF\n'),
             ('# TYPE a gaugehistogram\na_bucket{le="+Inf"} -1\na_gcount -1\n# EOF\n'),
             ('# TYPE a gaugehistogram\na_bucket{le="+Inf"} -1\n# EOF\n'),
             ('# TYPE a gaugehistogram\na_bucket{le="+Inf"} 1\na_gsum -1\n# EOF\n'),
             ('# TYPE a gaugehistogram\na_bucket{le="+Inf"} 1\na_gsum NaN\n# EOF\n'),
+            ('# TYPE a gaugehistogram\na_bucket{le="+Inf"} 0.5\n# EOF\n'),
+            ('# TYPE a gaugehistogram\na_bucket{le="+Inf"} 0.5\na_gsum 0.5\na_gcount 0\n# EOF\n'),
             ('# TYPE a summary\na_sum NaN\n# EOF\n'),
             ('# TYPE a summary\na_count NaN\n# EOF\n'),
             ('# TYPE a summary\na_sum -1\n# EOF\n'),
             ('# TYPE a summary\na_count -1\n# EOF\n'),
+            ('# TYPE a summary\na_count 0.5\n# EOF\n'),
             ('# TYPE a summary\na{quantile="0.5"} -1\n# EOF\n'),
             # Bad info and stateset values.
             ('# TYPE a info\na_info{foo="bar"} 2\n# EOF\n'),
diff --git a/tests/test_asgi.py b/tests/test_asgi.py
index 139c871..77278bb 100644
--- a/tests/test_asgi.py
+++ b/tests/test_asgi.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, unicode_literals
-
 from unittest import skipUnless, TestCase
 
 from prometheus_client import CollectorRegistry, Counter
diff --git a/tests/test_core.py b/tests/test_core.py
index 733e9dd..349e475 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -1,8 +1,4 @@
-# coding=utf-8
-from __future__ import unicode_literals
-
 from concurrent.futures import ThreadPoolExecutor
-import sys
 import time
 
 import pytest
@@ -556,7 +552,7 @@ class TestMetricWrapper(unittest.TestCase):
         self.assertEqual(None, self.registry.get_sample_value('c_total', {'l': 'None'}))
 
     def test_non_string_labels_raises(self):
-        class Test(object):
+        class Test:
             __str__ = None
 
         self.assertRaises(TypeError, self.counter.labels, Test())
@@ -618,7 +614,7 @@ class TestMetricFamilies(unittest.TestCase):
         self.registry = CollectorRegistry()
 
     def custom_collector(self, metric_family):
-        class CustomCollector(object):
+        class CustomCollector:
             def collect(self):
                 return [metric_family]
 
@@ -811,7 +807,7 @@ class TestCollectorRegistry(unittest.TestCase):
         Gauge('s_count', 'help', registry=registry)
 
     def custom_collector(self, metric_family, registry):
-        class CustomCollector(object):
+        class CustomCollector:
             def collect(self):
                 return [metric_family]
 
@@ -875,7 +871,6 @@ class TestCollectorRegistry(unittest.TestCase):
         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()
diff --git a/tests/test_exposition.py b/tests/test_exposition.py
index c48751e..a52805e 100644
--- a/tests/test_exposition.py
+++ b/tests/test_exposition.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
 import threading
 import time
 import unittest
@@ -36,7 +34,7 @@ class TestGenerateText(unittest.TestCase):
         time.time = self.old_time
 
     def custom_collector(self, metric_family):
-        class CustomCollector(object):
+        class CustomCollector:
             def collect(self):
                 return [metric_family]
 
@@ -159,14 +157,14 @@ gh_gsum 7.0
                          generate_latest(self.registry))
 
     def test_nonnumber(self):
-        class MyNumber(object):
+        class MyNumber:
             def __repr__(self):
                 return "MyNumber(123)"
 
             def __float__(self):
                 return 123.0
 
-        class MyCollector(object):
+        class MyCollector:
             def collect(self):
                 metric = Metric("nonnumber", "Non number", 'untyped')
                 metric.add_sample("nonnumber", {}, MyNumber())
@@ -177,7 +175,7 @@ gh_gsum 7.0
                          generate_latest(self.registry))
 
     def test_timestamp(self):
-        class MyCollector(object):
+        class MyCollector:
             def collect(self):
                 metric = Metric("ts", "help", 'untyped')
                 metric.add_sample("ts", {"foo": "a"}, 0, 123.456)
@@ -231,7 +229,7 @@ class TestPushGateway(unittest.TestCase):
         # which will cause the request handler to return 201.
         httpd_redirect = HTTPServer(('localhost', 0), TestHandler)
         self.redirect_address = TestHandler.redirect_address = \
-            'http://localhost:{0}/{1}'.format(httpd_redirect.server_address[1], redirect_flag)
+            f'http://localhost:{httpd_redirect.server_address[1]}/{redirect_flag}'
 
         class TestRedirectServer(threading.Thread):
             def run(self):
@@ -243,7 +241,7 @@ class TestPushGateway(unittest.TestCase):
 
         # set up the normal server to serve the example requests across test cases.
         httpd = HTTPServer(('localhost', 0), TestHandler)
-        self.address = 'http://localhost:{0}'.format(httpd.server_address[1])
+        self.address = f'http://localhost:{httpd.server_address[1]}'
 
         class TestServer(threading.Thread):
             def run(self):
@@ -383,7 +381,7 @@ class TestPushGateway(unittest.TestCase):
         self.assertEqual(handler.registry, self.registry)
 
     def test_metrics_handler_subclassing(self):
-        subclass = type(str('MetricsHandlerSubclass'), (MetricsHandler, object), {})
+        subclass = type('MetricsHandlerSubclass', (MetricsHandler, object), {})
         handler = subclass.factory(self.registry)
 
         self.assertTrue(issubclass(handler, (MetricsHandler, subclass)))
diff --git a/tests/test_gc_collector.py b/tests/test_gc_collector.py
index 0071442..59b9058 100644
--- a/tests/test_gc_collector.py
+++ b/tests/test_gc_collector.py
@@ -1,16 +1,13 @@
-from __future__ import unicode_literals
-
 import gc
 import platform
-import sys
 import unittest
 
 from prometheus_client import CollectorRegistry, GCCollector
 
-SKIP = sys.version_info < (3, 4) or platform.python_implementation() != "CPython"
+SKIP = platform.python_implementation() != "CPython"
 
 
-@unittest.skipIf(SKIP, "Test requires CPython 3.4 +")
+@unittest.skipIf(SKIP, "Test requires CPython")
 class TestGCCollector(unittest.TestCase):
     def setUp(self):
         gc.disable()
diff --git a/tests/test_multiprocess.py b/tests/test_multiprocess.py
index 310f8c7..9ec0578 100644
--- a/tests/test_multiprocess.py
+++ b/tests/test_multiprocess.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
 import glob
 import os
 import shutil
@@ -193,7 +191,7 @@ class TestMultiProcess(unittest.TestCase):
     def test_collect(self):
         pid = 0
         values.ValueClass = MultiProcessValue(lambda: pid)
-        labels = dict((i, i) for i in 'abcd')
+        labels = {i: i for i in 'abcd'}
 
         def add_label(key, value):
             l = labels.copy()
@@ -214,7 +212,7 @@ class TestMultiProcess(unittest.TestCase):
         g.labels(**labels).set(1)
         h.labels(**labels).observe(5)
 
-        metrics = dict((m.name, m) for m in self.collector.collect())
+        metrics = {m.name: m for m in self.collector.collect()}
 
         self.assertEqual(
             metrics['c'].samples, [Sample('c_total', labels, 2.0)]
@@ -253,7 +251,7 @@ class TestMultiProcess(unittest.TestCase):
     def test_merge_no_accumulate(self):
         pid = 0
         values.ValueClass = MultiProcessValue(lambda: pid)
-        labels = dict((i, i) for i in 'abcd')
+        labels = {i: i for i in 'abcd'}
 
         def add_label(key, value):
             l = labels.copy()
@@ -267,9 +265,9 @@ class TestMultiProcess(unittest.TestCase):
 
         path = os.path.join(os.environ['PROMETHEUS_MULTIPROC_DIR'], '*.db')
         files = glob.glob(path)
-        metrics = dict(
-            (m.name, m) for m in self.collector.merge(files, accumulate=False)
-        )
+        metrics = {
+            m.name: m for m in self.collector.merge(files, accumulate=False)
+        }
 
         metrics['h'].samples.sort(
             key=lambda x: (x[0], float(x[1].get('le', 0)))
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 780f0bc..1d20d27 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
 import math
 import unittest
 
@@ -13,7 +11,7 @@ from prometheus_client.parser import text_string_to_metric_families
 
 class TestParse(unittest.TestCase):
     def assertEqualMetrics(self, first, second, msg=None):
-        super(TestParse, self).assertEqual(first, second, msg)
+        super().assertEqual(first, second, msg)
 
         # Test that samples are actually named tuples of type Sample.
         for a, b in zip(first, second):
@@ -325,7 +323,7 @@ prometheus_local_storage_chunk_ops_total{type="unpin"} 32662.0
 """
         families = list(text_string_to_metric_families(text))
 
-        class TextCollector(object):
+        class TextCollector:
             def collect(self):
                 return families
 
diff --git a/tests/test_platform_collector.py b/tests/test_platform_collector.py
index 529d397..069eb3c 100644
--- a/tests/test_platform_collector.py
+++ b/tests/test_platform_collector.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
 import unittest
 
 from prometheus_client import CollectorRegistry, PlatformCollector
@@ -44,7 +42,7 @@ class TestPlatformCollector(unittest.TestCase):
         assert False
 
 
-class _MockPlatform(object):
+class _MockPlatform:
     def __init__(self):
         self._system = "system"
 
diff --git a/tests/test_process_collector.py b/tests/test_process_collector.py
index 86f6aa7..81be6a5 100644
--- a/tests/test_process_collector.py
+++ b/tests/test_process_collector.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
 import os
 import unittest
 
diff --git a/tests/test_twisted.py b/tests/test_twisted.py
index ea1796f..3f72d1b 100644
--- a/tests/test_twisted.py
+++ b/tests/test_twisted.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, unicode_literals
-
 from unittest import skipUnless
 
 from prometheus_client import CollectorRegistry, Counter, generate_latest
@@ -39,7 +37,7 @@ class MetricsResourceTest(TestCase):
 
         agent = Agent(reactor)
         port = server.getHost().port
-        url = "http://localhost:{port}/metrics".format(port=port)
+        url = f"http://localhost:{port}/metrics"
         d = agent.request(b"GET", url.encode("ascii"))
 
         d.addCallback(readBody)
diff --git a/tests/test_wsgi.py b/tests/test_wsgi.py
index 7a285a9..050c5ad 100644
--- a/tests/test_wsgi.py
+++ b/tests/test_wsgi.py
@@ -1,7 +1,3 @@
-from __future__ import absolute_import, unicode_literals
-
-import sys
-import unittest
 from unittest import TestCase
 from wsgiref.util import setup_testing_defaults
 
@@ -57,7 +53,6 @@ class WSGITest(TestCase):
     def test_report_metrics_4(self):
         self.validate_metrics("failed_requests", "Number of failed requests", 7)
 
-    @unittest.skipIf(sys.version_info < (3, 3), "Test requires Python 3.3+.")
     def test_favicon_path(self):
         from unittest.mock import patch