ssb-sgis 1.0.1__py3-none-any.whl → 1.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. sgis/__init__.py +97 -115
  2. sgis/exceptions.py +3 -1
  3. sgis/geopandas_tools/__init__.py +1 -0
  4. sgis/geopandas_tools/bounds.py +75 -38
  5. sgis/geopandas_tools/buffer_dissolve_explode.py +38 -34
  6. sgis/geopandas_tools/centerlines.py +53 -44
  7. sgis/geopandas_tools/cleaning.py +87 -104
  8. sgis/geopandas_tools/conversion.py +149 -101
  9. sgis/geopandas_tools/duplicates.py +31 -17
  10. sgis/geopandas_tools/general.py +76 -48
  11. sgis/geopandas_tools/geometry_types.py +21 -7
  12. sgis/geopandas_tools/neighbors.py +20 -8
  13. sgis/geopandas_tools/overlay.py +136 -53
  14. sgis/geopandas_tools/point_operations.py +9 -8
  15. sgis/geopandas_tools/polygon_operations.py +48 -56
  16. sgis/geopandas_tools/polygons_as_rings.py +121 -78
  17. sgis/geopandas_tools/sfilter.py +14 -14
  18. sgis/helpers.py +114 -56
  19. sgis/io/dapla_functions.py +32 -23
  20. sgis/io/opener.py +13 -6
  21. sgis/io/read_parquet.py +1 -1
  22. sgis/maps/examine.py +39 -26
  23. sgis/maps/explore.py +112 -66
  24. sgis/maps/httpserver.py +12 -12
  25. sgis/maps/legend.py +124 -65
  26. sgis/maps/map.py +66 -41
  27. sgis/maps/maps.py +31 -29
  28. sgis/maps/thematicmap.py +46 -33
  29. sgis/maps/tilesources.py +3 -8
  30. sgis/networkanalysis/_get_route.py +5 -4
  31. sgis/networkanalysis/_od_cost_matrix.py +44 -1
  32. sgis/networkanalysis/_points.py +10 -4
  33. sgis/networkanalysis/_service_area.py +5 -2
  34. sgis/networkanalysis/closing_network_holes.py +20 -62
  35. sgis/networkanalysis/cutting_lines.py +55 -43
  36. sgis/networkanalysis/directednetwork.py +15 -7
  37. sgis/networkanalysis/finding_isolated_networks.py +4 -3
  38. sgis/networkanalysis/network.py +15 -13
  39. sgis/networkanalysis/networkanalysis.py +72 -54
  40. sgis/networkanalysis/networkanalysisrules.py +20 -16
  41. sgis/networkanalysis/nodes.py +2 -3
  42. sgis/networkanalysis/traveling_salesman.py +5 -2
  43. sgis/parallel/parallel.py +337 -127
  44. sgis/raster/__init__.py +6 -0
  45. sgis/raster/base.py +9 -3
  46. sgis/raster/cube.py +280 -208
  47. sgis/raster/cubebase.py +15 -29
  48. sgis/raster/indices.py +3 -7
  49. sgis/raster/methods_as_functions.py +0 -124
  50. sgis/raster/raster.py +313 -127
  51. sgis/raster/torchgeo.py +58 -37
  52. sgis/raster/zonal.py +38 -13
  53. {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.2.dist-info}/LICENSE +1 -1
  54. {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.2.dist-info}/METADATA +87 -16
  55. ssb_sgis-1.0.2.dist-info/RECORD +61 -0
  56. {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.2.dist-info}/WHEEL +1 -1
  57. sgis/raster/bands.py +0 -48
  58. sgis/raster/gradient.py +0 -78
  59. ssb_sgis-1.0.1.dist-info/RECORD +0 -63
@@ -1,59 +1,26 @@
1
- from typing import Any, Callable, Iterable
1
+ from collections.abc import Callable
2
+ from typing import Any
2
3
 
