Repository: pysal/momepy

Issue:
copyright and license
* license
  * `martinfleis/momepy` was licensed as MIT where PySAL stuff is BSD-3 Clause
* copyright
  *  PySAL stuff is (generally) stamped with a PySAL Developers copyright


These should likely be update in `momepy`.

Discussion:

Patch:
diff --git a/LICENSE b/LICENSE
index 300b336..f10b969 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,25 @@
-MIT License
+Copyright (c) 2018-2021, Martin Fleischmann and PySAL Developers.
+All rights reserved.
 
-Copyright (c) 2018-2020 Martin Fleischmann, University of Strathclyde
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
 
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+ * Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+ * Neither the name of the copyright holder nor the names of its contributors
+   may be used to endorse or promote products derived from this software without
+   specific prior written permission.
 
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/README.md b/README.md
index 7abbbfe..21b069b 100644
--- a/README.md
+++ b/README.md
@@ -110,4 +110,4 @@ If you have a question regarding momepy, feel free to open an issue on GitHub. E
 Initial release of momepy was a result of a research of [Urban Design Studies Unit (UDSU)](http://udsu-strath.com) supported by the Axel and Margaret Ax:son Johnson Foundation as a part of “The Urban Form Resilience Project” in partnership with University of Strathclyde in Glasgow, UK. Further development was supported by [Geographic Data Science Lab](https://www.liverpool.ac.uk/geographic-data-science/) of the University of Liverpool wihtin [Urban Grammar AI](https://urbangrammarai.xyz) research project.
 
 ---
-Copyright (c) 2018- Martin Fleischmann
+Copyright (c) 2018-, Martin Fleischmann and PySAL Developers
diff --git a/ci/envs/stable.yaml b/ci/envs/stable.yaml
index 9833838..a851ee1 100644
--- a/ci/envs/stable.yaml
+++ b/ci/envs/stable.yaml
@@ -3,7 +3,7 @@ channels:
   - conda-forge
 dependencies:
   - python=3.7
-  - geopandas=0.8
+  - geopandas=0.9
   - libpysal
   - networkx
   - tqdm
diff --git a/docs/conf.py b/docs/conf.py
index 9575e2b..2b22716 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -42,7 +42,7 @@ autodoc_mock_imports = [
 # -- Project information -----------------------------------------------------
 
 project = "momepy"
-copyright = "2018-2021, Martin Fleischmann"
+copyright = "2018-2021, Martin Fleischmann and PySAL Developers"
 author = "Martin Fleischmann"
 
 # The short X.Y version
diff --git a/momepy/distribution.py b/momepy/distribution.py
index 34ce3bf..72e503e 100644
--- a/momepy/distribution.py
+++ b/momepy/distribution.py
@@ -5,6 +5,7 @@
 # definitions of spatial distribution characters
 
 import math
+import warnings
 
 import networkx as nx
 import numpy as np
diff --git a/momepy/elements.py b/momepy/elements.py
index 752b529..b122987 100644
--- a/momepy/elements.py
+++ b/momepy/elements.py
@@ -3,6 +3,8 @@
 
 # elements.py
 # generating derived elements (street edge, block)
+from distutils.version import LooseVersion
+
 import geopandas as gpd
 import libpysal
 import warnings
@@ -14,9 +16,6 @@ from shapely.geometry.base import BaseGeometry
 from shapely.ops import polygonize
 from tqdm.auto import tqdm
 
-# TODO: this should not be needed with shapely 2.0
-from geopandas._vectorized import _pygeos_to_shapely
-
 __all__ = [
     "buffered_limit",
     "Tessellation",
@@ -27,6 +26,8 @@ __all__ = [
     "get_network_ratio",
 ]
 
+GPD_10 = str(gpd.__version__) >= LooseVersion("0.10")
+
 
 def buffered_limit(gdf, buffer=100):
     """
@@ -276,8 +277,10 @@ class Tessellation:
             objects.loc[mask, objects.geometry.name] = objects[mask].buffer(
                 -shrink, cap_style=2, join_style=2
             )
-
-        objects = objects.reset_index(drop=True).explode()
+        if GPD_10:
+            objects = objects.reset_index(drop=True).explode(ignore_index=True)
+        else:
+            objects = objects.reset_index(drop=True).explode(ignore_index=True)
         objects = objects.set_index(unique_id)
 
         print("Generating input point array...") if verbose else None
@@ -629,29 +632,54 @@ class Blocks:
             tessellation,
             gpd.GeoDataFrame(geometry=edges.buffer(0.001)),
             how="difference",
-        ).explode()
+        )
+        if GPD_10:
+            cut = cut.explode(ignore_index=True)
+        else:
+            cut = cut.explode()
 
         W = libpysal.weights.Queen.from_dataframe(cut, silence_warnings=True)
         cut["component"] = W.component_labels
         buildings_c = buildings.copy()
         buildings_c.geometry = buildings_c.representative_point()  # make points
-        centroids_tempID = gpd.sjoin(
-            buildings_c,
-            cut[[cut.geometry.name, "component"]],
-            how="left",
-            op="intersects",
-        )
+        if GPD_10:
+            centroids_tempID = gpd.sjoin(
+                buildings_c,
+                cut[[cut.geometry.name, "component"]],
+                how="left",
+                predicate="intersects",
+            )
+        else:
+            centroids_tempID = gpd.sjoin(
+                buildings_c,
+                cut[[cut.geometry.name, "component"]],
+                how="left",
+                op="intersects",
+            )
+
         cells_copy = tessellation[[unique_id, tessellation.geometry.name]].merge(
             centroids_tempID[[unique_id, "component"]], on=unique_id, how="left"
         )
-        blocks = cells_copy.dissolve(by="component").explode().reset_index(drop=True)
+        if GPD_10:
+            blocks = cells_copy.dissolve(by="component").explode(ignore_index=True)
+        else:
+            blocks = (
+                cells_copy.dissolve(by="component").explode().reset_index(drop=True)
+            )
         blocks[id_name] = range(len(blocks))
         blocks[blocks.geometry.name] = gpd.GeoSeries(
             pygeos.polygons(blocks.exterior.values.data), crs=blocks.crs
         )
         blocks = blocks[[id_name, blocks.geometry.name]]
 
-        centroids_w_bl_ID2 = gpd.sjoin(buildings_c, blocks, how="left", op="intersects")
+        if GPD_10:
+            centroids_w_bl_ID2 = gpd.sjoin(
+                buildings_c, blocks, how="left", predicate="intersects"
+            )
+        else:
+            centroids_w_bl_ID2 = gpd.sjoin(
+                buildings_c, blocks, how="left", op="intersects"
+            )
         bl_ID_to_uID = centroids_w_bl_ID2[[unique_id, id_name]]
 
         buildings_m = buildings[[unique_id]].merge(
@@ -930,6 +958,7 @@ def get_network_ratio(df, edges, initial_buffer=500):
     nans = df.index.difference(edge_dicts.index)
     buffered = df.iloc[nans].buffer(initial_buffer)
     additional = []
+
     for orig, geom in zip(df.iloc[nans].geometry, buffered.geometry):
         query = edges.sindex.query(geom, predicate="intersects")
         b = initial_buffer
@@ -1031,11 +1060,9 @@ def enclosures(
             union = pygeos.union_all(
                 np.append(crossing_ins, pygeos.boundary(poly))
             )  # union
-            polygons = np.array(
-                list(polygonize(_pygeos_to_shapely(union)))
-            )  # polygonize
+            polygons = pygeos.get_parts(pygeos.polygonize([union]))  # polygonize
             within = pygeos.covered_by(
-                pygeos.from_shapely(polygons), buf
+                polygons, buf
             )  # keep only those within original polygon
             new += list(polygons[within])
 
diff --git a/momepy/intensity.py b/momepy/intensity.py
index 20d8b97..e9b6666 100644
--- a/momepy/intensity.py
+++ b/momepy/intensity.py
@@ -488,33 +488,22 @@ class Reached:
                 for nid in ids:
                     counts.append(count[nid])
                 results_list.append(sum(counts))
-            elif mode == "sum":
-                if values:
-                    results_list.append(
-                        sum(right.loc[right[right_id].isin(ids)][values])
-                    )
-                else:
-                    results_list.append(
-                        sum(right.loc[right[right_id].isin(ids)].geometry.area)
-                    )
-            elif mode == "mean":
-                if values:
-                    results_list.append(
-                        np.nanmean(right.loc[right[right_id].isin(ids)][values])
-                    )
-                else:
-                    results_list.append(
-                        np.nanmean(right.loc[right[right_id].isin(ids)].geometry.area)
-                    )
-            elif mode == "std":
-                if values:
-                    results_list.append(
-                        np.nanstd(right.loc[right[right_id].isin(ids)][values])
-                    )
+            else:
+                if mode == "sum":
+                    func = sum
+                elif mode == "mean":
+                    func = np.nanmean
+                elif mode == "std":
+                    func = np.nanstd
+
+                mask = right[right_id].isin(ids)
+                if mask.any():
+                    if values:
+                        results_list.append(func(right.loc[mask][values]))
+                    else:
+                        results_list.append(func(right.loc[mask].geometry.area))
                 else:
-                    results_list.append(
-                        np.nanstd(right.loc[right[right_id].isin(ids)].geometry.area)
-                    )
+                    results_list.append(np.nan)
 
         self.series = pd.Series(results_list, index=left.index)
 
diff --git a/momepy/preprocessing.py b/momepy/preprocessing.py
index b02c4ba..db1b1a8 100644
--- a/momepy/preprocessing.py
+++ b/momepy/preprocessing.py
@@ -4,6 +4,8 @@
 import collections
 import math
 import operator
+from distutils.version import LooseVersion
+import warnings
 
 import geopandas as gpd
 import libpysal
@@ -23,6 +25,8 @@ __all__ = [
     "extend_lines",
 ]
 
+GPD_10 = str(gpd.__version__) >= LooseVersion("0.10")
+
 
 def preprocess(
     buildings, size=30, compactness=0.2, islands=True, loops=2, verbose=True
@@ -70,8 +74,11 @@ def preprocess(
         GeoDataFrame containing preprocessed geometry
     """
     blg = buildings.copy()
-    blg = blg.explode()
-    blg.reset_index(drop=True, inplace=True)
+    if GPD_10:
+        blg = blg.explode(ignore_index=True)
+    else:
+        blg = blg.explode()
+        blg.reset_index(drop=True, inplace=True)
     for loop in range(0, loops):
         print("Loop", loop + 1, f"out of {loops}.") if verbose else None
         blg.reset_index(inplace=True, drop=True)
@@ -199,8 +206,11 @@ def remove_false_nodes(gdf):
     """
     if isinstance(gdf, (gpd.GeoDataFrame, gpd.GeoSeries)):
         # explode to avoid MultiLineStrings
-        # double reset index due to the bug in GeoPandas explode
-        df = gdf.reset_index(drop=True).explode().reset_index(drop=True)
+        # reset index due to the bug in GeoPandas explode
+        if GPD_10:
+            df = gdf.reset_index(drop=True).explode(ignore_index=True)
+        else:
+            df = gdf.reset_index(drop=True).explode().reset_index(drop=True)
 
         # get underlying pygeos geometry
         geom = df.geometry.values.data
@@ -255,7 +265,10 @@ def remove_false_nodes(gdf):
 
         # remove incorrect geometries and append fixed versions
         df = df.drop(merge)
-        final = gpd.GeoSeries(new).explode().reset_index(drop=True)
+        if GPD_10:
+            final = gpd.GeoSeries(new).explode(ignore_index=True)
+        else:
+            final = gpd.GeoSeries(new).explode().reset_index(drop=True)
         if isinstance(gdf, gpd.GeoDataFrame):
             return df.append(
                 gpd.GeoDataFrame({df.geometry.name: final}, geometry=df.geometry.name),
@@ -348,7 +361,9 @@ class CheckTessellationInput:
 
         if overlap:
             shrink = shrink.reset_index(drop=True)
-            shrink = shrink[~(shrink.is_empty | shrink.geometry.isna())]
+            with warnings.catch_warnings():
+                warnings.filterwarnings("ignore", "GeoSeries.isna", UserWarning)
+                shrink = shrink[~(shrink.is_empty | shrink.geometry.isna())]
             sindex = shrink.sindex
             hits = shrink.bounds.apply(
                 lambda row: list(sindex.intersection(row)), axis=1
@@ -476,8 +491,11 @@ def extend_lines(gdf, tolerance, target=None, barrier=None, extension=0):
 
     """
     # explode to avoid MultiLineStrings
-    # double reset index due to the bug in GeoPandas explode
-    df = gdf.reset_index(drop=True).explode().reset_index(drop=True)
+    # reset index due to the bug in GeoPandas explode
+    if GPD_10:
+        df = gdf.reset_index(drop=True).explode(ignore_index=True)
+    else:
+        df = gdf.reset_index(drop=True).explode().reset_index(drop=True)
 
     if target is None:
         target = df
diff --git a/setup.py b/setup.py
index 8ee025a..f4f0817 100644
--- a/setup.py
+++ b/setup.py
@@ -26,7 +26,7 @@ setup(
     description="Urban Morphology Measuring Toolkit",
     long_description=long_description,
     long_description_content_type="text/markdown",
-    license="MIT",
+    license="BSD",
     author="Martin Fleischmann",
     author_email="martin@martinfleischmann.net",
     keywords=["urban morphology", "urban morphometrics", "tessellation"],
@@ -35,7 +35,7 @@ setup(
     package_data={"momepy": data_files},
     classifiers=[
         "Programming Language :: Python :: 3",
-        "License :: OSI Approved :: MIT License",
+        "License :: OSI Approved :: BSD License",
         "Operating System :: OS Independent",
         "Intended Audience :: Science/Research",
         "Topic :: Scientific/Engineering :: GIS",


Test patch:
diff --git a/tests/test_coins.py b/tests/test_coins.py
index 26576dd..3e87b9a 100644
--- a/tests/test_coins.py
+++ b/tests/test_coins.py
@@ -44,9 +44,7 @@ class TestCOINS:
             ],
             index=expected_index,
         )
-        assert_series_equal(
-            result.length, expected, check_less_precise=6, check_exact=False
-        )
+        assert_series_equal(result.length, expected, atol=0.0001, check_exact=False)
 
     def test_stroke_attribute(self):
         coins = mm.COINS(self.gdf)
diff --git a/tests/test_distribution.py b/tests/test_distribution.py
index 135527d..e90d76c 100644
--- a/tests/test_distribution.py
+++ b/tests/test_distribution.py
@@ -156,7 +156,7 @@ class TestDistribution:
         assert self.df_streets["dev"].mean() == check
 
     def test_BuildingAdjacency(self):
-        sw = Queen.from_dataframe(self.df_buildings, ids="uID")
+        sw = Queen.from_dataframe(self.df_buildings, ids="uID", silence_warnings=True)
         swh = mm.sw_high(k=3, gdf=self.df_tessellation, ids="uID")
         self.df_buildings["adj_sw"] = mm.BuildingAdjacency(
             self.df_buildings,
diff --git a/tests/test_intensity.py b/tests/test_intensity.py
index 01c932e..615ecdb 100644
--- a/tests/test_intensity.py
+++ b/tests/test_intensity.py
@@ -94,7 +94,7 @@ class TestIntensity:
 
     def test_Courtyards(self):
         courtyards = mm.Courtyards(self.df_buildings).series
-        sw = Queen.from_dataframe(self.df_buildings)
+        sw = Queen.from_dataframe(self.df_buildings, silence_warnings=True)
         courtyards_wm = mm.Courtyards(self.df_buildings, sw).series
         check = 0.6805555555555556
         assert courtyards.mean() == check
diff --git a/tests/test_preprocess.py b/tests/test_preprocess.py
index 507b526..8051b50 100644
--- a/tests/test_preprocess.py
+++ b/tests/test_preprocess.py
@@ -1,3 +1,5 @@
+from distutils.version import LooseVersion
+
 import geopandas as gpd
 import momepy as mm
 import numpy as np
@@ -6,6 +8,8 @@ import pytest
 from shapely.geometry import Polygon, MultiPoint, LineString
 from shapely import affinity
 
+GPD_10 = str(gpd.__version__) >= LooseVersion("0.10")
+
 
 class TestPreprocessing:
     def setup_method(self):
@@ -35,7 +39,10 @@ class TestPreprocessing:
         assert len(fixed_series) == 56
         assert isinstance(fixed_series, gpd.GeoSeries)
         # assert self.false_network.crs.equals(fixed_series.crs) GeoPandas 0.8 BUG
-        multiindex = self.false_network.explode()
+        if GPD_10:
+            multiindex = self.false_network.explode(index_parts=True)
+        else:
+            multiindex = self.false_network.explode()
         fixed_multiindex = mm.remove_false_nodes(multiindex)
         assert len(fixed_multiindex) == 56
         assert isinstance(fixed, gpd.GeoDataFrame)
diff --git a/tests/test_utils.py b/tests/test_utils.py
index a504a1b..d19f9b3 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -130,7 +130,9 @@ class TestUtils:
             attributes = dict(zip(list(self.df_streets.columns), data))
             G.add_edge(first, last, key=key, **attributes)
             key += 1
-        nodes, edges = mm.nx_to_gdf(G)
+
+        with pytest.warns(UserWarning, match="Approach is not set"):
+            nodes, edges = mm.nx_to_gdf(G)
         assert len(nodes) == 29
         assert len(edges) == 35
 
@@ -147,7 +149,8 @@ class TestUtils:
     def test_nx_to_gdf_osmnx(self):
         # osmnx compatibility
         G = ox.graph_from_place("Preborov, Czechia", network_type="drive")
-        pts, lines = mm.nx_to_gdf(G)
+        with pytest.warns(UserWarning, match="Approach is not set"):
+            pts, lines = mm.nx_to_gdf(G)
         assert len(pts) == 7
         assert len(lines) == 16