Repository: geopandas/geopandas

Issue:
IO: reading a CSV gives GeoDataFrame with fiona but DataFrame with pyogrio
I've just noticed one inconsistency in IO engines. Reading a CSV without geometry gives different results depending on the engine.

```py
import geopandas as gpd
import geodatasets


df = gpd.read_file(geodatasets.get_path("geoda.cars"), engine="fiona")
print(type(df))
print(df.head())
```
```
<class 'geopandas.geodataframe.GeoDataFrame'>
  Creation Date           Status  ...                                 Location geometry
0    01/01/2011  Completed - Dup  ...  (41.983680361597564, -87.7319663736746)     None
1    01/01/2011  Completed - Dup  ...  (41.983680361597564, -87.7319663736746)     None
2    01/01/2011  Completed - Dup  ...  (41.91858774162382, -87.73868431751842)     None
3    01/01/2011  Completed - Dup  ...  (41.95860696269331, -87.64887590959788)     None
4    01/01/2011  Completed - Dup  ...  (41.95860696269331, -87.64887590959788)     None

[5 rows x 22 columns]
```

With fiona you get a GeoDataFrame with `None` in `geometry`.

```py
df = gpd.read_file(geodatasets.get_path("geoda.cars"), engine="pyogrio")
print(type(df))
print(df.head())
```
```
<class 'pandas.core.frame.DataFrame'>
  Creation Date           Status  ...           Longitude                                 Location
0    01/01/2011  Completed - Dup  ...   -87.7319663736746  (41.983680361597564, -87.7319663736746)
1    01/01/2011  Completed - Dup  ...   -87.7319663736746  (41.983680361597564, -87.7319663736746)
2    01/01/2011  Completed - Dup  ...  -87.73868431751842  (41.91858774162382, -87.73868431751842)
3    01/01/2011  Completed - Dup  ...  -87.64887590959788  (41.95860696269331, -87.64887590959788)
4    01/01/2011  Completed - Dup  ...  -87.64887590959788  (41.95860696269331, -87.64887590959788)

[5 rows x 21 columns]
```

With pyogrio it is a DataFrame without any geometry column.

I am not sure what the ideal behaviour shall be but it probably should be consistent.


Discussion:
One could argue that if there is no geometry, it should be a pandas DataFrame instead of a GeoDataFrame, since none of the GeoDataFrame specific attributes / methods will return useful results.  The Fiona engine supports passing `ignore_geometry`, which then causes it to return a DataFrame.  It would probably be best to do the same thing when there is no geometry present in the data source.

It looks like the schema available from Fiona returns `{..., 'geometry': 'None'}` which could be used to detect the absence of a geometry column.


I tend to agree that we should return a DataFrame in this case. It is probably a good corner case that can be broken by 1.0.
I'm also in favour of returning a dataframe. As an aside on static typing, I think we would still want to indicate read file returns a geodataframe and not a union type, it would be rather annoying with mypy to have to cast every use of read file. ( and those wanting faithful annotations in this case could opt into ignore geometry)

Resolving patch:
diff --git a/geopandas/io/file.py b/geopandas/io/file.py
--- a/geopandas/io/file.py
+++ b/geopandas/io/file.py
@@ -387,7 +387,10 @@ def _read_file_fiona(
             datetime_fields = [
                 k for (k, v) in features.schema["properties"].items() if v == "datetime"
             ]
-            if kwargs.get("ignore_geometry", False):
+            if (
+                kwargs.get("ignore_geometry", False)
+                or features.schema["geometry"] == "None"
+            ):
                 df = pd.DataFrame(
                     [record["properties"] for record in f_filt], columns=columns
                 )


Test patch:
diff --git a/geopandas/io/tests/test_file.py b/geopandas/io/tests/test_file.py
--- a/geopandas/io/tests/test_file.py
+++ b/geopandas/io/tests/test_file.py
@@ -11,7 +11,7 @@
 import pytz
 from packaging.version import Version
 from pandas.api.types import is_datetime64_any_dtype
-from pandas.testing import assert_series_equal
+from pandas.testing import assert_series_equal, assert_frame_equal
 from shapely.geometry import Point, Polygon, box
 
 import geopandas
@@ -799,6 +799,24 @@ def test_read_file__ignore_all_fields(engine):
     assert gdf.columns.tolist() == ["geometry"]
 
 
+def test_read_file_missing_geometry(tmpdir, engine):
+    filename = str(tmpdir / "test.csv")
+
+    expected = pd.DataFrame(
+        {"col1": np.array([1, 2, 3], dtype="int64"), "col2": ["a", "b", "c"]}
+    )
+    expected.to_csv(filename, index=False)
+
+    df = geopandas.read_file(filename, engine=engine)
+    # both engines read integers as strings; force back to original type
+    df["col1"] = df["col1"].astype("int64")
+
+    assert isinstance(df, pd.DataFrame)
+    assert not isinstance(df, geopandas.GeoDataFrame)
+
+    assert_frame_equal(df, expected)
+
+
 def test_read_file__where_filter(engine):
     if FIONA_GE_19 or engine == "pyogrio":
         gdf = geopandas.read_file(