Repository: kinverarity1/lasio

Issue:
Add SectionItems.get(mnemonic, default, add=False)
Analogous to dict.get but with extra "add" kwarg.

SectionItems.get should accept strings or HeaderItem objects for the "default" arg. If it is a string, it should be converted to a HeaderItem with the appropriate mnemonic.

Always returns a HeaderItem.

If add is True, the returned value should also be appended to the SectionItems instance.

How do we handle CurveItems? Set the data attribute to all nans, with the appropriate length?

Discussion:

Patch:
diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst
index 2d4cf26..8798ca0 100644
--- a/docs/source/changelog.rst
+++ b/docs/source/changelog.rst
@@ -1,6 +1,55 @@
 List of changes
 ===============================
 
+Unreleased changes (Available on GitHub)
+----------------------------------------
+- Fix `#322`_ - provide a way to consistently retrieve header items which may
+  or may not be present in the header:
+
+  If you try ordinary item-style access,
+  as is normal in Python, a KeyError exception will be raised if it is missing:
+
+  .. code-block:: python
+
+      >>> permit = las.well['PRMT']
+      Traceback (most recent call last):
+        File "<stdin>", line 1, in <module>
+        File "c:\devapps\kinverarity\projects\lasio\lasio\las_items.py", line 313, in __getitem__
+          raise KeyError("%s not in %s" % (key, self.keys()))
+      KeyError: "PRMT not in ['STRT', 'STOP', 'STEP', 'NULL', 'COMP', 'WELL', 'FLD', 'LOC', 'PROV', 'SRVC', 'DATE', 'UWI']"
+
+  A better pattern is to use the :meth:`lasio.SectionItems.get` method, which
+  allows you to specify a default value in the case of it missing:
+
+  .. code-block:: python
+
+      >>> permit = las.well.get('PRMT', 'unknown')
+      >>> permit
+      HeaderItem(mnemonic="PRMT", unit="", value="unknown", descr="")
+
+  You can use the ``add=True`` keyword argument if you would like this 
+  header item to be added, as well as returned:
+
+  .. code-block:: python
+
+      >>> permit = las.well.get('PRMT', 'unknown', add=True)
+      >>> las.well
+      [HeaderItem(mnemonic="STRT", unit="M", value="0.05", descr="FIRST INDEX VALUE"),
+      HeaderItem(mnemonic="STOP", unit="M", value="136.6", descr="LAST INDEX VALUE"),
+      HeaderItem(mnemonic="STEP", unit="M", value="0.05", descr="STEP"),
+      HeaderItem(mnemonic="NULL", unit="", value="-99999", descr="NULL VALUE"),
+      HeaderItem(mnemonic="COMP", unit="", value="", descr="COMP"),
+      HeaderItem(mnemonic="WELL", unit="", value="Scorpio E1", descr="WELL"),
+      HeaderItem(mnemonic="FLD", unit="", value="", descr=""),
+      HeaderItem(mnemonic="LOC", unit="", value="Mt Eba", descr="LOC"),
+      HeaderItem(mnemonic="SRVC", unit="", value="", descr=""),
+      HeaderItem(mnemonic="CTRY", unit="", value="", descr=""),
+      HeaderItem(mnemonic="STAT", unit="", value="SA", descr="STAT"),
+      HeaderItem(mnemonic="CNTY", unit="", value="", descr=""),
+      HeaderItem(mnemonic="DATE", unit="", value="15/03/2015", descr="DATE"),
+      HeaderItem(mnemonic="UWI", unit="", value="6038-187", descr="WUNT"),
+      HeaderItem(mnemonic="PRMT", unit="", value="unknown", descr="")]
+
 Version 0.30 (12 May 2022)
 --------------------------
 - Fixes for `#446`_ (Implement a numpy-based reader for the data section; `#452`_) and 
@@ -308,6 +357,7 @@ Version 0.2 (2015-07-08)
 .. _#318: https://github.com/kinverarity1/lasio/issues/318
 .. _#319: https://github.com/kinverarity1/lasio/issues/319
 .. _#321: https://github.com/kinverarity1/lasio/issues/321
+.. _#322: https://github.com/kinverarity1/lasio/issues/322
 .. _#325: https://github.com/kinverarity1/lasio/issues/325
 .. _#326: https://github.com/kinverarity1/lasio/issues/326
 .. _#327: https://github.com/kinverarity1/lasio/issues/327
@@ -387,7 +437,7 @@ Version 0.2 (2015-07-08)
 .. _#268: https://github.com/kinverarity1/lasio/issues/268
 .. _#451: https://github.com/kinverarity1/lasio/issues/451
 .. _#453: https://github.com/kinverarity1/lasio/issues/453
