Skip to content

Commit

Permalink
Support VOTable 1.5
Browse files Browse the repository at this point in the history
  • Loading branch information
tomdonaldson committed Aug 19, 2024
1 parent bbc6511 commit a32832d
Show file tree
Hide file tree
Showing 9 changed files with 1,021 additions and 18 deletions.
634 changes: 634 additions & 0 deletions astropy/io/votable/data/VOTable.v1.5.xsd

Large diffs are not rendered by default.

20 changes: 17 additions & 3 deletions astropy/io/votable/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,12 +660,12 @@ class W20(VOTableSpecWarning):
class W21(UnimplementedWarning):
"""
Unknown issues may arise using ``astropy.io.votable`` with VOTable files
from a version other than 1.1, 1.2, 1.3, or 1.4.
from a version not in 1.1 through 1.5.
"""

message_template = (
"astropy.io.votable is designed for VOTable version 1.1, 1.2, 1.3,"
" and 1.4, but this file is {}"
"astropy.io.votable is designed for VOTable versions 1.1 through 1.5,"
" but this file is {}"
)
default_args = ("x",)

Expand Down Expand Up @@ -1156,6 +1156,20 @@ class W56(VOTableSpecWarning):
)


class W57(VOTableSpecWarning):
"""
The ``refposition`` attribute on the ``COOSYS`` element is only allowed on
VOTABLE versions 1.5 and greater.
favor of a reference to the Space-Time Coordinate (STC) data
model (see `utype
<http://www.ivoa.net/documents/VOTable/20091130/REC-VOTable-1.2.html#sec:utype>`__
and the IVOA note `referencing STC in VOTable
<http://ivoa.net/Documents/latest/VOTableSTC.html>`__.
"""

message_template = "refposition only allowed on VOTABLE v1.5 and greater"


class E01(VOWarning, ValueError):
"""Invalid size specifier for a field.
Expand Down
22 changes: 22 additions & 0 deletions astropy/io/votable/tests/data/coosys.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<VOTABLE xmlns="http://www.ivoa.net/xml/VOTable/v1.3" version="1.5">
<RESOURCE>
<!-- "equinox", "epoch", "system", "refposition" -->
<COOSYS ID="coosys1" system="ICRS" equinox="J2000" epoch="J2015.5" refposition="BARYCENTER"/>
<COOSYS ID="coosys2" system="FK4" equinox="B1950" epoch="B1950.0" refposition="UNKNOWN"/>
<TABLE name="sources">
<FIELD name="RA" ID="ra" ucd="pos.eq.ra;meta.main" datatype="double" unit="deg" ref="my_coosys"/>
<FIELD name="Dec" ID="dec" ucd="pos.eq.dec;meta.main" datatype="double" unit="deg" ref="my_coosys"/>
<FIELD name="Name" ID="name" ucd="meta.id;meta.main" datatype="char" arraysize="8*"/>
<DATA>
<TABLEDATA>
<TR>
<TD>010.68</TD>
<TD>+41.27</TD>
<TD>N 224</TD>
</TR>
</TABLEDATA>
</DATA>
</TABLE>
</RESOURCE>
</VOTABLE>
212 changes: 212 additions & 0 deletions astropy/io/votable/tests/test_coosys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst
import io
from contextlib import nullcontext

import pytest

from astropy.io.votable import tree
from astropy.io.votable.exceptions import W07, W08, W21, W27, W41, W57
from astropy.io.votable.table import parse
from astropy.io.votable.tree import Resource, VOTableFile
from astropy.tests.helper import PYTEST_LT_8_0
from astropy.utils.data import get_pkg_data_filename

COOSYS_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
<VOTABLE xmlns="http://www.ivoa.net/xml/VOTable/v{}" version="{}">
<RESOURCE>
{}
</RESOURCE>
</VOTABLE>
"""

COOSYS_REFPOSITION = (
'<COOSYS ID="my_coosys" epoch="J2015.5" system="ICRS" refposition="BARYCENTER"/>'
)
COOSYS_NO_REFPOSITION = '<COOSYS ID="my_coosys" epoch="J2015.5" system="ICRS"/>'
NO_COOSYS = ""


