
import astropy.units as u
import astropy.wcs.utils
from astropy.coordinates import BaseCoordinateFrame, SkyCoord
from astropy.wcs import WCS
from astropy.wcs.utils import obsgeo_to_frame

from sunpy import log
from .frames import (
    Heliocentric,
    HeliographicCarrington,
    HeliographicStonyhurst,
    Helioprojective,
    SunPyBaseCoordinateFrame,
)

__all__ = ['solar_wcs_frame_mapping', 'solar_frame_to_wcs_mapping']


def solar_wcs_frame_mapping(wcs):
    """
    This function registers the coordinates frames to their FITS-WCS coordinate
    type values in the `astropy.wcs.utils.wcs_to_celestial_frame` registry.

    Parameters
    ----------
    wcs : astropy.wcs.WCS

    Returns
    -------
    astropy.coordinates.BaseCoordinateFrame
    """

    if hasattr(wcs, "coordinate_frame"):
        return wcs.coordinate_frame

    dateobs = wcs.wcs.dateavg or wcs.wcs.dateobs or None

    # Get observer coordinate from the WCS auxiliary information
    # Note: the order of the entries is important, as it determines which set
    # of header keys is given priority below. Stonyhurst should usually be
    # prioritized, as it is defined more consistently across implementations,
    # and so it should occur before Carrington here.
    required_attrs = {HeliographicStonyhurst: ['hgln_obs', 'hglt_obs', 'dsun_obs'],
                      HeliographicCarrington: ['crln_obs', 'hglt_obs', 'dsun_obs']}

    # Get rsun from the WCS auxiliary information
    rsun = wcs.wcs.aux.rsun_ref
    if rsun is not None:
        rsun *= u.m

    # TODO: remove these errors in sunpy 4.1
    bad_attrs = [f'.{attr}' for attr in ['rsun', 'heliographic_observer']
                 if hasattr(wcs, attr)]
    if len(bad_attrs):
        raise ValueError(f"The {' and '.join(bad_attrs)} attribute(s) on a WCS "
                         "are no longer supported.")

    observer = None
    for frame, attr_names in required_attrs.items():
        attrs = [getattr(wcs.wcs.aux, attr_name) for attr_name in attr_names]
        if all([attr is not None for attr in attrs]):
            kwargs = {'obstime': dateobs}
            if rsun is not None:
                kwargs['rsun'] = rsun
            if issubclass(frame, HeliographicCarrington):
                kwargs['observer'] = 'self'

            observer = frame(attrs[0] * u.deg,
                             attrs[1] * u.deg,
                             attrs[2] * u.m,
                             **kwargs)
            break

    # Read the observer out of obsgeo for ground based observers
    if observer is None:
        try:
            observer = obsgeo_to_frame(wcs.wcs.obsgeo, dateobs)
            observer = SkyCoord(observer, rsun=rsun)
        except ValueError as e:
            # The helper function assumes you know the obsgeo coords you are
            # parsing are good, we are not sure, so catch the error.

            # This approach could lead to an invalid observer (i.e. one of the
            # coords being NaN), but only if the WCS has been constructed like that.
            log.debug(f"Could not parse obsgeo coordinates from WCS:\n{e}")

    # Collect all of the possible frame attributes, although some may be removed later
    frame_args = {'obstime': dateobs}
    if observer is not None:
        frame_args['observer'] = observer
    if rsun is not None:
        frame_args['rsun'] = rsun

    frame_class = _sunpy_frame_class_from_ctypes(wcs.wcs.ctype)

    if frame_class:
        if frame_class == HeliographicStonyhurst:
            frame_args.pop('observer', None)
        if frame_class == Heliocentric:
            frame_args.pop('rsun', None)

        return frame_class(**frame_args)


def _sunpy_frame_class_from_ctypes(ctypes):
    # Truncate the ctype to the first four letters
    ctypes = {c[:4] for c in ctypes}

    mapping = {
        Helioprojective: {'HPLN', 'HPLT'},
        HeliographicStonyhurst: {'HGLN', 'HGLT'},
        HeliographicCarrington: {'CRLN', 'CRLT'},
        Heliocentric: {'SOLX', 'SOLY'},
    }

    for frame_class, ctype_pair in mapping.items():
        if ctype_pair <= ctypes:
            return frame_class


