pysits 1.5.3.dev0__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 (73) hide show
  1. LICENSE +339 -0
  2. README.md +83 -0
  3. pysits/__init__.py +243 -0
  4. pysits/backend/__init__.py +18 -0
  5. pysits/backend/functions.py +50 -0
  6. pysits/backend/loaders.py +92 -0
  7. pysits/backend/pkgs.py +35 -0
  8. pysits/conversions/__init__.py +18 -0
  9. pysits/conversions/clojure.py +68 -0
  10. pysits/conversions/common.py +280 -0
  11. pysits/conversions/decorators.py +148 -0
  12. pysits/conversions/dsl/__init__.py +18 -0
  13. pysits/conversions/dsl/base.py +43 -0
  14. pysits/conversions/dsl/mask.py +337 -0
  15. pysits/conversions/dsl/tuning.py +129 -0
  16. pysits/conversions/tibble.py +444 -0
  17. pysits/conversions/tibble_arrow.py +469 -0
  18. pysits/conversions/vector.py +137 -0
  19. pysits/conversions/xarray.py +207 -0
  20. pysits/docs/__init__.py +22 -0
  21. pysits/docs/content/sits_get_data.md +85 -0
  22. pysits/docs/decorators.py +58 -0
  23. pysits/extras/__init__.py +82 -0
  24. pysits/extras/earthdatalogin.py +44 -0
  25. pysits/extras/torch.py +44 -0
  26. pysits/jinja.py +56 -0
  27. pysits/models/__init__.py +18 -0
  28. pysits/models/base.py +34 -0
  29. pysits/models/data/__init__.py +22 -0
  30. pysits/models/data/accuracy.py +94 -0
  31. pysits/models/data/base.py +51 -0
  32. pysits/models/data/cube.py +210 -0
  33. pysits/models/data/frame.py +138 -0
  34. pysits/models/data/frame_accessor.py +133 -0
  35. pysits/models/data/matrix.py +119 -0
  36. pysits/models/data/table.py +49 -0
  37. pysits/models/data/ts.py +161 -0
  38. pysits/models/data/tuning.py +307 -0
  39. pysits/models/data/vector.py +47 -0
  40. pysits/models/frame.py +191 -0
  41. pysits/models/ml.py +27 -0
  42. pysits/models/resolver.py +147 -0
  43. pysits/settings.py +44 -0
  44. pysits/sits/__init__.py +18 -0
  45. pysits/sits/classification.py +42 -0
  46. pysits/sits/colors.py +50 -0
  47. pysits/sits/config.py +45 -0
  48. pysits/sits/context.py +36 -0
  49. pysits/sits/cube.py +214 -0
  50. pysits/sits/data.py +220 -0
  51. pysits/sits/exporters/__init__.py +29 -0
  52. pysits/sits/exporters/files.py +37 -0
  53. pysits/sits/exporters/sf.py +30 -0
  54. pysits/sits/exporters/xarray.py +65 -0
  55. pysits/sits/impute.py +38 -0
  56. pysits/sits/ml.py +110 -0
  57. pysits/sits/segment.py +38 -0
  58. pysits/sits/tiles.py +35 -0
  59. pysits/sits/ts.py +216 -0
  60. pysits/sits/tuning.py +82 -0
  61. pysits/sits/utils.py +126 -0
  62. pysits/sits/visualization.py +122 -0
  63. pysits/templates/cube.html +196 -0
  64. pysits/templates/tuning.html +183 -0
  65. pysits/visualization/__init__.py +24 -0
  66. pysits/visualization/base.py +163 -0
  67. pysits/visualization/image.py +48 -0
  68. pysits/visualization/leaflet.py +94 -0
  69. pysits/visualization/tmap.py +85 -0
  70. pysits-1.5.3.dev0.dist-info/METADATA +123 -0
  71. pysits-1.5.3.dev0.dist-info/RECORD +73 -0
  72. pysits-1.5.3.dev0.dist-info/WHEEL +4 -0
  73. pysits-1.5.3.dev0.dist-info/licenses/LICENSE +339 -0
