Repository: open-telemetry/opentelemetry-python

Issue:
Sum/Histogram/Gauge.data_points annotated as Sequence but actual type is generator
This applies to all of the MetricsData data types `Sum`, `Histogram` and `Gauge`. The type annotation is `Sequence`:
https://github.com/open-telemetry/opentelemetry-python/blob/778c4b1cffdd448c531c868a9470618eb8a6517c/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py#L68

But the implementation assigns an `Iterable` (specifically a generator which is not a valid `Sequence`):
https://github.com/open-telemetry/opentelemetry-python/blob/778c4b1cffdd448c531c868a9470618eb8a6517c/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py#L160

**What is the expected behavior?**
Type annotated as `Sequence` should work with `len()`, subscription, [etc.](https://docs.python.org/3/library/stdtypes.html#common-sequence-operations). A more subtle bug the generator causes is that `data_points` can only be iterated over once, which will consume the generator.

**What is the actual behavior?**
```
TypeError: object of type 'generator' has no len()
```

**Additional context**
Possible fixes

I'd recommend adding calls to `list()` or `tuple()` to convert the generator to a sequence or alter `_ViewInstrumentMatch.collect()` to return a sequence itself. I think this is appropriate as the generators are actually [holding SDK locks](https://github.com/open-telemetry/opentelemetry-python/blob/778c4b1cffdd448c531c868a9470618eb8a6517c/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py#L137) which can remain locked if the generator is not consumed.
Sum/Histogram/Gauge.data_points annotated as Sequence but actual type is generator
This applies to all of the MetricsData data types `Sum`, `Histogram` and `Gauge`. The type annotation is `Sequence`:
https://github.com/open-telemetry/opentelemetry-python/blob/778c4b1cffdd448c531c868a9470618eb8a6517c/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py#L68

But the implementation assigns an `Iterable` (specifically a generator which is not a valid `Sequence`):
https://github.com/open-telemetry/opentelemetry-python/blob/778c4b1cffdd448c531c868a9470618eb8a6517c/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py#L160

**What is the expected behavior?**
Type annotated as `Sequence` should work with `len()`, subscription, [etc.](https://docs.python.org/3/library/stdtypes.html#common-sequence-operations). A more subtle bug the generator causes is that `data_points` can only be iterated over once, which will consume the generator.

**What is the actual behavior?**
```
TypeError: object of type 'generator' has no len()
```

**Additional context**
Possible fixes

I'd recommend adding calls to `list()` or `tuple()` to convert the generator to a sequence or alter `_ViewInstrumentMatch.collect()` to return a sequence itself. I think this is appropriate as the generators are actually [holding SDK locks](https://github.com/open-telemetry/opentelemetry-python/blob/778c4b1cffdd448c531c868a9470618eb8a6517c/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py#L137) which can remain locked if the generator is not consumed.


Discussion:
This seems like an approachable first issue for me, would give me some introduction to the metrics implementation and force me to get the project up and running. Do you mind if I give it a shot?
This seems like an approachable first issue for me, would give me some introduction to the metrics implementation and force me to get the project up and running. Do you mind if I give it a shot?

Resolving patch:
diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py
--- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py
+++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py
@@ -16,7 +16,7 @@
 from logging import getLogger
 from threading import Lock
 from time import time_ns
-from typing import Dict, Iterable
+from typing import Dict, List, Sequence
 
 from opentelemetry.metrics import Instrument
 from opentelemetry.sdk.metrics._internal.aggregation import (
@@ -126,12 +126,14 @@ def collect(
         self,
         aggregation_temporality: AggregationTemporality,
         collection_start_nanos: int,
-    ) -> Iterable[DataPointT]:
+    ) -> Sequence[DataPointT]:
 
+        data_points: List[DataPointT] = []
         with self._lock:
             for aggregation in self._attributes_aggregation.values():
                 data_point = aggregation.collect(
                     aggregation_temporality, collection_start_nanos
                 )
                 if data_point is not None:
-                    yield data_point
+                    data_points.append(data_point)
+        return data_points


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -10,7 +10,7 @@ env:
   # Otherwise, set variable to the commit of your branch on
   # opentelemetry-python-contrib which is compatible with these Core repo
   # changes.
-  CONTRIB_REPO_SHA: c6134843900e2eeb1b8b3383a897b38cc0905c38
+  CONTRIB_REPO_SHA: main
   # This is needed because we do not clone the core repo in contrib builds anymore.
   # When running contrib builds as part of core builds, we use actions/checkout@v2 which
   # does not set an environment variable (simply just runs tox), which is different when
diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py
--- a/opentelemetry-sdk/tests/metrics/test_metrics.py
+++ b/opentelemetry-sdk/tests/metrics/test_metrics.py
@@ -514,11 +514,11 @@ def test_duplicate_instrument_aggregate_data(self):
         self.assertEqual(metric_0.name, "counter")
         self.assertEqual(metric_0.unit, "unit")
         self.assertEqual(metric_0.description, "description")
-        self.assertEqual(next(metric_0.data.data_points).value, 3)
+        self.assertEqual(next(iter(metric_0.data.data_points)).value, 3)
 
         metric_1 = scope_metrics[1].metrics[0]
 
         self.assertEqual(metric_1.name, "counter")
         self.assertEqual(metric_1.unit, "unit")
         self.assertEqual(metric_1.description, "description")
-        self.assertEqual(next(metric_1.data.data_points).value, 7)
+        self.assertEqual(next(iter(metric_1.data.data_points)).value, 7)