def test_accept_refposition():
for ns_version, version, coosys, err in [
("1.1", "1.1", COOSYS_NO_REFPOSITION, None),
("1.2", "1.2", COOSYS_NO_REFPOSITION, (W27)),
("1.3", "1.3", COOSYS_NO_REFPOSITION, None),
("1.3", "1.4", COOSYS_NO_REFPOSITION, None),
("1.3", "1.5", COOSYS_NO_REFPOSITION, None),
("1.1", "1.1", COOSYS_REFPOSITION, (W57)),
("1.2", "1.2", COOSYS_REFPOSITION, (W27, W57)),
("1.3", "1.3", COOSYS_REFPOSITION, (W57)),
("1.3", "1.4", COOSYS_REFPOSITION, (W57)),
("1.3", "1.5", COOSYS_REFPOSITION, None),
]:
xml_string = COOSYS_TEMPLATE.format(ns_version, version, coosys)
input = io.BytesIO(bytes(xml_string, "utf-8"))
if err is not None:
with pytest.warns() as record:
vot = parse(input, verify="warn")
else:
vot = parse(input, verify="warn")


def _coosys_tests(votable):
assert len(list(votable.iter_coosys())) == 2

coosys = votable.get_coosys_by_id("coosys1")
assert coosys.system == "ICRS"
assert coosys.equinox == "J2000"
assert coosys.epoch == "J2015.5"
assert coosys.refposition == "BARYCENTER"

coosys = votable.get_coosys_by_id("coosys2")
assert coosys.system == "FK4"
assert coosys.equinox == "B1950"
assert coosys.epoch == "B1950.0"
assert coosys.refposition == "UNKNOWN"


def test_coosys():
votable = parse(get_pkg_data_filename("data/coosys.xml"))
_coosys_tests(votable)


def test_coosys_roundtrip():
orig_votable = parse(get_pkg_data_filename("data/coosys.xml"))
bio = io.BytesIO()
orig_votable.to_xml(bio)
bio.seek(0)
votable = parse(bio)
_coosys_tests(votable)


if __name__ == "__main__":
test_accept_refposition()


def test_check_astroyear_fail():
config = {"verify": "exception"}
field = tree.Field(None, name="astroyear", arraysize="1")
with pytest.raises(W07):
tree.check_astroyear("X2100", field, config)


def test_string_fail():
config = {"verify": "exception"}
with pytest.raises(W08):
tree.check_string(42, "foo", config)


def test_make_Fields():
votable = VOTableFile()
# ...with one resource...
resource = Resource()
votable.resources.append(resource)

# ... with one table
table = tree.TableElement(votable)
resource.tables.append(table)

table.fields.extend(
[tree.Field(votable, name="Test", datatype="float", unit="mag")]
)


def test_unit_format():
data = parse(get_pkg_data_filename("data/irsa-nph-error.xml"))
assert data._config["version"] == "1.0"
assert tree._get_default_unit_format(data._config) == "cds"
data = parse(get_pkg_data_filename("data/names.xml"))
assert data._config["version"] == "1.1"
assert tree._get_default_unit_format(data._config) == "cds"
data = parse(get_pkg_data_filename("data/gemini.xml"))
assert data._config["version"] == "1.2"
assert tree._get_default_unit_format(data._config) == "cds"
data = parse(get_pkg_data_filename("data/binary2_masked_strings.xml"))
assert data._config["version"] == "1.3"
assert tree._get_default_unit_format(data._config) == "cds"
data = parse(get_pkg_data_filename("data/timesys.xml"))
assert data._config["version"] == "1.4"
assert tree._get_default_unit_format(data._config) == "vounit"


def test_namespace_warning():
"""
A version 1.4 VOTable must use the same namespace as 1.3.
(see https://www.ivoa.net/documents/VOTable/20191021/REC-VOTable-1.4-20191021.html#ToC16).
"""
bad_namespace = b"""<?xml version="1.0" encoding="utf-8"?>
<VOTABLE version="1.4" xmlns="http://www.ivoa.net/xml/VOTable/v1.4"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<RESOURCE/>
</VOTABLE>
"""
with pytest.warns(W41):
parse(io.BytesIO(bad_namespace), verify="exception")

good_namespace_14 = b"""<?xml version="1.0" encoding="utf-8"?>
<VOTABLE version="1.4" xmlns="http://www.ivoa.net/xml/VOTable/v1.3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<RESOURCE/>
</VOTABLE>
"""
parse(io.BytesIO(good_namespace_14), verify="exception")

good_namespace_13 = b"""<?xml version="1.0" encoding="utf-8"?>
<VOTABLE version="1.3" xmlns="http://www.ivoa.net/xml/VOTable/v1.3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<RESOURCE/>
</VOTABLE>
"""
parse(io.BytesIO(good_namespace_13), verify="exception")


