Repository: Unidata/MetPy

Issue:
add_station_lat_lon yields columns with object dtype
```python
df = pd.DataFrame({'station': ['KOUN', 'KVPZ', 'KDEN', 'PAAA']})
df = add_station_lat_lon(df, 'station')
df.info()
```
gives
```
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4 entries, 0 to 3
Data columns (total 3 columns):
 #   Column     Non-Null Count  Dtype 
---  ------     --------------  ----- 
 0   station    4 non-null      object
 1   latitude   3 non-null      object
 2   longitude  3 non-null      object
dtypes: object(3)
memory usage: 224.0+ bytes
```

Apparently it's good enough to pass e.g. `assert_almost_equal(df.loc[df.station == 'KOUN'].latitude.values[0], 35.25)`, but it's painful to pass to e.g. `tricontour()`.

Discussion:

Patch:
diff --git a/.github/workflows/backport-prs.yml b/.github/workflows/backport-prs.yml
index f93bfbc9b7..5246fdb47a 100644
--- a/.github/workflows/backport-prs.yml
+++ b/.github/workflows/backport-prs.yml
@@ -16,7 +16,7 @@ jobs:
     if: github.event.pull_request.merged && contains( github.event.pull_request.labels.*.name, 'backport' )
     steps:
       - name: Checkout PR HEAD
-        uses: actions/checkout@v4
+        uses: actions/checkout@v3
         with:
           ref: ${{ github.event.pull_request.head.sha }}
           fetch-depth: 100
diff --git a/.github/workflows/cffcheck.yml b/.github/workflows/cffcheck.yml
index 6826803b73..9bcbfc1b87 100644
--- a/.github/workflows/cffcheck.yml
+++ b/.github/workflows/cffcheck.yml
@@ -17,7 +17,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Check out a copy of the repository
-        uses: actions/checkout@v4
+        uses: actions/checkout@v3
 
       - name: Check validity of CITATION.cff
         uses: citation-file-format/cffconvert-github-action@2.0.0
diff --git a/.github/workflows/code-analysis.yml b/.github/workflows/code-analysis.yml
index 43d963a1f0..e970a3e1c5 100644
--- a/.github/workflows/code-analysis.yml
+++ b/.github/workflows/code-analysis.yml
@@ -28,7 +28,7 @@ jobs:
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v4
+      uses: actions/checkout@v3
 
     - name: Initialize CodeQL
       uses: github/codeql-action/init@v2
diff --git a/.github/workflows/docs-conda.yml b/.github/workflows/docs-conda.yml
index 2cfe2dff47..08efa7556a 100644
--- a/.github/workflows/docs-conda.yml
+++ b/.github/workflows/docs-conda.yml
@@ -31,7 +31,7 @@ jobs:
 
     steps:
     - name: Checkout source
-      uses: actions/checkout@v4
+      uses: actions/checkout@v3
       with:
         fetch-depth: 150
         fetch-tags: true
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 1f1f431d20..e344c65fd4 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -42,7 +42,7 @@ jobs:
 
     steps:
     - name: Checkout source
-      uses: actions/checkout@v4
+      uses: actions/checkout@v3
       with:
         fetch-depth: 150
         fetch-tags: true
diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml
index bf8e1b8eeb..8aa818a9d5 100644
--- a/.github/workflows/linting.yml
+++ b/.github/workflows/linting.yml
@@ -14,7 +14,7 @@ jobs:
     name: Flake8
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v3
 
       - name: Set up Python 3
         uses: actions/setup-python@v4
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d625fc06c7..e766ba358b 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -10,7 +10,7 @@ jobs:
     name: Build Release Packages
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v4
+    - uses: actions/checkout@v3
       with:
         fetch-depth: 10
         fetch-tags: true
diff --git a/.github/workflows/unstable-builds.yml b/.github/workflows/unstable-builds.yml
index f275ae51b8..18c28878b8 100644
--- a/.github/workflows/unstable-builds.yml
+++ b/.github/workflows/unstable-builds.yml
@@ -17,7 +17,7 @@ jobs:
       result: ${{ steps.tests.outcome }}
     steps:
     - name: Checkout source
-      uses: actions/checkout@v4
+      uses: actions/checkout@v3
       with:
         fetch-depth: 150
         fetch-tags: true
@@ -57,7 +57,7 @@ jobs:
       result: ${{ steps.build.outcome }}
     steps:
     - name: Checkout source
-      uses: actions/checkout@v4
+      uses: actions/checkout@v3
       with:
         fetch-depth: 150
         fetch-tags: true
diff --git a/ci/linting_requirements.txt b/ci/linting_requirements.txt
index bdae656e60..cf8d8c2c03 100644
--- a/ci/linting_requirements.txt
+++ b/ci/linting_requirements.txt
@@ -1,4 +1,4 @@
-ruff==0.0.289
+ruff==0.0.287
 
 flake8==6.1.0
 pycodestyle==2.11.0
diff --git a/src/metpy/io/nexrad.py b/src/metpy/io/nexrad.py
index ff76a1dae9..1a2931643d 100644
--- a/src/metpy/io/nexrad.py
+++ b/src/metpy/io/nexrad.py
@@ -11,6 +11,7 @@ import logging
 import pathlib
 import re
 import struct