def _set_wcs_aux_obs_coord(wcs, obs_frame):
    """
    Set (in-place) observer coordinate information on a WCS.

    Parameters
    ----------
    wcs : astropy.wcs.WCS
    obs_frame : astropy.coordinates.SkyCoord, astropy.coordinates.CoordinateFrame
    """
    # Sometimes obs_coord can be a SkyCoord, so convert down to a frame
    if hasattr(obs_frame, 'frame'):
        obs_frame = obs_frame.frame

    if isinstance(obs_frame, HeliographicStonyhurst):
        wcs.wcs.aux.hgln_obs = obs_frame.lon.to_value(u.deg)
    elif isinstance(obs_frame, HeliographicCarrington):
        wcs.wcs.aux.crln_obs = obs_frame.lon.to_value(u.deg)
    else:
        raise ValueError('obs_coord must be in a Stonyhurst or Carrington frame')
    # These two keywords are the same for Carrington and Stonyhurst
    wcs.wcs.aux.hglt_obs = obs_frame.lat.to_value(u.deg)
    wcs.wcs.aux.dsun_obs = obs_frame.radius.to_value(u.m)


def solar_frame_to_wcs_mapping(frame, projection='TAN'):
    """
    For a given frame, this function returns the corresponding WCS object.
    It registers the WCS coordinates types from their associated frame in the
    `astropy.wcs.utils.celestial_frame_to_wcs` registry.

    Parameters
    ----------
    frame : astropy.coordinates.BaseCoordinateFrame
    projection : str, optional

    Returns
    -------
    astropy.wcs.WCS
    """
    wcs = WCS(naxis=2)

    if hasattr(frame, 'rsun'):
        wcs.wcs.aux.rsun_ref = frame.rsun.to_value(u.m)

    if hasattr(frame, 'observer') and frame.observer is not None:
        if isinstance(frame.observer, BaseCoordinateFrame):
            observer = frame.observer
        elif frame.observer == 'self':
            observer = frame
        _set_wcs_aux_obs_coord(wcs, observer)

    if isinstance(frame, SunPyBaseCoordinateFrame):

        if frame.obstime:
            wcs.wcs.dateobs = frame.obstime.utc.isot

        if isinstance(frame, Helioprojective):
            xcoord = 'HPLN' + '-' + projection
            ycoord = 'HPLT' + '-' + projection
            wcs.wcs.cunit = ['arcsec', 'arcsec']
        elif isinstance(frame, Heliocentric):
            xcoord = 'SOLX'
            ycoord = 'SOLY'
            wcs.wcs.cunit = ['deg', 'deg']
        elif isinstance(frame, HeliographicCarrington):
            xcoord = 'CRLN' + '-' + projection
            ycoord = 'CRLT' + '-' + projection
            wcs.wcs.cunit = ['deg', 'deg']
        elif isinstance(frame, HeliographicStonyhurst):
            xcoord = 'HGLN' + '-' + projection
            ycoord = 'HGLT' + '-' + projection
            wcs.wcs.cunit = ['deg', 'deg']

    else:
        return None

    wcs.wcs.ctype = [xcoord, ycoord]

    return wcs


astropy.wcs.utils.WCS_FRAME_MAPPINGS.append([solar_wcs_frame_mapping])
astropy.wcs.utils.FRAME_WCS_MAPPINGS.append([solar_frame_to_wcs_mapping])

sunpy/coordinates/wcs_utils.py

import astropy.units as u
import astropy.wcs.utils
from astropy.coordinates import BaseCoordinateFrame, SkyCoord
from astropy.wcs import WCS
from astropy.wcs.utils import obsgeo_to_frame

from sunpy import log
from .frames import (
    Heliocentric,
    HeliographicCarrington,
    HeliographicStonyhurst,
    Helioprojective,
    SunPyBaseCoordinateFrame,
)

__all__ = ['solar_wcs_frame_mapping', 'solar_frame_to_wcs_mapping']