3
- import geopandas as gpd
4
- import igraph
5
- import networkx as nx
6
4
  import numpy as np
7
5
  import pandas as pd
8
- from geopandas import GeoDataFrame, GeoSeries
6
+ from geopandas import GeoDataFrame
7
+ from geopandas import GeoSeries
9
8
  from geopandas.array import GeometryArray
10
- from IPython.display import display
11
- from networkx.algorithms import approximation as approx
12
- from numpy import ndarray
13
9
  from numpy.typing import NDArray
14
- from pandas import Index
15
- from shapely import (
16
- Geometry,
17
- STRtree,
18
- area,
19
- box,
20
- buffer,
21
- centroid,
22
- difference,
23
- distance,
24
- extract_unique_points,
25
- get_coordinates,
26
- get_exterior_ring,
27
- get_interior_ring,
28
- get_num_interior_rings,
29
- get_parts,
30
- intersection,
31
- intersects,
32
- is_empty,
33
- is_ring,
34
- length,
35
- line_merge,
36
- linearrings,
37
- linestrings,
38
- make_valid,
39
- points,
40
- polygons,
41
- segmentize,
42
- simplify,
43
- unary_union,
44
- voronoi_polygons,
45
- )
46
- from shapely.errors import GEOSException
47
- from shapely.geometry import (
48
- LinearRing,
49
- LineString,
50
- MultiLineString,
51
- MultiPoint,
52
- Point,
53
- Polygon,
54
- )
55
-
56
- from .conversion import to_gdf, to_geoseries
10
+ from pyproj import CRS
11
+ from shapely import get_coordinates
12
+ from shapely import get_exterior_ring
13
+ from shapely import get_interior_ring
14
+ from shapely import get_num_interior_rings
15
+ from shapely import linearrings
16
+ from shapely import make_valid
17
+ from shapely import polygons
18
+ from shapely import unary_union
19
+ from shapely.geometry import LinearRing
20
+ from shapely.geometry import Polygon
21
+
22
+ from .conversion import to_gdf
23
+ from .conversion import to_geoseries
57
24
 
58
25
 
59
26
  class PolygonsAsRings:
@@ -62,10 +29,18 @@ class PolygonsAsRings:
62
29
  def __init__(
63
30
  self,
64
31
  polys: GeoDataFrame | GeoSeries | GeometryArray,
65
- crs=None,
32
+ crs: CRS | Any | None = None,
66
33
  allow_multipart: bool = False,
67
34
  gridsize: int | None = None,
68
- ):
35
+ ) -> None:
36
+ """Initialize the PolygonsAsRings object with polygons and optional CRS information.
37
+
38
+ Args:
39
+ polys: GeoDataFrame, GeoSeries, or GeometryArray containing polygon geometries.
40
+ crs: Coordinate Reference System to be used, defaults to None.
41
+ allow_multipart: Allow multipart polygons if True, defaults to False.
42
+ gridsize: Size of the grid for any grid operations, defaults to None.
43
+ """
69
44
  if not isinstance(polys, (pd.DataFrame, pd.Series, GeometryArray)):
70
45
  raise TypeError(type(polys))
71
46
 
@@ -125,7 +100,16 @@ class PolygonsAsRings:
125
100
 
126
101
  self.rings = pd.concat([exterior, interiors])
127
102
 
128
- def get_rings(self, agg: bool = False):
103
+ def get_rings(self, agg: bool = False) -> GeoDataFrame | GeoSeries | np.ndarray:
104
+ """Retrieve rings from the polygons, optionally aggregating them.
105
+
106
+ Args:
107
+ agg: If True, aggregate the rings into single geometries.
108
+
109
+ Returns:
110
+ The rings either aggregated or separated, in the type of
111
+ the input polygons.
112
+ """
129
113
  gdf = self.gdf.copy()
130
114
  rings = self.rings.copy()
131
115
  if not len(rings):
