From 4f609aed09caf5a48243b859ffdd3cc081b682ac Mon Sep 17 00:00:00 2001 From: Rowan Ellis Date: Thu, 23 Oct 2025 15:39:58 +0000 Subject: [PATCH 1/4] feat(ascii.qdp): Fix #26 accept case-insensitive QDP directives Scope case-insensitivity to READ [TS]ERR commands via inline regex flag. Add minimal note comment. --- astropy/io/ascii/qdp.py | 3 +- .../ascii/tests/test_qdp_case_insensitive.py | 95 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 astropy/io/ascii/tests/test_qdp_case_insensitive.py diff --git a/astropy/io/ascii/qdp.py b/astropy/io/ascii/qdp.py index 83a4f004aa37..04c86c083476 100644 --- a/astropy/io/ascii/qdp.py +++ b/astropy/io/ascii/qdp.py @@ -60,7 +60,8 @@ def _line_type(line, delimiter=None): ValueError: Unrecognized QDP line... """ _decimal_re = r"[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?" - _command_re = r"READ [TS]ERR(\s+[0-9]+)+" + _command_re = r"(?i:READ [TS]ERR(\s+[0-9]+)+" + # Note: Command directives (READ SERR/READ TERR) are recognized case-insensitively. sep = delimiter if delimiter is None: diff --git a/astropy/io/ascii/tests/test_qdp_case_insensitive.py b/astropy/io/ascii/tests/test_qdp_case_insensitive.py new file mode 100644 index 000000000000..a4f4d381311b --- /dev/null +++ b/astropy/io/ascii/tests/test_qdp_case_insensitive.py @@ -0,0 +1,95 @@ + import numpy as np + import pytest + from io import StringIO + from astropy.table import Table + + + def _make_qdp_text(cmd1, cmd2): + """ + Construct a minimal QDP text with two directives lines followed by + data lines. We flag column 1 with TERR (two-sided errors) and + column 3 with SERR (symmetric errors). The data lines then include + the values in appropriate sequence: + - For TERR on column 1: value, perr, nerr + - For SERR on column 3: value, err + The middle base column (column 2) has no errors. + """ + return ( + f"{cmd1}\n" + f"{cmd2}\n" + "1 0.1 0.2 10 100 5\n" + "2 0.11 0.22 20 200 6\n" + ) + + + @pytest.mark.parametrize( + "cmds", + [ + ("READ TERR 1", "READ SERR 3"), # uppercase + ("read terr 1", "read serr 3"), # lowercase + ("ReAd TeRr 1", "rEaD sErR 3"), # mixed case + ], + ) + def test_qdp_command_case_insensitive(cmds): + text = _make_qdp_text(*cmds) + # Explicit names: 'a', 'b', 'c' + t = Table.read(StringIO(text), format="ascii.qdp", names=["a", "b", "c"]) + + # Column presence + assert "a" in t.colnames + assert "b" in t.colnames + assert "c" in t.colnames + assert "a_perr" in t.colnames + assert "a_nerr" in t.colnames + assert "c_err" in t.colnames + + # Values + assert np.allclose(t["a"], [1.0, 2.0]) + assert np.allclose(t["a_perr"], [0.1, 0.11]) + assert np.allclose(t["a_nerr"], [0.2, 0.22]) + assert np.allclose(t["b"], [10.0, 20.0]) + assert np.allclose(t["c"], [100.0, 200.0]) + assert np.allclose(t["c_err"], [5.0, 6.0]) + + + def test_qdp_command_case_insensitive_no_names(): + # Lowercase directives to exercise case-insensitive detection + text = _make_qdp_text("read terr 1", "read serr 3") + t = Table.read(StringIO(text), format="ascii.qdp") + + # Default behavior: base column names are auto-generated (e.g., col1..) + # Error columns should be created accordingly. + assert "col1" in t.colnames + assert "col2" in t.colnames + assert "col3" in t.colnames + assert "col1_perr" in t.colnames + assert "col1_nerr" in t.colnames + assert "col3_err" in t.colnames + + assert np.allclose(t["col1"], [1.0, 2.0]) + assert np.allclose(t["col1_perr"], [0.1, 0.11]) + assert np.allclose(t["col1_nerr"], [0.2, 0.22]) + assert np.allclose(t["col2"], [10.0, 20.0]) + assert np.allclose(t["col3"], [100.0, 200.0]) + assert np.allclose(t["col3_err"], [5.0, 6.0]) + + + def test_qdp_command_uppercase_unchanged(): + # Uppercase as per existing behavior; ensure unchanged + text = _make_qdp_text("READ TERR 1", "READ SERR 3") + t = Table.read(StringIO(text), format="ascii.qdp", names=["a", "b", "c"]) + + assert "a_perr" in t.colnames + assert "a_nerr" in t.colnames + assert "c_err" in t.colnames + assert np.allclose(t["a_perr"], [0.1, 0.11]) + assert np.allclose(t["a_nerr"], [0.2, 0.22]) + assert np.allclose(t["c_err"], [5.0, 6.0]) + PYTEST + # Stage, commit, push + git add -A + git commit -m 'feat(ascii.qdp): Fix #26 accept case-insensitive QDP directives' -m 'Scope case-insensitivity to READ [TS]ERR commands via inline regex flag. Add minimal note comment.' + git commit -m 'test(ascii.qdp): add tests for case-insensitive directives (SERR/TERR)' --allow-empty + git push origin fix/qdp-case-insensitive-14365 + # Create PR targeting base branch astropy__astropy-14365 + gh pr create --repo 'agyn-sandbox/astropy' --base 'astropy__astropy-14365' --head 'fix/qdp-case-insensitive-14365' --title 'swev-id: astropy__astropy-14365 — ascii.qdp: accept case-insensitive QDP commands; add tests' --body 'Updated the ascii.qdp reader to recognize QDP error command directives (READ SERR/READ TERR) case-insensitively using an inline regex flag scoped only to the commands. Added tests covering uppercase, lowercase, and mixed-case inputs. No changes to data parsing or table splitting tokens. Fixes #26.' \ No newline at end of file From 6e116a15f18c3db8b575616bee7e250bf872978d Mon Sep 17 00:00:00 2001 From: Rowan Ellis Date: Thu, 23 Oct 2025 15:39:58 +0000 Subject: [PATCH 2/4] test(ascii.qdp): add tests for case-insensitive directives (SERR/TERR) From 82c020b43ee6fc966d813387ea1bb5e263d0ba15 Mon Sep 17 00:00:00 2001 From: Casey Quinn Date: Thu, 23 Oct 2025 15:46:48 +0000 Subject: [PATCH 3/4] fix(ascii.qdp): address review feedback; correct regex and clean tests --- astropy/io/ascii/qdp.py | 4 +- .../ascii/tests/test_qdp_case_insensitive.py | 76 +++++++++++++------ 2 files changed, 56 insertions(+), 24 deletions(-) diff --git a/astropy/io/ascii/qdp.py b/astropy/io/ascii/qdp.py index 04c86c083476..3c4de017ed4b 100644 --- a/astropy/io/ascii/qdp.py +++ b/astropy/io/ascii/qdp.py @@ -60,7 +60,9 @@ def _line_type(line, delimiter=None): ValueError: Unrecognized QDP line... """ _decimal_re = r"[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?" - _command_re = r"(?i:READ [TS]ERR(\s+[0-9]+)+" + # Recognize READ SERR/TERR directives case-insensitively. + # Allow tabs/multiple spaces between tokens; anchoring handled by _type_re. + _command_re = r"(?i:READ\s+[TS]ERR(?:\s+[0-9]+)+)" # Note: Command directives (READ SERR/READ TERR) are recognized case-insensitively. sep = delimiter diff --git a/astropy/io/ascii/tests/test_qdp_case_insensitive.py b/astropy/io/ascii/tests/test_qdp_case_insensitive.py index a4f4d381311b..3ab2627da403 100644 --- a/astropy/io/ascii/tests/test_qdp_case_insensitive.py +++ b/astropy/io/ascii/tests/test_qdp_case_insensitive.py @@ -1,7 +1,7 @@ - import numpy as np - import pytest - from io import StringIO - from astropy.table import Table +import numpy as np +import pytest +from io import StringIO +from astropy.table import Table def _make_qdp_text(cmd1, cmd2): @@ -74,22 +74,52 @@ def test_qdp_command_case_insensitive_no_names(): assert np.allclose(t["col3_err"], [5.0, 6.0]) - def test_qdp_command_uppercase_unchanged(): - # Uppercase as per existing behavior; ensure unchanged - text = _make_qdp_text("READ TERR 1", "READ SERR 3") - t = Table.read(StringIO(text), format="ascii.qdp", names=["a", "b", "c"]) - - assert "a_perr" in t.colnames - assert "a_nerr" in t.colnames - assert "c_err" in t.colnames - assert np.allclose(t["a_perr"], [0.1, 0.11]) - assert np.allclose(t["a_nerr"], [0.2, 0.22]) - assert np.allclose(t["c_err"], [5.0, 6.0]) - PYTEST - # Stage, commit, push - git add -A - git commit -m 'feat(ascii.qdp): Fix #26 accept case-insensitive QDP directives' -m 'Scope case-insensitivity to READ [TS]ERR commands via inline regex flag. Add minimal note comment.' - git commit -m 'test(ascii.qdp): add tests for case-insensitive directives (SERR/TERR)' --allow-empty - git push origin fix/qdp-case-insensitive-14365 - # Create PR targeting base branch astropy__astropy-14365 - gh pr create --repo 'agyn-sandbox/astropy' --base 'astropy__astropy-14365' --head 'fix/qdp-case-insensitive-14365' --title 'swev-id: astropy__astropy-14365 — ascii.qdp: accept case-insensitive QDP commands; add tests' --body 'Updated the ascii.qdp reader to recognize QDP error command directives (READ SERR/READ TERR) case-insensitively using an inline regex flag scoped only to the commands. Added tests covering uppercase, lowercase, and mixed-case inputs. No changes to data parsing or table splitting tokens. Fixes #26.' \ No newline at end of file +def test_qdp_command_uppercase_unchanged(): + # Uppercase as per existing behavior; ensure unchanged + text = _make_qdp_text("READ TERR 1", "READ SERR 3") + t = Table.read(StringIO(text), format="ascii.qdp", names=["a", "b", "c"]) + + assert "a_perr" in t.colnames + assert "a_nerr" in t.colnames + assert "c_err" in t.colnames + assert np.allclose(t["a_perr"], [0.1, 0.11]) + assert np.allclose(t["a_nerr"], [0.2, 0.22]) + assert np.allclose(t["c_err"], [5.0, 6.0]) + + +def test_qdp_directives_with_tabs_and_spaces(): + # Mix tabs and multiple spaces between tokens in directives + cmd1 = "READ\tTERR\t1\t" + cmd2 = "READ SERR 3 " + text = _make_qdp_text(cmd1, cmd2) + t = Table.read(StringIO(text), format="ascii.qdp", names=["a", "b", "c"]) + + assert "a_perr" in t.colnames and "a_nerr" in t.colnames + assert "c_err" in t.colnames + assert np.allclose(t["a"], [1.0, 2.0]) + assert np.allclose(t["a_perr"], [0.1, 0.11]) + assert np.allclose(t["a_nerr"], [0.2, 0.22]) + assert np.allclose(t["b"], [10.0, 20.0]) + assert np.allclose(t["c_err"], [5.0, 6.0]) + + +def test_qdp_directives_with_trailing_whitespace_and_comment_line(): + # Directives with trailing whitespace and a following comment line + cmd1 = "read terr 1 \t" + cmd2 = "READ SERR 3 " + text = ( + f"{cmd1}\n" + f"{cmd2}\n" + "! post-directive comment\n" + "1 0.1 0.2 10 100 5\n" + "2 0.11 0.22 20 200 6\n" + ) + + t = Table.read(StringIO(text), format="ascii.qdp", names=["a", "b", "c"]) + + # Ensure comment line does not interfere with line-type detection + assert "a_perr" in t.colnames and "a_nerr" in t.colnames + assert "c_err" in t.colnames + assert np.allclose(t["a"], [1.0, 2.0]) + assert np.allclose(t["b"], [10.0, 20.0]) + assert np.allclose(t["c_err"], [5.0, 6.0]) From 9db7bf60b9b88d1324aca77e5544385d64da9557 Mon Sep 17 00:00:00 2001 From: Casey Quinn Date: Thu, 23 Oct 2025 15:58:55 +0000 Subject: [PATCH 4/4] chore(tests): fix indentation and trailing whitespace in test_qdp_case_insensitive.py --- .../io/ascii/tests/test_qdp_case_insensitive.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/astropy/io/ascii/tests/test_qdp_case_insensitive.py b/astropy/io/ascii/tests/test_qdp_case_insensitive.py index 3ab2627da403..764fbdbe97ce 100644 --- a/astropy/io/ascii/tests/test_qdp_case_insensitive.py +++ b/astropy/io/ascii/tests/test_qdp_case_insensitive.py @@ -2,9 +2,8 @@ import pytest from io import StringIO from astropy.table import Table - - - def _make_qdp_text(cmd1, cmd2): + +def _make_qdp_text(cmd1, cmd2): """ Construct a minimal QDP text with two directives lines followed by data lines. We flag column 1 with TERR (two-sided errors) and @@ -20,17 +19,15 @@ def _make_qdp_text(cmd1, cmd2): "1 0.1 0.2 10 100 5\n" "2 0.11 0.22 20 200 6\n" ) - - - @pytest.mark.parametrize( +@pytest.mark.parametrize( "cmds", [ ("READ TERR 1", "READ SERR 3"), # uppercase ("read terr 1", "read serr 3"), # lowercase ("ReAd TeRr 1", "rEaD sErR 3"), # mixed case ], - ) - def test_qdp_command_case_insensitive(cmds): +) +def test_qdp_command_case_insensitive(cmds): text = _make_qdp_text(*cmds) # Explicit names: 'a', 'b', 'c' t = Table.read(StringIO(text), format="ascii.qdp", names=["a", "b", "c"]) @@ -51,8 +48,8 @@ def test_qdp_command_case_insensitive(cmds): assert np.allclose(t["c"], [100.0, 200.0]) assert np.allclose(t["c_err"], [5.0, 6.0]) - - def test_qdp_command_case_insensitive_no_names(): + +def test_qdp_command_case_insensitive_no_names(): # Lowercase directives to exercise case-insensitive detection text = _make_qdp_text("read terr 1", "read serr 3") t = Table.read(StringIO(text), format="ascii.qdp")