diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c2fbc9..1a672ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Version 0.10.0 - 0.10.4 +## Version 0.10.0 - 0.10.5 - Added methods to write to RDS/RData files. - Supports atomic types, generic dictionaries/lists, and **BiocPy objects**. @@ -8,6 +8,7 @@ - Fixed an issue with S4 classes not properly saved as RDS files. - Implement `save_rds` generic for sparse matrix formats (csc, csr and coo). - Implement `save_rds` for NumPy scalars. +- Added `register_parser` decorator to dynamically register custom R-to-Python class parser functions. ## Version 0.9.0 - 0.9.1 diff --git a/README.md b/README.md index 81db4ee..82abca7 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,19 @@ write_rda(objects, "workspace.rda") ### 3. Custom Extensions -If you have custom S4 representations or class mapping needs, you can parse the raw RDS structure into Python dictionary representations using `parse_rds`/`parse_rda` and apply your custom deserializers: +For custom R classes or S4 structures, you can register custom parser functions dynamically using the `register_parser` decorator: + +```python +import rds2py + +@rds2py.register_parser("MyCustomRClass") +def parse_my_custom_class(robject, **kwargs): + # Construct your custom Python representation from the raw RDS dictionary + value = robject.get("data", None) + return {"coerced": True, "value": value} +``` + +You can also parse the raw RDS structure into Python dictionary representations using `parse_rds`/`parse_rda` and apply your custom deserializers: ```python from rds2py import parse_rds diff --git a/src/rds2py/__init__.py b/src/rds2py/__init__.py index 63be5d3..f6b672c 100644 --- a/src/rds2py/__init__.py +++ b/src/rds2py/__init__.py @@ -16,5 +16,5 @@ del version, PackageNotFoundError -from .generics import read_rds, read_rda, save_rds +from .generics import read_rds, read_rda, save_rds, register_parser from .rdsutils import parse_rds, parse_rda, write_rds, write_rda diff --git a/src/rds2py/generics.py b/src/rds2py/generics.py index b878056..220ccc9 100644 --- a/src/rds2py/generics.py +++ b/src/rds2py/generics.py @@ -165,6 +165,22 @@ def _dispatcher(robject: dict, **kwargs): return robject +def register_parser(class_name: str): + """Decorator to register a custom R-to-Python class parser. + + Args: + class_name: + The R class name to register. + """ + + def decorator(func): + REGISTRY[class_name] = func + + return func + + return decorator + + @singledispatch def save_rds(x: Any, path: Optional[str] = None): """Save a Python object as RDS file. diff --git a/tests/test_custom_parser.py b/tests/test_custom_parser.py new file mode 100644 index 0000000..cb286b3 --- /dev/null +++ b/tests/test_custom_parser.py @@ -0,0 +1,17 @@ +import rds2py +from rds2py.generics import _dispatcher + +__author__ = "jkanche" +__copyright__ = "jkanche" +__license__ = "MIT" + + +def test_register_parser(): + @rds2py.register_parser("DummyRClass") + def parse_dummy(robj, **kwargs): + return {"coerced": True, "value": robj.get("data", None)} + + robj = {"type": "S4", "class_name": "DummyRClass", "data": 42} + res = _dispatcher(robj) + + assert res == {"coerced": True, "value": 42}