@@ -0,0 +1,50 @@
1
+ #
2
+ # Copyright (C) 2025 sits developers.
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify it
5
+ # under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 2 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
16
+ #
17
+
18
+ """backend functions."""
19
+
20
+ from pysits.backend.loaders import load_function_from_package
21
+
22
+ # Base - plot
23
+ r_fnc_plot = load_function_from_package("base::plot")
24
+
25
+ # Base - set column
26
+ r_fnc_set_column = load_function_from_package("base::$<-")
27
+
28
+ # Base - summary (base)
29
+ r_fnc_summary = load_function_from_package("base::summary")
30
+
31
+ # Base - readRDS (base)
32
+ r_fnc_read_rds = load_function_from_package("base::readRDS")
33
+
34
+ # Base - system.file (base)
35
+ r_fnc_system_file = load_function_from_package("base::system_file")
36
+
37
+ # Base - set.seed (base)
38
+ r_fnc_set_seed = load_function_from_package("base::set_seed")
39
+
40
+ # Base - class (base)
41
+ r_fnc_class = load_function_from_package("base::class")
42
+
43
+ # Base - as.data.frame (base)
44
+ r_fnc_as_data_frame = load_function_from_package("base::as_data_frame")
45
+
46
+ # Base - colnames (base)
47
+ r_fnc_colnames = load_function_from_package("base::colnames")
48
+
49
+ # Base - rownames (base)
50
+ r_fnc_rownames = load_function_from_package("base::rownames")
@@ -0,0 +1,92 @@
1
+ #
2
+ # Copyright (C) 2025 sits developers.
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify it
5
+ # under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 2 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
16
+ #
17
+
18
+ """backend loaders."""
19
+
20
+ from collections.abc import Callable
21
+ from typing import Any
22
+
23
+ from rpy2.robjects import r
24
+ from rpy2.robjects.packages import importr
25
+
26
+
27
+ def load_data_from_package(name: str, package: str, **kwargs) -> object:
28
+ """Load data from package.
29
+
30
+ This function loads data from a package. It uses `data` behind the scenes.
31
+
32
+ Args:
33
+ name (str): Dataset name.
34
+
35
+ package (str): Package name.
36
+
37
+ **kwargs: Additional arguments to pass to the function.
38
+ """
39
+ return r.data(name, package=package, **kwargs)
40
+
41
+
42
+ def load_data_from_global(name: str) -> object:
43
+ """Load data from global environment.
44
+
45
+ This function loads data from the global environment.
46
+
47
+ Args:
48
+ name (str): Dataset name.
49
+ """
50
+ return r[name]
51
+
52
+
53
+ def load_function_from_package(name: str) -> Callable[..., Any]:
54
+ """Load an R function from a specified package.
55
+
56
+ This function takes a fully qualified R function name in the format
57
+ 'package::function' and returns a callable Python wrapper for that R function.
58
+
59
+ Args:
60
+ name (str): The fully qualified name of the R function in the format
61
+ 'package::function'. For example, 'stats::median' or 'base::mean'.
62
+
63
+ Returns:
64
+ Callable[..., Any]: A Python callable that wraps the R function.
65
+ The exact signature depends on the underlying R function.
66
+
67
+ Raises:
68
+ ValueError: If the ``name`` doesn't follow the 'package::function' format.
69
+
70
+ PackageNotFoundError: If the specified R package is not installed.
71
+
72
+ AttributeError: If the function doesn't exist in the specified package.
73
+
74
+ Examples:
75
+ >>> median_func = load_function_from_package('stats::median')
76
+ >>> result = median_func([1, 2, 3, 4, 5])
77
+ """
78
+ # Parse package and function
79
+ try:
80
+ package_name, func_name = name.split("::")
81
+
82
+ except ValueError as e:
83
+ raise ValueError(
84
+ f"Invalid function name format: {name}. "
85
+ "Expected format: 'package::function'"
86
+ ) from e
87
+
88
+ # Import the package
89
+ pkg = importr(package_name, on_conflict="warn")
90
+
91
+ # Return function
92
+ return getattr(pkg, func_name)
pysits/backend/pkgs.py ADDED
@@ -0,0 +1,35 @@
1
+ #
2
+ # Copyright (C) 2025 sits developers.
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify it
5
+ # under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 2 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
16
+ #
17
+
18
+ """backend packages."""
19
+
20
+ from rpy2.robjects.packages import importr
21
+
22
+ # system pakage
23
+ r_pkg_base = importr("base")
24
+ r_pkg_grdevices = importr("grDevices")
25
+
26
+ # sits package
27
+ r_pkg_sits = importr("sits")
28
+
29
+ # sits-dependencies packages
30
+ r_pkg_tibble = importr("tibble")
31
+ r_pkg_leaflet = importr("leaflet")
32
+ r_pkg_kohonen = importr("kohonen")
33
+ r_pkg_sf = importr("sf")
34
+ r_pkg_htmlwidgets = importr("htmlwidgets")
35
+ r_pkg_arrow = importr("arrow")
@@ -0,0 +1,18 @@
1
+ #
2
+ # Copyright (C) 2025 sits developers.
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify it
5
+ # under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 2 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
16
+ #
17
+
18
+ """conversions module."""
@@ -0,0 +1,68 @@
1
+ #
2
+ # Copyright (C) 2025 sits developers.
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify it
5
+ # under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 2 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
16
+ #
17
+
18
+ """Clojure utilities."""
19
+
20
+ from collections.abc import Callable
21
+ from typing import Any, ParamSpec, TypeVar
22
+
23
+ from pysits.backend.pkgs import r_pkg_sits
24
+ from pysits.conversions.decorators import rpy2_fix_type, rpy2_fix_type_custom
25
+
26
+ #
27
+ # Generics
28
+ #
29
+ R = TypeVar("R")
30
+ P = ParamSpec("P")
31
+
32
+
33
+ #
34
+ # Factory function
35
+ #
36
+ def closure_factory(
37
+ name: str, converters: dict[str, Callable[[Any], Any]] = {}
38
+ ) -> Callable[P, R]:
39
+ """Factory to create sits-based closure functions.
40
+
41
+ This function creates a closure that wraps R package sits functions,
42
+ making them callable from Python. It checks if the requested function
43
+ exists in the R sits package before creating the wrapper.
44
+
45
+ Args:
46
+ name (str): Name of the sits-based function to be wrapped from the R package.
47
+
48
+ Returns:
49
+ Callable[P, R]: A wrapped function that forwards calls to the R sits package.
50
+ The function accepts any arguments (*args, **kwargs) and returns the result
51
+ from the R function.
52
+
53
+ Raises:
54
+ ValueError: If the specified function name does not exist in the R sits package.
55
+ """
56
+ if not hasattr(r_pkg_sits, name):
57
+ raise ValueError(f"Invalid function: {name}")
58
+
59
+ # define method closure
60
+ @rpy2_fix_type_custom(converters)
61
+ @rpy2_fix_type
62
+ def _fnc(*args: P.args, **kwargs: P.kwargs) -> R:
63
+ return getattr(r_pkg_sits, name)(*args, **kwargs)
64
+
65
+ # set function name
66
+ _fnc.__name__ = name
67
+
68
+ return _fnc
@@ -0,0 +1,280 @@
1
+ #
2
+ # Copyright (C) 2025 sits developers.
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify it
5
+ # under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 2 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
16
+ #
17
+
18
+ """Common conversions."""
19
+
20
+ from datetime import date, timedelta
21
+ from pathlib import Path, PosixPath
22
+
23
+ import rpy2.robjects as ro
24
+ from geopandas import GeoDataFrame as GeoPandasDataFrame
25
+ from pandas import DataFrame as PandasDataFrame
26
+ from rpy2.rinterface_lib.sexp import NULLType
27
+ from rpy2.robjects import pandas2ri
28
+ from rpy2.robjects.robject import RObjectMixin
29
+
30
+ from pysits.backend.pkgs import r_pkg_tibble
31
+ from pysits.conversions.dsl.base import DSLObject
32
+ from pysits.conversions.tibble import geopandas_to_tibble, pandas_to_tibble
33
+
34
+ #
35
+ # Type mapping dictionary
36
+ #
37
+ TYPE_CONVERSIONS = {
38
+ list: lambda obj: convert_list_like_to_r(obj),
39
+ tuple: lambda obj: convert_list_like_to_r(obj),
40
+ set: lambda obj: convert_list_like_to_r(list(obj)),
41
+ dict: lambda obj: convert_dict_like_to_r(obj),
42
+ bool: lambda obj: ro.BoolVector([obj]),
43
+ int: lambda obj: ro.IntVector([obj]),
44
+ float: lambda obj: ro.FloatVector([obj]),
45
+ str: lambda obj: ro.StrVector([obj]),
46
+ Path: lambda obj: ro.StrVector([obj.as_posix()]),
47
+ PosixPath: lambda obj: ro.StrVector([obj.as_posix()]),
48
+ PandasDataFrame: lambda obj: r_pkg_tibble.as_tibble(pandas_to_tibble(obj)),
49
+ GeoPandasDataFrame: lambda obj: geopandas_to_tibble(obj),
50
+ }
51
+
52
+
53
+ #
54
+ # R epoch reference
55
+ #
56
+ EPOCH_START = date(1970, 1, 1)
57
+
58
+
59
+ #
60
+ # Base utilities
61
+ #
62
+ def _is_numeric(x: int | float) -> bool:
63
+ """Helper function to check if a value is numeric (int or float)."""
64
+ return isinstance(x, int | float)
65
+
66
+
67
+ #
68
+ # Base conversions
69
+ #
70
+ def convert_list_like_to_r(obj: list | tuple | set) -> ro.vectors.Vector: # noqa: PLR0911
71
+ """
72
+ Converts a list-like object to the appropriate R vector type.
73
+
74
+ Args:
75
+ obj (list, tuple, set): The list-like Python object.
76
+
77
+ Returns:
78
+ An R-compatible vector.
79
+ """
80
+ if not obj:
81
+ return ro.vectors.ListVector({})
82
+
83
+ if all(isinstance(x, int) for x in obj):
84
+ return ro.vectors.IntVector(obj)
85
+
86
+ elif all(isinstance(x, float) for x in obj):
87
+ return ro.vectors.FloatVector(obj)
88
+
89
+ # Check if all elements are numeric (int or float)
90
+ elif all(_is_numeric(x) for x in obj):
91
+ return ro.vectors.FloatVector(obj)
92
+
93
+ elif all(isinstance(x, str) for x in obj):
94
+ return ro.vectors.StrVector(obj)
95
+
96
+ elif all(isinstance(x, bool) for x in obj):
97
+ return ro.BoolVector(obj)
98
+
99
+ else:
100
+ return ro.vectors.ListVector(
101
+ {str(i): convert_to_r(v) for i, v in enumerate(obj)}
102
+ )
103
+
104
+
105
+ def convert_dict_like_as_list_to_r(obj: dict) -> ro.vectors.ListVector:
106
+ """Convert a Python dict to a ListVector."""
107
+ return ro.vectors.ListVector({str(k): convert_to_r(v) for k, v in obj.items()})
108
+
109
+
110
+ def convert_dict_like_to_r(obj: dict) -> ro.vectors.Vector:
111
+ """Convert a Python dictionary to an R vector.
112
+
113
+ This function converts a Python dictionary to either a typed R vector or ListVector,
114
+ depending on the types of values in the dictionary:
115
+ - If all values are of the same type (e.g. all strings, all numeric), returns an
116
+ appropriate typed R vector with named elements
117
+ - Otherwise, returns an R ListVector with converted values
118
+
119
+ Args:
120
+ obj (dict): A Python dictionary to convert to an R vector.
121
+
122
+ Returns:
123
+ ro.vectors.Vector: Either a typed R vector (if all values are of same type) or
124
+ an R ListVector (for mixed value types). The resulting vector will
125
+ preserve the dictionary's keys as names in the R vector.
126
+ """
127
+ values = list(obj.values())
128
+ if not values:
129
+ return ro.vectors.ListVector({})
130
+
131
+ # Get data properties
132
+ has_unique_type = len(set(type(v) for v in values)) == 1
133
+ is_numeric = all(_is_numeric(v) for v in values)
134
+
135
+ # Handle homogeneous data
136
+ if has_unique_type or is_numeric:
137
+ vec = convert_list_like_to_r(values)
138
+ vec.names = list(obj.keys())
139
+
140
+ return vec
141
+
142
+ # For mixed data, use a list vector
143
+ return convert_dict_like_as_list_to_r(obj)
144
+
145
+
146
+ def convert_to_r(obj):
147
+ """Convert Python objects to R-compatible objects for use with rpy2.
148
+
149
+ Args:
150
+ obj: The Python object to convert.
151
+
152
+ Returns:
153
+ An R-compatible object.
154
+
155
+ Raises:
156
+ TypeError: If the object type cannot be converted.
157
+ """
158
+ if obj is None:
159
+ return ro.r("NULL") # Convert None to R's NULL
160
+
161
+ obj_type = type(obj)
162
+
163
+ # Handle ``SITSBase`` objects
164
+ if getattr(obj, "_instance", None):
165
+ # Sync instance with R
166
+ if getattr(obj, "_sync_instance", None):
167
+ obj._sync_instance()
168
+
169
+ return obj._instance
170
+
171
+ # Handle ``raw R`` / Expressions objects
172
+ if isinstance(obj, RObjectMixin | DSLObject):
173
+ return obj
174
+
175
+ # Check if the object type exists in the conversion dictionary
176
+ if obj_type in TYPE_CONVERSIONS:
177
+ return TYPE_CONVERSIONS[obj_type](obj)
178
+
179
+ raise TypeError(f"Cannot convert object of type {obj_type} to R format")
180
+
181
+
182
+ def convert_to_python(obj, as_type="str"):
183
+ """Convert an R object to a Python representation.
184
+
185
+ Args:
186
+ obj (ro.Vector): The R object to convert.
187
+
188
+ as_type (str): The target Python type. Supported values:
189
+ - ``str``: Converts R string vector to Python list of strings;
190
+
191
+ - ``date``: Converts R DateVector to list of YYYY-MM-DD formatted strings;
192
+
193
+ - ``int``: Converts R numeric vector to Python list of integers;
194
+
195
+ - ``float``: Converts R numeric vector to Python list of floats;
196
+
197
+ - ``bool``: Converts R logical vector to Python list of booleans.
198
+
199
+ Returns:
200
+ list: Converted values as a Python list.
201
+
202
+ Raises:
203
+ ValueError: If the specified ``as_type`` is not supported.
204
+ """
205
+
206
+ def _convert(value, type_):
207
+ result = []
208
+
209
+ if isinstance(value, ro.ListVector):
210
+ for k, v in value.items():
211
+ result.append(
212
+ {
213
+ str(k): convert_to_python(v, type_),
214
+ }
215
+ )
216
+
217
+ elif as_type and isinstance(value, ro.Vector):
218
+ for el in value:
219
+ if as_type == "str":
220
+ result.append(str(el))
221
+
222
+ elif as_type == "date":
223
+ result.append(
224
+ (EPOCH_START + timedelta(days=int(el))).strftime("%Y-%m-%d")
225
+ )
226
+
227
+ elif as_type == "int":
228
+ result.append(int(el))
229
+
230
+ elif as_type == "float":
231
+ result.append(float(el))
232
+
233
+ elif as_type == "bool":
234
+ result.append(bool(el))
235
+
236
+ elif isinstance(value, ro.Vector):
237
+ result = pandas2ri.rpy2py(value)
238
+ names = value.names
239
+
240
+ if not isinstance(names, NULLType):
241
+ result = {str(k): v for k, v in zip(names, result)}
242
+
243
+ return result
244
+
245
+ return _convert(obj, as_type)
246
+
247
+
248
+ #
249
+ # Parameters conversions
250
+ #
251
+ def fix_reserved_words_parameters(**kwargs) -> dict:
252
+ """Fix reserved words parameters.
253
+
254
+ Args:
255
+ **kwargs: The keyword arguments to fix.
256
+
257
+ Returns:
258
+ dict: The fixed keyword arguments.
259
+ """
260
+ new_values = {}
261
+ keys_to_remove = []
262
+
263
+ for key, _ in kwargs.items():
264
+ # Assuming all reserved words end with "_"
265
+ if key.endswith("_"):
266
+ # Save key to remove
267
+ keys_to_remove.append(key)
268
+
269
+ # Save new value
270
+ new_values[key[:-1]] = kwargs[key]
271
+
272
+ # Remove keys that were converted
273
+ for key in keys_to_remove:
274
+ kwargs.pop(key)
275
+
276
+ # Add remaining kwargs
277
+ if new_values:
278
+ kwargs.update(new_values)
279
+
280
+ return kwargs
@@ -0,0 +1,148 @@
1
+ #
2
+ # Copyright (C) 2025 sits developers.
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify it
5
+ # under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 2 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
16
+ #
17
+
18
+ """Decorators."""
19
+
20
+ import functools
21
+ import inspect
22
+ from collections.abc import Callable
23
+ from typing import Any, ParamSpec, TypeVar
24
+
25
+ from pysits.conversions.common import convert_to_r, fix_reserved_words_parameters
26
+
27
+ #
28
+ # Generics
29
+ #
30
+ T = TypeVar("T")
31
+ P = ParamSpec("P")
32
+ R = TypeVar("R")
33
+
34
+
35
+ #
36
+ # Decorator
37
+ #
38
+ def rpy2_fix_type(func: Callable[P, R]) -> Callable[P, R]:
39
+ """Decorator function to convert arguments to R-compatible objects.
40
+
41
+ Args:
42
+ func (Callable): The function whose arguments should be converted.
43
+
44
+ Returns:
45
+ Callable: A wrapped function that receives converted arguments.
46
+ """
47
+
48
+ @functools.wraps(func)
49
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
50
+ kwargs = fix_reserved_words_parameters(**kwargs)
51
+ converted_args = [convert_to_r(arg) for arg in args]
52
+ converted_kwargs = {k: convert_to_r(v) for k, v in kwargs.items()}
53
+ return func(*converted_args, **converted_kwargs)
54
+
55
+ return wrapper
56
+
57
+
58
+ def rpy2_fix_type_custom(
59
+ converters: dict[str, Callable[[Any], Any]],
60
+ ) -> Callable[[Callable[P, R]], Callable[P, R]]:
61
+ """Convert arguments to R objects using parameter-specific converters.
62
+
63
+ Args:
64
+ converters (dict[str, Callable[[Any], Any]]): A dictionary mapping
65
+ parameter names to converter functions. Each converter function should
66
+ take a Python object and return an R-compatible object. Parameters not
67
+ found in this dictionary will be passed as is.
68
+
69
+ Returns:
70
+ Callable: A decorator that wraps a function to convert its arguments using the
71
+ parameter-specific converters.
72
+
73
+ Example:
74
+ >>> converters = {
75
+ ... "optimizer": convert_optimizer,
76
+ ... "opt_hparams": convert_opt_hparams
77
+ ... }
78
+ >>> @rpy2_fix_type_custom(converters)
79
+ ... def my_function(optimizer, opt_hparams, other_arg):
80
+ ... # optimizer and opt_hparams will use their specific converters
81
+ ... # other_arg will use the default _convert_to_r
82
+ ... pass
83
+ """
84
+
85
+ def decorator(func: Callable[P, R]) -> Callable[P, R]:
86
+ @functools.wraps(func)
87
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
88
+ # Get the function's parameter names
89
+ param_names = inspect.signature(func).parameters.keys()
90
+ param_names = list(param_names)
91
+
92
+ # Convert positional arguments using their parameter names
93
+ converted_args = []
94
+ for i, arg in enumerate(args):
95
+ if i < len(param_names) and param_names[i] in converters:
96
+ converted_args.append(converters[param_names[i]](arg))
97
+ else:
98
+ converted_args.append(arg)
99
+
100
+ # Convert keyword arguments
101
+ converted_kwargs = {}
102
+ for k, v in kwargs.items():
103
+ if k in converters:
104
+ converted_kwargs[k] = converters[k](v)
105
+ else:
106
+ converted_kwargs[k] = v
107
+
108
+ return func(*converted_args, **converted_kwargs)
109
+
110
+ return wrapper
111
+
112
+ return decorator
113
+
114
+
115
+ def function_call(r_function: Callable[P, R], output_wrapper: Callable[[R], T]):
116
+ """Decorator function to call an R function and post-process the result.
117
+
118
+ This decorator is used to wrap Python stub functions that serve as documentation
119
+ and type hint shells. The resulting function performs the following steps:
120
+
121
+ 1. Converts all arguments to R-compatible types using `@rpy2_fix_type`.
122
+ 2. Calls the provided R function (`r_function`) with converted arguments.
123
+ 3. Wraps the result in a specified output Python class (`output_wrapper`).
124
+
125
+ This enables consistent logic while preserving per-function docstrings and
126
+ type hints for IDE support, autocompletion, and documentation generation.
127
+
128
+ Args:
129
+ r_function (Callable): The R function to be called via rpy2.
130
+
131
+ output_wrapper (Callable): A callable (usually a class) that wraps the
132
+ result returned by the R function.
133
+
134
+ Returns:
135
+ Callable: A decorator that wraps a Python function stub, providing the
136
+ R execution logic.
137
+ """
138
+
139
+ def decorator(func: Callable[P, T]) -> Callable[P, T]:
140
+ @rpy2_fix_type
141
+ @functools.wraps(func)
142
+ def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
143
+ result = r_function(*args, **kwargs)
144
+ return output_wrapper(result)
145
+
146
+ return wrapped
147
+
148
+ return decorator