rpy-bridge 0.3.8__tar.gz → 0.3.9__tar.gz
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.
- {rpy_bridge-0.3.8/src/rpy_bridge.egg-info → rpy_bridge-0.3.9}/PKG-INFO +5 -3
- {rpy_bridge-0.3.8 → rpy_bridge-0.3.9}/README.md +4 -2
- {rpy_bridge-0.3.8 → rpy_bridge-0.3.9}/pyproject.toml +1 -1
- {rpy_bridge-0.3.8 → rpy_bridge-0.3.9}/src/rpy_bridge/rpy2_utils.py +31 -59
- {rpy_bridge-0.3.8 → rpy_bridge-0.3.9/src/rpy_bridge.egg-info}/PKG-INFO +5 -3
- {rpy_bridge-0.3.8 → rpy_bridge-0.3.9}/LICENSE +0 -0
- {rpy_bridge-0.3.8 → rpy_bridge-0.3.9}/README.rst +0 -0
- {rpy_bridge-0.3.8 → rpy_bridge-0.3.9}/setup.cfg +0 -0
- {rpy_bridge-0.3.8 → rpy_bridge-0.3.9}/src/rpy_bridge/__init__.py +0 -0
- {rpy_bridge-0.3.8 → rpy_bridge-0.3.9}/src/rpy_bridge/py.typed +0 -0
- {rpy_bridge-0.3.8 → rpy_bridge-0.3.9}/src/rpy_bridge.egg-info/SOURCES.txt +0 -0
- {rpy_bridge-0.3.8 → rpy_bridge-0.3.9}/src/rpy_bridge.egg-info/dependency_links.txt +0 -0
- {rpy_bridge-0.3.8 → rpy_bridge-0.3.9}/src/rpy_bridge.egg-info/requires.txt +0 -0
- {rpy_bridge-0.3.8 → rpy_bridge-0.3.9}/src/rpy_bridge.egg-info/top_level.txt +0 -0
- {rpy_bridge-0.3.8 → rpy_bridge-0.3.9}/tests/test_package_call.py +0 -0
- {rpy_bridge-0.3.8 → rpy_bridge-0.3.9}/tests/test_py2r.py +0 -0
- {rpy_bridge-0.3.8 → rpy_bridge-0.3.9}/tests/test_roundtrip.py +0 -0
- {rpy_bridge-0.3.8 → rpy_bridge-0.3.9}/tests/test_wrapper.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rpy-bridge
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
4
4
|
Summary: Python-to-R interoperability engine with environment management, type-safe conversions, data normalization, and safe R function execution.
|
|
5
5
|
Author-email: Victoria Cheung <victoriakcheung@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -114,14 +114,16 @@ It enables Python developers to call R functions, scripts, and packages safely w
|
|
|
114
114
|
|
|
115
115
|
**From PyPI:**
|
|
116
116
|
|
|
117
|
+
Install rpy-bridge with rpy2 for full R support
|
|
118
|
+
|
|
117
119
|
```bash
|
|
118
|
-
python3 -m pip install rpy-bridge
|
|
120
|
+
python3 -m pip install rpy-bridge rpy2
|
|
119
121
|
```
|
|
120
122
|
|
|
121
123
|
or using `uv`:
|
|
122
124
|
|
|
123
125
|
```bash
|
|
124
|
-
uv add rpy-bridge
|
|
126
|
+
uv add rpy-bridge rpy2
|
|
125
127
|
```
|
|
126
128
|
|
|
127
129
|
**During development (editable install):**
|
|
@@ -55,14 +55,16 @@ It enables Python developers to call R functions, scripts, and packages safely w
|
|
|
55
55
|
|
|
56
56
|
**From PyPI:**
|
|
57
57
|
|
|
58
|
+
Install rpy-bridge with rpy2 for full R support
|
|
59
|
+
|
|
58
60
|
```bash
|
|
59
|
-
python3 -m pip install rpy-bridge
|
|
61
|
+
python3 -m pip install rpy-bridge rpy2
|
|
60
62
|
```
|
|
61
63
|
|
|
62
64
|
or using `uv`:
|
|
63
65
|
|
|
64
66
|
```bash
|
|
65
|
-
uv add rpy-bridge
|
|
67
|
+
uv add rpy-bridge rpy2
|
|
66
68
|
```
|
|
67
69
|
|
|
68
70
|
**During development (editable install):**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "rpy-bridge"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.9"
|
|
4
4
|
description = "Python-to-R interoperability engine with environment management, type-safe conversions, data normalization, and safe R function execution."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { file = "LICENSE" }
|
|
@@ -146,18 +146,14 @@ def find_r_home() -> str | None:
|
|
|
146
146
|
|
|
147
147
|
|
|
148
148
|
# Determine if we're running in CI / testing
|
|
149
|
-
CI_TESTING = (
|
|
150
|
-
os.environ.get("GITHUB_ACTIONS") == "true" or os.environ.get("TESTING") == "1"
|
|
151
|
-
)
|
|
149
|
+
CI_TESTING = os.environ.get("GITHUB_ACTIONS") == "true" or os.environ.get("TESTING") == "1"
|
|
152
150
|
|
|
153
151
|
R_HOME = os.environ.get("R_HOME")
|
|
154
152
|
if not R_HOME:
|
|
155
153
|
R_HOME = find_r_home()
|
|
156
154
|
if not R_HOME:
|
|
157
155
|
if CI_TESTING:
|
|
158
|
-
logger.warning(
|
|
159
|
-
"R not found; skipping all R-dependent setup in CI/testing environment."
|
|
160
|
-
)
|
|
156
|
+
logger.warning("R not found; skipping all R-dependent setup in CI/testing environment.")
|
|
161
157
|
R_HOME = None # Explicitly None to signal "no R available"
|
|
162
158
|
else:
|
|
163
159
|
raise RuntimeError("R not found. Please install R or add it to PATH.")
|
|
@@ -174,7 +170,7 @@ if R_HOME:
|
|
|
174
170
|
lib_path = os.path.join(R_HOME, "lib")
|
|
175
171
|
if lib_path not in os.environ.get("DYLD_FALLBACK_LIBRARY_PATH", ""):
|
|
176
172
|
os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = (
|
|
177
|
-
f"{lib_path}:{os.environ.get('DYLD_FALLBACK_LIBRARY_PATH','')}"
|
|
173
|
+
f"{lib_path}:{os.environ.get('DYLD_FALLBACK_LIBRARY_PATH', '')}"
|
|
178
174
|
)
|
|
179
175
|
|
|
180
176
|
elif sys.platform.startswith("linux"):
|
|
@@ -358,7 +354,6 @@ class RFunctionCaller:
|
|
|
358
354
|
packages: str | list[str] | None = None,
|
|
359
355
|
**kwargs, # catch unexpected keywords
|
|
360
356
|
):
|
|
361
|
-
|
|
362
357
|
# Handle path_to_renv safely
|
|
363
358
|
if path_to_renv is not None:
|
|
364
359
|
if not isinstance(path_to_renv, Path):
|
|
@@ -380,9 +375,7 @@ class RFunctionCaller:
|
|
|
380
375
|
scripts = script_path_value
|
|
381
376
|
else:
|
|
382
377
|
# Both provided → prioritize scripts and ignore script_path
|
|
383
|
-
logger.warning(
|
|
384
|
-
"'script_path' ignored because 'scripts' argument is also provided."
|
|
385
|
-
)
|
|
378
|
+
logger.warning("'script_path' ignored because 'scripts' argument is also provided.")
|
|
386
379
|
|
|
387
380
|
self.scripts = _normalize_scripts(scripts)
|
|
388
381
|
|
|
@@ -457,6 +450,17 @@ class RFunctionCaller:
|
|
|
457
450
|
self.ListVector = rpy2_dict["ListVector"]
|
|
458
451
|
self.NamedList = rpy2_dict["NamedList"]
|
|
459
452
|
|
|
453
|
+
# Activate renv once if requested
|
|
454
|
+
if self.path_to_renv and not self._renv_activated:
|
|
455
|
+
try:
|
|
456
|
+
activate_renv(self.path_to_renv)
|
|
457
|
+
self._renv_activated = True
|
|
458
|
+
logger.info(
|
|
459
|
+
f"[rpy-bridge.RFunctionCaller] renv activated for project: {self.path_to_renv}"
|
|
460
|
+
)
|
|
461
|
+
except Exception as e:
|
|
462
|
+
raise RuntimeError(f"Failed to activate renv at {self.path_to_renv}: {e}") from e
|
|
463
|
+
|
|
460
464
|
r = self.robjects.r
|
|
461
465
|
|
|
462
466
|
# Ensure required R package
|
|
@@ -505,9 +509,7 @@ class RFunctionCaller:
|
|
|
505
509
|
|
|
506
510
|
env_obj = r("env")
|
|
507
511
|
self._namespaces[ns_name] = {
|
|
508
|
-
name: env_obj[name]
|
|
509
|
-
for name in env_obj.keys()
|
|
510
|
-
if callable(env_obj[name])
|
|
512
|
+
name: env_obj[name] for name in env_obj.keys() if callable(env_obj[name])
|
|
511
513
|
}
|
|
512
514
|
|
|
513
515
|
logger.info(
|
|
@@ -588,9 +590,7 @@ class RFunctionCaller:
|
|
|
588
590
|
logger.warning(f"Failed to list functions for package '{pkg}'")
|
|
589
591
|
return []
|
|
590
592
|
|
|
591
|
-
def list_all_functions(
|
|
592
|
-
self, include_packages: bool = False
|
|
593
|
-
) -> dict[str, list[str]]:
|
|
593
|
+
def list_all_functions(self, include_packages: bool = False) -> dict[str, list[str]]:
|
|
594
594
|
"""
|
|
595
595
|
Return all callable R functions grouped by script namespace and package.
|
|
596
596
|
"""
|
|
@@ -620,9 +620,7 @@ class RFunctionCaller:
|
|
|
620
620
|
|
|
621
621
|
return all_funcs
|
|
622
622
|
|
|
623
|
-
def print_function_tree(
|
|
624
|
-
self, include_packages: bool = False, max_display: int = 10
|
|
625
|
-
):
|
|
623
|
+
def print_function_tree(self, include_packages: bool = False, max_display: int = 10):
|
|
626
624
|
"""
|
|
627
625
|
Pretty-print available R functions grouped by namespace.
|
|
628
626
|
|
|
@@ -695,17 +693,11 @@ class RFunctionCaller:
|
|
|
695
693
|
|
|
696
694
|
types = set(type(x) for x in obj if not is_na(x))
|
|
697
695
|
if types <= {int, float}:
|
|
698
|
-
return FloatVector(
|
|
699
|
-
[robjects.NA_Real if is_na(x) else float(x) for x in obj]
|
|
700
|
-
)
|
|
696
|
+
return FloatVector([robjects.NA_Real if is_na(x) else float(x) for x in obj])
|
|
701
697
|
if types <= {bool}:
|
|
702
|
-
return BoolVector(
|
|
703
|
-
[robjects.NA_Logical if is_na(x) else x for x in obj]
|
|
704
|
-
)
|
|
698
|
+
return BoolVector([robjects.NA_Logical if is_na(x) else x for x in obj])
|
|
705
699
|
if types <= {str}:
|
|
706
|
-
return StrVector(
|
|
707
|
-
[robjects.NA_Character if is_na(x) else x for x in obj]
|
|
708
|
-
)
|
|
700
|
+
return StrVector([robjects.NA_Character if is_na(x) else x for x in obj])
|
|
709
701
|
return ListVector({str(i): self._py2r(v) for i, v in enumerate(obj)})
|
|
710
702
|
if isinstance(obj, dict):
|
|
711
703
|
return ListVector({k: self._py2r(v) for k, v in obj.items()})
|
|
@@ -758,9 +750,7 @@ class RFunctionCaller:
|
|
|
758
750
|
r(f'suppressMessages(library("{pkg}", character.only=TRUE))')
|
|
759
751
|
except Exception:
|
|
760
752
|
logger.info(f"[rpy-bridge.RFunctionCaller] Package '{pkg}' not found.")
|
|
761
|
-
logger.warning(
|
|
762
|
-
f"[rpy-bridge.RFunctionCaller] Installing missing R package: {pkg}"
|
|
763
|
-
)
|
|
753
|
+
logger.warning(f"[rpy-bridge.RFunctionCaller] Installing missing R package: {pkg}")
|
|
764
754
|
r(f'install.packages("{pkg}", repos="https://cloud.r-project.org")')
|
|
765
755
|
r(f'suppressMessages(library("{pkg}", character.only=TRUE))')
|
|
766
756
|
|
|
@@ -821,9 +811,7 @@ class RFunctionCaller:
|
|
|
821
811
|
func = self.robjects.r(f"{ns_name}::{fname}")
|
|
822
812
|
source_info = f"R package '{ns_name}'"
|
|
823
813
|
except Exception as e:
|
|
824
|
-
raise RuntimeError(
|
|
825
|
-
f"Failed to resolve R function '{func_name}': {e}"
|
|
826
|
-
) from e
|
|
814
|
+
raise RuntimeError(f"Failed to resolve R function '{func_name}': {e}") from e
|
|
827
815
|
|
|
828
816
|
else:
|
|
829
817
|
for ns_name, ns_env in self._namespaces.items():
|
|
@@ -927,9 +915,7 @@ def fix_r_dataframe_types(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
927
915
|
values = series.dropna()
|
|
928
916
|
if not values.empty and values.between(10000, 40000).all():
|
|
929
917
|
try:
|
|
930
|
-
df[col] = pd.to_datetime("1970-01-01") + pd.to_timedelta(
|
|
931
|
-
series, unit="D"
|
|
932
|
-
)
|
|
918
|
+
df[col] = pd.to_datetime("1970-01-01") + pd.to_timedelta(series, unit="D")
|
|
933
919
|
except Exception:
|
|
934
920
|
pass
|
|
935
921
|
if pd.api.types.is_datetime64tz_dtype(series):
|
|
@@ -975,20 +961,14 @@ def clean_r_missing(obj, caller: RFunctionCaller):
|
|
|
975
961
|
# ---------------------------------------------------------------------
|
|
976
962
|
# DataFrame comparison utilities
|
|
977
963
|
# ---------------------------------------------------------------------
|
|
978
|
-
def normalize_dtypes(
|
|
979
|
-
df1: pd.DataFrame, df2: pd.DataFrame
|
|
980
|
-
) -> tuple[pd.DataFrame, pd.DataFrame]:
|
|
964
|
+
def normalize_dtypes(df1: pd.DataFrame, df2: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]:
|
|
981
965
|
for col in df1.columns.intersection(df2.columns):
|
|
982
966
|
df1[col] = df1[col].replace("", pd.NA)
|
|
983
967
|
df2[col] = df2[col].replace("", pd.NA)
|
|
984
968
|
s1, s2 = df1[col], df2[col]
|
|
985
969
|
dtype1, dtype2 = s1.dtype, s2.dtype
|
|
986
|
-
if (
|
|
987
|
-
pd.api.types.
|
|
988
|
-
and pd.api.types.is_object_dtype(dtype2)
|
|
989
|
-
) or (
|
|
990
|
-
pd.api.types.is_object_dtype(dtype1)
|
|
991
|
-
and pd.api.types.is_numeric_dtype(dtype2)
|
|
970
|
+
if (pd.api.types.is_numeric_dtype(dtype1) and pd.api.types.is_object_dtype(dtype2)) or (
|
|
971
|
+
pd.api.types.is_object_dtype(dtype1) and pd.api.types.is_numeric_dtype(dtype2)
|
|
992
972
|
):
|
|
993
973
|
try:
|
|
994
974
|
df1[col] = pd.to_numeric(s1, errors="coerce")
|
|
@@ -996,9 +976,7 @@ def normalize_dtypes(
|
|
|
996
976
|
continue
|
|
997
977
|
except Exception:
|
|
998
978
|
pass
|
|
999
|
-
if pd.api.types.is_numeric_dtype(dtype1) and pd.api.types.is_numeric_dtype(
|
|
1000
|
-
dtype2
|
|
1001
|
-
):
|
|
979
|
+
if pd.api.types.is_numeric_dtype(dtype1) and pd.api.types.is_numeric_dtype(dtype2):
|
|
1002
980
|
df1[col] = df1[col].astype("float64")
|
|
1003
981
|
df2[col] = df2[col].astype("float64")
|
|
1004
982
|
continue
|
|
@@ -1008,9 +986,7 @@ def normalize_dtypes(
|
|
|
1008
986
|
return df1, df2
|
|
1009
987
|
|
|
1010
988
|
|
|
1011
|
-
def align_numeric_dtypes(
|
|
1012
|
-
df1: pd.DataFrame, df2: pd.DataFrame
|
|
1013
|
-
) -> tuple[pd.DataFrame, pd.DataFrame]:
|
|
989
|
+
def align_numeric_dtypes(df1: pd.DataFrame, df2: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]:
|
|
1014
990
|
for col in df1.columns.intersection(df2.columns):
|
|
1015
991
|
s1, s2 = df1[col].replace("", pd.NA), df2[col].replace("", pd.NA)
|
|
1016
992
|
try:
|
|
@@ -1026,9 +1002,7 @@ def align_numeric_dtypes(
|
|
|
1026
1002
|
return df1, df2
|
|
1027
1003
|
|
|
1028
1004
|
|
|
1029
|
-
def compare_r_py_dataframes(
|
|
1030
|
-
df1: pd.DataFrame, df2: pd.DataFrame, float_tol: float = 1e-8
|
|
1031
|
-
) -> dict:
|
|
1005
|
+
def compare_r_py_dataframes(df1: pd.DataFrame, df2: pd.DataFrame, float_tol: float = 1e-8) -> dict:
|
|
1032
1006
|
results: dict[str, Any] = {
|
|
1033
1007
|
"shape_mismatch": False,
|
|
1034
1008
|
"columns_mismatch": False,
|
|
@@ -1055,9 +1029,7 @@ def compare_r_py_dataframes(
|
|
|
1055
1029
|
df1_aligned, df2_aligned = df1.loc[:, common_cols], df2.loc[:, common_cols]
|
|
1056
1030
|
for col in common_cols:
|
|
1057
1031
|
col_py, col_r = df1_aligned[col], df2_aligned[col]
|
|
1058
|
-
if pd.api.types.is_numeric_dtype(col_py) and pd.api.types.is_numeric_dtype(
|
|
1059
|
-
col_r
|
|
1060
|
-
):
|
|
1032
|
+
if pd.api.types.is_numeric_dtype(col_py) and pd.api.types.is_numeric_dtype(col_r):
|
|
1061
1033
|
col_py, col_r = col_py.align(col_r)
|
|
1062
1034
|
close = np.isclose(
|
|
1063
1035
|
col_py.fillna(np.nan),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rpy-bridge
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
4
4
|
Summary: Python-to-R interoperability engine with environment management, type-safe conversions, data normalization, and safe R function execution.
|
|
5
5
|
Author-email: Victoria Cheung <victoriakcheung@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -114,14 +114,16 @@ It enables Python developers to call R functions, scripts, and packages safely w
|
|
|
114
114
|
|
|
115
115
|
**From PyPI:**
|
|
116
116
|
|
|
117
|
+
Install rpy-bridge with rpy2 for full R support
|
|
118
|
+
|
|
117
119
|
```bash
|
|
118
|
-
python3 -m pip install rpy-bridge
|
|
120
|
+
python3 -m pip install rpy-bridge rpy2
|
|
119
121
|
```
|
|
120
122
|
|
|
121
123
|
or using `uv`:
|
|
122
124
|
|
|
123
125
|
```bash
|
|
124
|
-
uv add rpy-bridge
|
|
126
|
+
uv add rpy-bridge rpy2
|
|
125
127
|
```
|
|
126
128
|
|
|
127
129
|
**During development (editable install):**
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|