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 ebec79154bd6858f2fd69d9715abf5dded9b29f3 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 257350ef7b5f5c779c7fd7154403bbfd292a8794 Mon Sep 17 00:00:00 2001 From: Rowan Ellis Date: Sun, 19 Oct 2025 20:09:21 +0000 Subject: [PATCH 3/3] Cleanup: remove stray duplicated docstring block left by automated edit --- astropy/coordinates/sky_coordinate.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/astropy/coordinates/sky_coordinate.py b/astropy/coordinates/sky_coordinate.py index a103d295ad33..661658b37342 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? @@ -2230,4 +2222,4 @@ def from_name(cls, name, frame="icrs", parse=False, cache=True): if frame in ("icrs", icrs_coord.__class__): return icrs_sky_coord else: - return icrs_sky_coord.transform_to(frame) + return icrs_sky_coord.transform_to(frame) \ No newline at end of file