-.. _#455: https://github.com/kinverarity1/lasio/issues/455.. _#83: https://github.com/kinverarity1/lasio/issues/83
+.. _#455: https://github.com/kinverarity1/lasio/issues/455
 .. _#265: https://github.com/kinverarity1/lasio/issues/265
 .. _#332: https://github.com/kinverarity1/lasio/issues/332
 .. _#375: https://github.com/kinverarity1/lasio/issues/375
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 9d8a8f0..beb3cf2 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -26,7 +26,7 @@ source_suffix = ".rst"
 master_doc = "index"
 
 project = u"lasio"
-copyright = u"2013-2021, lasio contributors"
+copyright = u"2013-2022, lasio contributors"
 author = u"lasio contributors"
 
 from pkg_resources import get_distribution
diff --git a/docs/source/header-section.rst b/docs/source/header-section.rst
index 730e39d..9b86752 100644
--- a/docs/source/header-section.rst
+++ b/docs/source/header-section.rst
@@ -258,6 +258,51 @@ There are methods intended for removing curves. Say you want to remove the PR cu
      CurveItem(mnemonic="DFAR", unit="G/CM3", value="", descr="DFAR", original_mnemonic="DFAR", data.shape=(121,)),
      CurveItem(mnemonic="DNEAR", unit="G/CM3", value="", descr="DNEAR", original_mnemonic="DNEAR", data.shape=(121,))]
 
+Another common task is to retrieve a header item that may or may not be in the
+file. If you try ordinary item-style access,
+as is normal in Python, a KeyError exception will be raised if it is missing:
+
+.. code-block:: python
+
+    >>> permit = las.well['PRMT']
+    Traceback (most recent call last):
+      File "<stdin>", line 1, in <module>
+      File "c:\devapps\kinverarity\projects\lasio\lasio\las_items.py", line 313, in __getitem__
+        raise KeyError("%s not in %s" % (key, self.keys()))
+    KeyError: "PRMT not in ['STRT', 'STOP', 'STEP', 'NULL', 'COMP', 'WELL', 'FLD', 'LOC', 'PROV', 'SRVC', 'DATE', 'UWI']"
+
+A better pattern is to use the :meth:`lasio.SectionItems.get` method, which
+allows you to specify a default value in the case of it missing:
+
+.. code-block:: python
+
+    >>> permit = las.well.get('PRMT', 'unknown')
+    >>> permit
+    HeaderItem(mnemonic="PRMT", unit="", value="unknown", descr="")
+
+You can use the ``add=True`` keyword argument if you would like this 
+header item to be added, as well as returned:
+
+.. code-block:: python
+
+    >>> permit = las.well.get('PRMT', 'unknown', add=True)
+    >>> las.well
+    [HeaderItem(mnemonic="STRT", unit="M", value="0.05", descr="FIRST INDEX VALUE"),
+    HeaderItem(mnemonic="STOP", unit="M", value="136.6", descr="LAST INDEX VALUE"),
+    HeaderItem(mnemonic="STEP", unit="M", value="0.05", descr="STEP"),
+    HeaderItem(mnemonic="NULL", unit="", value="-99999", descr="NULL VALUE"),
+    HeaderItem(mnemonic="COMP", unit="", value="", descr="COMP"),
+    HeaderItem(mnemonic="WELL", unit="", value="Scorpio E1", descr="WELL"),
+    HeaderItem(mnemonic="FLD", unit="", value="", descr=""),
+    HeaderItem(mnemonic="LOC", unit="", value="Mt Eba", descr="LOC"),
+    HeaderItem(mnemonic="SRVC", unit="", value="", descr=""),
+    HeaderItem(mnemonic="CTRY", unit="", value="", descr=""),
+    HeaderItem(mnemonic="STAT", unit="", value="SA", descr="STAT"),
+    HeaderItem(mnemonic="CNTY", unit="", value="", descr=""),
+    HeaderItem(mnemonic="DATE", unit="", value="15/03/2015", descr="DATE"),
+    HeaderItem(mnemonic="UWI", unit="", value="6038-187", descr="WUNT"),
+    HeaderItem(mnemonic="PRMT", unit="", value="unknown", descr="")]
+
 Handling special cases of header lines
 --------------------------------------
 
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 24a0c4a..a51f70f 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -45,8 +45,7 @@ The final version of lasio with Python 2.7 support is v0.26.
    encodings
    lasio
    contributing
-
-.. include:: changelog.rst
+   changelog
 
 * :ref:`genindex`
 * :ref:`search`
diff --git a/docs/source/lasio.rst b/docs/source/lasio.rst
index 355693f..1365082 100644
--- a/docs/source/lasio.rst
+++ b/docs/source/lasio.rst
@@ -41,6 +41,11 @@ Reading data
 .. autoattribute:: lasio.LASFile.data
 .. automethod:: lasio.LASFile.stack_curves
 