def test_version():
"""
VOTableFile.__init__ allows versions of '1.1' through '1.5'.
VOTableFile.__init__ does not allow version of '1.0' anymore and now raises a ValueError as it does to other versions not supported.
"""
# Exercise the checks in __init__
for version in ("1.1", "1.2", "1.3", "1.4", "1.5"):
VOTableFile(version=version)
for version in ("0.9", "1.0", "1.6", "2.0"):
with pytest.raises(
ValueError, match=r"should be in \('1.1', '1.2', '1.3', '1.4', '1.5'\)."
):
VOTableFile(version=version)

# Exercise the checks in the setter
vot = VOTableFile()
for version in ("1.1", "1.2", "1.3", "1.4"):
vot.version = version
for version in ("1.0", "1.6", "2.0"):
with pytest.raises(
ValueError,
match=r"supports VOTable versions '1.1', '1.2', '1.3', '1.4', '1.5'$",
):
vot.version = version

# Exercise the checks in the parser.
begin = b'<?xml version="1.0" encoding="utf-8"?><VOTABLE version="'
middle = b'" xmlns="http://www.ivoa.net/xml/VOTable/v'
end = (
b'" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><RESOURCE/></VOTABLE>'
)

# Valid versions
for bversion in (b"1.1", b"1.2", b"1.3"):
parse(
io.BytesIO(begin + bversion + middle + bversion + end), verify="exception"
)
parse(io.BytesIO(begin + b"1.4" + middle + b"1.3" + end), verify="exception")

if PYTEST_LT_8_0:
ctx = nullcontext()
else:
ctx = pytest.warns(W41)

# Invalid versions
for bversion in (b"1.0", b"2.0"):
with pytest.warns(W21), ctx:
parse(
io.BytesIO(begin + bversion + middle + bversion + end),
verify="exception",
)
82 changes: 82 additions & 0 deletions astropy/io/votable/tests/test_schema_versions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import re
import sys

import pytest

from astropy.io.votable.xmlutil import validate_schema
from astropy.utils.data import get_pkg_data_filename

# Each test file uses a feature that is new with its version.
V_1_2_TABLE = get_pkg_data_filename("data/empty_table.xml")
V_1_3_TABLE = get_pkg_data_filename("data/binary2_masked_strings.xml")
V_1_4_TABLE = get_pkg_data_filename("data/timesys.xml")
V_1_5_TABLE = get_pkg_data_filename("data/coosys.xml")

# Schema versions to test against
V_1_2 = "1.2"
V_1_3 = "1.3"
V_1_4 = "1.4"
V_1_5 = "1.5"
V_1_6 = "1.6" # Next (unimplemented) schema

# Starting with version 1.4, schemas were upward compatible.
test_cases = [
(V_1_2_TABLE, V_1_2, 0, None),
(
V_1_2_TABLE,
V_1_3,
3,
b"No matching global declaration available for the validation root",
),
(V_1_3_TABLE, V_1_3, 0, None),
(V_1_3_TABLE, V_1_4, 0, None),
(V_1_3_TABLE, V_1_5, 0, None),
(
V_1_3_TABLE,
V_1_6,
3,
b"No matching global declaration available for the validation root",
),
(V_1_4_TABLE, V_1_3, 3, b"TIMESYS.*element is not expected"),
(V_1_4_TABLE, V_1_4, 0, None),
(V_1_4_TABLE, V_1_5, 0, None),
(
V_1_4_TABLE,
V_1_6,
3,
b"No matching global declaration available for the validation root",
),
(V_1_5_TABLE, V_1_3, 3, b"attribute 'refposition' is not allowed"),
(V_1_5_TABLE, V_1_4, 3, b"attribute 'refposition' is not allowed"),
(V_1_5_TABLE, V_1_5, 0, None),
(
V_1_5_TABLE,
V_1_6,
3,
b"No matching global declaration available for the validation root",
),
]


@pytest.mark.parametrize(
"votable_file,schema_version,expected_return_code,expected_msg_re", test_cases
)
def test_schema_versions(
votable_file, schema_version, expected_return_code, expected_msg_re
):
"""Test that xmllint gives expected results for the given file and schema version."""

# We need xmllint so won't try with Windows.
if sys.platform.startswith("win"):
return

try:
rc, stdout, stderr = validate_schema(votable_file, schema_version)
except OSError:
# If xmllint is not installed, we want the test to pass anyway
return

assert rc == expected_return_code
if rc == 3:
# Schema validation error. Check the error message content.
assert re.search(expected_msg_re, stderr)
Loading

0 comments on commit a32832d

Please sign in to comment.