@@ -144,9 +128,21 @@ class PolygonsAsRings:
144
128
  return self.polyclass(gdf.geometry.values)
145
129
 
146
130
  def apply_numpy_func_to_interiors(
147
- self, func: Callable, args: tuple | None = None, kwargs: dict | None = None
148
- ):
149
- """Run an array function on only the interior rings of the polygons."""
131
+ self,
132
+ func: Callable,
133
+ args: tuple | None = None,
134
+ kwargs: dict | None = None,
135
+ ) -> "PolygonsAsRings":
136
+ """Apply a numpy function specifically to the interior rings of the polygons.
137
+
138
+ Args:
139
+ func: Numpy function to apply.
140
+ args: Tuple of positional arguments for the function.
141
+ kwargs: Dictionary of keyword arguments for the function.
142
+
143
+ Returns:
144
+ PolygonsAsRings: The instance itself after applying the function.
145
+ """
150
146
  kwargs = kwargs or {}
151
147
  args = args or ()
152
148
  arr: NDArray[LinearRing] = self.rings.loc[self.is_interior].values
@@ -159,9 +155,21 @@ class PolygonsAsRings:
159
155
  return self
160
156
 
161
157
  def apply_numpy_func(
162
- self, func: Callable, args: tuple | None = None, kwargs: dict | None = None
163
- ):
164
- """Run a function that takes an array of lines/rings and returns an array of lines/rings."""
158
+ self,
159
+ func: Callable,
160
+ args: tuple | None = None,
161
+ kwargs: dict | None = None,
162
+ ) -> "PolygonsAsRings":
163
+ """Apply a numpy function to all rings of the polygons.
164
+
165
+ Args:
166
+ func: Numpy function to apply.
167
+ args: Tuple of positional arguments for the function.
168
+ kwargs: Dictionary of keyword arguments for the function.
169
+
170
+ Returns:
171
+ PolygonsAsRings: The instance itself after applying the function.
172
+ """
165
173
  kwargs = kwargs or {}
166
174
  args = args or ()
167
175
 
@@ -172,18 +180,30 @@ class PolygonsAsRings:
172
180
  f"Different length of results. Got {len(results)} and {len(self.rings)} original rings"
173
181
  )
174
182
 
175
- self.rings.loc[:] = results
183
+ self.rings.loc[:] = results # type: ignore [call-overload]
176
184
 
177
185
  return self
178
186
 
179
187
  def apply_geoseries_func(
180
- self, func: Callable, args: tuple | None = None, kwargs: dict | None = None
181
- ):
182
- """Run a function that takes a GeoSeries and returns a GeoSeries."""
188
+ self,
189
+ func: Callable,
190
+ args: tuple | None = None,
191
+ kwargs: dict | None = None,
192
+ ) -> "PolygonsAsRings":
193
+ """Apply a function that operates on a GeoSeries to the rings.
194
+
195
+ Args:
196
+ func: Function to apply that expects a GeoSeries.
197
+ args: Tuple of positional arguments for the function.
198
+ kwargs: Dictionary of keyword arguments for the function.
199
+
200
+ Returns:
201
+ PolygonsAsRings: The instance itself after applying the function.
202
+ """
183
203
  kwargs = kwargs or {}
184
204
  args = args or ()
185
205
 
