diff --git a/.circleci/config.yml b/.circleci/config.yml index f8cbe31..624e4ea 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -75,12 +75,11 @@ workflows: matrix: parameters: python: - - "3.6" - - "3.7" - "3.8" - "3.9" - "3.10" - "3.11" + - "3.12" - test_nooptionals: matrix: parameters: @@ -90,4 +89,4 @@ workflows: matrix: parameters: python: - - "3.7" + - "3.8" diff --git a/README.md b/README.md index 0f3cb45..ef9930a 100644 --- a/README.md +++ b/README.md @@ -252,6 +252,20 @@ h = Histogram('request_latency_seconds', 'Description of histogram') h.observe(4.7, {'trace_id': 'abc123'}) ``` +Exemplars are only rendered in the OpenMetrics exposition format. If using the +HTTP server or apps in this library, content negotiation can be used to specify +OpenMetrics (which is done by default in Prometheus). Otherwise it will be +necessary to use `generate_latest` from +`prometheus_client.openmetrics.exposition` to view exemplars. + +To view exemplars in Prometheus it is also necessary to enable the the +exemplar-storage feature flag: +``` +--enable-feature=exemplar-storage +``` +Additional information is available in [the Prometheus +documentation](https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage). + ### Disabling `_created` metrics By default counters, histograms, and summaries export an additional series @@ -590,8 +604,9 @@ To do so you need to create a custom collector, for example: ```python from prometheus_client.core import GaugeMetricFamily, CounterMetricFamily, REGISTRY +from prometheus_client.registry import Collector -class CustomCollector(object): +class CustomCollector(Collector): def collect(self): yield GaugeMetricFamily('my_gauge', 'Help text', value=7) c = CounterMetricFamily('my_counter_total', 'Help text', labels=['foo']) @@ -696,9 +711,10 @@ Gauges have several modes they can run in, which can be selected with the `multi - 'min': Return a single timeseries that is the minimum of the values of all processes (alive or dead). - 'max': Return a single timeseries that is the maximum of the values of all processes (alive or dead). - 'sum': Return a single timeseries that is the sum of the values of all processes (alive or dead). +- 'mostrecent': Return a single timeseries that is the most recent value among all processes (alive or dead). Prepend 'live' to the beginning of the mode to return the same result but only considering living processes -(e.g., 'liveall, 'livesum', 'livemax', 'livemin'). +(e.g., 'liveall, 'livesum', 'livemax', 'livemin', 'livemostrecent'). ```python from prometheus_client import Gauge @@ -722,6 +738,34 @@ for family in text_string_to_metric_families(u"my_gauge 1.0\n"): print("Name: {0} Labels: {1} Value: {2}".format(*sample)) ``` +## Restricted registry + +Registries support restriction to only return specific metrics. +If you’re using the built-in HTTP server, you can use the GET parameter "name[]", since it’s an array it can be used multiple times. +If you’re directly using `generate_latest`, you can use the function `restricted_registry()`. + +```python +curl --get --data-urlencode "name[]=python_gc_objects_collected_total" --data-urlencode "name[]=python_info" http://127.0.0.1:9200/metrics +``` + +```python +from prometheus_client import generate_latest + +generate_latest(REGISTRY.restricted_registry(['python_gc_objects_collected_total', 'python_info'])) +``` + +```python +# HELP python_info Python platform information +# TYPE python_info gauge +python_info{implementation="CPython",major="3",minor="9",patchlevel="3",version="3.9.3"} 1.0 +# HELP python_gc_objects_collected_total Objects collected during gc +# TYPE python_gc_objects_collected_total counter +python_gc_objects_collected_total{generation="0"} 73129.0 +python_gc_objects_collected_total{generation="1"} 8594.0 +python_gc_objects_collected_total{generation="2"} 296.0 +``` + + ## Links * [Releases](https://github.com/prometheus/client_python/releases): The releases page shows the history of the project and acts as a changelog. diff --git a/debian/patches/0001-import-unvendorized-decorator.patch b/debian/patches/0001-import-unvendorized-decorator.patch index c6cca47..9b2da8a 100644 --- a/debian/patches/0001-import-unvendorized-decorator.patch +++ b/debian/patches/0001-import-unvendorized-decorator.patch @@ -9,9 +9,9 @@ Index: python3-prometheus-client/prometheus_client/context_managers.py =================================================================== --- python3-prometheus-client.orig/prometheus_client/context_managers.py +++ python3-prometheus-client/prometheus_client/context_managers.py -@@ -6,7 +6,7 @@ from typing import Any, Callable, Option - if sys.version_info >= (3, 8, 0): - from typing import Literal +@@ -5,7 +5,7 @@ from typing import ( + Union, + ) -from .decorator import decorate +from decorator import decorate diff --git a/prometheus_client/__init__.py b/prometheus_client/__init__.py index a8fb88d..84a7ba8 100644 --- a/prometheus_client/__init__.py +++ b/prometheus_client/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python from . import ( exposition, gc_collector, metrics, metrics_core, platform_collector, @@ -11,7 +11,10 @@ write_to_textfile, ) from .gc_collector import GC_COLLECTOR, GCCollector -from .metrics import Counter, Enum, Gauge, Histogram, Info, Summary +from .metrics import ( + Counter, disable_created_metrics, enable_created_metrics, Enum, Gauge, + Histogram, Info, Summary, +) from .metrics_core import Metric from .platform_collector import PLATFORM_COLLECTOR, PlatformCollector from .process_collector import PROCESS_COLLECTOR, ProcessCollector @@ -27,6 +30,8 @@ 'Histogram', 'Info', 'Enum', + 'enable_created_metrics', + 'disable_created_metrics', 'CONTENT_TYPE_LATEST', 'generate_latest', 'MetricsHandler', diff --git a/prometheus_client/context_managers.py b/prometheus_client/context_managers.py index 964a930..3988ec2 100644 --- a/prometheus_client/context_managers.py +++ b/prometheus_client/context_managers.py @@ -1,13 +1,10 @@ -import sys from timeit import default_timer from types import TracebackType from typing import ( - Any, Callable, Optional, Tuple, Type, TYPE_CHECKING, TypeVar, Union, + Any, Callable, Literal, Optional, Tuple, Type, TYPE_CHECKING, TypeVar, + Union, ) -if sys.version_info >= (3, 8, 0): - from typing import Literal - from .decorator import decorate if TYPE_CHECKING: @@ -23,7 +20,7 @@ def __init__(self, counter: "Counter", exception: Union[Type[BaseException], Tup def __enter__(self) -> None: pass - def __exit__(self, typ: Optional[Type[BaseException]], value: Optional[BaseException], traceback: Optional[TracebackType]) -> "Literal[False]": + def __exit__(self, typ: Optional[Type[BaseException]], value: Optional[BaseException], traceback: Optional[TracebackType]) -> Literal[False]: if isinstance(value, self._exception): self._counter.inc() return False diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index deaa6ed..13af927 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -38,7 +38,6 @@ CONTENT_TYPE_LATEST = 'text/plain; version=0.0.4; charset=utf-8' """Content type of the latest text format""" -PYTHON376_OR_NEWER = sys.version_info > (3, 7, 5) class _PrometheusRedirectHandler(HTTPRedirectHandler): @@ -545,10 +544,7 @@ def _use_gateway( ) -> None: 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 - and gateway_url.scheme not in ['http', 'https'] - ): + if not gateway_url.scheme or gateway_url.scheme not in ['http', 'https']: gateway = f'http://{gateway}' gateway = gateway.rstrip('/') diff --git a/prometheus_client/metrics.py b/prometheus_client/metrics.py index 392e1e4..7e5b030 100644 --- a/prometheus_client/metrics.py +++ b/prometheus_client/metrics.py @@ -3,8 +3,8 @@ import time import types from typing import ( - Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Type, - TypeVar, Union, + Any, Callable, Dict, Iterable, List, Literal, Optional, Sequence, Tuple, + Type, TypeVar, Union, ) from . import values # retain this import style for testability @@ -70,6 +70,18 @@ def _get_use_created() -> bool: _use_created = _get_use_created() +def disable_created_metrics(): + """Disable exporting _created metrics on counters, histograms, and summaries.""" + global _use_created + _use_created = False + + +def enable_created_metrics(): + """Enable exporting _created metrics on counters, histograms, and summaries.""" + global _use_created + _use_created = True + + class MetricWrapperBase(Collector): _type: Optional[str] = None _reserved_labelnames: Sequence[str] = () @@ -346,7 +358,8 @@ def f(): d.set_function(lambda: len(my_dict)) """ _type = 'gauge' - _MULTIPROC_MODES = frozenset(('all', 'liveall', 'min', 'livemin', 'max', 'livemax', 'sum', 'livesum')) + _MULTIPROC_MODES = frozenset(('all', 'liveall', 'min', 'livemin', 'max', 'livemax', 'sum', 'livesum', 'mostrecent', 'livemostrecent')) + _MOST_RECENT_MODES = frozenset(('mostrecent', 'livemostrecent')) def __init__(self, name: str, @@ -357,7 +370,7 @@ def __init__(self, unit: str = '', registry: Optional[CollectorRegistry] = REGISTRY, _labelvalues: Optional[Sequence[str]] = None, - multiprocess_mode: str = 'all', + multiprocess_mode: Literal['all', 'liveall', 'min', 'livemin', 'max', 'livemax', 'sum', 'livesum', 'mostrecent', 'livemostrecent'] = 'all', ): self._multiprocess_mode = multiprocess_mode if multiprocess_mode not in self._MULTIPROC_MODES: @@ -373,6 +386,7 @@ def __init__(self, _labelvalues=_labelvalues, ) self._kwargs['multiprocess_mode'] = self._multiprocess_mode + self._is_most_recent = self._multiprocess_mode in self._MOST_RECENT_MODES def _metric_init(self) -> None: self._value = values.ValueClass( @@ -382,18 +396,25 @@ def _metric_init(self) -> None: def inc(self, amount: float = 1) -> None: """Increment gauge by the given amount.""" + if self._is_most_recent: + raise RuntimeError("inc must not be used with the mostrecent mode") self._raise_if_not_observable() self._value.inc(amount) def dec(self, amount: float = 1) -> None: """Decrement gauge by the given amount.""" + if self._is_most_recent: + raise RuntimeError("dec must not be used with the mostrecent mode") self._raise_if_not_observable() self._value.inc(-amount) def set(self, value: float) -> None: """Set gauge to the given value.""" self._raise_if_not_observable() - self._value.set(float(value)) + if self._is_most_recent: + self._value.set(float(value), timestamp=time.time()) + else: + self._value.set(float(value)) def set_to_current_time(self) -> None: """Set gauge to the current unixtime.""" diff --git a/prometheus_client/mmap_dict.py b/prometheus_client/mmap_dict.py index c3de38f..edd895c 100644 --- a/prometheus_client/mmap_dict.py +++ b/prometheus_client/mmap_dict.py @@ -6,17 +6,18 @@ _INITIAL_MMAP_SIZE = 1 << 16 _pack_integer_func = struct.Struct(b'i').pack -_pack_double_func = struct.Struct(b'd').pack +_pack_two_doubles_func = struct.Struct(b'dd').pack _unpack_integer = struct.Struct(b'i').unpack_from -_unpack_double = struct.Struct(b'd').unpack_from +_unpack_two_doubles = struct.Struct(b'dd').unpack_from # struct.pack_into has atomicity issues because it will temporarily write 0 into # the mmap, resulting in false reads to 0 when experiencing a lot of writes. # Using direct assignment solves this issue. -def _pack_double(data, pos, value): - data[pos:pos + 8] = _pack_double_func(value) + +def _pack_two_doubles(data, pos, value, timestamp): + data[pos:pos + 16] = _pack_two_doubles_func(value, timestamp) def _pack_integer(data, pos, value): @@ -24,7 +25,7 @@ def _pack_integer(data, pos, value): def _read_all_values(data, used=0): - """Yield (key, value, pos). No locking is performed.""" + """Yield (key, value, timestamp, pos). No locking is performed.""" if used <= 0: # If not valid `used` value is passed in, read it from the file. @@ -41,9 +42,9 @@ def _read_all_values(data, used=0): encoded_key = data[pos:pos + encoded_len] padded_len = encoded_len + (8 - (encoded_len + 4) % 8) pos += padded_len - value = _unpack_double(data, pos)[0] - yield encoded_key.decode('utf-8'), value, pos - pos += 8 + value, timestamp = _unpack_two_doubles(data, pos) + yield encoded_key.decode('utf-8'), value, timestamp, pos + pos += 16 class MmapedDict: @@ -53,7 +54,8 @@ class MmapedDict: Then 4 bytes of padding. There's then a number of entries, consisting of a 4 byte int which is the size of the next field, a utf-8 encoded string key, padding to a 8 byte - alignment, and then a 8 byte float which is the value. + alignment, and then a 8 byte float which is the value and a 8 byte float + which is a UNIX timestamp in seconds. Not thread safe. """ @@ -76,7 +78,7 @@ def __init__(self, filename, read_mode=False): _pack_integer(self._m, 0, self._used) else: if not read_mode: - for key, _, pos in self._read_all_values(): + for key, _, _, pos in self._read_all_values(): self._positions[key] = pos @staticmethod @@ -95,7 +97,7 @@ def _init_value(self, key): encoded = key.encode('utf-8') # Pad to be 8-byte aligned. padded = encoded + (b' ' * (8 - (len(encoded) + 4) % 8)) - value = struct.pack(f'i{len(padded)}sd'.encode(), len(encoded), padded, 0.0) + value = struct.pack(f'i{len(padded)}sdd'.encode(), len(encoded), padded, 0.0, 0.0) while self._used + len(value) > self._capacity: self._capacity *= 2 self._f.truncate(self._capacity) @@ -105,30 +107,28 @@ def _init_value(self, key): # Update how much space we've used. self._used += len(value) _pack_integer(self._m, 0, self._used) - self._positions[key] = self._used - 8 + self._positions[key] = self._used - 16 def _read_all_values(self): """Yield (key, value, pos). No locking is performed.""" return _read_all_values(data=self._m, used=self._used) def read_all_values(self): - """Yield (key, value). No locking is performed.""" - for k, v, _ in self._read_all_values(): - yield k, v + """Yield (key, value, timestamp). No locking is performed.""" + for k, v, ts, _ in self._read_all_values(): + yield k, v, ts def read_value(self, key): if key not in self._positions: self._init_value(key) pos = self._positions[key] - # We assume that reading from an 8 byte aligned value is atomic - return _unpack_double(self._m, pos)[0] + return _unpack_two_doubles(self._m, pos) - def write_value(self, key, value): + def write_value(self, key, value, timestamp): if key not in self._positions: self._init_value(key) pos = self._positions[key] - # We assume that writing to an 8 byte aligned value is atomic - _pack_double(self._m, pos, value) + _pack_two_doubles(self._m, pos, value, timestamp) def close(self): if self._f: diff --git a/prometheus_client/multiprocess.py b/prometheus_client/multiprocess.py index dd34391..7021b49 100644 --- a/prometheus_client/multiprocess.py +++ b/prometheus_client/multiprocess.py @@ -68,7 +68,7 @@ def _parse_key(key): # the file is missing continue raise - for key, value, _ in file_values: + for key, value, timestamp, _ in file_values: metric_name, name, labels, labels_key, help_text = _parse_key(key) metric = metrics.get(metric_name) @@ -79,7 +79,7 @@ def _parse_key(key): if typ == 'gauge': pid = parts[2][:-3] metric._multiprocess_mode = parts[1] - metric.add_sample(name, labels_key + (('pid', pid),), value) + metric.add_sample(name, labels_key + (('pid', pid),), value, timestamp) else: # The duplicates and labels are fixed in the next for. metric.add_sample(name, labels_key, value) @@ -89,6 +89,7 @@ def _parse_key(key): def _accumulate_metrics(metrics, accumulate): for metric in metrics.values(): samples = defaultdict(float) + sample_timestamps = defaultdict(float) buckets = defaultdict(lambda: defaultdict(float)) samples_setdefault = samples.setdefault for s in metric.samples: @@ -105,6 +106,12 @@ def _accumulate_metrics(metrics, accumulate): samples[without_pid_key] = value elif metric._multiprocess_mode in ('sum', 'livesum'): samples[without_pid_key] += value + elif metric._multiprocess_mode in ('mostrecent', 'livemostrecent'): + current_timestamp = sample_timestamps[without_pid_key] + timestamp = float(timestamp or 0) + if current_timestamp < timestamp: + samples[without_pid_key] = value + sample_timestamps[without_pid_key] = timestamp else: # all/liveall samples[(name, labels)] = value diff --git a/prometheus_client/openmetrics/exposition.py b/prometheus_client/openmetrics/exposition.py index d1c1f06..b019030 100644 --- a/prometheus_client/openmetrics/exposition.py +++ b/prometheus_client/openmetrics/exposition.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python from ..utils import floatToGoString diff --git a/prometheus_client/openmetrics/parser.py b/prometheus_client/openmetrics/parser.py index fc25f08..6128a0d 100644 --- a/prometheus_client/openmetrics/parser.py +++ b/prometheus_client/openmetrics/parser.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python import io as StringIO diff --git a/prometheus_client/platform_collector.py b/prometheus_client/platform_collector.py index f99caa6..6040fcc 100644 --- a/prometheus_client/platform_collector.py +++ b/prometheus_client/platform_collector.py @@ -9,7 +9,7 @@ class PlatformCollector(Collector): """Collector for python platform information""" def __init__(self, - registry: CollectorRegistry = REGISTRY, + registry: Optional[CollectorRegistry] = REGISTRY, platform: Optional[Any] = None, ): self._platform = pf if platform is None else platform diff --git a/prometheus_client/process_collector.py b/prometheus_client/process_collector.py index 8a38d05..2894e87 100644 --- a/prometheus_client/process_collector.py +++ b/prometheus_client/process_collector.py @@ -1,5 +1,5 @@ import os -from typing import Callable, Iterable, Union +from typing import Callable, Iterable, Optional, Union from .metrics_core import CounterMetricFamily, GaugeMetricFamily, Metric from .registry import Collector, CollectorRegistry, REGISTRY @@ -20,7 +20,7 @@ def __init__(self, namespace: str = '', pid: Callable[[], Union[int, str]] = lambda: 'self', proc: str = '/proc', - registry: CollectorRegistry = REGISTRY): + registry: Optional[CollectorRegistry] = REGISTRY): self._namespace = namespace self._pid = pid self._proc = proc diff --git a/prometheus_client/values.py b/prometheus_client/values.py index 3373379..6ff85e3 100644 --- a/prometheus_client/values.py +++ b/prometheus_client/values.py @@ -19,7 +19,7 @@ def inc(self, amount): with self._lock: self._value += amount - def set(self, value): + def set(self, value, timestamp=None): with self._lock: self._value = value @@ -82,7 +82,7 @@ def __reset(self): files[file_prefix] = MmapedDict(filename) self._file = files[file_prefix] self._key = mmap_key(metric_name, name, labelnames, labelvalues, help_text) - self._value = self._file.read_value(self._key) + self._value, self._timestamp = self._file.read_value(self._key) def __check_for_pid_change(self): actual_pid = process_identifier() @@ -99,13 +99,15 @@ def inc(self, amount): with lock: self.__check_for_pid_change() self._value += amount - self._file.write_value(self._key, self._value) + self._timestamp = 0.0 + self._file.write_value(self._key, self._value, self._timestamp) - def set(self, value): + def set(self, value, timestamp=None): with lock: self.__check_for_pid_change() self._value = value - self._file.write_value(self._key, self._value) + self._timestamp = timestamp or 0.0 + self._file.write_value(self._key, self._value, self._timestamp) def set_exemplar(self, exemplar): # TODO: Implement exemplars for multiprocess mode. diff --git a/setup.py b/setup.py index 4b320dc..92f6f47 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name="prometheus_client", - version="0.17.1", + version="0.18.0", author="Brian Brazil", author_email="brian.brazil@robustperception.io", description="Python client for the Prometheus monitoring system.", @@ -30,7 +30,7 @@ 'twisted': ['twisted'], }, test_suite="tests", - python_requires=">=3.6", + python_requires=">=3.8", classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", @@ -38,12 +38,11 @@ "Intended Audience :: System Administrators", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: System :: Monitoring", diff --git a/tests/test_multiprocess.py b/tests/test_multiprocess.py index 10990ad..6e188e5 100644 --- a/tests/test_multiprocess.py +++ b/tests/test_multiprocess.py @@ -185,6 +185,26 @@ def test_gauge_livesum(self): mark_process_dead(123, os.environ['PROMETHEUS_MULTIPROC_DIR']) self.assertEqual(2, self.registry.get_sample_value('g')) + def test_gauge_mostrecent(self): + g1 = Gauge('g', 'help', registry=None, multiprocess_mode='mostrecent') + values.ValueClass = MultiProcessValue(lambda: 456) + g2 = Gauge('g', 'help', registry=None, multiprocess_mode='mostrecent') + g2.set(2) + g1.set(1) + self.assertEqual(1, self.registry.get_sample_value('g')) + mark_process_dead(123, os.environ['PROMETHEUS_MULTIPROC_DIR']) + self.assertEqual(1, self.registry.get_sample_value('g')) + + def test_gauge_livemostrecent(self): + g1 = Gauge('g', 'help', registry=None, multiprocess_mode='livemostrecent') + values.ValueClass = MultiProcessValue(lambda: 456) + g2 = Gauge('g', 'help', registry=None, multiprocess_mode='livemostrecent') + g2.set(2) + g1.set(1) + self.assertEqual(1, self.registry.get_sample_value('g')) + mark_process_dead(123, os.environ['PROMETHEUS_MULTIPROC_DIR']) + self.assertEqual(2, self.registry.get_sample_value('g')) + def test_namespace_subsystem(self): c1 = Counter('c', 'help', registry=None, namespace='ns', subsystem='ss') c1.inc(1) @@ -369,28 +389,28 @@ def setUp(self): self.d = mmap_dict.MmapedDict(self.tempfile) def test_process_restart(self): - self.d.write_value('abc', 123.0) + self.d.write_value('abc', 123.0, 987.0) self.d.close() self.d = mmap_dict.MmapedDict(self.tempfile) - self.assertEqual(123, self.d.read_value('abc')) - self.assertEqual([('abc', 123.0)], list(self.d.read_all_values())) + self.assertEqual((123, 987.0), self.d.read_value('abc')) + self.assertEqual([('abc', 123.0, 987.0)], list(self.d.read_all_values())) def test_expansion(self): key = 'a' * mmap_dict._INITIAL_MMAP_SIZE - self.d.write_value(key, 123.0) - self.assertEqual([(key, 123.0)], list(self.d.read_all_values())) + self.d.write_value(key, 123.0, 987.0) + self.assertEqual([(key, 123.0, 987.0)], list(self.d.read_all_values())) def test_multi_expansion(self): key = 'a' * mmap_dict._INITIAL_MMAP_SIZE * 4 - self.d.write_value('abc', 42.0) - self.d.write_value(key, 123.0) - self.d.write_value('def', 17.0) + self.d.write_value('abc', 42.0, 987.0) + self.d.write_value(key, 123.0, 876.0) + self.d.write_value('def', 17.0, 765.0) self.assertEqual( - [('abc', 42.0), (key, 123.0), ('def', 17.0)], + [('abc', 42.0, 987.0), (key, 123.0, 876.0), ('def', 17.0, 765.0)], list(self.d.read_all_values())) def test_corruption_detected(self): - self.d.write_value('abc', 42.0) + self.d.write_value('abc', 42.0, 987.0) # corrupt the written data self.d._m[8:16] = b'somejunk' with self.assertRaises(RuntimeError): diff --git a/tests/test_twisted.py b/tests/test_twisted.py index 3f72d1b..e63c903 100644 --- a/tests/test_twisted.py +++ b/tests/test_twisted.py @@ -3,6 +3,8 @@ from prometheus_client import CollectorRegistry, Counter, generate_latest try: + from warnings import filterwarnings + from twisted.internet import reactor from twisted.trial.unittest import TestCase from twisted.web.client import Agent, readBody @@ -40,6 +42,10 @@ def test_reports_metrics(self): url = f"http://localhost:{port}/metrics" d = agent.request(b"GET", url.encode("ascii")) + # Ignore expected DeprecationWarning. + filterwarnings("ignore", category=DeprecationWarning, message="Using readBody " + "with a transport that does not have an abortConnection method") + d.addCallback(readBody) d.addCallback(self.assertEqual, generate_latest(self.registry)) diff --git a/tox.ini b/tox.ini index 3a5e23b..6d6b756 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,15 @@ [tox] -envlist = coverage-clean,py3.6,py3.7,py3.8,py3.9,py3.10,py3.11,pypy3.7,py3.9-nooptionals,coverage-report,flake8,isort,mypy +envlist = coverage-clean,py{3.8,3.9,3.10,3.11,3.12,py3.8,3.9-nooptionals},coverage-report,flake8,isort,mypy [testenv] deps = coverage pytest attrs - {py3.7,pypy3.7}: twisted - py3.7: asgiref + {py3.8,pypy3.8}: twisted + py3.8: asgiref # See https://github.com/django/asgiref/issues/393 for why we need to pin asgiref for pypy - pypy3.7: asgiref==3.6.0 + pypy3.8: asgiref==3.6.0 commands = coverage run --parallel -m pytest {posargs} [testenv:py3.9-nooptionals]