From fb30b4b68327e1af3270a1d36d704027b1e660a2 Mon Sep 17 00:00:00 2001 From: Nadia Dencheva Date: Sat, 22 Aug 2026 12:41:52 +0300 Subject: [PATCH 1/4] initial commit --- pyproject.toml | 15 +++++ regions/io/__init__.py | 1 + regions/io/asdf/__init__.py | 0 regions/io/asdf/converters/__init__.py | 6 ++ regions/io/asdf/converters/pixel_coords.py | 33 ++++++++++ regions/io/asdf/converters/tests/__init__.py | 0 regions/io/asdf/converters/tests/conftest.py | 13 ++++ regions/io/asdf/converters/tests/examples.py | 10 +++ regions/io/asdf/converters/tests/test.py | 36 ++++++++++ regions/io/asdf/extension.py | 66 +++++++++++++++++++ regions/io/asdf/resources/__init__.py | 0 .../resources/manifests/regions-1.0.0.yaml | 10 +++ .../resources/schemas/pixcoord-1.0.0.yaml | 33 ++++++++++ 13 files changed, 223 insertions(+) create mode 100644 regions/io/asdf/__init__.py create mode 100644 regions/io/asdf/converters/__init__.py create mode 100644 regions/io/asdf/converters/pixel_coords.py create mode 100644 regions/io/asdf/converters/tests/__init__.py create mode 100644 regions/io/asdf/converters/tests/conftest.py create mode 100644 regions/io/asdf/converters/tests/examples.py create mode 100644 regions/io/asdf/converters/tests/test.py create mode 100644 regions/io/asdf/extension.py create mode 100644 regions/io/asdf/resources/__init__.py create mode 100644 regions/io/asdf/resources/manifests/regions-1.0.0.yaml create mode 100644 regions/io/asdf/resources/schemas/pixcoord-1.0.0.yaml diff --git a/pyproject.toml b/pyproject.toml index e2286a61f..5da16a40d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ all = [ 'shapely >= 2.1', ] test = [ + 'pytest-asdf-plugin', 'gwcs >= 0.22', 'pytest-arraydiff >= 0.6.1', 'pytest-astropy >= 0.12', @@ -80,9 +81,13 @@ include-package-data = false namespaces = false [tool.setuptools.package-data] +'regions.io.asdf.resources' = [ + '**/*.yaml' +] '*' = [ 'data/*', ] + 'regions.shapes.tests' = [ 'reference/*.txt', 'data/*.fits', @@ -97,6 +102,11 @@ namespaces = false 'data/*.fits', ] + +[project.entry-points] +'asdf.extensions' = {regions = 'regions.io.asdf.extension:get_extensions'} +'asdf.resource_mappings' = {regions = 'regions.io.asdf.extension:get_resource_mappings'} + [tool.pytest.ini_options] minversion = "8.0" testpaths = [ @@ -117,6 +127,9 @@ addopts = [ '--strict-config', '--strict-markers', ] +asdf_schema_root = 'regions/io/asdf/resources/schemas' +asdf_schema_tests_enabled = true + log_level = 'INFO' log_cli_level = 'INFO' xfail_strict = true @@ -142,6 +155,8 @@ omit = [ '*/regions/*/tests/*', '*/regions/extern/*', '*/regions/version*', + 'to_yaml_tree', # ASDF converter + 'from_yaml_tree', # ASDF converter ] [tool.coverage.report] diff --git a/regions/io/__init__.py b/regions/io/__init__.py index c58b0dbd1..d361cd42a 100644 --- a/regions/io/__init__.py +++ b/regions/io/__init__.py @@ -15,3 +15,4 @@ from .fits.core import * # noqa: F401, F403 from .fits.read import * # noqa: F401, F403 from .fits.write import * # noqa: F401, F403 +from .asdf import extension # noqa: F401 diff --git a/regions/io/asdf/__init__.py b/regions/io/asdf/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/regions/io/asdf/converters/__init__.py b/regions/io/asdf/converters/__init__.py new file mode 100644 index 000000000..c026749b8 --- /dev/null +++ b/regions/io/asdf/converters/__init__.py @@ -0,0 +1,6 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +from .pixel_coords import PixCoordConverter + +__all__ = [ + 'PixCoordConverter', +] diff --git a/regions/io/asdf/converters/pixel_coords.py b/regions/io/asdf/converters/pixel_coords.py new file mode 100644 index 000000000..81818a5e4 --- /dev/null +++ b/regions/io/asdf/converters/pixel_coords.py @@ -0,0 +1,33 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst + +""" +ASDF converter for PixCoord object. +""" +from asdf.extension import Converter + +__all__ = [ + 'PixCoordConverter', +] + + +class PixCoordConverter(Converter): + """ + ASDF converter for pixel coordinates. + """ + + tags = ('tag:astropy.org:regions/pixcoord-*',) + types = ('regions.core.pixcoord.PixCoord',) + + def to_yaml_tree(self, obj, tag, ctx): # noqa: ARG002 + + return { + 'x': obj.x, + 'y': obj.y, + } + + def from_yaml_tree(self, node, tag, ctx): # noqa: ARG002 + from regions.core.pixcoord import PixCoord + return PixCoord( + x=node['x'], + y=node['y'], + ) diff --git a/regions/io/asdf/converters/tests/__init__.py b/regions/io/asdf/converters/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/regions/io/asdf/converters/tests/conftest.py b/regions/io/asdf/converters/tests/conftest.py new file mode 100644 index 000000000..9b6363218 --- /dev/null +++ b/regions/io/asdf/converters/tests/conftest.py @@ -0,0 +1,13 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst + +import pytest + +from regions.io.asdf.converters.tests import examples + + +@pytest.fixture +def test_pixcoord(): + """ + Test that PixCoord can be serialized and deserialized correctly. + """ + return examples.pixcoord() diff --git a/regions/io/asdf/converters/tests/examples.py b/regions/io/asdf/converters/tests/examples.py new file mode 100644 index 000000000..cd3cfea23 --- /dev/null +++ b/regions/io/asdf/converters/tests/examples.py @@ -0,0 +1,10 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +from regions.core import PixCoord + +parameters = { + 'PixCoord': ['x', 'y'], +} + + +def pixcoord(): + return PixCoord(1, 2), parameters['PixCoord'] diff --git a/regions/io/asdf/converters/tests/test.py b/regions/io/asdf/converters/tests/test.py new file mode 100644 index 000000000..f9b2db5b5 --- /dev/null +++ b/regions/io/asdf/converters/tests/test.py @@ -0,0 +1,36 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst + +import asdf +import pytest +from numpy.testing import assert_array_equal + + +@pytest.fixture +def obj(request): + """ + A pytest fixture that returns a 'Region' instance and the + list of parameters to test. + """ + return request.getfixturevalue(request.param) + + +region_params = pytest.mark.parametrize('obj', [ + 'test_pixcoord', +], indirect=True) + + +@region_params +def test_region_converters(tmp_path, obj): + """ + Test that the Region converters can round-trip a Region object. + """ + region, pars = obj + with asdf.AsdfFile() as af: + af['region'] = region + af.write_to(tmp_path / 'region.asdf') + + with asdf.open(tmp_path / 'region.asdf') as af: + region2 = af['region'] + for parameter in pars: + assert_array_equal(getattr(region, parameter), + getattr(region2, parameter)) diff --git a/regions/io/asdf/extension.py b/regions/io/asdf/extension.py new file mode 100644 index 000000000..d52afa7ae --- /dev/null +++ b/regions/io/asdf/extension.py @@ -0,0 +1,66 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +ASDF extension for regions. +""" +import importlib.resources as importlib_resources + +from asdf.extension import ManifestExtension +from asdf.resource import DirectoryResourceMapping + +from regions.io.asdf.converters.pixel_coords import PixCoordConverter + +__all__ = ['PixCoordConverter'] + + +REGIONS_CONVERTERS = [ + PixCoordConverter(), +] + +# The order here is important; asdf will prefer to use extensions +# that occur earlier in the list. +REGIONS_MANIFEST_URIS = [ + 'asdf://astropy.org/regions/manifests/regions-1.0.0', +] + + +def get_extensions(): + """ + Get the regions extension. + This method is registered with the asdf.extensions entry point. + + Returns + ------- + list + A list of ASDF extensions. + """ + return [ + ManifestExtension.from_uri( + uri, + converters=REGIONS_CONVERTERS, + ) + for uri in REGIONS_MANIFEST_URIS + ] + + +def get_resource_mappings(): + """ + Get the resource mapping instances for the regions schemas + and manifests. This method is registered with the + asdf.resource_mappings entry point. + + Returns + ------- + list + A list of collections.abc.Mapping of ASDF resource mappings. + """ + from regions.io.asdf import resources + + resources_root = importlib_resources.files(resources) + + return [ + DirectoryResourceMapping(resources_root / 'schemas', + 'asdf://astropy.org/regions/schemas', + recursive=True), + DirectoryResourceMapping(resources_root / 'manifests', + 'asdf://astropy.org/regions/manifests'), + ] diff --git a/regions/io/asdf/resources/__init__.py b/regions/io/asdf/resources/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/regions/io/asdf/resources/manifests/regions-1.0.0.yaml b/regions/io/asdf/resources/manifests/regions-1.0.0.yaml new file mode 100644 index 000000000..f5f3dc9e5 --- /dev/null +++ b/regions/io/asdf/resources/manifests/regions-1.0.0.yaml @@ -0,0 +1,10 @@ +id: asdf://astropy.org/regions/manifests/regions-1.0.0 +extension_uri: asdf://astropy.org/regions/extensions/regions-1.0.0 +title: Regions extension 1.0.0 +description: |- + A set of tags for serializing astropy regions objects. +tags: + - tag_uri: tag:astropy.org:regions/pixcoord-1.0.0 + schema_uri: asdf://astropy.org/regions/schemas/pixcoord-1.0.0 + title: PixCoord schema + description: Pixel coordinates for regions. diff --git a/regions/io/asdf/resources/schemas/pixcoord-1.0.0.yaml b/regions/io/asdf/resources/schemas/pixcoord-1.0.0.yaml new file mode 100644 index 000000000..9117cb8af --- /dev/null +++ b/regions/io/asdf/resources/schemas/pixcoord-1.0.0.yaml @@ -0,0 +1,33 @@ +%YAML 1.1 +--- +$schema: 'http://stsci.edu/schemas/yaml-schema/draft-01' +id: 'asdf://astropy.org/regions/schemas/pixcoord-1.0.0' +title: Pixel coordinates +description: | + ASDF schema for serializing a pixel coordinate. + +examples: + - - A 'PixCoord' object with x=1.0 and y=2.0 + - | + ! {x: 1, y: 2} + +type: object +properties: + x: + title: X coordinate. + anyOf: + - type: number + - type: array + - tag: 'tag:stsci.edu:asdf/core/ndarray-1.*' + description: | + The x coordinate of the pixel. + y: + title: Y coordinate. + anyOf: + - type: number + - type: array + - tag: 'tag:stsci.edu:asdf/core/ndarray-1.*' + description: | + The y coordinate of the pixel. +required: [x, y] +... From 1d9fd59a89928b0d1ff49ab6eba75c4e0a307f52 Mon Sep 17 00:00:00 2001 From: Nadia Dencheva Date: Fri, 11 Sep 2026 16:24:07 +0300 Subject: [PATCH 2/4] add a change log and another test --- CHANGES.rst | 1 + regions/io/asdf/converters/tests/conftest.py | 9 +++++++++ regions/io/asdf/converters/tests/examples.py | 6 ++++++ regions/io/asdf/converters/tests/test.py | 1 + 4 files changed, 17 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index e22b15a35..50c43307c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,7 @@ New Features scale is computed in closed form, and the elliptical and rectangular annuli convert both of their shapes with one WCS evaluation in each direction. [#707] +- Add serialization of regions to ASDF files. [#702] Bug Fixes --------- diff --git a/regions/io/asdf/converters/tests/conftest.py b/regions/io/asdf/converters/tests/conftest.py index 9b6363218..680ce50f8 100644 --- a/regions/io/asdf/converters/tests/conftest.py +++ b/regions/io/asdf/converters/tests/conftest.py @@ -11,3 +11,12 @@ def test_pixcoord(): Test that PixCoord can be serialized and deserialized correctly. """ return examples.pixcoord() + + +@pytest.fixture +def test_pixcoord_array(): + """ + Test that PixCoord can be serialized and deserialized correctly. + """ + return examples.pixcoord_array() + diff --git a/regions/io/asdf/converters/tests/examples.py b/regions/io/asdf/converters/tests/examples.py index cd3cfea23..38e6ff1b8 100644 --- a/regions/io/asdf/converters/tests/examples.py +++ b/regions/io/asdf/converters/tests/examples.py @@ -1,6 +1,8 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst +import numpy as np from regions.core import PixCoord + parameters = { 'PixCoord': ['x', 'y'], } @@ -8,3 +10,7 @@ def pixcoord(): return PixCoord(1, 2), parameters['PixCoord'] + + +def pixcoord_array(): + return PixCoord(np.array([1, 2]), np.array([3, 4])), parameters['PixCoord'] diff --git a/regions/io/asdf/converters/tests/test.py b/regions/io/asdf/converters/tests/test.py index f9b2db5b5..244c320aa 100644 --- a/regions/io/asdf/converters/tests/test.py +++ b/regions/io/asdf/converters/tests/test.py @@ -16,6 +16,7 @@ def obj(request): region_params = pytest.mark.parametrize('obj', [ 'test_pixcoord', + 'test_pixcoord_array', ], indirect=True) From 85078f344a2ffdafcb554cbab7c1516dbfb57598 Mon Sep 17 00:00:00 2001 From: Nadia Dencheva Date: Fri, 11 Sep 2026 16:28:44 +0300 Subject: [PATCH 3/4] fix codestyle --- regions/io/asdf/converters/tests/conftest.py | 1 - regions/io/asdf/converters/tests/examples.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/regions/io/asdf/converters/tests/conftest.py b/regions/io/asdf/converters/tests/conftest.py index 680ce50f8..7325137eb 100644 --- a/regions/io/asdf/converters/tests/conftest.py +++ b/regions/io/asdf/converters/tests/conftest.py @@ -19,4 +19,3 @@ def test_pixcoord_array(): Test that PixCoord can be serialized and deserialized correctly. """ return examples.pixcoord_array() - diff --git a/regions/io/asdf/converters/tests/examples.py b/regions/io/asdf/converters/tests/examples.py index 38e6ff1b8..b37d8cc16 100644 --- a/regions/io/asdf/converters/tests/examples.py +++ b/regions/io/asdf/converters/tests/examples.py @@ -1,7 +1,7 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst import numpy as np -from regions.core import PixCoord +from regions.core import PixCoord parameters = { 'PixCoord': ['x', 'y'], From 56845ab0750fd2e327f7544d4006c455d2a36285 Mon Sep 17 00:00:00 2001 From: Nadia Dencheva Date: Fri, 11 Sep 2026 16:59:49 +0300 Subject: [PATCH 4/4] add asdf as a dependency --- pyproject.toml | 6 ++++-- regions/io/asdf/converters/__init__.py | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5da16a40d..1dc700831 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ requires-python = '>=3.11' dependencies = [ 'astropy >= 7.0', 'numpy >= 2.0', + 'asdf', ] [project.urls] @@ -155,8 +156,7 @@ omit = [ '*/regions/*/tests/*', '*/regions/extern/*', '*/regions/version*', - 'to_yaml_tree', # ASDF converter - 'from_yaml_tree', # ASDF converter + 'regions/io/asdf/converters/tests/*', ] [tool.coverage.report] @@ -239,6 +239,8 @@ exclude = [ '\._.*', # private functions/methods '^test_*', # test code '^conftest.*$', # pytest configuration + 'to_yaml_tree', # ASDF converter + 'from_yaml_tree', # ASDF converter ] [tool.ruff] diff --git a/regions/io/asdf/converters/__init__.py b/regions/io/asdf/converters/__init__.py index c026749b8..c2f7e2a54 100644 --- a/regions/io/asdf/converters/__init__.py +++ b/regions/io/asdf/converters/__init__.py @@ -1,4 +1,8 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +ASDF converters. +""" + from .pixel_coords import PixCoordConverter __all__ = [