186
- self.rings.loc[:] = np.array(
206
+ self.rings.loc[:] = np.array( # type: ignore [call-overload]
187
207
  func(
188
208
  GeoSeries(
189
209
  self.rings.values,
@@ -198,9 +218,21 @@ class PolygonsAsRings:
198
218
  return self
199
219
 
200
220
  def apply_gdf_func(
201
- self, func: Callable, args: tuple | None = None, kwargs: dict | None = None
202
- ):
203
- """Run a function that takes a GeoDataFrame and returns a GeoDataFrame."""
221
+ self,
222
+ func: Callable,
223
+ args: tuple | None = None,
224
+ kwargs: dict | None = None,
225
+ ) -> "PolygonsAsRings":
226
+ """Apply a function that operates on a GeoDataFrame to the rings.
227
+
228
+ Args:
229
+ func: Function to apply that expects a GeoDataFrame.
230
+ args: Tuple of positional arguments for the function.
231
+ kwargs: Dictionary of keyword arguments for the function.
232
+
233
+ Returns:
234
+ PolygonsAsRings: The instance itself after applying the function.
235
+ """
204
236
  kwargs = kwargs or {}
205
237
  args = args or ()
206
238
 
@@ -214,7 +246,7 @@ class PolygonsAsRings:
214
246
 
215
247
  gdf.index = self.rings.index
216
248
 
217
- self.rings.loc[:] = func(
249
+ self.rings.loc[:] = func( # type: ignore [call-overload]
218
250
  gdf,
219
251
  *args,
220
252
  **kwargs,
@@ -223,15 +255,17 @@ class PolygonsAsRings:
223
255
  return self
224
256
 
225
257
  @property
226
- def is_interior(self):
258
+ def is_interior(self) -> bool:
259
+ """Returns a boolean Series of whether the row is an interior ring."""
227
260
  return self.rings.index.get_level_values(0) == 1
228
261
 
229
262
  @property
230
- def is_exterior(self):
263
+ def is_exterior(self) -> bool:
264
+ """Returns a boolean Series of whether the row is an exterior ring."""
231
265
  return self.rings.index.get_level_values(0) == 0
232
266
 
233
267
  @property
234
- def _interiors_index(self):
268
+ def _interiors_index(self) -> pd.MultiIndex:
235
269
  """A three-leveled MultiIndex.
236
270
 
237
271
  Used to separate interior and exterior and sort the interior in
@@ -254,7 +288,7 @@ class PolygonsAsRings:
254
288
  )
255
289
 
256
290
  @property
257
- def _exterior_index(self):
291
+ def _exterior_index(self) -> pd.MultiIndex:
258
292
  """A three-leveled MultiIndex.
259
293
 
260
294
  Used to separate interior and exterior in the 'to_numpy' method.
@@ -274,8 +308,8 @@ class PolygonsAsRings:
274
308
  self.gdf.geometry = self.to_numpy()
275
309
  return self.gdf
276
310
 
277
- def to_geoseries(self) -> GeoDataFrame:
278
- """Return the GeoDataFrame with polygons."""
311
+ def to_geoseries(self) -> GeoSeries:
312
+ """Return the GeoSeries with polygons."""
279
313
  self.gdf.geometry = self.to_numpy()
280
314
  return self.gdf.geometry
281
315
 
@@ -293,7 +327,7 @@ class PolygonsAsRings:
293
327
  try:
294
328
  return make_valid(polygons(exterior.values))
295
329
  except Exception:
296
- return _geoms_to_linearrings_fallback(exterior)
330
+ return _geoms_to_linearrings_fallback(exterior).values
297
331
 
298
332
  empty_interiors = pd.Series(
299
333
  [None for _ in range(len(self.gdf) * self.max_rings)],
@@ -312,10 +346,18 @@ class PolygonsAsRings:
312
346
  try:
313
347
  return make_valid(polygons(exterior.values, interiors.values))
314
348
  except Exception:
315
- return _geoms_to_linearrings_fallback(exterior, interiors)
349
+ return _geoms_to_linearrings_fallback(exterior, interiors).values
350
+
351
+
352
+ def get_linearring_series(geoms: GeoDataFrame | GeoSeries) -> pd.Series:
353
+ """Convert geometries into a series of LinearRings.
316
354
 
355
+ Args:
356
+ geoms: GeoDataFrame or GeoSeries from which to extract LinearRings.
317
357
 
318
- def get_linearring_series(geoms: Any) -> pd.Series:
358
+ Returns:
359
+ pd.Series: A series containing LinearRings.
360
+ """
319
361
  geoms = to_geoseries(geoms).explode(index_parts=False)
320
362
  coords, indices = get_coordinates(geoms, return_index=True)
321
363
  return pd.Series(linearrings(coords, indices=indices), index=geoms.index)
@@ -325,6 +367,7 @@ def _geoms_to_linearrings_fallback(
325
367
  exterior: pd.Series, interiors: pd.Series | None = None
326
368
  ) -> pd.Series:
327
369
  exterior.index = exterior.index.get_level_values(1)
370
+ assert exterior.index.is_monotonic_increasing
328
371
 
329
372
  exterior = get_linearring_series(exterior)
330
373
 
@@ -2,12 +2,12 @@ import warnings
2
2
 
3
3
  import numpy as np
4
4
  import pandas as pd
5
- from geopandas import GeoDataFrame, GeoSeries
5
+ from geopandas import GeoDataFrame
6
+ from geopandas import GeoSeries
6
7
  from shapely import Geometry
7
8
 
8
9
  from .conversion import to_gdf
9
10
 
10
-
11
11
  gdf_type_error_message = "'gdf' should be of type GeoDataFrame or GeoSeries."
12
12
 
13
13
 
@@ -34,9 +34,9 @@ def sfilter(
34
34
  A copy of 'gdf' with only the rows matching the
35
35
  spatial predicate with 'other'.
36
36
 
37
- Examples
37
+ Examples:
38
38
  --------
39
-
39
+ >>> import sgis as sg
40
40
  >>> df1 = sg.to_gdf([(0, 0), (0, 1)])
41
41
  >>> df1
42
42
  geometry
@@ -71,7 +71,7 @@ def sfilter(
71
71
  0 POINT (0.00000 0.00000)
72
72
 
73
73
  """
74
- if not isinstance(gdf, (GeoDataFrame, GeoSeries)):
74
+ if not isinstance(gdf, (GeoDataFrame | GeoSeries)):
75
75
  raise TypeError(gdf_type_error_message)
76
76
 
77
77
  other = _sfilter_checks(other, crs=gdf.crs)
@@ -100,9 +100,9 @@ def sfilter_split(
100
100
  A tuple of GeoDataFrames, one with the rows that match the spatial predicate
101
101
  and one with the rows that do not.
102
102
 
103
- Examples
103
+ Examples:
104
104
  --------
105
-
105
+ >>> import sgis as sg
106
106
  >>> df1 = sg.to_gdf([(0, 0), (0, 1)])
107
107
  >>> df1
108
108
  geometry
@@ -140,7 +140,7 @@ def sfilter_split(
140
140
  >>> not_intersecting = df1.loc[~filt]
141
141
 
142
142
  """
143
- if not isinstance(gdf, (GeoDataFrame, GeoSeries)):
143
+ if not isinstance(gdf, (GeoDataFrame | GeoSeries)):
144
144
  raise TypeError(gdf_type_error_message)
145
145
 
146
146
  other = _sfilter_checks(other, crs=gdf.crs)
@@ -149,7 +149,7 @@ def sfilter_split(
149
149
 
150
150
  return (
151
151
  gdf.iloc[indices],
152
- gdf.iloc[pd.Index(range(len(gdf))).difference(indices)],
152
+ gdf.iloc[pd.Index(range(len(gdf))).difference(pd.Index(indices))],
153
153
  )
154
154
 
155
155
 
@@ -171,9 +171,9 @@ def sfilter_inverse(
171
171
  A copy of 'gdf' with only the rows that do not match the
172
172
  spatial predicate with 'other'.
173
173
 
174
- Examples
174
+ Examples:
175
175
  --------
176
-
176
+ >>> import sgis as sg
177
177
  >>> df1 = sg.to_gdf([(0, 0), (0, 1)])
178
178
  >>> df1
179
179
  geometry
@@ -205,14 +205,14 @@ def sfilter_inverse(
205
205
  >>> not_intersecting = df1.loc[~df1.intersects(df2.unary_union)]
206
206
 
207
207
  """
208
- if not isinstance(gdf, (GeoDataFrame, GeoSeries)):
208
+ if not isinstance(gdf, (GeoDataFrame | GeoSeries)):
209
209
  raise TypeError(gdf_type_error_message)
210
210
 
211
211
  other = _sfilter_checks(other, crs=gdf.crs)
212
212
 
213
213
  indices = _get_sfilter_indices(gdf, other, predicate)
214
214
 
215
- return gdf.iloc[pd.Index(range(len(gdf))).difference(indices)]
215
+ return gdf.iloc[pd.Index(range(len(gdf))).difference(pd.Index(indices))]
216
216
 
217
217
 
218
218
  def _sfilter_checks(other, crs):
@@ -256,7 +256,7 @@ def _get_sfilter_indices(
256
256
  predicate : string
257
257
  Binary predicate to query.
258
258
 
259
- Returns
259
+ Returns:
260
260
  -------
261
261
  DataFrame
262
262
  DataFrame with matching indices in
sgis/helpers.py CHANGED
@@ -1,15 +1,29 @@
1
1
  """Small helper functions."""
2
+
2
3
  import glob
3
4
  import inspect
4
5
  import os
5
6
  import warnings
6
7
  from collections.abc import Callable
8
+ from collections.abc import Generator
9
+ from pathlib import Path
10
+ from typing import Any
7
11
 
8
12
  import numpy as np
13
+ import pandas as pd
9
14
  from geopandas import GeoDataFrame
10
15
 
11
16
 
12
- def get_numpy_func(text, error_message: str | None = None) -> Callable:
17
+ def get_numpy_func(text: str, error_message: str | None = None) -> Callable:
18
+ """Fetch a numpy function based on its name.
19
+
20
+ Args:
21
+ text: The name of the numpy function to retrieve.
22
+ error_message: Custom error message if the function is not found.
23
+
24
+ Returns:
25
+ The numpy function corresponding to the provided text.
26
+ """
13
27
  f = getattr(np, text, None)
14
28
  if f is not None:
15
29
  return f
@@ -19,20 +33,36 @@ def get_numpy_func(text, error_message: str | None = None) -> Callable:
19
33
  raise ValueError(error_message)
20
34
 
21
35
 
22
- def get_func_name(func):
36
+ def get_func_name(func: Callable) -> str:
37
+ """Return the name of a function.
38
+
39
+ Args:
40
+ func: The function object whose name is to be retrieved.
41
+
42
+ Returns:
43
+ The name of the function.
44
+ """
23
45
  try:
24
46
  return func.__name__
25
47
  except AttributeError:
26
48
  return str(func)
27
49
 
28
50
 
29
- def get_non_numpy_func_name(f):
51
+ def get_non_numpy_func_name(f: Callable | str) -> str:
30
52
  if callable(f):
31
53
  return f.__name__
32
54
  return str(f).replace("np.", "").replace("numpy.", "")
33
55
 
34
56
 
35
- def to_numpy_func(text):
57
+ def to_numpy_func(text: str) -> Callable:
58
+ """Convert a text identifier into a numpy function.
59
+
60
+ Args:
61
+ text: Name of the numpy function.
62
+
63
+ Returns:
64
+ The numpy function.
65
+ """
36
66
  f = getattr(np, text, None)
37
67
  if f is not None:
38
68
  return f
@@ -42,13 +72,22 @@ def to_numpy_func(text):
42
72
  raise ValueError
43
73
 
44
74
 
45
- def is_property(obj, attribute) -> bool:
75
+ def is_property(obj: object, attribute: str) -> bool:
76
+ """Determine if a class attribute is a property.
77
+
78
+ Args:
79
+ obj: The object to check.
80
+ attribute: The attribute name to check on the object.
81
+
82
+ Returns:
83
+ True if the attribute is a property, False otherwise.
84
+ """
46
85
  return hasattr(obj.__class__, attribute) and isinstance(
47
86
  getattr(obj.__class__, attribute), property
48
87
  )
49
88
 
50
89
 
51
- def dict_zip_intersection(*dicts):
90
+ def dict_zip_intersection(*dicts: dict) -> Generator[tuple[Any, ...], None, None]:
52
91
  """From mCoding (YouTube)."""
53
92
  if not dicts:
54
93
  return
@@ -58,7 +97,9 @@ def dict_zip_intersection(*dicts):
58
97
  yield key, *(d[key] for d in dicts)
59
98
 
60
99
 
61
- def dict_zip_union(*dicts, fillvalue=None):
100
+ def dict_zip_union(
101
+ *dicts: dict, fillvalue: Any | None = None
102
+ ) -> Generator[tuple[Any, ...], None, None]:
62
103
  """From mCoding (YouTube)."""
63
104
  if not dicts:
64
105
  return
@@ -68,7 +109,7 @@ def dict_zip_union(*dicts, fillvalue=None):
68
109
  yield key, *(d.get(key, fillvalue) for d in dicts)
69
110
 
70
111
 
71
- def dict_zip(*dicts):
112
+ def dict_zip(*dicts: dict) -> Generator[tuple[Any, ...], None, None]:
72
113
  """From mCoding (YouTube)."""
73
114
  if not dicts:
74
115
  return
@@ -81,15 +122,24 @@ def dict_zip(*dicts):
81
122
  yield key, first_val, *(other[key] for other in dicts[1:])
82
123
 
83
124
 
84
- def in_jupyter():
125
+ def in_jupyter() -> bool:
85
126
  try:
86
- get_ipython
127
+ get_ipython # type: ignore[name-defined]
87
128
  return True
88
129
  except NameError:
89
130
  return False
90
131
 
91
132
 
92
- def get_all_files(root, recursive=True):
133
+ def get_all_files(root: str, recursive: bool = True) -> list[str]:
134
+ """Fetch all files in a directory.
135
+
136
+ Args:
137
+ root: The root directory path.
138
+ recursive: Whether to include subdirectories.
139
+
140
+ Returns:
141
+ A list of file paths.
142
+ """
93
143
  if not recursive:
94
144
  return [path for path in glob.glob(str(Path(root)) + "/*")]
95
145
  paths = []
@@ -101,8 +151,8 @@ def get_all_files(root, recursive=True):
101
151
 
102
152
 
103
153
  def return_two_vals(
104
- vals: tuple[str | None, str | None] | list[str] | str | int | float
105
- ) -> tuple[str | int | float, str | int | float | None]:
154
+ vals: tuple[str, str] | list[str] | str | int | float
155
+ ) -> tuple[str | int | float, str | int | float]:
106
156
  """Return a two-length tuple from a str/int/float or list/tuple of length 1 or 2.
107
157
 
108
158
  Returns 'vals' as a 2-length tuple. If the input is a string, return
@@ -117,7 +167,7 @@ def return_two_vals(
117
167
  """
118
168
  if isinstance(vals, str):
119
169
  return vals, vals
120
- if hasattr(vals, "__iter__"):
170
+ if isinstance(vals, (tuple, list)):
121
171
  if len(vals) == 2:
122
172
  return vals[0], vals[1]
123
173
  if len(vals) == 1:
@@ -156,44 +206,31 @@ def unit_is_degrees(gdf: GeoDataFrame) -> bool:
156
206
  def get_object_name(
157
207
  var: object, start: int = 2, stop: int = 7, ignore_self: bool = True
158
208
  ) -> str | None:
159
- """Searches through the local variables down one level at a time."""
160
- frame = inspect.currentframe()
161
-
162
- for _ in range(start):
163
- frame = frame.f_back
164
-
165
- for _ in np.arange(start, stop):
166
- names = [
167
- var_name for var_name, var_val in frame.f_locals.items() if var_val is var
168
- ]
169
- if names and len(names) == 1:
170
- if ignore_self and names[0] == "self":
171
- frame = frame.f_back
172
- continue
173
- return names[0]
174
-
175
- names = [name for name in names if not name.startswith("_")]
176
-
177
- if names and len(names) == 1:
178
- if ignore_self and names[0] == "self":
179
- frame = frame.f_back
180
- continue
181
-
182
- return names[0]
183
-
184
- if names and len(names) > 1:
185
- if ignore_self and names[0] == "self":
186
- frame = frame.f_back
187
- continue
188
- warnings.warn(
189
- "More than one local variable matches the object. Name might be wrong."
190
- )
191
- return names[0]
192
-
193
- frame = frame.f_back
194
-
195
- if not frame:
196
- return
209
+ frame = inspect.currentframe() # frame can be FrameType or None
210
+ if frame:
211
+ try:
212
+ for _ in range(start):
213
+ frame = frame.f_back if frame else None
214
+ for _ in range(start, stop):
215
+ if frame:
216
+ names = [
217
+ var_name
218
+ for var_name, var_val in frame.f_locals.items()
219
+ if var_val is var and not (ignore_self and var_name == "self")
220
+ ]
221
+ names = [name for name in names if not name.startswith("_")]
222
+ if names:
223
+ if len(names) != 1:
224
+ warnings.warn(
225
+ "More than one local variable matches the object. Name might be wrong.",
226
+ stacklevel=2,
227
+ )
228
+ return names[0]
229
+ frame = frame.f_back if frame else None
230
+ finally:
231
+ if frame:
232
+ del frame # Explicitly delete frame reference to assist with garbage collection
233
+ return None
197
234
 
198
235
 
199
236
  def make_namedict(gdfs: tuple[GeoDataFrame]) -> dict[int, str]:
@@ -207,7 +244,16 @@ def make_namedict(gdfs: tuple[GeoDataFrame]) -> dict[int, str]:
207
244
  return namedict
208
245
 
209
246
 
210
- def sort_nans_last(df, ignore_index: bool = False):
247
+ def sort_nans_last(df: pd.DataFrame, ignore_index: bool = False) -> pd.DataFrame:
248
+ """Sort a DataFrame placing rows with the most NaNs last.
249
+
250
+ Args:
251
+ df: DataFrame to sort.
252
+ ignore_index: If True, the index will be reset.
253
+
254
+ Returns:
255
+ Sorted DataFrame with NaNs last.
256
+ """
211
257
  if not len(df):
212
258
  return df
213
259
  df["n_nan"] = df.isna().sum(axis=1).values
@@ -219,7 +265,15 @@ def sort_nans_last(df, ignore_index: bool = False):
219
265
  return df.reset_index(drop=True) if ignore_index else df
220
266
 
221
267
 
222
- def is_number(text) -> bool:
268
+ def is_number(text: str) -> bool:
269
+ """Check if a string can be converted to a number.
270
+
271
+ Args:
272
+ text: The string to check.
273
+
274
+ Returns:
275
+ True if the string can be converted to a number, False otherwise.
276
+ """
223
277
  try:
224
278
  float(text)
225
279
  return True
@@ -228,10 +282,14 @@ def is_number(text) -> bool:
228
282
 
229
283
 
230
284
  class LocalFunctionError(ValueError):
231
- def __init__(self, func: str):
285
+ """Exception for when a locally defined function is used in Jupyter, which is incompatible with multiprocessing."""
286
+
287
+ def __init__(self, func: Callable) -> None:
288
+ """Initialiser."""
232
289
  self.func = func.__name__
233
290
 
234
- def __str__(self):
291
+ def __str__(self) -> str:
292
+ """Error message representation."""
235
293
  return (
236
294
  f"{self.func}. "
237
295
  "In Jupyter, functions to be parallelized must \n"