+from xdrlib import Unpacker
 
 import numpy as np
 from scipy.constants import day, milli
@@ -2300,37 +2301,11 @@ class Level3File:
                   0xba07: _unpack_packet_raster_data}
 
 
-class Level3XDRParser:
+class Level3XDRParser(Unpacker):
     """Handle XDR-formatted Level 3 NEXRAD products."""
 
-    def __init__(self, data):
-        """Initialize the parser with a buffer for the bytes."""
-        self._buf = IOBuffer(data)
-
-    def unpack_uint(self):
-        """Unpack a 4-byte unsigned integer."""
-        return self._buf.read_int(4, 'big', False)
-
-    def unpack_int(self):
-        """Unpack a 4-byte signed integer."""
-        return self._buf.read_int(4, 'big', True)
-
-    def unpack_float(self):
-        """Unpack a 4-byte floating-point value."""
-        return self._buf.read_binary(1, '>f')[0]
-
-    def unpack_string(self):
-        """Unpack a string."""
-        n = self.unpack_uint()
-        return self._buf.read_ascii((n + 3) // 4 * 4)[:n]
-
-    def unpack_int_array(self):
-        """Unpack an int array."""
-        n = self.unpack_uint()
-        return self._buf.read_binary(n, '>l')
-
     def __call__(self, code):
-        """Perform the actual product unpacking."""
+        """Perform the actual unpacking."""
         xdr = OrderedDict()
 
         if code == 28:
@@ -2339,11 +2314,13 @@ class Level3XDRParser:
             log.warning('XDR: code %d not implemented', code)
 
         # Check that we got it all
-        if not self._buf.at_end():
-            log.warning('Data remains in XDR buffer.')
-
+        self.done()
         return xdr
 
+    def unpack_string(self):
+        """Unpack the internal data as a string."""
+        return Unpacker.unpack_string(self).decode('ascii')
+
     def _unpack_prod_desc(self):
         xdr = OrderedDict()
 
@@ -2438,7 +2415,7 @@ class Level3XDRParser:
                                              width=self.unpack_float(),
                                              num_bins=self.unpack_int(),
                                              attributes=self.unpack_string(),
-                                             data=self.unpack_int_array()))
+                                             data=self.unpack_array(self.unpack_int)))
         return ret._replace(radials=rads)
 
     text_fmt = namedtuple('TextComponent', ['parameters', 'text'])
diff --git a/src/metpy/io/station_data.py b/src/metpy/io/station_data.py
index 4db0f2ec5e..df7e153b5b 100644
--- a/src/metpy/io/station_data.py
+++ b/src/metpy/io/station_data.py
@@ -191,8 +191,9 @@ def add_station_lat_lon(df, stn_var=None):
         raise KeyError('Second argument not provided to add_station_lat_lon, but none of '
                        f'{names_to_try} were found.')
 
-    df['latitude'] = None
-    df['longitude'] = None
+    df['latitude'] = np.nan
+    df['longitude'] = np.nan
+
     if stn_var is None:
         stn_var = key_finder(df)
     for stn in df[stn_var].unique():


Test patch:
diff --git a/.github/workflows/tests-conda.yml b/.github/workflows/tests-conda.yml
index d118d8725b..27190048c3 100644
--- a/.github/workflows/tests-conda.yml
+++ b/.github/workflows/tests-conda.yml
@@ -33,7 +33,7 @@ jobs:
 
     steps:
     - name: Checkout source
-      uses: actions/checkout@v4
+      uses: actions/checkout@v3
       with:
         fetch-depth: 150
         fetch-tags: true
diff --git a/.github/workflows/tests-pypi.yml b/.github/workflows/tests-pypi.yml
index 876fe5cabc..554463d7ba 100644
--- a/.github/workflows/tests-pypi.yml
+++ b/.github/workflows/tests-pypi.yml
@@ -38,7 +38,7 @@ jobs:
 
     steps:
     - name: Checkout source
-      uses: actions/checkout@v4
+      uses: actions/checkout@v3
       with:
         fetch-depth: 150
         fetch-tags: true
diff --git a/ci/test_requirements.txt b/ci/test_requirements.txt
index 6c666d72b2..d2925e63de 100644
--- a/ci/test_requirements.txt
+++ b/ci/test_requirements.txt
@@ -1,6 +1,6 @@
 packaging==23.1
-pytest==7.4.2
+pytest==7.4.1
 pytest-mpl==0.16.1
 netCDF4==1.6.4
-coverage==7.3.1
-dask==2023.9.1
+coverage==7.3.0
+dask==2023.9.0
diff --git a/tests/io/test_station_data.py b/tests/io/test_station_data.py
index 7e0e9b0d70..a281ad830d 100644
--- a/tests/io/test_station_data.py
+++ b/tests/io/test_station_data.py
@@ -23,6 +23,7 @@ def test_add_lat_lon_station_data():
     assert_almost_equal(df.loc[df.station == 'KDEN'].longitude.values[0], -104.65)
     assert_almost_equal(df.loc[df.station == 'PAAA'].latitude.values[0], np.nan)
     assert_almost_equal(df.loc[df.station == 'PAAA'].longitude.values[0], np.nan)
+    assert df['longitude'].dtype == np.float64
 
 
 def test_add_lat_lon_station_data_optional():