Repository: geopandas/geopandas

Issue:
ENH: geopandas explore() ability to customise style_function with geojson attributes
#### Is your feature request related to a problem?

`explore()` provide **style_kwds** to pass constants to **style_function**.  There is no ability to change behaviour of **style_function** to use *geojson* properties

 
#### Describe the solution you'd like

Extend **style_kwds** parameter such that *geojson* properties can be passed and used in internally created style functions.

#### API breaking implications

None

#### Describe alternatives you've considered

Have used monkey patching to work around.  Details in python section.

#### Additional context

Would like additional input, would like input ideas before creating a PR to implement a solution to this enhancement

```python
import io
import pandas as pd
import geopandas as gpd
import shapely
import folium
import wrapt

df = pd.read_csv(
    io.StringIO(
        """|             geometry        | count |
|   POINT (6.92334 50.91695)  |   10  |
|   POINT (6.91970 50.93167)  |    8  |
|   POINT (6.96946 50.91469)  |    2  |
|   POINT (6.96266 50.94713)  |   72  |
|   POINT (6.93751 50.93832)  |  267  |"""
    ),
    sep="|",
).pipe(
    lambda d: d.drop(columns=[c for c in d.columns if "Unnamed" in c]).rename(
        columns={c: c.strip() for c in d.columns}
    )
)

# wanna intercept geopandas.explore() style function so can set size too
@wrapt.patch_function_wrapper(folium, "GeoJson")
def new_style(wrapped, instance, args, kwargs):
    def style_fn(x):
        return {
            "fillColor": x["properties"]["__folium_color"],
            "color": x["properties"]["__folium_color"],
            "radius": x["properties"]["size"],
            "fillOpacity":.8
        }

    if "_style_column" in str(kwargs["style_function"]):
        kwargs["style_function"] = style_fn
    return wrapped(*args, **kwargs)


gdf = gpd.GeoDataFrame(
    df, geometry=df["geometry"].apply(shapely.wkt.loads), crs="epsg:4326"
)
# normalize sizes...
gdf["size"] = gdf["count"].rank() * 20

gdf.explore(cmap="Reds", column="count")


```



Discussion:
rraymondgh: replaces another pull request where commit history was messed up.  

Example:
```
import geopandas as gpd
import numpy as np

world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres"))
world["geometry"] = world["geometry"].centroid
world["bubble_size"] = world["gdp_md_est"].rank() / len(world) * 9
world["border_color"] = np.random.choice(["red", "green", "blue"], len(world))

world.loc[
    world["continent"].eq("Europe") & ~world["iso_a3"].isin(["RUS", "-99"])
].explore(
    column="gdp_md_est",
    height=300,
    width=600,
    style_kwds={
        "style_function": lambda x: {
            "radius": x["properties"]["bubble_size"],
            "color": x["properties"]["border_color"],
        }
    },
)
```

<img width="616" alt="image" src="https://user-images.githubusercontent.com/42769112/158030604-a63f4781-18c9-487f-8d63-622fe07ce87c.png">


Martin Fleischmann <notifications@github.com>
> Thanks!
> 
> Can you clean up this PR to have diff showing only your commits? It now includes some that are already merged.
> I am thinking about the best API here. Because when you pass a style function, you should probably override everything else, instead of trying to merge explicit args with the function and waiting for issues to happen. So I would maybe go for the API like this:
> ```
> world.explore(
>     style_kwds=lambda x: {
>             "fillColor": "red" if x["properties"]["gdp_md_est"] < 10 ** 6 else "green",
>             "color": "black" if x["properties"]["gdp_md_est"] < 10 ** 6 else "white",
>         }
> )
> ```
> Instead of passing the function as one of the items in a style_kwds dictionary, you would pass it directly. And you would take it and send it to folium as it is, overriding every other kwarg. This needs to be properly documented but it should be much more error-resistant than current solution mixing it all together. One direct issue is that while we use internally lambda x, user may use something like lambda row and then it all breaks.
> 

I actually think flexibility of `explore()` style functions extended with a style function is more useful.   The work to create colors in `explore()` is useful and best not removed by replacing instead of extending.  Additionally `lambda x:` or `;lambda row:` will not cause issues as this is just standard **python** scoping

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 0ec40998..22f14602 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,12 +1,12 @@
 files: 'geopandas\/'
 repos:
