From d90dcdf086a179adf10ff9f7dde4f92dec101bf1 Mon Sep 17 00:00:00 2001 From: Rowan Ellis Date: Sun, 19 Oct 2025 20:08:17 +0000 Subject: [PATCH 1/3] SkyCoord.__getattr__: preserve inner AttributeError from subclass descriptors\n\nWhen a SkyCoord subclass defines a property whose getter raises AttributeError (e.g., accessing a missing attribute on self), Python falls back to __getattr__, which then raises an AttributeError for the original attribute name, masking the root cause.\n\nAdd an early guard in __getattr__ to detect class-level attributes (incl. properties) via the MRO and access them via object.__getattribute__ so that any AttributeError from the descriptor propagates unchanged.\n\nFixes #14. --- astropy/coordinates/sky_coordinate.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/astropy/coordinates/sky_coordinate.py b/astropy/coordinates/sky_coordinate.py index ab475f7d0d29..a103d295ad33 100644 --- a/astropy/coordinates/sky_coordinate.py +++ b/astropy/coordinates/sky_coordinate.py @@ -871,6 +871,16 @@ def __getattr__(self, attr): Overrides getattr to return coordinates that this can be transformed to, based on the alias attr in the primary transform graph. """ + # If this attribute exists on the class (e.g., a subclass property), + # try to access it via normal attribute lookup so that any + # AttributeError raised by the descriptor propagates unchanged. + for cls in type(self).mro(): + if attr in getattr(cls, "__dict__", {}): + return object.__getattribute__(self, attr) + """ + Overrides getattr to return coordinates that this can be transformed + to, based on the alias attr in the primary transform graph. + """ if "_sky_coord_frame" in self.__dict__: if self._is_name(attr): return self # Should this be a deepcopy of self? From e1a913d083657d797615f28da58c49fa755e18d6 Mon Sep 17 00:00:00 2001 From: Rowan Ellis Date: Sun, 19 Oct 2025 20:08:29 +0000 Subject: [PATCH 2/3] Add unit test to ensure SkyCoord subclass property AttributeError preserves inner missing attribute message --- .../tests/test_sky_coord_subclass_attr.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 astropy/coordinates/tests/test_sky_coord_subclass_attr.py diff --git a/astropy/coordinates/tests/test_sky_coord_subclass_attr.py b/astropy/coordinates/tests/test_sky_coord_subclass_attr.py new file mode 100644 index 000000000000..a3196202ec7d --- /dev/null +++ b/astropy/coordinates/tests/test_sky_coord_subclass_attr.py @@ -0,0 +1,19 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +import pytest +import astropy.units as u +from astropy.coordinates import SkyCoord + +class CustomCoord(SkyCoord): + @property + def prop(self): + # Access a non-existent attribute to trigger AttributeError from the descriptor + return self.random_attr + + +def test_subclass_property_inner_attributeerror_message(): + c = CustomCoord('00h42m30s', '+41d12m00s', frame='icrs') + with pytest.raises(AttributeError) as excinfo: + _ = c.prop + # Ensure the error message mentions the missing inner attribute, not the property name + assert "random_attr" in str(excinfo.value) + assert "prop" not in str(excinfo.value) From 95acd434befbef187b93fa8f6de5f085bdedc9c5 Mon Sep 17 00:00:00 2001 From: Rowan Ellis Date: Sun, 19 Oct 2025 20:27:25 +0000 Subject: [PATCH 3/3] Remove duplicated inline docstring block from __getattr__ after adding MRO guard --- astropy/coordinates/sky_coordinate.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/astropy/coordinates/sky_coordinate.py b/astropy/coordinates/sky_coordinate.py index a103d295ad33..bd50b304c109 100644 --- a/astropy/coordinates/sky_coordinate.py +++ b/astropy/coordinates/sky_coordinate.py @@ -867,20 +867,12 @@ def _is_name(self, string): ) def __getattr__(self, attr): - """ - Overrides getattr to return coordinates that this can be transformed - to, based on the alias attr in the primary transform graph. - """ # If this attribute exists on the class (e.g., a subclass property), # try to access it via normal attribute lookup so that any # AttributeError raised by the descriptor propagates unchanged. for cls in type(self).mro(): if attr in getattr(cls, "__dict__", {}): return object.__getattribute__(self, attr) - """ - Overrides getattr to return coordinates that this can be transformed - to, based on the alias attr in the primary transform graph. - """ if "_sky_coord_frame" in self.__dict__: if self._is_name(attr): return self # Should this be a deepcopy of self?