def graph_to_gdfs(
    G: nx.MultiGraph | nx.MultiDiGraph,
    *,
    nodes: bool = True,
    edges: bool = True,
    node_geometry: bool = True,
    fill_edge_geometry: bool = True,
) -> gpd.GeoDataFrame | tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]:
    """
    Convert a MultiGraph or MultiDiGraph to node and/or edge GeoDataFrames.

    This function is the inverse of `graph_from_gdfs`.

    Parameters
    ----------
    G
        Input graph.
    nodes
        If True, convert graph nodes to a GeoDataFrame and return it.
    edges
        If True, convert graph edges to a GeoDataFrame and return it.
    node_geometry
        If True, create a geometry column from node "x" and "y" attributes.
    fill_edge_geometry
        If True, fill missing edge geometry fields using endpoint nodes'
        coordinates to create a LineString.

    Returns
    -------
    gdf_nodes or gdf_edges or (gdf_nodes, gdf_edges)
        `gdf_nodes` is indexed by `osmid` and `gdf_edges` is multi-indexed by
        `(u, v, key)` following normal MultiGraph/MultiDiGraph structure.
    """
    crs = G.graph["crs"]

    if nodes:
        if len(G.nodes) == 0:  # pragma: no cover
            msg = "Graph contains no nodes."
            raise ValueError(msg)

        uvk, data = zip(*G.nodes(data=True))

        if node_geometry:
            # convert node x/y attributes to Points for geometry column
            node_geoms = (Point(d["x"], d["y"]) for d in data)
            gdf_nodes = gpd.GeoDataFrame(data, index=uvk, crs=crs, geometry=list(node_geoms))
        else:
            gdf_nodes = gpd.GeoDataFrame(data, index=uvk)

        gdf_nodes.index = gdf_nodes.index.rename("osmid")
        msg = "Created nodes GeoDataFrame from graph"
        utils.log(msg, level=lg.INFO)

    if edges:
        if len(G.edges) == 0:  # pragma: no cover
            msg = "Graph contains no edges."
            raise ValueError(msg)

        u, v, k, data = zip(*G.edges(keys=True, data=True))

        if fill_edge_geometry:
            node_coords = {n: (G.nodes[n]["x"], G.nodes[n]["y"]) for n in G}
            edge_geoms = (
                d.get("geometry", LineString((node_coords[u], node_coords[v])))
                for u, v, _, d in G.edges(keys=True, data=True)
            )
            gdf_edges = gpd.GeoDataFrame(data, crs=crs, geometry=list(edge_geoms))

        else:
            gdf_edges = gpd.GeoDataFrame(data)
            if "geometry" not in gdf_edges.columns:
                # if no edges have a geometry attribute, create null column
                gdf_edges = gdf_edges.set_geometry([None] * len(gdf_edges))
            gdf_edges = gdf_edges.set_crs(crs)

        # add u, v, key attributes as index
        gdf_edges["u"] = u
        gdf_edges["v"] = v
        gdf_edges["key"] = k
        gdf_edges = gdf_edges.set_index(["u", "v", "key"])

        msg = "Created edges GeoDataFrame from graph"
        utils.log(msg, level=lg.INFO)

    if nodes and edges:
        return gdf_nodes, gdf_edges

    if nodes:
        return gdf_nodes

    if edges:
        return gdf_edges

    # otherwise
    msg = "You must request nodes or edges or both."
    raise ValueError(msg)

    Convert a MultiGraph or MultiDiGraph to node and/or edge GeoDataFrames.

    This function is the inverse of `graph_from_gdfs`.

    Parameters
    ----------
    G
        Input graph.
    nodes
        If True, convert graph nodes to a GeoDataFrame and return it.
    edges
        If True, convert graph edges to a GeoDataFrame and return it.
    node_geometry
        If True, create a geometry column from node "x" and "y" attributes.
    fill_edge_geometry
        If True, fill missing edge geometry fields using endpoint nodes'
        coordinates to create a LineString.

    Returns
    -------
    gdf_nodes or gdf_edges or (gdf_nodes, gdf_edges)
        `gdf_nodes` is indexed by `osmid` and `gdf_edges` is multi-indexed by
        `(u, v, key)` following normal MultiGraph/MultiDiGraph structure.
    
osmnx/convert.py