Repository: sunpy/sunpy

Issue:
wcs_to_celestial_frame should not assume it gets passed a sunpy frame
Currenlty we assume all frames have an obstime attribute, but we shouldn't assume this. See https://github.com/astropy/astropy/issues/9094


Discussion:

Resolving patch:
diff --git a/sunpy/coordinates/frames.py b/sunpy/coordinates/frames.py
--- a/sunpy/coordinates/frames.py
+++ b/sunpy/coordinates/frames.py
@@ -26,10 +26,13 @@
 
 class SunPyBaseCoordinateFrame(BaseCoordinateFrame):
     """
-    * Defines a default longitude wrap angle of 180 degrees, which can be overridden by the class
-      variable `_wrap_angle`.
+    * Defines the frame attribute ``obstime`` for observation time.
+    * Defines a default longitude wrap angle of 180 degrees, which can be overridden via the class
+      variable ``_wrap_angle``.
     * Inject a nice way of representing the object which the coordinate represents.
     """
+    obstime = TimeFrameAttributeSunPy()
+
     _wrap_angle = 180*u.deg
 
     def __init__(self, *args, **kwargs):
@@ -149,8 +152,6 @@ class HeliographicStonyhurst(SunPyBaseCoordinateFrame):
                                                         framename='z')]
     }
 
-    obstime = TimeFrameAttributeSunPy()
-
     def __init__(self, *args, **kwargs):
         _rep_kwarg = kwargs.get('representation_type', None)
 
@@ -230,31 +231,8 @@ class HeliographicCarrington(HeliographicStonyhurst):
     <SkyCoord (HeliographicCarrington: obstime=2011-01-05T00:00:50.000): (lon, lat, radius) in (deg, deg, km)
         (90., 2.54480438, 45.04442252)>
     """
-
     name = "heliographic_carrington"
-    default_representation = SphericalRepresentation
-
-    frame_specific_representation_info = {
-        SphericalRepresentation: [RepresentationMapping(reprname='lon',
-                                                        framename='lon',
-                                                        defaultunit=u.deg),
-                                  RepresentationMapping(reprname='lat',
-                                                        framename='lat',
-                                                        defaultunit=u.deg),
-                                  RepresentationMapping(reprname='distance',
-                                                        framename='radius',
-                                                        defaultunit=None)],
-
-        UnitSphericalRepresentation: [RepresentationMapping(reprname='lon',
-                                                            framename='lon',
-                                                            defaultunit=u.deg),
-                                      RepresentationMapping(reprname='lat',
-                                                            framename='lat',
-                                                            defaultunit=u.deg)],
-    }
-
     _wrap_angle = 360*u.deg
-    obstime = TimeFrameAttributeSunPy()
 
 
 @add_common_docstring(**_variables_for_parse_time_docstring())
@@ -329,14 +307,12 @@ class Heliocentric(SunPyBaseCoordinateFrame):
     <SkyCoord (Heliocentric: obstime=2011-01-05T00:00:50.000, observer=<HeliographicStonyhurst Coordinate for 'earth'>): (x, y, z) in km
         (5., 8.66025404, 10.)>
     """
-
     default_representation = CartesianRepresentation
 
