Repository: pvlib/pvlib-python
Pull request #2466 (2025-06-03 16:21:35)

Deprecate `parse_bsrn`
 - [x] Towards #2444
 - [x] I am familiar with the [contributing guidelines](https://pvlib-python.readthedocs.io/en/latest/contributing/index.html)
 - [x] Tests added
 - [x] Updates entries in [`docs/sphinx/source/reference`](https://github.com/pvlib/pvlib-python/blob/main/docs/sphinx/source/reference) for API changes.
 - [x] Adds description and name entries in the appropriate "what's new" file in [`docs/sphinx/source/whatsnew`](https://github.com/pvlib/pvlib-python/tree/main/docs/sphinx/source/whatsnew) for all changes. Includes link to the GitHub Issue with `` :issue:`num` `` or this Pull Request with `` :pull:`num` ``. Includes contributor name and/or GitHub username (link with `` :ghuser:`user` ``).
 - [x] New code is fully documented. Includes [numpydoc](https://numpydoc.readthedocs.io/en/latest/format.html) compliant docstrings, examples, and comments where necessary.
 - [x] Pull request is nearly complete and ready for detailed review.
 - [x] Maintainer: Appropriate GitHub Labels (including `remote-data`) and Milestone are assigned to the Pull Request and linked Issue.



Patch:
diff --git a/docs/sphinx/source/whatsnew/v0.12.1.rst b/docs/sphinx/source/whatsnew/v0.12.1.rst
index 06907e7..883aa1b 100644
--- a/docs/sphinx/source/whatsnew/v0.12.1.rst
+++ b/docs/sphinx/source/whatsnew/v0.12.1.rst
@@ -17,10 +17,11 @@ Breaking Changes
 Deprecations
 ~~~~~~~~~~~~
 * The following ``parse_`` functions in :py:mod:`pvlib.iotools` are deprecated,
-  with the corresponding ``read_`` functions taking their place: (:issue:`2444`, :pull:`2458`)
+  with the corresponding ``read_`` functions taking their place: (:issue:`2444`, :pull:`2458`, :pull:`2466`)
 
   - :py:func:`~pvlib.iotools.parse_psm3`
   - :py:func:`~pvlib.iotools.parse_cams`
+  - :py:func:`~pvlib.iotools.parse_bsrn`
 
 * The ``server`` parameter in :py:func:`~pvlib.iotools.get_cams` has been renamed
   to ``url`` to be consistent with the other iotools.
diff --git a/pvlib/iotools/bsrn.py b/pvlib/iotools/bsrn.py
index 777eab9..3cca48c 100644
--- a/pvlib/iotools/bsrn.py
+++ b/pvlib/iotools/bsrn.py
@@ -9,6 +9,9 @@ import warnings
 import io
 import os
 
+from pvlib.tools import _file_context_manager
+from pvlib._deprecation import deprecated
+
 BSRN_FTP_URL = "ftp.bsrn.awi.de"
 
 BSRN_LR0100_COL_SPECS = [(0, 3), (4, 9), (10, 16), (16, 22), (22, 27),
@@ -136,7 +139,7 @@ def get_bsrn(station, start, end, username, password,
 
     See Also
     --------
-    pvlib.iotools.read_bsrn, pvlib.iotools.parse_bsrn
+    pvlib.iotools.read_bsrn
 
     References
     ----------
@@ -191,7 +194,7 @@ def get_bsrn(station, start, end, username, password,
                 bio.seek(0)  # reset buffer to start of file
                 gzip_file = io.TextIOWrapper(gzip.GzipFile(fileobj=bio),
                                              encoding='latin1')
-                dfi, metadata = parse_bsrn(gzip_file, logical_records)
+                dfi, metadata = _parse_bsrn(gzip_file, logical_records)
                 dfs.append(dfi)
             # FTP client raises an error if the file does not exist on server
             except ftplib.error_perm as e:
@@ -217,7 +220,7 @@ def get_bsrn(station, start, end, username, password,
     return data, metadata
 
 
-def parse_bsrn(fbuf, logical_records=('0100',)):
+def _parse_bsrn(fbuf, logical_records=('0100',)):
     """
     Parse a file-like buffer of a BSRN station-to-archive file.
 
@@ -382,7 +385,7 @@ def read_bsrn(filename, logical_records=('0100',)):
     Parameters
     ----------
     filename: str or path-like
-        Name or path of a BSRN station-to-archive data file
+        Name, path, or in-memory buffer of a BSRN station-to-archive data file
     logical_records: list or tuple, default: ('0100',)
         List of the logical records (LR) to parse. Options include: '0100',
         '0300', and '0500'.
@@ -439,7 +442,7 @@ def read_bsrn(filename, logical_records=('0100',)):
 
     See Also
     --------
-    pvlib.iotools.parse_bsrn, pvlib.iotools.get_bsrn
+    pvlib.iotools.get_bsrn
 
     References
     ----------
@@ -457,7 +460,11 @@ def read_bsrn(filename, logical_records=('0100',)):
     if str(filename).endswith('.gz'):  # check if file is a gzipped (.gz) file
         open_func, mode = gzip.open, 'rt'
     else:
-        open_func, mode = open, 'r'
+        open_func, mode = _file_context_manager, 'r'
     with open_func(filename, mode) as f:
-        content = parse_bsrn(f, logical_records)
+        content = _parse_bsrn(f, logical_records)
     return content
+
+
+parse_bsrn = deprecated(since="0.13.0", name="parse_bsrn",
+                        alternative="read_bsrn")(read_bsrn)


Test patch:
diff --git a/tests/iotools/test_bsrn.py b/tests/iotools/test_bsrn.py
index 6a01511..8d8f905 100644
--- a/tests/iotools/test_bsrn.py
+++ b/tests/iotools/test_bsrn.py
@@ -6,7 +6,7 @@ import pandas as pd
 import pytest
 import os
 import tempfile
-from pvlib.iotools import read_bsrn, get_bsrn
+from pvlib.iotools import read_bsrn, get_bsrn, parse_bsrn
 from tests.conftest import (
     TESTS_DATA_DIR,
     RERUNS,
@@ -15,6 +15,8 @@ from tests.conftest import (
     requires_bsrn_credentials,
 )
 
+from pvlib._deprecation import pvlibDeprecationWarning
+
 
 @pytest.fixture(scope="module")
 def bsrn_credentials():
@@ -33,6 +35,12 @@ def expected_index():
                          tz='UTC')
 
 
+def test_parse_bsrn_deprecated():
+    with pytest.warns(pvlibDeprecationWarning, match='Use read_bsrn instead'):
+        with open(TESTS_DATA_DIR / 'bsrn-lr0100-pay0616.dat') as fbuf:
+            data, metadata = parse_bsrn(fbuf)
+
+
 @pytest.mark.parametrize('testfile', [
     ('bsrn-pay0616.dat.gz'),
     ('bsrn-lr0100-pay0616.dat'),
@@ -47,6 +55,17 @@ def test_read_bsrn(testfile, expected_index):
     assert 'relative_humidity' in data.columns
 
 
+def test_read_bsrn_buffer(expected_index):
+    with open(TESTS_DATA_DIR / 'bsrn-lr0100-pay0616.dat') as fbuf:
+        data, metadata = read_bsrn(fbuf)
+    assert_index_equal(expected_index, data.index)
+    assert 'ghi' in data.columns
+    assert 'dni_std' in data.columns
+    assert 'dhi_min' in data.columns
+    assert 'lwd_max' in data.columns
+    assert 'relative_humidity' in data.columns
+
+
 def test_read_bsrn_logical_records(expected_index):
     # Test if logical records 0300 and 0500 are correct parsed
     # and that 0100 is not passed when not specified