Repository: equinor/fmu-dataio

Issue:
Bug(?): failure to open Grid metadata
A user reports this error:

```sh
Write grid properties...
Traceback (most recent call last):
  Python script, line 78
  Python script, line 71, in export_geogrid_parameters
  File "/<snip>/lib/python3.11/site-packages/fmu/dataio/dataio.py", line 922, in export
    metadata = self.generate_metadata(obj, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/<snip>/lib/python3.11/site-packages/fmu/dataio/dataio.py", line 871, in generate_metadata
    return generate_export_metadata(
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/<snip>/lib/python3.11/site-packages/fmu/dataio/_metadata.py", line 104, in generate_export_metadata
    objdata = objectdata_provider_factory(obj, dataio)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/<snip>/lib/python3.11/site-packages/fmu/dataio/providers/objectdata/_provider.py", line 146, in objectdata_provider_factory
    return CPGridPropertyDataProvider(obj=obj, dataio=dataio)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  Python script, line 9, in __init__
  File "/<snip>/lib/python3.11/site-packages/fmu/dataio/providers/objectdata/_base.py", line 95, in __post_init__
    metadata["geometry"] = self.get_geometry()
                           ^^^^^^^^^^^^^^^^^^^
  File "/<snip>2/lib/python3.11/site-packages/fmu/dataio/providers/objectdata/_xtgeo.py", line 453, in get_geometry
    name, relpath = get_geometry_ref(self.dataio.geometry, self.obj)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/<snip>/lib/python3.11/site-packages/fmu/dataio/_utils.py", line 373, in get_geometry_ref
    gmeta = read_metadata_from_file(geometrypath)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/<snip>/lib/python3.11/site-packages/fmu/dataio/_utils.py", line 351, in read_metadata_from_file
    return yaml.safe_load(stream)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/<snip>/lib/python3.11/site-packages/yaml/__init__.py", line 125, in safe_load
    return load(stream, SafeLoader)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/<snip>/lib/python3.11/site-packages/yaml/__init__.py", line 79, in load
    loader = Loader(stream)
             ^^^^^^^^^^^^^^
  File "/<snip>/lib/python3.11/site-packages/yaml/loader.py", line 34, in __init__
    Reader.__init__(self, stream)
  File "/<snip>/lib/python3.11/site-packages/yaml/reader.py", line 85, in __init__
    self.determine_encoding()
  File "/<snip>/lib/python3.11/site-packages/yaml/reader.py", line 124, in determine_encoding
    self.update_raw()
  File "/<snip>/lib/python3.11/site-packages/yaml/reader.py", line 178, in update_raw
    data = self.stream.read(size)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/<snip>/lib/python3.11/encodings/ascii.py", line 26, in decode
    return codecs.ascii_decode(input, self.errors)[0]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 480: ordinal not in range(128)
```

This looks like a classic `æ | ø | å` decoding error but it is not clear why this occurs with the later version of RMS/Python.

Discussion:

Solution patch:
diff --git a/src/fmu/dataio/_utils.py b/src/fmu/dataio/_utils.py
index dd1f5909b..0dfcd9a89 100644
--- a/src/fmu/dataio/_utils.py
+++ b/src/fmu/dataio/_utils.py
@@ -130,7 +130,7 @@ def export_file(
             serialized_json = json.dumps(obj)
 
         if isinstance(file, Path):
-            with open(file, "w") as stream:
+            with open(file, "w", encoding="utf-8") as stream:
                 stream.write(serialized_json)
         else:
             file.write(serialized_json.encode("utf-8"))
@@ -220,7 +220,7 @@ def read_parameters_txt(pfile: Path | str) -> types.Parameters:
 
     res: types.Parameters = {}
 
-    with open(pfile) as f:
+    with open(pfile, encoding="utf-8") as f:
         for line in f:
             line_parts = shlex.split(line)
             if len(line_parts) == 2:
@@ -346,7 +346,7 @@ def read_metadata_from_file(filename: str | Path) -> dict:
     metafilepath = Path(metafile)
     if not metafilepath.exists():
         raise OSError(f"Cannot find requested metafile: {metafile}")
-    with open(metafilepath) as stream:
+    with open(metafilepath, encoding="utf-8") as stream:
         return yaml.safe_load(stream)
 
 


Test patch:
diff --git a/tests/test_units/test_rms_context.py b/tests/test_units/test_rms_context.py
index a365369d8..25f8c6f45 100644
--- a/tests/test_units/test_rms_context.py
+++ b/tests/test_units/test_rms_context.py
@@ -4,9 +4,11 @@ In this case a user sits in RMS, which is in folder rms/model and runs
 interactive or from ERT. Hence the rootpath will be ../../
 """
 
+import builtins
 import logging
 import os
 import shutil
+from copy import deepcopy
 from pathlib import Path
 
 import pandas as pd
@@ -616,6 +618,41 @@ def test_gridproperty_export_with_geometry(inside_rms_setup, grid, gridproperty)
     assert "this_is_parent" in output
 
 
+@inside_rms
+def test_gridproperty_export_with_geometry_and_bad_character(
+    inside_rms_setup, grid, gridproperty, monkeypatch
+):
+    """Ensures a non-ascii character in masterdata does not cause encoding parsing
+    failures"""
+    original_open = builtins.open
+
+    def open_with_ansi(file, mode="r", *args, **kwargs):
+        if "r" in mode and "b" not in mode and "encoding" not in kwargs:
+            kwargs["encoding"] = "ANSI_X3.4-1968"
+        return original_open(file, mode, *args, **kwargs)
+
+    monkeypatch.setattr(builtins, "open", open_with_ansi)
+
+    cfg = deepcopy(inside_rms_setup["config"])
+
+    cfg["masterdata"]["smda"]["field"][0]["identifier"] = "Drogøn"
+
+    grd_edata = dataio.ExportData(
+        config=cfg,
+        name="geogrid",
+        content={"property": {"is_discrete": False}},
+    )
+    outgrid = grd_edata.export(grid)
+
+    dataio.ExportData(
+        config=cfg,
+        name="geogrid",
+        content={"property": {"is_discrete": False}},
+        geometry=outgrid,
+    ).export(gridproperty)
+    # Will raise an exception if decoding fails
+
+
 # ======================================================================================
 # Dataframe and PyArrow
 # ======================================================================================