--   repo: https://github.com/python/black
-    rev: 20.8b1
-    hooks:
-    -   id: black
-        language_version: python3
--   repo: https://gitlab.com/pycqa/flake8
-    rev: 3.8.3
-    hooks:
-    -   id: flake8
-        language: python_venv
+    - repo: https://github.com/psf/black
+      rev: 22.3.0
+      hooks:
+          - id: black
+            language_version: python3
+    - repo: https://gitlab.com/pycqa/flake8
+      rev: 3.8.3
+      hooks:
+          - id: flake8
+            language: python_venv
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 40fecd6b..76bbf2fb 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -13,7 +13,6 @@ In general, GeoPandas follows the conventions of the pandas project
 where applicable. Please read the [contributing
 guidelines](https://geopandas.readthedocs.io/en/latest/community/contributing.html).
 
-
 In particular, when submitting a pull request:
 
 - Install the requirements for the development environment (one can do this
@@ -39,7 +38,7 @@ is a great way to get started if you'd like to make a contribution.
 Style
 -----
 
-- GeoPandas supports Python 3.7+ only. The last version of GeoPandas
+- GeoPandas supports Python 3.8+ only. The last version of GeoPandas
   supporting Python 2 is 0.6.
 
 - GeoPandas follows [the PEP 8
diff --git a/README.md b/README.md
index b66c11f2..3fe6cd42 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,12 @@
-GeoPandas [![Actions Status](https://github.com/geopandas/geopandas/workflows/Tests/badge.svg)](https://github.com/geopandas/geopandas/actions?query=workflow%3ATests) [![Coverage Status](https://codecov.io/gh/geopandas/geopandas/branch/main/graph/badge.svg)](https://codecov.io/gh/geopandas/geopandas) [![Join the chat at https://gitter.im/geopandas/geopandas](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/geopandas/geopandas?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Binder](https://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/geopandas/geopandas/main) [![DOI](https://zenodo.org/badge/11002815.svg)](https://zenodo.org/badge/latestdoi/11002815)
-=========
+[![pypi](https://img.shields.io/pypi/v/geopandas.svg)](https://pypi.python.org/pypi/geopandas/)
+[![Actions Status](https://github.com/geopandas/geopandas/workflows/Tests/badge.svg)](https://github.com/geopandas/geopandas/actions?query=workflow%3ATests)
+[![Coverage Status](https://codecov.io/gh/geopandas/geopandas/branch/main/graph/badge.svg)](https://codecov.io/gh/geopandas/geopandas)
+[![Join the chat at https://gitter.im/geopandas/geopandas](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/geopandas/geopandas?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[![Binder](https://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/geopandas/geopandas/main)
+[![DOI](https://zenodo.org/badge/11002815.svg)](https://zenodo.org/badge/latestdoi/11002815)
+
+GeoPandas
+---------
 
 Python tools for geographic data
 
diff --git a/ci/envs/37-minimal.yaml b/ci/envs/38-minimal.yaml
similarity index 96%
rename from ci/envs/37-minimal.yaml
rename to ci/envs/38-minimal.yaml
index 9df7b6df..b697c63c 100644
--- a/ci/envs/37-minimal.yaml
+++ b/ci/envs/38-minimal.yaml
@@ -3,7 +3,7 @@ channels:
   - defaults
   - conda-forge
 dependencies:
-  - python=3.7
+  - python=3.8
   # required
   - numpy=1.18
   - pandas==1.0.5
diff --git a/ci/envs/37-pd11.yaml b/ci/envs/38-pd11-defaults.yaml
similarity index 96%
rename from ci/envs/37-pd11.yaml
rename to ci/envs/38-pd11-defaults.yaml
index a58c7ec5..eb524c2b 100644
--- a/ci/envs/37-pd11.yaml
+++ b/ci/envs/38-pd11-defaults.yaml
@@ -2,7 +2,7 @@ name: test
 channels:
   - defaults
 dependencies:
-  - python=3.7
+  - python=3.8
   # required
   - pandas=1.1
   - shapely
diff --git a/ci/envs/38-no-optional-deps.yaml b/ci/envs/39-no-optional-deps.yaml
similarity index 92%
rename from ci/envs/38-no-optional-deps.yaml
rename to ci/envs/39-no-optional-deps.yaml
index 7261cc6f..cb6f036a 100644
--- a/ci/envs/38-no-optional-deps.yaml
+++ b/ci/envs/39-no-optional-deps.yaml
@@ -2,7 +2,7 @@ name: test
 channels:
   - conda-forge
 dependencies:
-  - python=3.8
+  - python=3.9
   # required
   - pandas
   - shapely
diff --git a/doc/source/community/contributing.rst b/doc/source/community/contributing.rst
index 21444db8..1eb9520e 100644
--- a/doc/source/community/contributing.rst
+++ b/doc/source/community/contributing.rst
@@ -44,7 +44,7 @@ In particular, when submitting a pull request:
   imports when possible, and explicit relative imports for local
   imports when necessary in tests.
 
-- GeoPandas supports Python 3.7+ only. The last version of GeoPandas
+- GeoPandas supports Python 3.8+ only. The last version of GeoPandas
   supporting Python 2 is 0.6.
 
 
diff --git a/geopandas/_compat.py b/geopandas/_compat.py
index ab6edc24..8a68cdb9 100644
--- a/geopandas/_compat.py
+++ b/geopandas/_compat.py
@@ -149,7 +149,6 @@ if shapely_warning is not None and not SHAPELY_GE_20:
             )
             yield
 
-
 elif (Version(np.__version__) >= Version("1.21")) and not SHAPELY_GE_20:
 
     @contextlib.contextmanager
@@ -162,7 +161,6 @@ elif (Version(np.__version__) >= Version("1.21")) and not SHAPELY_GE_20:
             )
             yield
 
-
 else:
 
     @contextlib.contextmanager
diff --git a/geopandas/_config.py b/geopandas/_config.py
index 99a2fca4..cdfa5578 100644
--- a/geopandas/_config.py
+++ b/geopandas/_config.py
@@ -50,7 +50,7 @@ class Options(object):
         cls = self.__class__.__name__
         description = ""
         for key, option in self._options.items():
-            descr = u"{key}: {cur!r} [default: {default!r}]\n".format(
+            descr = "{key}: {cur!r} [default: {default!r}]\n".format(
                 key=key, cur=self._config[key], default=option.default_value
             )
             description += descr
@@ -58,7 +58,7 @@ class Options(object):
             if option.doc:
                 doc_text = "\n".join(textwrap.wrap(option.doc, width=70))
             else:
-                doc_text = u"No description available."
+                doc_text = "No description available."
             doc_text = textwrap.indent(doc_text, prefix="    ")
             description += doc_text + "\n"
         space = "\n  "
diff --git a/geopandas/explore.py b/geopandas/explore.py
index 7efab95d..3c0f500c 100644
--- a/geopandas/explore.py
+++ b/geopandas/explore.py
@@ -183,6 +183,16 @@ def _explore(
             Fill color. Defaults to the value of the color option
         fillOpacity : float (default 0.5)
             Fill opacity.
+        style_function : callable
+            Function mapping a GeoJson Feature to a style ``dict``.
+
+            * Style properties :func:`folium.vector_layers.path_options`
+            * GeoJson features :class:`GeoDataFrame.__geo_interface__`
+
+            e.g.::
+
+                lambda x: {"color":"red" if x["properties"]["gdp_md_est"]<10**6
+                                             else "blue"}
 
         Plus all supported by :func:`folium.vector_layers.path_options`. See the
         documentation of :class:`folium.features.GeoJson` for details.
@@ -367,7 +377,11 @@ GON (((180.00000 -16.06713, 180.00000...
                     "Cannot specify 'categories' when column has categorical dtype"
                 )
             categorical = True
-        elif gdf[column].dtype is np.dtype("O") or categories:
+        elif (
+            gdf[column].dtype is np.dtype("O")
+            or gdf[column].dtype is np.dtype(bool)
+            or categories
+        ):
             categorical = True
 
         nan_idx = pd.isna(gdf[column])
@@ -444,6 +458,17 @@ GON (((180.00000 -16.06713, 180.00000...
         style_kwds["fillOpacity"] = 0.5
     if "weight" not in style_kwds:
         style_kwds["weight"] = 2
+    if "style_function" in style_kwds:
+        style_kwds_function = style_kwds["style_function"]
+        if not callable(style_kwds_function):
+            raise ValueError("'style_function' has to be a callable")
+        style_kwds.pop("style_function")
+    else:
+
+        def _no_style(x):
+            return {}
+
+        style_kwds_function = _no_style
 
     # specify color
     if color is not None:
@@ -454,10 +479,14 @@ GON (((180.00000 -16.06713, 180.00000...
         ):  # use existing column
 
             def _style_color(x):
-                return {
+                base_style = {
                     "fillColor": x["properties"][color],
                     **style_kwds,
                 }
+                return {
+                    **base_style,
+                    **style_kwds_function(x),
+                }
 
             style_function = _style_color
         else:  # assign new column
@@ -476,27 +505,35 @@ GON (((180.00000 -16.06713, 180.00000...
             if not stroke_color:
 
                 def _style_column(x):
-                    return {
+                    base_style = {
                         "fillColor": x["properties"]["__folium_color"],
                         "color": x["properties"]["__folium_color"],
                         **style_kwds,
                     }
+                    return {
+                        **base_style,
+                        **style_kwds_function(x),
+                    }
 
                 style_function = _style_column
             else:
 
                 def _style_stroke(x):
-                    return {
+                    base_style = {
                         "fillColor": x["properties"]["__folium_color"],
                         "color": stroke_color,
                         **style_kwds,
                     }
+                    return {
+                        **base_style,
+                        **style_kwds_function(x),
+                    }
 
                 style_function = _style_stroke
     else:  # use folium default
 
         def _style_default(x):
-            return {**style_kwds}
+            return {**style_kwds, **style_kwds_function(x)}
 
         style_function = _style_default
 
@@ -810,6 +847,7 @@ def _explore_geoseries(
     marker_kwds={},
     style_kwds={},
     highlight_kwds={},
+    map_kwds={},
     **kwargs,
 ):
     """Interactive map based on GeoPandas and folium/leaflet.js
@@ -880,6 +918,17 @@ def _explore_geoseries(
             Fill color. Defaults to the value of the color option
         fillOpacity : float (default 0.5)
             Fill opacity.
+        style_function : callable
+            Function mapping a GeoJson Feature to a style ``dict``.
+
+            * Style properties :func:`folium.vector_layers.path_options`
+            * GeoJson features :class:`GeoSeries.__geo_interface__`
+
+            e.g.::
+
+                lambda x: {"color":"red" if x["properties"]["gdp_md_est"]<10**6
+                                             else "blue"}
+
 
         Plus all supported by :func:`folium.vector_layers.path_options`. See the
         documentation of :class:`folium.features.GeoJson` for details.
@@ -887,6 +936,9 @@ def _explore_geoseries(
     highlight_kwds : dict (default {})
         Style to be passed to folium highlight_function. Uses the same keywords
         as ``style_kwds``. When empty, defaults to ``{"fillOpacity": 0.75}``.
+    map_kwds : dict (default {})
+        Additional keywords to be passed to folium :class:`~folium.folium.Map`,
+        e.g. ``dragging``, or ``scrollWheelZoom``.
 
     **kwargs : dict
         Additional options to be passed on to the folium.
@@ -911,5 +963,6 @@ def _explore_geoseries(
         marker_kwds=marker_kwds,
         style_kwds=style_kwds,
         highlight_kwds=highlight_kwds,
+        map_kwds=map_kwds,
         **kwargs,
     )
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index bfd298d4..c82cba5f 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -633,7 +633,10 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
                 "geometry": shape(feature["geometry"]) if feature["geometry"] else None
             }
             # load properties
-            row.update(feature["properties"])
+            properties = feature["properties"]
+            if properties is None:
+                properties = {}
+            row.update(properties)
             rows.append(row)
         return GeoDataFrame(rows, columns=columns, crs=crs)
 
@@ -865,7 +868,7 @@ individually so that features may have different properties
         if not self.columns.is_unique:
             raise ValueError("GeoDataFrame cannot contain duplicated column names.")
 
-        properties_cols = self.columns.difference([self._geometry_column_name])
+        properties_cols = self.columns.drop(self._geometry_column_name)
 
         if len(properties_cols) > 0:
             # convert to object to get python scalars.
@@ -1024,6 +1027,16 @@ individually so that features may have different properties
         GeoDataFrame.to_file : write GeoDataFrame to file
         """
 
+        # Accept engine keyword for compatibility with pandas.DataFrame.to_parquet
+        # The only engine currently supported by GeoPandas is pyarrow, so no
+        # other engine should be specified.
+        engine = kwargs.pop("engine", "auto")
+        if engine not in ("auto", "pyarrow"):
+            raise ValueError(
+                f"GeoPandas only supports using pyarrow as the engine for "
+                f"to_parquet: {engine!r} passed instead."
+            )
+
         from geopandas.io.arrow import _to_parquet
 
         _to_parquet(self, path, compression=compression, index=index, **kwargs)
@@ -1808,7 +1821,7 @@ individually so that features may have different properties
             - append: Insert new values to the existing table.
         schema : string, optional
             Specify the schema. If None, use default schema: 'public'.
-        index : bool, default True
+        index : bool, default False
             Write DataFrame index as a column.
             Uses *index_label* as the column name in the table.
         index_label : string or sequence, default None
diff --git a/geopandas/io/sql.py b/geopandas/io/sql.py
index 17e7c404..89e8016b 100644
--- a/geopandas/io/sql.py
+++ b/geopandas/io/sql.py
@@ -272,7 +272,7 @@ def _convert_linearring_to_linestring(gdf, geom_name):
 
 
 def _convert_to_ewkb(gdf, geom_name, srid):
-    """Convert geometries to ewkb. """
+    """Convert geometries to ewkb."""
     if compat.USE_PYGEOS:
         from pygeos import set_srid, to_wkb
 
diff --git a/setup.py b/setup.py
index ca1d0ec1..277d6847 100644
--- a/setup.py
+++ b/setup.py
@@ -71,7 +71,7 @@ setup(
         "geopandas.tools.tests",
     ],
     package_data={"geopandas": data_files},
-    python_requires=">=3.7",
+    python_requires=">=3.8",
     install_requires=INSTALL_REQUIRES,
     cmdclass=versioneer.get_cmdclass(),
 )


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 6e5e99e9..2a0da2bc 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -17,8 +17,8 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-python@v2
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v3
       - uses: pre-commit/action@v2.0.3
 
   Test:
@@ -35,28 +35,28 @@ jobs:
         postgis: [false]
         dev: [false]
         env:
-          - ci/envs/37-minimal.yaml
-          - ci/envs/38-no-optional-deps.yaml
-          - ci/envs/37-pd11.yaml
-          - ci/envs/37-latest-defaults.yaml
-          - ci/envs/37-latest-conda-forge.yaml
+          - ci/envs/38-minimal.yaml
+          - ci/envs/39-no-optional-deps.yaml
+          - ci/envs/38-pd11-defaults.yaml
+          - ci/envs/38-latest-defaults.yaml
           - ci/envs/38-latest-conda-forge.yaml
+          - ci/envs/39-pd12-conda-forge.yaml
           - ci/envs/39-latest-conda-forge.yaml
           - ci/envs/310-latest-conda-forge.yaml
         include:
-          - env: ci/envs/37-latest-conda-forge.yaml
+          - env: ci/envs/38-latest-conda-forge.yaml
             os: macos-latest
             postgis: false
             dev: false
-          - env: ci/envs/38-latest-conda-forge.yaml
+          - env: ci/envs/39-latest-conda-forge.yaml
             os: macos-latest
             postgis: false
             dev: false
-          - env: ci/envs/37-latest-conda-forge.yaml
+          - env: ci/envs/38-latest-conda-forge.yaml
             os: windows-latest
             postgis: false
             dev: false
-          - env: ci/envs/38-latest-conda-forge.yaml
+          - env: ci/envs/39-latest-conda-forge.yaml
             os: windows-latest
             postgis: false
             dev: false
@@ -65,7 +65,7 @@ jobs:
             dev: true
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
 
       - name: Setup Conda
         uses: conda-incubator/setup-miniconda@v2
@@ -106,7 +106,7 @@ jobs:
           pytest -v -r s -n auto --color=yes --cov=geopandas --cov-append --cov-report term-missing --cov-report xml geopandas/
 
       - name: Test with PostGIS
-        if: contains(matrix.env, '38-latest-conda-forge.yaml') && contains(matrix.os, 'ubuntu')
+        if: contains(matrix.env, '39-pd12-conda-forge.yaml') && contains(matrix.os, 'ubuntu')
         env:
           PGUSER: postgres
           PGPASSWORD: postgres
@@ -118,7 +118,7 @@ jobs:
           pytest -v -r s --color=yes --cov=geopandas --cov-append --cov-report term-missing --cov-report xml geopandas/io/tests/test_sql.py | tee /dev/stderr | if grep SKIPPED >/dev/null;then echo "TESTS SKIPPED, FAILING" && exit 1;fi
 
       - name: Test docstrings
-        if: contains(matrix.env, '38-latest-conda-forge.yaml') && contains(matrix.os, 'ubuntu')
+        if: contains(matrix.env, '39-pd12-conda-forge.yaml') && contains(matrix.os, 'ubuntu')
         env:
           USE_PYGEOS: 1
         run: |
diff --git a/ci/envs/38-latest-conda-forge.yaml b/ci/envs/38-latest-conda-forge.yaml
index 6d2348f1..c6ea7126 100644
--- a/ci/envs/38-latest-conda-forge.yaml
+++ b/ci/envs/38-latest-conda-forge.yaml
@@ -4,7 +4,7 @@ channels:
 dependencies:
   - python=3.8
   # required
-  - pandas=1.2
+  - pandas
   - shapely
   - fiona
   - pyproj
@@ -23,12 +23,6 @@ dependencies:
   - xyzservices
   - scipy
   - geopy
-  # installed in tests.yaml, because not available on windows
-  # - postgis
   - SQLalchemy
-  - psycopg2
   - libspatialite
-  - geoalchemy2
   - pyarrow
-  # doctest testing
-  - pytest-doctestplus
diff --git a/ci/envs/37-latest-defaults.yaml b/ci/envs/38-latest-defaults.yaml
similarity index 96%
rename from ci/envs/37-latest-defaults.yaml
rename to ci/envs/38-latest-defaults.yaml
index 071d3eac..a4c4ef7e 100644
--- a/ci/envs/37-latest-defaults.yaml
+++ b/ci/envs/38-latest-defaults.yaml
@@ -2,7 +2,7 @@ name: test
 channels:
   - defaults
 dependencies:
-  - python=3.7
+  - python=3.8
   # required
   - pandas
   - shapely
diff --git a/ci/envs/37-latest-conda-forge.yaml b/ci/envs/39-pd12-conda-forge.yaml
similarity index 65%
rename from ci/envs/37-latest-conda-forge.yaml
rename to ci/envs/39-pd12-conda-forge.yaml
index dadd2c40..fc675bf9 100644
--- a/ci/envs/37-latest-conda-forge.yaml
+++ b/ci/envs/39-pd12-conda-forge.yaml
@@ -2,9 +2,9 @@ name: test
 channels:
   - conda-forge
 dependencies:
-  - python=3.7
+  - python=3.9
   # required
-  - pandas
+  - pandas=1.2
   - shapely
   - fiona
   - pyproj
@@ -23,6 +23,12 @@ dependencies:
   - xyzservices
   - scipy
   - geopy
+  # installed in tests.yaml, because not available on windows
+  # - postgis
   - SQLalchemy
+  - psycopg2
   - libspatialite
+  - geoalchemy2
   - pyarrow
+  # doctest testing
+  - pytest-doctestplus
diff --git a/geopandas/io/tests/generate_legacy_storage_files.py b/geopandas/io/tests/generate_legacy_storage_files.py
index cc39273e..9cf4a0cc 100644
--- a/geopandas/io/tests/generate_legacy_storage_files.py
+++ b/geopandas/io/tests/generate_legacy_storage_files.py
@@ -31,7 +31,7 @@ from shapely.geometry import Point
 
 
 def create_pickle_data():
-    """ create the pickle data """
+    """create the pickle data"""
 
     # custom geometry column name
     gdf_the_geom = geopandas.GeoDataFrame(
diff --git a/geopandas/io/tests/test_arrow.py b/geopandas/io/tests/test_arrow.py
index 1e701581..4c8d82c5 100644
--- a/geopandas/io/tests/test_arrow.py
+++ b/geopandas/io/tests/test_arrow.py
@@ -7,7 +7,7 @@ import pytest
 from pandas import DataFrame, read_parquet as pd_read_parquet
 from pandas.testing import assert_frame_equal
 import numpy as np
-from shapely.geometry import box
+from shapely.geometry import box, Point
 
 import geopandas
 from geopandas import GeoDataFrame, read_file, read_parquet, read_feather
@@ -23,6 +23,7 @@ from geopandas.io.arrow import (
     METADATA_VERSION,
 )
 from geopandas.testing import assert_geodataframe_equal, assert_geoseries_equal
+from geopandas.tests.util import mock
 
 
 # Skip all tests in this module if pyarrow is not available
@@ -172,6 +173,28 @@ def test_validate_metadata_invalid(metadata, error):
         _validate_metadata(metadata)
 
 
+def test_to_parquet_fails_on_invalid_engine(tmpdir):
+    df = GeoDataFrame(data=[[1, 2, 3]], columns=["a", "b", "a"], geometry=[Point(1, 1)])
+
+    with pytest.raises(
+        ValueError,
+        match=(
+            "GeoPandas only supports using pyarrow as the engine for "
+            "to_parquet: 'fastparquet' passed instead."
+        ),
+    ):
+        df.to_parquet(tmpdir / "test.parquet", engine="fastparquet")
+
+
+@mock.patch("geopandas.io.arrow._to_parquet")
+def test_to_parquet_does_not_pass_engine_along(mock_to_parquet):
+    df = GeoDataFrame(data=[[1, 2, 3]], columns=["a", "b", "a"], geometry=[Point(1, 1)])
+    df.to_parquet("", engine="pyarrow")
+    # assert that engine keyword is not passed through to _to_parquet (and thus
+    # parquet.write_table)
+    mock_to_parquet.assert_called_with(df, "", compression="snappy", index=None)
+
+
 # TEMPORARY: used to determine if pyarrow fails for roundtripping pandas data
 # without geometries
 def test_pandas_parquet_roundtrip1(tmpdir):
diff --git a/geopandas/tests/test_crs.py b/geopandas/tests/test_crs.py
index 04a4270a..93eb6b4b 100644
--- a/geopandas/tests/test_crs.py
+++ b/geopandas/tests/test_crs.py
@@ -1,5 +1,4 @@
 from packaging.version import Version
-import os
 
 import random
 
@@ -19,6 +18,7 @@ import pytest
 # pyproj 2.3.1 fixed a segfault for the case working in an environment with
 # 'init' dicts (https://github.com/pyproj4/pyproj/issues/415)
 PYPROJ_LT_231 = Version(pyproj.__version__) < Version("2.3.1")
+PYPROJ_GE_3 = Version(pyproj.__version__) >= Version("3.0.0")
 
 
 def _create_df(x, y=None, crs=None):
@@ -125,7 +125,9 @@ def test_transform2(epsg4326, epsg26918):
     # with PROJ >= 7, the transformation using EPSG code vs proj4 string is
     # slightly different due to use of grid files or not -> turn off network
     # to not use grid files at all for this test
-    os.environ["PROJ_NETWORK"] = "OFF"
+    if PYPROJ_GE_3:
+        pyproj.network.set_network_enabled(False)
+
     df = df_epsg26918()
     lonlat = df.to_crs(**epsg4326)
     utm = lonlat.to_crs(**epsg26918)
diff --git a/geopandas/tests/test_explore.py b/geopandas/tests/test_explore.py
index 73917a32..51493121 100644
--- a/geopandas/tests/test_explore.py
+++ b/geopandas/tests/test_explore.py
@@ -253,6 +253,14 @@ class TestExplore:
         with pytest.raises(ValueError, match="Cannot specify 'categories'"):
             df.explore("categorical", categories=["Brooklyn", "Staten Island"])
 
+    def test_bool(self):
+        df = self.nybb.copy()
+        df["bool"] = [True, False, True, False, True]
+        m = df.explore("bool")
+        out_str = self._fetch_map_string(m)
+        assert '"__folium_color":"#9edae5","bool":true' in out_str
+        assert '"__folium_color":"#1f77b4","bool":false' in out_str
+
     def test_column_values(self):
         """
         Check that the dataframe plot method returns same values with an
@@ -309,6 +317,37 @@ class TestExplore:
         m = self.world.explore(column="pop_est", style_kwds=dict(color="black"))
         assert '"color":"black"' in self._fetch_map_string(m)
 
+        # custom style_function - geopandas/issues/2350
+        m = self.world.explore(
+            style_kwds={
+                "style_function": lambda x: {
+                    "fillColor": "red"
+                    if x["properties"]["gdp_md_est"] < 10**6
+                    else "green",
+                    "color": "black"
+                    if x["properties"]["gdp_md_est"] < 10**6
+                    else "white",
+                }
+            }
+        )
+        # two lines with formatting instructions from style_function.
+        # make sure each passes test
+        assert all(
+            [
+                ('"fillColor":"green"' in t and '"color":"white"' in t)
+                or ('"fillColor":"red"' in t and '"color":"black"' in t)
+                for t in [
+                    "".join(line.split())
+                    for line in m._parent.render().split("\n")
+                    if "return" in line and "color" in line
+                ]
+            ]
+        )
+
+        # style function has to be callable
+        with pytest.raises(ValueError, match="'style_function' has to be a callable"):
+            self.world.explore(style_kwds={"style_function": "not callable"})
+
     def test_tooltip(self):
         """Test tooltip"""
         # default with no tooltip or popup
diff --git a/geopandas/tests/test_geodataframe.py b/geopandas/tests/test_geodataframe.py
index a5ec71bb..59f93592 100644
--- a/geopandas/tests/test_geodataframe.py
+++ b/geopandas/tests/test_geodataframe.py
@@ -619,7 +619,7 @@ class TestDataFrame:
         p3 = Point(3, 3)
         f3 = {
             "type": "Feature",
-            "properties": {"a": 2},
+            "properties": None,
             "geometry": p3.__geo_interface__,
         }
 
@@ -627,10 +627,103 @@ class TestDataFrame:
 
         result = df[["a", "b"]]
         expected = pd.DataFrame.from_dict(
-            [{"a": 0, "b": np.nan}, {"a": np.nan, "b": 1}, {"a": 2, "b": np.nan}]
+            [{"a": 0, "b": np.nan}, {"a": np.nan, "b": 1}, {"a": np.nan, "b": np.nan}]
         )
         assert_frame_equal(expected, result)
 
+    def test_from_features_empty_properties(self):
+        geojson_properties_object = """{
+          "type": "FeatureCollection",
+          "features": [
+            {
+              "type": "Feature",
+              "properties": {},
+              "geometry": {
+                "type": "Polygon",
+                "coordinates": [
+                  [
+                    [
+                      11.3456529378891,
+                      46.49461446367692
+                    ],
+                    [
+                      11.345674395561216,
+                      46.494097442978195
+                    ],
+                    [
+                      11.346918940544128,
+                      46.49385370294394
+                    ],
+                    [
+                      11.347616314888,
+                      46.4938352377453
+                    ],
+                    [
+                      11.347514390945435,
+                      46.49466985846028
+                    ],
+                    [
+                      11.3456529378891,
+                      46.49461446367692
+                    ]
+                  ]
+                ]
+              }
+            }
+          ]
+        }"""
+
+        geojson_properties_null = """{
+          "type": "FeatureCollection",
+          "features": [
+            {
+              "type": "Feature",
+              "properties": null,
+              "geometry": {
+                "type": "Polygon",
+                "coordinates": [
+                  [
+                    [
+                      11.3456529378891,
+                      46.49461446367692
+                    ],
+                    [
+                      11.345674395561216,
+                      46.494097442978195
+                    ],
+                    [
+                      11.346918940544128,
+                      46.49385370294394
+                    ],
+                    [
+                      11.347616314888,
+                      46.4938352377453
+                    ],
+                    [
+                      11.347514390945435,
+                      46.49466985846028
+                    ],
+                    [
+                      11.3456529378891,
+                      46.49461446367692
+                    ]
+                  ]
+                ]
+              }
+            }
+          ]
+        }"""
+
+        # geoJSON with empty properties
+        gjson_po = json.loads(geojson_properties_object)
+        gdf1 = GeoDataFrame.from_features(gjson_po)
+
+        # geoJSON with null properties
+        gjson_null = json.loads(geojson_properties_null)
+        gdf2 = GeoDataFrame.from_features(gjson_null)
+
+        assert_frame_equal(gdf1, gdf2)
+
     def test_from_features_geom_interface_feature(self):
         class Placemark(object):
             def __init__(self, geom, val):
@@ -659,7 +752,7 @@ class TestDataFrame:
         geometry = [Point(xy) for xy in zip(df["lon"], df["lat"])]
         gdf = GeoDataFrame(df, geometry=geometry)
         # from_features returns sorted columns
-        expected = gdf[["geometry", "lat", "lon", "name"]]
+        expected = gdf[["geometry", "name", "lat", "lon"]]
 
         # test FeatureCollection
         res = GeoDataFrame.from_features(gdf.__geo_interface__)
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
index 05e3d5c1..705b5eb1 100644
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -490,7 +490,7 @@ class TestGeomMethods:
         )
         assert_array_dtype_equal(expected, self.na_none.distance(self.p0))
 
-        expected = Series(np.array([np.sqrt(4 ** 2 + 4 ** 2), np.nan]), self.g6.index)
+        expected = Series(np.array([np.sqrt(4**2 + 4**2), np.nan]), self.g6.index)
         assert_array_dtype_equal(expected, self.g6.distance(self.na_none))
 
         expected = Series(np.array([np.nan, 0, 0, 0, 0, 0, np.nan, np.nan]), range(8))
diff --git a/geopandas/tests/test_op_output_types.py b/geopandas/tests/test_op_output_types.py
index 5e9b5d96..dd25ff91 100644
--- a/geopandas/tests/test_op_output_types.py
+++ b/geopandas/tests/test_op_output_types.py
@@ -88,8 +88,8 @@ def test_getitem(df):
     assert_object(df[["value1", "value2"]], pd.DataFrame)
     assert_object(df[[geo_name, "geometry2"]], GeoDataFrame, geo_name)
     assert_object(df[[geo_name]], GeoDataFrame, geo_name)
-    assert_object(df[["geometry2", "value1"]], GeoDataFrame, None, None)
-    assert_object(df[["geometry2"]], GeoDataFrame, None, None)
+    assert_object(df[["geometry2", "value1"]], GeoDataFrame, None)
+    assert_object(df[["geometry2"]], GeoDataFrame, None)
     assert_object(df[["value1"]], pd.DataFrame)
     # Series
     assert_object(df[geo_name], GeoSeries, geo_name)
@@ -102,12 +102,8 @@ def test_loc(df):
     assert_object(df.loc[:, ["value1", "value2"]], pd.DataFrame)
     assert_object(df.loc[:, [geo_name, "geometry2"]], GeoDataFrame, geo_name)
     assert_object(df.loc[:, [geo_name]], GeoDataFrame, geo_name)
-    # TODO: should this set the geometry column to geometry2 or return a DataFrame?
     assert_object(df.loc[:, ["geometry2", "value1"]], GeoDataFrame, None)
     assert_object(df.loc[:, ["geometry2"]], GeoDataFrame, None)
-    # Ideally this would mirror __getitem__ and below would be true
-    # assert_object(df.loc[:, ["geometry2", "value1"]], pd.DataFrame)
-    # assert_object(df.loc[:, ["geometry2"]], pd.DataFrame)
     assert_object(df.loc[:, ["value1"]], pd.DataFrame)
     # Series
     assert_object(df.loc[:, geo_name], GeoSeries, geo_name)
@@ -120,12 +116,8 @@ def test_iloc(df):
     assert_object(df.iloc[:, 0:2], pd.DataFrame)
     assert_object(df.iloc[:, 2:4], GeoDataFrame, geo_name)
     assert_object(df.iloc[:, [2]], GeoDataFrame, geo_name)
-    # TODO: should this set the geometry column to geometry2 or return a DataFrame?
     assert_object(df.iloc[:, [3, 0]], GeoDataFrame, None)
     assert_object(df.iloc[:, [3]], GeoDataFrame, None)
-    # Ideally this would mirror __getitem__ and below would be true
-    # assert_object(df.iloc[:, [3, 0]], pd.DataFrame)
-    # assert_object(df.iloc[:, [3]], pd.DataFrame)
     assert_object(df.iloc[:, [0]], pd.DataFrame)
     # Series
     assert_object(df.iloc[:, 2], GeoSeries, geo_name)
@@ -157,12 +149,8 @@ def test_reindex(df):
     assert_object(df.reindex(columns=[geo_name, "geometry2"]), GeoDataFrame, geo_name)
     assert_object(df.reindex(columns=[geo_name]), GeoDataFrame, geo_name)
     assert_object(df.reindex(columns=["new_col", geo_name]), GeoDataFrame, geo_name)
-    # TODO: should this set the geometry column to geometry2 or return a DataFrame?
     assert_object(df.reindex(columns=["geometry2", "value1"]), GeoDataFrame, None)
     assert_object(df.reindex(columns=["geometry2"]), GeoDataFrame, None)
-    # Ideally this would mirror __getitem__ and below would be true
-    # assert_object(df.reindex(columns=["geometry2", "value1"]), pd.DataFrame)
-    # assert_object(df.reindex(columns=["geometry2"]), pd.DataFrame)
     assert_object(df.reindex(columns=["value1"]), pd.DataFrame)
 
     # reindexing the rows always preserves the GeoDataFrame
@@ -181,12 +169,8 @@ def test_drop(df):
     assert_object(df.drop(columns=["value1", "value2"]), GeoDataFrame, geo_name)
     cols = ["value1", "value2", "geometry2"]
     assert_object(df.drop(columns=cols), GeoDataFrame, geo_name)
-    # TODO: should this set the geometry column to geometry2 or return a DataFrame?
     assert_object(df.drop(columns=[geo_name, "value2"]), GeoDataFrame, None)
     assert_object(df.drop(columns=["value1", "value2", geo_name]), GeoDataFrame, None)
-    # Ideally this would mirror __getitem__ and below would be true
-    # assert_object(df.drop(columns=[geo_name, "value2"]), pd.DataFrame)
-    # assert_object(df.drop(columns=["value1", "value2", geo_name]), pd.DataFrame)
     assert_object(df.drop(columns=["geometry2", "value2", geo_name]), pd.DataFrame)
 
 
@@ -226,7 +210,6 @@ def test_apply(df):
     assert_object(df[["geometry2", "value1"]].apply(identity, axis=1), pd.DataFrame)
 
     assert_object(df[["value1"]].apply(identity, axis=1), pd.DataFrame)
-    # if compat # https://github.com/pandas-dev/pandas/pull/30091
 
 
 def test_apply_axis1_secondary_geo_cols(df):
diff --git a/geopandas/tests/test_overlay.py b/geopandas/tests/test_overlay.py
index 14ea914a..166e9da5 100644
--- a/geopandas/tests/test_overlay.py
+++ b/geopandas/tests/test_overlay.py
@@ -14,6 +14,7 @@ from geopandas import _compat
 from geopandas.testing import assert_geodataframe_equal, assert_geoseries_equal
 import pytest
 
+
 DATA = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data", "overlay")
 
 
@@ -795,3 +796,79 @@ def test_no_intersection():
     expected = GeoDataFrame(columns=["foo", "bar", "geometry"])
     result = overlay(gdf1, gdf2, how="intersection")
     assert_geodataframe_equal(result, expected, check_index_type=False)
+
+
+class TestOverlayWikiExample:
+    def setup_method(self):
+        self.layer_a = GeoDataFrame(geometry=[box(0, 2, 6, 6)])
+
+        self.layer_b = GeoDataFrame(geometry=[box(4, 0, 10, 4)])
+
+        self.intersection = GeoDataFrame(geometry=[box(4, 2, 6, 4)])
+
+        self.union = GeoDataFrame(
+            geometry=[
+                box(4, 2, 6, 4),
+                Polygon([(4, 2), (0, 2), (0, 6), (6, 6), (6, 4), (4, 4), (4, 2)]),
+                Polygon([(10, 0), (4, 0), (4, 2), (6, 2), (6, 4), (10, 4), (10, 0)]),
+            ]
+        )
+
+        self.a_difference_b = GeoDataFrame(
+            geometry=[Polygon([(4, 2), (0, 2), (0, 6), (6, 6), (6, 4), (4, 4), (4, 2)])]
+        )
+
+        self.b_difference_a = GeoDataFrame(
+            geometry=[
+                Polygon([(10, 0), (4, 0), (4, 2), (6, 2), (6, 4), (10, 4), (10, 0)])
+            ]
+        )
+
+        self.symmetric_difference = GeoDataFrame(
+            geometry=[
+                Polygon([(4, 2), (0, 2), (0, 6), (6, 6), (6, 4), (4, 4), (4, 2)]),
+                Polygon([(10, 0), (4, 0), (4, 2), (6, 2), (6, 4), (10, 4), (10, 0)]),
+            ]
+        )
+
+        self.a_identity_b = GeoDataFrame(
+            geometry=[
+                box(4, 2, 6, 4),
+                Polygon([(4, 2), (0, 2), (0, 6), (6, 6), (6, 4), (4, 4), (4, 2)]),
+            ]
+        )
+
+        self.b_identity_a = GeoDataFrame(
+            geometry=[
+                box(4, 2, 6, 4),
+                Polygon([(10, 0), (4, 0), (4, 2), (6, 2), (6, 4), (10, 4), (10, 0)]),
+            ]
+        )
+
+    def test_intersection(self):
+        df_result = overlay(self.layer_a, self.layer_b, how="intersection")
+        assert df_result.geom_equals(self.intersection).bool()
+
+    def test_union(self):
+        df_result = overlay(self.layer_a, self.layer_b, how="union")
+        assert_geodataframe_equal(df_result, self.union)
+
+    def test_a_difference_b(self):
+        df_result = overlay(self.layer_a, self.layer_b, how="difference")
+        assert_geodataframe_equal(df_result, self.a_difference_b)
+
+    def test_b_difference_a(self):
+        df_result = overlay(self.layer_b, self.layer_a, how="difference")
+        assert_geodataframe_equal(df_result, self.b_difference_a)
+
+    def test_symmetric_difference(self):
+        df_result = overlay(self.layer_a, self.layer_b, how="symmetric_difference")
+        assert_geodataframe_equal(df_result, self.symmetric_difference)
+
+    def test_a_identity_b(self):
+        df_result = overlay(self.layer_a, self.layer_b, how="identity")
+        assert_geodataframe_equal(df_result, self.a_identity_b)
+
+    def test_b_identity_a(self):
+        df_result = overlay(self.layer_b, self.layer_a, how="identity")
+        assert_geodataframe_equal(df_result, self.b_identity_a)
diff --git a/geopandas/tests/test_plotting.py b/geopandas/tests/test_plotting.py
index 39cf3de4..368a0440 100644
--- a/geopandas/tests/test_plotting.py
+++ b/geopandas/tests/test_plotting.py
@@ -1090,9 +1090,9 @@ class TestMapclassifyPlotting:
             )
         labels = [t.get_text() for t in ax.get_legend().get_texts()]
         expected = [
-            u"       140.00,    5217064.00",
-            u"   5217064.00,   19532732.33",
-            u"  19532732.33, 1379302771.00",
+            "       140.00,    5217064.00",
+            "   5217064.00,   19532732.33",
+            "  19532732.33, 1379302771.00",
         ]
         assert labels == expected
 
@@ -1125,7 +1125,7 @@ class TestMapclassifyPlotting:
             column="NEGATIVES", scheme="FISHER_JENKS", k=3, cmap="OrRd", legend=True
         )
         labels = [t.get_text() for t in ax.get_legend().get_texts()]
-        expected = [u"-10.00,  -3.41", u" -3.41,   3.30", u"  3.30,  10.00"]
+        expected = ["-10.00,  -3.41", " -3.41,   3.30", "  3.30,  10.00"]
         assert labels == expected
 
     def test_fmt(self):
@@ -1138,7 +1138,7 @@ class TestMapclassifyPlotting:
             legend_kwds={"fmt": "{:.0f}"},
         )
         labels = [t.get_text() for t in ax.get_legend().get_texts()]
-        expected = [u"-10,  -3", u" -3,   3", u"  3,  10"]
+        expected = ["-10,  -3", " -3,   3", "  3,  10"]
         assert labels == expected
 
     def test_interval(self):
@@ -1151,7 +1151,7 @@ class TestMapclassifyPlotting:
             legend_kwds={"interval": True},
         )
         labels = [t.get_text() for t in ax.get_legend().get_texts()]
-        expected = [u"[-10.00,  -3.41]", u"( -3.41,   3.30]", u"(  3.30,  10.00]"]
+        expected = ["[-10.00,  -3.41]", "( -3.41,   3.30]", "(  3.30,  10.00]"]
         assert labels == expected
 
     @pytest.mark.parametrize("scheme", ["FISHER_JENKS", "FISHERJENKS"])
diff --git a/geopandas/tests/test_sindex.py b/geopandas/tests/test_sindex.py
index 0b53a8d4..c36c77d1 100644
--- a/geopandas/tests/test_sindex.py
+++ b/geopandas/tests/test_sindex.py
@@ -174,6 +174,31 @@ class TestFrameSindex:
         subset2 = self.df[["A", "geom"]]
         assert subset2.sindex is original_index
 
+    def test_rebuild_on_update_inplace(self):
+        gdf = self.df.copy()
+        old_sindex = gdf.sindex
+        # sorting in place
+        gdf.sort_values("A", ascending=False, inplace=True)
+        # spatial index should be invalidated
+        assert not gdf.has_sindex
+        new_sindex = gdf.sindex
+        # and should be different
+        assert new_sindex is not old_sindex
+
+        # sorting should still have happened though
+        assert gdf.index.tolist() == [4, 3, 2, 1, 0]
+
+    @pytest.mark.skipif(not compat.PANDAS_GE_11, reason="fails on pd<1.1.0")
+    def test_update_inplace_no_rebuild(self):
+        gdf = self.df.copy()
+        old_sindex = gdf.sindex
+        gdf.rename(columns={"A": "AA"}, inplace=True)
+        # a rename shouldn't invalidate the index
+        assert gdf.has_sindex
+        # and the "new" should be the same
+        new_sindex = gdf.sindex
+        assert old_sindex is new_sindex
+
 
 # Skip to accommodate Shapely geometries being unhashable
 @pytest.mark.skip
diff --git a/geopandas/tools/tests/test_sjoin.py b/geopandas/tools/tests/test_sjoin.py
index c14367c2..80a9972d 100644
--- a/geopandas/tools/tests/test_sjoin.py
+++ b/geopandas/tools/tests/test_sjoin.py
@@ -767,14 +767,14 @@ class TestNearest:
                 [Point(1, 1), Point(0.25, 1)],
                 [0, 1],
                 [1, 0],
-                [math.sqrt(0.25 ** 2 + 1), 0],
+                [math.sqrt(0.25**2 + 1), 0],
             ),
             (
                 [Point(0, 0), Point(1, 1)],
                 [Point(-10, -10), Point(100, 100)],
                 [0, 1],
                 [0, 0],
-                [math.sqrt(10 ** 2 + 10 ** 2), math.sqrt(11 ** 2 + 11 ** 2)],
+                [math.sqrt(10**2 + 10**2), math.sqrt(11**2 + 11**2)],
             ),
             (
                 [Point(0, 0), Point(1, 1)],
@@ -788,7 +788,7 @@ class TestNearest:
                 [Point(1.1, 1.1), Point(0, 0)],
                 [0, 1, 2],
                 [1, 0, 1],
-                [0, np.sqrt(0.1 ** 2 + 0.1 ** 2), 0],
+                [0, np.sqrt(0.1**2 + 0.1**2), 0],
             ),
         ],
     )
@@ -852,21 +852,21 @@ class TestNearest:
                 [Point(-10, -10), Point(100, 100)],
                 [0, 1],
                 [0, 1],
-                [math.sqrt(10 ** 2 + 10 ** 2), math.sqrt(99 ** 2 + 99 ** 2)],
+                [math.sqrt(10**2 + 10**2), math.sqrt(99**2 + 99**2)],
             ),
             (
                 [Point(0, 0), Point(1, 1)],
                 [Point(x, y) for x, y in zip(np.arange(10), np.arange(10))],
                 [0, 1] + [1] * 8,
                 list(range(10)),
-                [0, 0] + [np.sqrt(x ** 2 + x ** 2) for x in np.arange(1, 9)],
+                [0, 0] + [np.sqrt(x**2 + x**2) for x in np.arange(1, 9)],
             ),
             (
                 [Point(0, 0), Point(1, 1), Point(0, 0)],
                 [Point(1.1, 1.1), Point(0, 0)],
                 [1, 0, 2],
                 [0, 1, 1],
-                [np.sqrt(0.1 ** 2 + 0.1 ** 2), 0, 0],
+                [np.sqrt(0.1**2 + 0.1**2), 0, 0],
             ),
         ],
     )