rpy-bridge 0.3.1__tar.gz → 0.3.2__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.1/src/rpy_bridge.egg-info → rpy_bridge-0.3.2}/PKG-INFO +1 -1
- {rpy_bridge-0.3.1 → rpy_bridge-0.3.2}/pyproject.toml +30 -17
- {rpy_bridge-0.3.1 → rpy_bridge-0.3.2}/src/rpy_bridge/rpy2_utils.py +60 -32
- {rpy_bridge-0.3.1 → rpy_bridge-0.3.2/src/rpy_bridge.egg-info}/PKG-INFO +1 -1
- rpy_bridge-0.3.2/tests/test_package_call.py +16 -0
- {rpy_bridge-0.3.1 → rpy_bridge-0.3.2}/tests/test_py2r.py +3 -14
- {rpy_bridge-0.3.1 → rpy_bridge-0.3.2}/tests/test_roundtrip.py +3 -19
- {rpy_bridge-0.3.1 → rpy_bridge-0.3.2}/tests/test_wrapper.py +1 -0
- rpy_bridge-0.3.1/tests/test_package_call.py +0 -26
- {rpy_bridge-0.3.1 → rpy_bridge-0.3.2}/LICENSE +0 -0
- {rpy_bridge-0.3.1 → rpy_bridge-0.3.2}/README.md +0 -0
- {rpy_bridge-0.3.1 → rpy_bridge-0.3.2}/README.rst +0 -0
- {rpy_bridge-0.3.1 → rpy_bridge-0.3.2}/setup.cfg +0 -0
- {rpy_bridge-0.3.1 → rpy_bridge-0.3.2}/src/rpy_bridge/__init__.py +1 -1
- {rpy_bridge-0.3.1 → rpy_bridge-0.3.2}/src/rpy_bridge/py.typed +0 -0
- {rpy_bridge-0.3.1 → rpy_bridge-0.3.2}/src/rpy_bridge.egg-info/SOURCES.txt +0 -0
- {rpy_bridge-0.3.1 → rpy_bridge-0.3.2}/src/rpy_bridge.egg-info/dependency_links.txt +0 -0
- {rpy_bridge-0.3.1 → rpy_bridge-0.3.2}/src/rpy_bridge.egg-info/requires.txt +0 -0
- {rpy_bridge-0.3.1 → rpy_bridge-0.3.2}/src/rpy_bridge.egg-info/top_level.txt +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.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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "rpy-bridge"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.2"
|
|
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" }
|
|
@@ -9,7 +9,6 @@ authors = [
|
|
|
9
9
|
]
|
|
10
10
|
requires-python = ">=3.11"
|
|
11
11
|
|
|
12
|
-
# Core dependencies: safe to install without system R
|
|
13
12
|
dependencies = [
|
|
14
13
|
"numpy>=1.24",
|
|
15
14
|
"pandas>=2.0",
|
|
@@ -26,21 +25,9 @@ classifiers = [
|
|
|
26
25
|
]
|
|
27
26
|
|
|
28
27
|
[project.optional-dependencies]
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
]
|
|
33
|
-
|
|
34
|
-
# Interactive / notebook usage
|
|
35
|
-
dev = [
|
|
36
|
-
"ipykernel>=7.1.0",
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
# Documentation build dependencies (optional but recommended)
|
|
40
|
-
docs = [
|
|
41
|
-
"sphinx",
|
|
42
|
-
"myst-parser",
|
|
43
|
-
]
|
|
28
|
+
r = ["rpy2>=3.5"]
|
|
29
|
+
dev = ["ipykernel>=7.1.0"]
|
|
30
|
+
docs = ["sphinx", "myst-parser"]
|
|
44
31
|
|
|
45
32
|
[project.urls]
|
|
46
33
|
Homepage = "https://github.com/vic-cheung/rpy-bridge"
|
|
@@ -50,6 +37,9 @@ Homepage = "https://github.com/vic-cheung/rpy-bridge"
|
|
|
50
37
|
requires = ["setuptools>=61"]
|
|
51
38
|
build-backend = "setuptools.build_meta"
|
|
52
39
|
|
|
40
|
+
# -------------------------------
|
|
41
|
+
# uv dev dependencies
|
|
42
|
+
# -------------------------------
|
|
53
43
|
[tool.uv]
|
|
54
44
|
dev-dependencies = [
|
|
55
45
|
"ruff>=0.6",
|
|
@@ -57,4 +47,27 @@ dev-dependencies = [
|
|
|
57
47
|
"build>=1.0",
|
|
58
48
|
"twine>=4.0",
|
|
59
49
|
"certifi>=2025.0",
|
|
50
|
+
"ty>=0.0.1a34",
|
|
60
51
|
]
|
|
52
|
+
|
|
53
|
+
# -------------------------------
|
|
54
|
+
# Ruff configuration
|
|
55
|
+
# -------------------------------
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
line-length = 100
|
|
58
|
+
select = ["E", "F", "W", "I"]
|
|
59
|
+
ignore = ["E501"] # optional, ignore long lines
|
|
60
|
+
|
|
61
|
+
# -------------------------------
|
|
62
|
+
# Ty configuration
|
|
63
|
+
# -------------------------------
|
|
64
|
+
[tool.ty.environment]
|
|
65
|
+
root = ["src/rpy_bridge"]
|
|
66
|
+
|
|
67
|
+
[tool.ty.rules]
|
|
68
|
+
possibly-missing-attribute = "ignore" # suppress warnings from rpy2 dynamic attributes
|
|
69
|
+
non-subscriptable = "ignore" # suppress errors when typing unknown dict-like objects e.g. NULLType = r["NULLType"]
|
|
70
|
+
call-non-callable = "ignore" # suppress errors for dynamic callable objects
|
|
71
|
+
# invalid-assignment = "ignore" # suppress errors for dynamic dict assignment
|
|
72
|
+
# invalid-return-type = "ignore" # suppress return type mismatch warnings
|
|
73
|
+
unresolved-import = "ignore" # suppress errors for unable to resolve imported module
|
|
@@ -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
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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,16 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
# caller fixture is injected from conftest.py
|
|
4
|
+
def test_call_stats_functions(caller):
|
|
5
|
+
"""Smoke test: load `stats` and call `rnorm` and `median`."""
|
|
6
|
+
|
|
7
|
+
samples = caller.call("rnorm", 10, mean=5)
|
|
8
|
+
assert hasattr(samples, "__len__")
|
|
9
|
+
assert len(samples) == 10
|
|
10
|
+
|
|
11
|
+
med = caller.call("stats::median", samples)
|
|
12
|
+
# median may come back as a scalar or a single-element array
|
|
13
|
+
if hasattr(med, "__len__") and not isinstance(med, (int, float)):
|
|
14
|
+
assert len(med) == 1
|
|
15
|
+
else:
|
|
16
|
+
assert isinstance(med, (int, float))
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
# %%
|
|
2
|
-
from pathlib import Path
|
|
3
2
|
import pandas as pd
|
|
4
|
-
from rpy_bridge import RFunctionCaller
|
|
5
3
|
|
|
6
4
|
|
|
7
|
-
#
|
|
8
|
-
def test_py2r_edge_cases(caller
|
|
5
|
+
# The caller fixture is now injected by pytest
|
|
6
|
+
def test_py2r_edge_cases(caller):
|
|
9
7
|
test_cases = {
|
|
10
8
|
"simple_int_list": [1, 2, 3],
|
|
11
9
|
"simple_float_list": [1.0, 2.5, 3.7],
|
|
@@ -26,7 +24,7 @@ def test_py2r_edge_cases(caller: RFunctionCaller):
|
|
|
26
24
|
"scalar_str": "hello",
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
caller._ensure_r_loaded() #
|
|
27
|
+
caller._ensure_r_loaded() # Ensure R is loaded
|
|
30
28
|
|
|
31
29
|
print("\n--- Testing _py2r edge cases ---")
|
|
32
30
|
for name, value in test_cases.items():
|
|
@@ -34,7 +32,6 @@ def test_py2r_edge_cases(caller: RFunctionCaller):
|
|
|
34
32
|
r_obj = caller._py2r(value)
|
|
35
33
|
print(f"\n--- Test case: {name} ---")
|
|
36
34
|
print("R object type:", type(r_obj))
|
|
37
|
-
# Handle R vectors or ListVector
|
|
38
35
|
if hasattr(r_obj, "__len__") and not isinstance(
|
|
39
36
|
r_obj, (int, float, bool, str)
|
|
40
37
|
):
|
|
@@ -46,11 +43,3 @@ def test_py2r_edge_cases(caller: RFunctionCaller):
|
|
|
46
43
|
print("R object:", r_obj)
|
|
47
44
|
except Exception as e:
|
|
48
45
|
print(f"Error in {name}: {e}")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# Example usage
|
|
52
|
-
if __name__ == "__main__":
|
|
53
|
-
caller = RFunctionCaller()
|
|
54
|
-
test_py2r_edge_cases(caller)
|
|
55
|
-
|
|
56
|
-
# %%
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# %%
|
|
2
|
-
import pandas as pd
|
|
3
2
|
import numpy as np
|
|
4
|
-
|
|
3
|
+
import pandas as pd
|
|
5
4
|
|
|
6
5
|
|
|
7
|
-
#
|
|
8
|
-
def test_py2r_r2py_roundtrip(caller
|
|
6
|
+
# The caller fixture is now injected by pytest
|
|
7
|
+
def test_py2r_r2py_roundtrip(caller):
|
|
9
8
|
"""
|
|
10
9
|
Test round-trip conversion: Python -> R -> Python
|
|
11
10
|
Handles edge cases including scalars, lists, nested structures, dicts, and NA values.
|
|
@@ -34,13 +33,9 @@ def test_py2r_r2py_roundtrip(caller: RFunctionCaller):
|
|
|
34
33
|
for name, py_obj in test_cases.items():
|
|
35
34
|
print(f"\n--- Test case: {name} ---")
|
|
36
35
|
try:
|
|
37
|
-
# Python -> R
|
|
38
36
|
r_obj = caller._py2r(py_obj)
|
|
39
|
-
|
|
40
|
-
# R -> Python
|
|
41
37
|
py_roundtrip = caller._r2py(r_obj)
|
|
42
38
|
|
|
43
|
-
# Normalize NA values for comparison
|
|
44
39
|
def normalize(obj):
|
|
45
40
|
if isinstance(obj, pd.Series):
|
|
46
41
|
return obj.replace({pd.NA: None}).tolist()
|
|
@@ -63,14 +58,3 @@ def test_py2r_r2py_roundtrip(caller: RFunctionCaller):
|
|
|
63
58
|
|
|
64
59
|
except Exception as e:
|
|
65
60
|
print("Error during round-trip:", e)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# Example usage:
|
|
69
|
-
if __name__ == "__main__":
|
|
70
|
-
from rpy_bridge import RFunctionCaller
|
|
71
|
-
|
|
72
|
-
caller = RFunctionCaller()
|
|
73
|
-
caller._ensure_r_loaded() # Ensure R is loaded
|
|
74
|
-
test_py2r_r2py_roundtrip(caller)
|
|
75
|
-
|
|
76
|
-
# %%
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from rpy_bridge import RFunctionCaller
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def test_call_stats_functions():
|
|
7
|
-
"""Smoke test: load `stats` and call `rnorm` and `median`.
|
|
8
|
-
|
|
9
|
-
This test will be skipped if R or rpy2 are not available in the test
|
|
10
|
-
environment.
|
|
11
|
-
"""
|
|
12
|
-
try:
|
|
13
|
-
caller = RFunctionCaller(path_to_renv=None, packages=["stats"])
|
|
14
|
-
except Exception as e:
|
|
15
|
-
pytest.skip(f"Skipping because R/rpy2 not available: {e}")
|
|
16
|
-
|
|
17
|
-
samples = caller.call("rnorm", 10, mean=5)
|
|
18
|
-
assert hasattr(samples, "__len__")
|
|
19
|
-
assert len(samples) == 10
|
|
20
|
-
|
|
21
|
-
med = caller.call("stats::median", samples)
|
|
22
|
-
# median may come back as a scalar or a single-element array
|
|
23
|
-
if hasattr(med, "__len__") and not isinstance(med, (int, float)):
|
|
24
|
-
assert len(med) == 1
|
|
25
|
-
else:
|
|
26
|
-
assert isinstance(med, (int, float))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|