-    _frame_specific_representation_info = {
+    frame_specific_representation_info = {
         CylindricalRepresentation: [RepresentationMapping('phi', 'psi', u.deg)]
     }
 
-    obstime = TimeFrameAttributeSunPy()
     observer = ObserverCoordinateAttribute(HeliographicStonyhurst, default="earth")
 
 
@@ -396,7 +372,6 @@ class Helioprojective(SunPyBaseCoordinateFrame):
     <SkyCoord (Helioprojective: obstime=2011-01-05T00:00:50.000, rsun=695700.0 km, observer=<HeliographicStonyhurst Coordinate for 'earth'>): (Tx, Ty, distance) in (arcsec, arcsec, AU)
         (137.87948623, -275.75878762, 1.00000112)>
     """
-
     default_representation = SphericalRepresentation
 
     frame_specific_representation_info = {
@@ -418,7 +393,6 @@ class Helioprojective(SunPyBaseCoordinateFrame):
                                                             defaultunit=u.arcsec)],
     }
 
-    obstime = TimeFrameAttributeSunPy()
     rsun = Attribute(default=_RSUN.to(u.km))
     observer = ObserverCoordinateAttribute(HeliographicStonyhurst, default="earth")
 
diff --git a/sunpy/coordinates/wcs_utils.py b/sunpy/coordinates/wcs_utils.py
--- a/sunpy/coordinates/wcs_utils.py
+++ b/sunpy/coordinates/wcs_utils.py
@@ -3,7 +3,8 @@
 from astropy.wcs import WCSSUB_CELESTIAL
 from astropy.wcs import WCS
 
-from .frames import BaseCoordinateFrame, Helioprojective, Heliocentric, HeliographicStonyhurst, HeliographicCarrington
+from .frames import (BaseCoordinateFrame, SunPyBaseCoordinateFrame,
+                     Helioprojective, Heliocentric, HeliographicStonyhurst, HeliographicCarrington)
 
 __all__ = ['solar_wcs_frame_mapping', 'solar_frame_to_wcs_mapping']
 
@@ -57,12 +58,16 @@ def solar_frame_to_wcs_mapping(frame, projection='TAN'):
     else:
         wcs.rsun = None
 
-    if hasattr(frame, 'observer'):
+    if hasattr(frame, 'observer') and isinstance(frame.observer, BaseCoordinateFrame):
         wcs.heliographic_observer = frame.observer
+    else:
+        wcs.heliographic_observer = None
+
+    if isinstance(frame, SunPyBaseCoordinateFrame):
 
-    if isinstance(frame, BaseCoordinateFrame):
+        if frame.obstime:
+            wcs.wcs.dateobs = frame.obstime.utc.isot
 
-        wcs.wcs.dateobs = frame.obstime.utc.isot
         if isinstance(frame, Helioprojective):
             xcoord = 'HPLN' + '-' + projection
             ycoord = 'HPLT' + '-' + projection


Test patch:
diff --git a/sunpy/coordinates/tests/test_wcs_utils.py b/sunpy/coordinates/tests/test_wcs_utils.py
--- a/sunpy/coordinates/tests/test_wcs_utils.py
+++ b/sunpy/coordinates/tests/test_wcs_utils.py
@@ -3,6 +3,7 @@
 import numpy as np
 
 import sunpy.map
+from astropy.coordinates import BaseCoordinateFrame
 from astropy.wcs import WCS
 
 from sunpy.coordinates.frames import Helioprojective, Heliocentric, HeliographicStonyhurst, HeliographicCarrington
@@ -127,6 +128,18 @@ def test_hpc_frame_to_wcs():
     assert isinstance(result_wcs.heliographic_observer, HeliographicStonyhurst)
     assert result_wcs.rsun == frame.rsun
 
+    # Test a frame with no obstime and no observer
+    frame = Helioprojective()
+    result_wcs = solar_frame_to_wcs_mapping(frame)
+
+    assert isinstance(result_wcs, WCS)
+
+    assert result_wcs.wcs.ctype[0] == 'HPLN-TAN'
+    assert result_wcs.wcs.cunit[0] == 'arcsec'
+    assert result_wcs.wcs.dateobs == ''
+    assert result_wcs.heliographic_observer is None
+    assert result_wcs.rsun == frame.rsun
+
 
 def test_hgs_frame_to_wcs():
     frame = HeliographicStonyhurst(obstime='2013-10-28')
@@ -138,6 +151,16 @@ def test_hgs_frame_to_wcs():
     assert result_wcs.wcs.cunit[0] == 'deg'
     assert result_wcs.wcs.dateobs == '2013-10-28T00:00:00.000'
 
+    # Test a frame with no obstime
+    frame = HeliographicStonyhurst()
+    result_wcs = solar_frame_to_wcs_mapping(frame)
+
+    assert isinstance(result_wcs, WCS)
+
+    assert result_wcs.wcs.ctype[0] == 'HGLN-TAN'
+    assert result_wcs.wcs.cunit[0] == 'deg'
+    assert result_wcs.wcs.dateobs == ''
+
 
 def test_hgc_frame_to_wcs():
     frame = HeliographicCarrington(obstime='2013-10-28')
@@ -147,6 +170,17 @@ def test_hgc_frame_to_wcs():
 
     assert result_wcs.wcs.ctype[0] == 'CRLN-TAN'
     assert result_wcs.wcs.cunit[0] == 'deg'
+    assert result_wcs.wcs.dateobs == '2013-10-28T00:00:00.000'
+
+    # Test a frame with no obstime
+    frame = HeliographicCarrington()
+    result_wcs = solar_frame_to_wcs_mapping(frame)
+
+    assert isinstance(result_wcs, WCS)
+
+    assert result_wcs.wcs.ctype[0] == 'CRLN-TAN'
+    assert result_wcs.wcs.cunit[0] == 'deg'
+    assert result_wcs.wcs.dateobs == ''
 
 
 def test_hcc_frame_to_wcs():
@@ -156,3 +190,21 @@ def test_hcc_frame_to_wcs():
     assert isinstance(result_wcs, WCS)
 
     assert result_wcs.wcs.ctype[0] == 'SOLX'
+    assert result_wcs.wcs.dateobs == '2013-10-28T00:00:00.000'
+    assert isinstance(result_wcs.heliographic_observer, HeliographicStonyhurst)
+
+    # Test a frame with no obstime and no observer
+    frame = Heliocentric()
+    result_wcs = solar_frame_to_wcs_mapping(frame)
+
+    assert isinstance(result_wcs, WCS)
+
+    assert result_wcs.wcs.ctype[0] == 'SOLX'
+    assert result_wcs.wcs.dateobs == ''
+    assert result_wcs.heliographic_observer is None
+
+
+def test_non_sunpy_frame_to_wcs():
+    # For a non-SunPy frame, our mapping should return None
+    frame = BaseCoordinateFrame()
+    assert solar_frame_to_wcs_mapping(frame) is None