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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rpy-bridge
3
- Version: 0.3.8
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.8"
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.is_numeric_dtype(dtype1)
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.8
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