def graph_from_gdfs(
    gdf_nodes: gpd.GeoDataFrame,
    gdf_edges: gpd.GeoDataFrame,
    *,
    graph_attrs: dict[str, Any] | None = None,
) -> nx.MultiDiGraph:
    """
    Convert node and edge GeoDataFrames to a MultiDiGraph.

    This function is the inverse of `graph_to_gdfs` and is designed to work in
    conjunction with it. However, you can convert arbitrary node and edge
    GeoDataFrames as long as 1) `gdf_nodes` is uniquely indexed by `osmid`, 2)
    `gdf_nodes` contains `x` and `y` coordinate columns representing node
    geometries, 3) `gdf_edges` is uniquely multi-indexed by `(u, v, key)`
    (following normal MultiDiGraph structure). This allows you to load any
    node/edge Shapefiles or GeoPackage layers as GeoDataFrames then convert
    them to a MultiDiGraph for network analysis.

    Note that any `geometry` attribute on `gdf_nodes` is discarded, since `x`
    and `y` provide the necessary node geometry information instead.

    Parameters
    ----------
    gdf_nodes
        GeoDataFrame of graph nodes uniquely indexed by `osmid`.
    gdf_edges
        GeoDataFrame of graph edges uniquely multi-indexed by `(u, v, key)`.
    graph_attrs
        The new `G.graph` attribute dictionary. If None, use `gdf_edges`'s CRS
        as the only graph-level attribute (`gdf_edges` must have its `crs`
        attribute set).

    Returns
    -------
    G
        The converted MultiDiGraph.
    """
    _validate_node_edge_gdfs(gdf_nodes, gdf_edges)

    # drop geometry column from gdf_nodes (since we use x and y for geometry
    # information), but warn the user if the geometry values differ from the
    # coordinates in the x and y columns. this results in a df instead of gdf.
    if gdf_nodes.active_geometry_name is None:  # pragma: no cover
        df_nodes = pd.DataFrame(gdf_nodes)
    else:
        df_nodes = gdf_nodes.drop(columns=gdf_nodes.active_geometry_name)

    # create graph and add graph-level attribute dict
    if graph_attrs is None:
        graph_attrs = {"crs": gdf_edges.crs}
    G = nx.MultiDiGraph(**graph_attrs)

    # add edges and their attributes to graph, but filter out null attribute
    # values so that edges only get attributes with non-null values
    attr_names = gdf_edges.columns.to_list()
    for (u, v, k), attr_vals in zip(gdf_edges.index, gdf_edges.to_numpy()):
        data_all = zip(attr_names, attr_vals)
        data = {name: val for name, val in data_all if isinstance(val, list) or pd.notna(val)}
        G.add_edge(u, v, key=k, **data)

    # add any nodes with no incident edges, since they wouldn't be added above
    G.add_nodes_from(set(df_nodes.index) - set(G.nodes))

    # now all nodes are added, so set nodes' attributes
    for col in df_nodes.columns:
        nx.set_node_attributes(G, name=col, values=df_nodes[col].dropna())

    msg = "Created graph from node/edge GeoDataFrames"
    utils.log(msg, level=lg.INFO)
    return G

    Convert node and edge GeoDataFrames to a MultiDiGraph.

    This function is the inverse of `graph_to_gdfs` and is designed to work in
    conjunction with it. However, you can convert arbitrary node and edge
    GeoDataFrames as long as 1) `gdf_nodes` is uniquely indexed by `osmid`, 2)
    `gdf_nodes` contains `x` and `y` coordinate columns representing node
    geometries, 3) `gdf_edges` is uniquely multi-indexed by `(u, v, key)`
    (following normal MultiDiGraph structure). This allows you to load any
    node/edge Shapefiles or GeoPackage layers as GeoDataFrames then convert
    them to a MultiDiGraph for network analysis.

    Note that any `geometry` attribute on `gdf_nodes` is discarded, since `x`
    and `y` provide the necessary node geometry information instead.

    Parameters
    ----------
    gdf_nodes
        GeoDataFrame of graph nodes uniquely indexed by `osmid`.
    gdf_edges
        GeoDataFrame of graph edges uniquely multi-indexed by `(u, v, key)`.
    graph_attrs
        The new `G.graph` attribute dictionary. If None, use `gdf_edges`'s CRS
        as the only graph-level attribute (`gdf_edges` must have its `crs`
        attribute set).

    Returns
    -------
    G
        The converted MultiDiGraph.
    
osmnx/convert.py