Repository: prometheus/client_python
Pull request #87 (2016-07-03 15:32:48)

Allow passing label values as keyword arguments


Patch:
diff --git a/README.md b/README.md
index 5af4cc5..67220f9 100644
--- a/README.md
+++ b/README.md
@@ -185,13 +185,13 @@ c.labels('get', '/').inc()
 c.labels('post', '/submit').inc()
 ```
 
-Labels can also be provided as a dict:
+Labels can also be passed as keyword-arguments:
 
 ```python
 from prometheus_client import Counter
 c = Counter('my_requests_total', 'HTTP Failures', ['method', 'endpoint'])
-c.labels({'method': 'get', 'endpoint': '/'}).inc()
-c.labels({'method': 'post', 'endpoint': '/submit'}).inc()
+c.labels(method='get', endpoint='/').inc()
+c.labels(method='post', endpoint='/submit').inc()
 ```
 
 ### Process Collector
@@ -369,7 +369,7 @@ REGISTRY.register(CustomCollector())
 ## Parser
 
 The Python client supports parsing the Promeheus text format.
-This is intended for advanced use cases where you have servers 
+This is intended for advanced use cases where you have servers
 exposing Prometheus metrics and need to get them into some other
 system.
 
@@ -379,4 +379,3 @@ for family in text_string_to_metric_families("my_gauge 1.0\n"):
   for sample in family.samples:
     print("Name: {0} Labels: {1} Value: {2}".format(*sample))
 ```
-
diff --git a/prometheus_client/core.py b/prometheus_client/core.py
index 6619802..1752b71 100644
--- a/prometheus_client/core.py
+++ b/prometheus_client/core.py
@@ -253,7 +253,7 @@ class _LabelWrapper(object):
             if l.startswith('__'):
                 raise ValueError('Invalid label metric name: ' + l)
 
-    def labels(self, *labelvalues):
+    def labels(self, *labelvalues, **labelkwargs):
         '''Return the child for the given labelset.
 
         All metrics can have labels, allowing grouping of related time series.
@@ -276,10 +276,13 @@ class _LabelWrapper(object):
         See the best practices on [naming](http://prometheus.io/docs/practices/naming/)
         and [labels](http://prometheus.io/docs/practices/instrumentation/#use-labels).
         '''
-        if len(labelvalues) == 1 and type(labelvalues[0]) == dict:
-            if sorted(labelvalues[0].keys()) != sorted(self._labelnames):
+        if labelvalues and labelkwargs:
+            raise ValueError("Can't pass both *args and **kwargs")
+
+        if labelkwargs:
+            if sorted(labelkwargs) != sorted(self._labelnames):
                 raise ValueError('Incorrect label names')
-            labelvalues = tuple([unicode(labelvalues[0][l]) for l in self._labelnames])
+            labelvalues = tuple([unicode(labelkwargs[l]) for l in self._labelnames])
         else:
             if len(labelvalues) != len(self._labelnames):
                 raise ValueError('Incorrect label count')


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index 83b15e8..d8d8311 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -249,7 +249,7 @@ class TestMetricWrapper(unittest.TestCase):
 
     def test_labels_coerced_to_string(self):
         self.counter.labels(None).inc()
-        self.counter.labels({'l': None}).inc()
+        self.counter.labels(l=None).inc()
         self.assertEqual(2, self.registry.get_sample_value('c', {'l': 'None'}))
 
         self.counter.remove(None)
@@ -259,26 +259,27 @@ class TestMetricWrapper(unittest.TestCase):
         class Test(object):
             __str__ = None
         self.assertRaises(TypeError, self.counter.labels, Test())
-        self.assertRaises(TypeError, self.counter.labels, {'l': Test()})
+        self.assertRaises(TypeError, self.counter.labels, l=Test())
 
     def test_namespace_subsystem_concatenated(self):
         c = Counter('c', 'help', namespace='a', subsystem='b', registry=self.registry)
         c.inc()
         self.assertEqual(1, self.registry.get_sample_value('a_b_c'))
 
-    def test_labels_by_dict(self):
-        self.counter.labels({'l': 'x'}).inc()
+    def test_labels_by_kwarg(self):
+        self.counter.labels(l='x').inc()
         self.assertEqual(1, self.registry.get_sample_value('c', {'l': 'x'}))
-        self.assertRaises(ValueError, self.counter.labels, {'l': 'x', 'm': 'y'})
-        self.assertRaises(ValueError, self.counter.labels, {'m': 'y'})
-        self.assertRaises(ValueError, self.counter.labels, {})
-        self.two_labels.labels({'a': 'x', 'b': 'y'}).inc()
+        self.assertRaises(ValueError, self.counter.labels, l='x', m='y')
+        self.assertRaises(ValueError, self.counter.labels, m='y')
+        self.assertRaises(ValueError, self.counter.labels)
+        self.two_labels.labels(a='x', b='y').inc()
         self.assertEqual(1, self.registry.get_sample_value('two', {'a': 'x', 'b': 'y'}))
-        self.assertRaises(ValueError, self.two_labels.labels, {'a': 'x', 'b': 'y', 'c': 'z'})
-        self.assertRaises(ValueError, self.two_labels.labels, {'a': 'x', 'c': 'z'})
-        self.assertRaises(ValueError, self.two_labels.labels, {'b': 'y', 'c': 'z'})
-        self.assertRaises(ValueError, self.two_labels.labels, {'c': 'z'})
-        self.assertRaises(ValueError, self.two_labels.labels, {})
+        self.assertRaises(ValueError, self.two_labels.labels, a='x', b='y', c='z')
+        self.assertRaises(ValueError, self.two_labels.labels, a='x', c='z')
+        self.assertRaises(ValueError, self.two_labels.labels, b='y', c='z')
+        self.assertRaises(ValueError, self.two_labels.labels, c='z')
+        self.assertRaises(ValueError, self.two_labels.labels)
+        self.assertRaises(ValueError, self.two_labels.labels, {'a': 'x'}, b='y')
 
     def test_invalid_names_raise(self):
         self.assertRaises(ValueError, Counter, '', 'help')