def solar_wcs_frame_mapping(wcs):
    """
    This function registers the coordinates frames to their FITS-WCS coordinate
    type values in the `astropy.wcs.utils.wcs_to_celestial_frame` registry.

    Parameters
    ----------
    wcs : astropy.wcs.WCS

    Returns
    -------
    astropy.coordinates.BaseCoordinateFrame
    """

    if hasattr(wcs, "coordinate_frame"):
        return wcs.coordinate_frame

    dateobs = wcs.wcs.dateavg or wcs.wcs.dateobs or None

    # Get observer coordinate from the WCS auxiliary information
    # Note: the order of the entries is important, as it determines which set
    # of header keys is given priority below. Stonyhurst should usually be
    # prioritized, as it is defined more consistently across implementations,
    # and so it should occur before Carrington here.
    required_attrs = {HeliographicStonyhurst: ['hgln_obs', 'hglt_obs', 'dsun_obs'],
                      HeliographicCarrington: ['crln_obs', 'hglt_obs', 'dsun_obs']}

    # Get rsun from the WCS auxiliary information
    rsun = wcs.wcs.aux.rsun_ref
    if rsun is not None:
        rsun *= u.m

    # TODO: remove these errors in sunpy 4.1
    bad_attrs = [f'.{attr}' for attr in ['rsun', 'heliographic_observer']
                 if hasattr(wcs, attr)]
    if len(bad_attrs):
        raise ValueError(f"The {' and '.join(bad_attrs)} attribute(s) on a WCS "
                         "are no longer supported.")

    observer = None
    for frame, attr_names in required_attrs.items():
        attrs = [getattr(wcs.wcs.aux, attr_name) for attr_name in attr_names]
        if all([attr is not None for attr in attrs]):
            kwargs = {'obstime': dateobs}
            if rsun is not None:
                kwargs['rsun'] = rsun
            if issubclass(frame, HeliographicCarrington):
                kwargs['observer'] = 'self'

            observer = frame(attrs[0] * u.deg,
                             attrs[1] * u.deg,
                             attrs[2] * u.m,
                             **kwargs)
            break

    # Read the observer out of obsgeo for ground based observers
    if observer is None:
        try:
            observer = obsgeo_to_frame(wcs.wcs.obsgeo, dateobs)
            observer = SkyCoord(observer, rsun=rsun)
        except ValueError as e:
            # The helper function assumes you know the obsgeo coords you are
            # parsing are good, we are not sure, so catch the error.

            # This approach could lead to an invalid observer (i.e. one of the
            # coords being NaN), but only if the WCS has been constructed like that.
            log.debug(f"Could not parse obsgeo coordinates from WCS:\n{e}")

    # Collect all of the possible frame attributes, although some may be removed later
    frame_args = {'obstime': dateobs}
    if observer is not None:
        frame_args['observer'] = observer
    if rsun is not None:
        frame_args['rsun'] = rsun

    frame_class = _sunpy_frame_class_from_ctypes(wcs.wcs.ctype)

    if frame_class:
        if frame_class == HeliographicStonyhurst:
            frame_args.pop('observer', None)
        if frame_class == Heliocentric:
            frame_args.pop('rsun', None)

        return frame_class(**frame_args)


def _sunpy_frame_class_from_ctypes(ctypes):
    # Truncate the ctype to the first four letters
    ctypes = {c[:4] for c in ctypes}

    mapping = {
        Helioprojective: {'HPLN', 'HPLT'},
        HeliographicStonyhurst: {'HGLN', 'HGLT'},
        HeliographicCarrington: {'CRLN', 'CRLT'},
        Heliocentric: {'SOLX', 'SOLY'},
    }

    for frame_class, ctype_pair in mapping.items():
        if ctype_pair <= ctypes:
            return frame_class


def _set_wcs_aux_obs_coord(wcs, obs_frame):
    """
    Set (in-place) observer coordinate information on a WCS.

    Parameters
    ----------
    wcs : astropy.wcs.WCS
    obs_frame : astropy.coordinates.SkyCoord, astropy.coordinates.CoordinateFrame
    """
    # Sometimes obs_coord can be a SkyCoord, so convert down to a frame
    if hasattr(obs_frame, 'frame'):
        obs_frame = obs_frame.frame

    if isinstance(obs_frame, HeliographicStonyhurst):
        wcs.wcs.aux.hgln_obs = obs_frame.lon.to_value(u.deg)
    elif isinstance(obs_frame, HeliographicCarrington):
        wcs.wcs.aux.crln_obs = obs_frame.lon.to_value(u.deg)
    else:
        raise ValueError('obs_coord must be in a Stonyhurst or Carrington frame')
    # These two keywords are the same for Carrington and Stonyhurst
    wcs.wcs.aux.hglt_obs = obs_frame.lat.to_value(u.deg)
    wcs.wcs.aux.dsun_obs = obs_frame.radius.to_value(u.m)


def solar_frame_to_wcs_mapping(frame, projection='TAN'):
    """
    For a given frame, this function returns the corresponding WCS object.
    It registers the WCS coordinates types from their associated frame in the
    `astropy.wcs.utils.celestial_frame_to_wcs` registry.

    Parameters
    ----------
    frame : astropy.coordinates.BaseCoordinateFrame
    projection : str, optional

    Returns
    -------
    astropy.wcs.WCS
    """
    wcs = WCS(naxis=2)

    if hasattr(frame, 'rsun'):
        wcs.wcs.aux.rsun_ref = frame.rsun.to_value(u.m)

    if hasattr(frame, 'observer') and frame.observer is not None:
        if isinstance(frame.observer, BaseCoordinateFrame):
            observer = frame.observer
        elif frame.observer == 'self':
            observer = frame
        _set_wcs_aux_obs_coord(wcs, observer)

    if isinstance(frame, SunPyBaseCoordinateFrame):

        if frame.obstime:
            wcs.wcs.dateobs = frame.obstime.utc.isot

        if isinstance(frame, Helioprojective):
            xcoord = 'HPLN' + '-' + projection
            ycoord = 'HPLT' + '-' + projection
            wcs.wcs.cunit = ['arcsec', 'arcsec']
        elif isinstance(frame, Heliocentric):
            xcoord = 'SOLX'
            ycoord = 'SOLY'
            wcs.wcs.cunit = ['deg', 'deg']
        elif isinstance(frame, HeliographicCarrington):
            xcoord = 'CRLN' + '-' + projection
            ycoord = 'CRLT' + '-' + projection
            wcs.wcs.cunit = ['deg', 'deg']
        elif isinstance(frame, HeliographicStonyhurst):
            xcoord = 'HGLN' + '-' + projection
            ycoord = 'HGLT' + '-' + projection
            wcs.wcs.cunit = ['deg', 'deg']
        else:
            # A subclass not supported by the core library
            return None

    else:
        return None

    wcs.wcs.ctype = [xcoord, ycoord]

    return wcs


