rpy-bridge 0.3.1__py3-none-any.whl → 0.3.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.
rpy_bridge/__init__.py CHANGED
@@ -10,13 +10,13 @@ from .rpy2_utils import (
10
10
  activate_renv,
11
11
  align_numeric_dtypes,
12
12
  clean_r_dataframe,
13
+ clean_r_missing,
13
14
  compare_r_py_dataframes,
14
15
  fix_r_dataframe_types,
15
16
  fix_string_nans,
16
17
  normalize_dtypes,
17
18
  normalize_single_df_dtypes,
18
19
  postprocess_r_dataframe,
19
- clean_r_missing,
20
20
  r_namedlist_to_dict,
21
21
  )
22
22
 
rpy_bridge/rpy2_utils.py CHANGED
@@ -10,24 +10,36 @@ Ensure compatibility with your R project's renv setup (or other virtual env/base
10
10
  # ruff: noqa: E402
11
11
  # %%
12
12
  # Import libraries
13
+ import importlib.util
13
14
  import os
15
+ import subprocess
16
+ import sys
14
17
  import warnings
15
-
16
- warnings.filterwarnings("ignore", message="Environment variable .* redefined by R")
17
-
18
18
  from pathlib import Path
19
- import sys
20
- import subprocess
19
+ from typing import TYPE_CHECKING, Any, Union
21
20
 
22
- import math
23
21
  import numpy as np
24
22
  import pandas as pd
25
23
 
24
+ warnings.filterwarnings("ignore", message="Environment variable .* redefined by R")
25
+
26
+
27
+ if TYPE_CHECKING:
28
+ import logging as logging_module
29
+
30
+ from loguru import Logger as LoguruLogger
31
+
32
+ LoggerType = LoggerType = Union[LoguruLogger, logging_module.Logger]
33
+ else:
34
+ LoggerType = None # runtime doesn’t need the type object
35
+
36
+ import logging
37
+
26
38
  try:
27
- from loguru import logger # type: ignore
28
- except Exception:
29
- import logging
39
+ from loguru import logger as loguru_logger # type: ignore
30
40
 
41
+ logger = loguru_logger
42
+ except ImportError:
31
43
  logging.basicConfig()
32
44
  logger = logging.getLogger("rpy-bridge")
33
45
 
@@ -35,41 +47,45 @@ except Exception:
35
47
  # ---------------------------------------------------------------------
36
48
  # R detection and rpy2 installation
37
49
  # ---------------------------------------------------------------------
