Repository: opendatacube/datacube-core

Issue:
datacube.load with malformed single-measurement queries
If `datacube.Datacube().load` is passed a string as the `measurements=` argument (instead of a list containing only that string) then it tries to iterate over the characters in the string (as separate, if somewhat terse, band names). 

This inevitably results in an error, that is not necessarily beginner-friendly. Instead I think the API function should validate the input (and either tolerate a single measurement as a string, or provide a more helpful error message). 


Discussion:
This seems like a similar issue to #1033, and could probably return a similar error message to direct the user to provide a valid input
I think in this case `measurements="red"` should be treated as `measurements=("red",)` without any error or warning.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Does anybody really want to close and tag this issue as "wontfix"?

IMO worth keeping this issue open because:

- It seems quite _easy_ to implement.
- Validating the API function input (then producing a more friendly error message for a common mistake) seems like an _uncontentious_ change.
- It would be a good exercise for any _new contributor_ who may be looking to gain familiarity with the codebase and/or processes.
- Low priority doesn't mean not worth doing, and it is harder to track outstanding tasks if they are closed.
- It's still a problem (still produces a terse `KeyError` message, that relates to the input in an unobvious way).
I found this issue on ovio.org and would love to contribute! I am new contributor, and since it seemed to be labeled easy, I would like to give it a try
@sjaddya that's great, go ahead, we need to make a new release soonish, so clean up like this is welcome.

Resolving patch:
diff --git a/datacube/model/__init__.py b/datacube/model/__init__.py
--- a/datacube/model/__init__.py
+++ b/datacube/model/__init__.py
@@ -14,7 +14,7 @@
 from uuid import UUID
 
 from affine import Affine
-from typing import Optional, List, Mapping, Any, Dict, Tuple, Iterator
+from typing import Optional, List, Mapping, Any, Dict, Tuple, Iterator, Iterable, Union
 
 from urllib.parse import urlparse
 from datacube.utils import geometry, without_lineage_sources, parse_time, cached_property, uri_to_local_path, \
@@ -513,14 +513,16 @@ def canonical_measurement(self, measurement: str) -> str:
 
         return m.canonical_name
 
-    def lookup_measurements(self, measurements: Optional[List[str]] = None) -> Mapping[str, Measurement]:
+    def lookup_measurements(self, measurements: Optional[Union[Iterable[str], str]] = None) -> Mapping[str, Measurement]:
         """
         Find measurements by name
 
-        :param measurements: list of measurement names
+        :param measurements: list of measurement names or a single measurement name, or None to get all
         """
         if measurements is None:
             return self.measurements
+        if isinstance(measurements, str):
+            measurements = [measurements]
 
         mm = self._resolve_aliases()
         return OrderedDict((m, mm[m]) for m in measurements)


Test patch:
diff --git a/tests/test_model.py b/tests/test_model.py
--- a/tests/test_model.py
+++ b/tests/test_model.py
@@ -108,6 +108,9 @@ def test_product_basics():
     assert hash(product) == hash(mk_sample_product('test_product'))
     assert 'time' in dir(product.metadata)
 
+    assert product.measurements == product.lookup_measurements()
+    assert product.lookup_measurements(['red']) == product.lookup_measurements('red')
+
 
 def test_product_dimensions():
     product = mk_sample_product('test_product')