astropy.wcs.utils.WCS_FRAME_MAPPINGS.append([solar_wcs_frame_mapping])
astropy.wcs.utils.FRAME_WCS_MAPPINGS.append([solar_frame_to_wcs_mapping])

sunpy/coordinates/wcs_utils.py
diff --git a/changelog/7594.bugfix.rst b/changelog/7594.bugfix.rst
new file mode 100644
index 00000000000..804869e7149
--- /dev/null
+++ b/changelog/7594.bugfix.rst
@@ -0,0 +1 @@
+Fixed a bug that interfered with :func:`astropy.wcs.utils.celestial_frame_to_wcs` when working with a custom subclass of :class:`~sunpy.coordinates.frames.SunPyBaseCoordinateFrame`.
diff --git a/sunpy/coordinates/tests/test_wcs_utils.py b/sunpy/coordinates/tests/test_wcs_utils.py
index 1a8e9ff20f3..dacdc942f28 100644
--- a/sunpy/coordinates/tests/test_wcs_utils.py
+++ b/sunpy/coordinates/tests/test_wcs_utils.py
@@ -15,6 +15,7 @@
     HeliographicCarrington,
     HeliographicStonyhurst,
     Helioprojective,
+    SunPyBaseCoordinateFrame,
 )
 from sunpy.coordinates.wcs_utils import (
     _set_wcs_aux_obs_coord,
@@ -379,3 +380,11 @@ def test_observer_hgln_crln_priority():
     # Note: don't test whether crlt or hglt is used---according to
     # _set_wcs_aux_obs_coord, those are expected to always be the same and so
     # the same one is always used
+
+
+def test_sunpybaseframe_external():
+    class MyFrame(SunPyBaseCoordinateFrame):
+        pass
+
+    out = solar_frame_to_wcs_mapping(MyFrame())
+    assert out is None
diff --git a/sunpy/coordinates/wcs_utils.py b/sunpy/coordinates/wcs_utils.py
index 08764436418..c2ffcce2232 100644
--- a/sunpy/coordinates/wcs_utils.py
+++ b/sunpy/coordinates/wcs_utils.py
@@ -191,6 +191,9 @@ def solar_frame_to_wcs_mapping(frame, projection='TAN'):
             xcoord = 'HGLN' + '-' + projection
             ycoord = 'HGLT' + '-' + projection
             wcs.wcs.cunit = ['deg', 'deg']
+        else:
+            # A subclass not supported by the core library
+            return None
 
     else:
         return None

Support the use of SunPyBaseCoordinateFrame outside of core library
### Describe the feature

* It would be useful to be able to use `SunPyBaseCoordinateFrame` outside of sunpy but currently doing so is alkward as the `frame_to_wcs` and `wcs_to_frame` mappings are hardcode to the current frames / wcs' and raise an error you you try to do so
* The name clearly implies this is SunPy specific but there a lot of boiler plate code that would have to be copied to provide the same thing.

At the moment to make this work you have to make sure what ever frames are added are added before the sunpy function e.g.

`astropy.wcs.utils.FRAME_WCS_MAPPINGS.insert(1, [my_wcs_to_frame])`

### Proposed solution

Don't raise an error the if the frame is a subclass of `SunPyBaseCoordinateFrame` but not one of the frame implemented in sunpy return `None` 

Specifically L198 raise an error as xcoord and xcoord are undefined
https://github.com/sunpy/sunpy/blob/2281a2198997e8671efd48dcb531b07a98b86ddf/sunpy/coordinates/wcs_utils.py#L173-L198

A simple solution would be to an a final else clause to this if Elias
```python
        elif isinstance(frame, HeliographicStonyhurst):
            xcoord = 'HGLN' + '-' + projection
            ycoord = 'HGLT' + '-' + projection
            wcs.wcs.cunit = ['deg', 'deg']
        else
            return None

```