38
- def ensure_rpy2_installed(r_home: str):
39
- os.environ["R_HOME"] = r_home
40
- try:
41
- import rpy2 # noqa: F401
42
- except ImportError:
43
- logger.info(
44
- f"[Info] rpy2 not installed or incompatible with R_HOME={r_home}. Installing..."
45
- )
46
- subprocess.check_call(
47
- [sys.executable, "-m", "pip", "install", "--force-reinstall", "rpy2"]
50
+ def ensure_rpy2_available() -> None:
51
+ """
52
+ Ensure rpy2 is importable.
53
+ Do NOT attempt to install dynamically; fail with clear instructions instead.
54
+ """
55
+ if importlib.util.find_spec("rpy2") is None:
56
+ raise RuntimeError(
57
+ "\n[Error] rpy2 is not installed. Please install it in your Python environment:\n"
58
+ " pip install rpy2\n\n"
59
+ "Make sure your Python environment can access your system R installation.\n"
60
+ "On macOS with Homebrew: brew install r\n"
61
+ "On Linux: apt install r-base (Debian/Ubuntu) or yum install R (CentOS/RHEL)\n"
62
+ "On Windows: install R from https://cran.r-project.org\n"
48
63
  )
49
- import rpy2 # noqa: F401
50
64
 
51
65
 
52
- def find_r_home():
66
+ def find_r_home() -> str | None:
67
+ """Detect system R installation."""
53
68
  try:
54
69
  r_home = subprocess.check_output(
55
70
  ["R", "--vanilla", "--slave", "-e", "cat(R.home())"],
56
71
  stderr=subprocess.PIPE,
57
72
  text=True,
58
73
  ).strip()
59
- if r_home.endswith(">"):
74
+ if r_home.endswith(">"): # sometimes R console prints >
60
75
  r_home = r_home[:-1].strip()
61
76
  return r_home
62
77
  except FileNotFoundError:
78
+ # fallback paths (Linux, macOS Homebrew, Windows)
63
79
  possible_paths = [
64
80
  "/usr/lib/R",
65
81
  "/usr/local/lib/R",
66
- "/opt/homebrew/Cellar/r/4.5.2/lib/R", # Homebrew macOS
82
+ "/opt/homebrew/Cellar/r/4.5.2/lib/R", # macOS Homebrew
67
83
  "C:\\Program Files\\R\\R-4.5.2", # Windows
68
84
  ]
69
85
  for p in possible_paths:
70
86
  if os.path.exists(p):
71
87
  return p
72
- return None
88
+ return None
73
89
 
74
90
 
75
91
  R_HOME = find_r_home()
@@ -78,7 +94,7 @@ if not R_HOME:
78
94
 
79
95
  logger.info(f"R_HOME = {R_HOME}")
80
96
  os.environ["R_HOME"] = R_HOME
81
- ensure_rpy2_installed(R_HOME)
97
+ ensure_rpy2_available()
82
98
 
83
99
  # macOS dynamic library path
84
100
  if sys.platform == "darwin":
@@ -107,6 +123,8 @@ def _require_rpy2(raise_on_missing: bool = True) -> dict | None:
107
123
  try:
108
124
  import rpy2.robjects as ro
109
125
  from rpy2 import robjects
126
+ from rpy2.rinterface_lib.sexp import NULLType
127
+ from rpy2.rlike.container import NamedList
110
128
  from rpy2.robjects import pandas2ri
111
129
  from rpy2.robjects.conversion import localconverter
112
130
  from rpy2.robjects.vectors import (
@@ -116,8 +134,6 @@ def _require_rpy2(raise_on_missing: bool = True) -> dict | None:
116
134
  ListVector,
117
135
  StrVector,
118
136
  )
119
- from rpy2.rinterface_lib.sexp import NULLType
120
- from rpy2.rlike.container import NamedList
121
137
 
122
138
  _RPY2 = {
123
139
  "ro": ro,
@@ -146,6 +162,7 @@ def _ensure_rpy2() -> dict:
146
162
  global _RPY2
147
163
  if _RPY2 is None:
148
164
  _RPY2 = _require_rpy2()
165
+ assert _RPY2 is not None, "_require_rpy2() returned None"
149
166
  return _RPY2
150
167
 
151
168
 
@@ -318,7 +335,6 @@ class RFunctionCaller:
318
335
  self._ensure_r_loaded()
319
336
  robjects = self.robjects
320
337
  pandas2ri = self.pandas2ri
321
- IntVector = self.IntVector
322
338
  FloatVector = self.FloatVector
323
339
  BoolVector = self.BoolVector
324
340
  StrVector = self.StrVector
@@ -470,13 +486,25 @@ class RFunctionCaller:
470
486
  self._ensure_r_loaded()
471
487
 
472
488
  # --- Find the function ---
489
+ func = None
473
490
  try:
474
491
  func = self.robjects.globalenv[func_name] # script-defined
475
492
  except KeyError:
476
493
  try:
477
494
  func = self.robjects.r[func_name] # base or package function
478
495
  except KeyError:
479
- raise ValueError(f"R function '{func_name}' not found.")
496
+ # --- Added: handle namespaced functions like stats::median ---
497
+ if "::" in func_name:
498
+ pkg, fname = func_name.split("::", 1)
499
+ try:
500
+ func = self.robjects.r(f"{pkg}::{fname}")
501
+ except Exception as e:
502
+ raise RuntimeError(
503
+ f"Failed to load R function '{func_name}' via namespace: {e}"
504
+ ) from e
505
+
506
+ if func is None:
507
+ raise ValueError(f"R function '{func_name}' not found.")
480
508
 
481
509
  # --- Convert Python args to R ---
482
510
  r_args = [self._py2r(a) for a in args]
@@ -749,12 +777,12 @@ def compare_r_py_dataframes(
749
777
  dict with mismatch diagnostics, preserving original indices in diffs.
750
778
  """
751
779
 
752
- results = {
780
+ results: dict[str, Any] = {
753
781
  "shape_mismatch": False,
754
782
  "columns_mismatch": False,
755
783
  "index_mismatch": False,
756
- "numeric_diffs": {},
757
- "non_numeric_diffs": {},
784
+ "numeric_diffs": {}, # type: dict[str, pd.DataFrame]
785
+ "non_numeric_diffs": {}, # type: dict[str, pd.DataFrame]
758
786
  }
759
787
 
760
788
  # --- Preprocessing: fix R-specific issues ---
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rpy-bridge
3
- Version: 0.3.1
3
+ Version: 0.3.2
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
@@ -0,0 +1,8 @@
1
+ rpy_bridge/__init__.py,sha256=1cyWVzhVnSqMRY6OkSo8RYjTKWjmaV9WR-otu4Y5dJc,829
2
+ rpy_bridge/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ rpy_bridge/rpy2_utils.py,sha256=n58oSoqkZRv320dtgxEW597G8PrzCO8jCeGPZQH_5t8,29234
4
+ rpy_bridge-0.3.2.dist-info/licenses/LICENSE,sha256=JwbWVcSfeoLfZ2M_ZiyygKVDvhBDW3zbqTWwXOJwmrA,1276
5
+ rpy_bridge-0.3.2.dist-info/METADATA,sha256=Yc5iO7Ggihznt4DMGEN1Ygf5CiaHdb07uPO96Dr6vyo,9267
6
+ rpy_bridge-0.3.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ rpy_bridge-0.3.2.dist-info/top_level.txt,sha256=z9UZ77ZuUPoLqMDQEpP4btstsaM1IpXb9Cn9yBVaHmU,11
8
+ rpy_bridge-0.3.2.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- rpy_bridge/__init__.py,sha256=fXINFO0OFsSkxNccFPlSPIVsye5WGLVzu6Puf7o1Ao4,829
2
- rpy_bridge/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- rpy_bridge/rpy2_utils.py,sha256=D8hCv9axFbHGM71NFNO6E4oZIIBA5fBioOvzLDEVAHM,27842
4
- rpy_bridge-0.3.1.dist-info/licenses/LICENSE,sha256=JwbWVcSfeoLfZ2M_ZiyygKVDvhBDW3zbqTWwXOJwmrA,1276
5
- rpy_bridge-0.3.1.dist-info/METADATA,sha256=LeJLe_ETz_1bJxnjHUG-LJg3U7fG7Mq6O8YHccNZiVg,9267
6
- rpy_bridge-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
- rpy_bridge-0.3.1.dist-info/top_level.txt,sha256=z9UZ77ZuUPoLqMDQEpP4btstsaM1IpXb9Cn9yBVaHmU,11
8
- rpy_bridge-0.3.1.dist-info/RECORD,,