+Reading and modifying header data
+---------------------------------
+.. autoclass:: lasio.SectionItems
+    :members:
+
 Modifying data
 --------------
 .. automethod:: lasio.LASFile.set_data
diff --git a/lasio/las_items.py b/lasio/las_items.py
index bf314f4..3e77095 100644
--- a/lasio/las_items.py
+++ b/lasio/las_items.py
@@ -312,6 +312,72 @@ class SectionItems(list):
         else:
             raise KeyError("%s not in %s" % (key, self.keys()))
 
+    def get(self, mnemonic, default="", add=False):
+        """Get an item, with a default value for the situation when it is missing.
+
+        Arguments:
+            mnemonic (str): mnemonic of item to retrieve
+            default (str, HeaderItem, or CurveItem): default to provide
+                if *mnemonic* is missing from the section. If a string is
+                provided, it will be used as the ``value`` attribute of a new
+                HeaderItem or the ``descr`` attribute of a new CurveItem.
+            add (bool): if True, the returned HeaderItem/CurveItem will also
+                be appended to the SectionItems. By default this is not done.
+
+        Returns:
+            :class:`lasio.HeaderItem`/:class:`lasio.CurveItem`: item from
+            the section, if it is in there, or
+            a new item, if it is not. If a CurveItem is returned, the
+            ``data`` attribute will contain ``numpy.nan`` values.
+
+        """
+        if mnemonic in self:
+            return self[mnemonic]
+        else:
+            if not (
+                isinstance(default, HeaderItem)
+                or isinstance(default, CurveItem)
+            ):
+                default = str(default)  
+                # Determine appropriate type of item to create (HeaderItem
+                # or CurveItem).
+
+                if len(self):
+                    first_item = self[0]
+                    item_type = type(first_item)
+                else:
+                    item_type = HeaderItem
+                
+                if item_type is CurveItem:
+                    new_data = np.asarray(first_item.data)
+                    new_data = new_data * np.nan
+
+                    item = CurveItem(
+                        mnemonic=mnemonic, 
+                        descr=default,
+                        data=new_data
+                    )
+                else:
+                    item = HeaderItem(
+                        mnemonic=mnemonic,
+                        value=default
+                    )
+            else:
+                assert type(default) in (HeaderItem, CurveItem)
+
+                item = type(default)(
+                    mnemonic=mnemonic,
+                    unit=default.unit,
+                    value=default.value,
+                    descr=default.descr
+                )
+                if type(item) is CurveItem:
+                    item.data = np.array(default.data)
+                
+        if add:
+            self.append(item)
+        return item
+
     def __delitem__(self, key):
         """Delete item by either mnemonic or index.
 


Test patch:
diff --git a/tests/test_sectionitems.py b/tests/test_sectionitems.py
index 5ff71ad..252e0e0 100644
--- a/tests/test_sectionitems.py
+++ b/tests/test_sectionitems.py
@@ -154,3 +154,44 @@ def test_mnemonic_rename_1():
     las = lasio.read(egfn("sample.las"))
     las.curves[-1].mnemonic = ""
     assert las.curves[-1].mnemonic == "UNKNOWN"
+
+
+def test_get_exists():
+    sitems = lasio.SectionItems()
+    sitems.append(
+        lasio.HeaderItem('WELL', value='1')
+    )
+    item = sitems.get('WELL', default='2')
+    assert item.value == '1'
+
+def test_get_missing_default_str():
+    sitems = lasio.SectionItems()
+    item = sitems.get('WELL', default='2')
+    assert item.value == '2'
+
+def test_get_missing_default_int():
+    sitems = lasio.SectionItems()
+    item = sitems.get('WELL', default=2)
+    assert item.value == '2'
+
+def test_get_missing_default_item():
+    sitems = lasio.SectionItems()
+    item = sitems.get('WELL', default=lasio.HeaderItem(mnemonic='XXX', value='3'))
+    assert item.mnemonic == 'WELL'
+    assert item.value == '3'
+    
+def test_get_missing_curveitem():
+    sitems = lasio.SectionItems()
+    sitems.append(
+        lasio.CurveItem('DEPT', data=[1, 2, 3])
+    )
+    item = sitems.get('GAMMA')
+    assert type(item) is lasio.CurveItem
+    assert np.isnan(item.data).all()
+
+def test_get_missing_add():
+    sitems = lasio.SectionItems()
+    item = sitems.get('WELL', default='3', add=True)
+    existing_item = sitems[0] 
+    assert existing_item.mnemonic == 'WELL'
+    assert existing_item.value == '3'
\ No newline at end of file