Skip to content

Commit f1c0a9e

Browse files
committed
Add extremely elaborate R link impl
1 parent ec5e596 commit f1c0a9e

File tree

6 files changed

+123
-57
lines changed

6 files changed

+123
-57
lines changed

README.rst

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,34 @@
22

33
.. |pypi| image:: https://img.shields.io/pypi/v/anndata2ri
44
:target: https://pypi.org/project/anndata2ri/
5+
:alt: PyPI Version
56

67
.. |conda| image:: https://img.shields.io/conda/vn/bioconda/anndata2ri
78
:target: https://anaconda.org/bioconda/anndata2ri
9+
:alt: Bioconda Version
810

911
.. |rtd| image:: https://readthedocs.com/projects/icb-anndata2ri/badge/?version=latest&token=ee358f7efe36cbbd7d04db1b708fa81cefc44634ae7f3f8e0afcd03a1f0b1158
10-
:target: https://icb-anndata2ri.readthedocs-hosted.com/en/latest/?badge=latest
12+
:target: docs_
1113
:alt: Documentation Status
1214

1315
.. |travis| image:: https://travis-ci.org/theislab/anndata2ri.svg?branch=master
1416
:target: https://travis-ci.org/theislab/anndata2ri
17+
:alt: Travis CI Status
1518

1619
.. |doi| image:: https://zenodo.org/badge/171714778.svg
1720
:target: https://zenodo.org/badge/latestdoi/171714778
21+
:alt: Publication DOI
1822

1923
AnnData ↭ SingleCellExperiment
2024
==============================
2125

22-
RPy2 converter from AnnData_ to SingleCellExperiment_ and back.
26+
RPy2 converter from AnnData_ to SingleCellExperiment_ and back. (For **details about conversion** see the docs_)
2327

2428
You can for example use it to process your data using both Scanpy_ and Seurat_, as described in this `example notebook`_
2529

2630
.. _AnnData: https://anndata.readthedocs.io/en/latest/
2731
.. _SingleCellExperiment: http://bioconductor.org/packages/release/bioc/vignettes/SingleCellExperiment/inst/doc/intro.html
32+
.. _docs: https://icb-anndata2ri.readthedocs-hosted.com/en/latest/
2833
.. _Scanpy: https://scanpy.readthedocs.io/en/stable/
2934
.. _Seurat: https://satijalab.org/seurat/
3035
.. _`example notebook`: https://github.com/LuckyMD/Code_snippets/blob/master/Seurat_to_anndata.ipynb

anndata2ri/__init__.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
1-
"""
1+
r"""
22
Converter between Python’s AnnData and R’s SingleCellExperiment.
3+
4+
5+
========================================================== ===== ================================================
6+
:rcls:`~SingleCellExperiment::SingleCellExperiment` :class:`~anndata.AnnData`
7+
========================================================== ===== ================================================
8+
:rman:`~SummarizedExperiment::assay`\ ``(d, 'X')`` ⇄ :attr:`~anndata.AnnData.X`
9+
:rman:`~SummarizedExperiment::assay`\ ``(d, 'counts')`` ⇄ :attr:`~anndata.AnnData.layers`\ ``['counts']``
10+
:rman:`~SummarizedExperiment::colData`\ ``(d)`` ⇄ :attr:`~anndata.AnnData.obs`
11+
:rman:`~SummarizedExperiment::rowData`\ ``(d)`` ⇄ :attr:`~anndata.AnnData.var`
12+
:rman:`~S4Vectors::metadata`\ ``(d)`` ⇄ :attr:`~anndata.AnnData.uns`
13+
:rman:`~SingleCellExperiment::reducedDim`\ ``(d, 'PCA')`` ⇄ :attr:`~anndata.AnnData.obsm`\ ``['X_pca']``
14+
:rman:`~SingleCellExperiment::reducedDim`\ ``(d, 'DM')`` ⇄ :attr:`~anndata.AnnData.obsm`\ ``['X_diffmap']``
15+
========================================================== ===== ================================================
316
"""
417
__all__ = ["activate", "deactivate", "py2rpy", "rpy2py", "converter"]
518

docs/_static/css/custom.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/* Make things like :func:`foo`\\ ``bar`` have no visible gap */
2+
table a.reference.external + code {
3+
border-left: 0;
4+
margin-left: -6px;
5+
padding-left: 0;
6+
}
7+
8+
/* Make sure columns don’t wrongly derive width from rst code */
9+
table.docutils col {
10+
width: auto !important;
11+
}

docs/conf.py

Lines changed: 6 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
1-
from typing import Tuple
21
from unittest.mock import MagicMock
32

4-
from docutils import nodes
5-
from sphinx.application import Sphinx
6-
from sphinx.environment import BuildEnvironment
7-
from sphinx.roles import XRefRole
8-
9-
103
import sys
114
from pathlib import Path
125
from datetime import datetime
@@ -20,15 +13,16 @@
2013
submods = ["embedded", "conversion", "memorymanagement", "sexp", "bufferprotocol", "callbacks", "_rinterface_capi"]
2114
sys.modules.update({f"rpy2.rinterface_lib.{sub}": MagicMock() for sub in submods})
2215
sexp = sys.modules["rpy2.rinterface_lib.sexp"]
23-
sexp.Sexp = sexp.SexpVector = sexp.SexpEnvironment = sexp.StrSexpVector = MagicMock
16+
sexp.Sexp = type("Sexp", (MagicMock,), dict(__module__="rpy2.rinterface"))
17+
sexp.SexpVector = sexp.SexpEnvironment = sexp.StrSexpVector = MagicMock
2418
sexp.SexpVector.from_iterable = MagicMock()
2519

2620
import rpy2.rinterface
2721

2822
rpy2.rinterface._MissingArgType = object
2923

3024
# now we can import it!
31-
sys.path.insert(0, str(HERE.parent))
25+
sys.path[:0] = [str(HERE.parent), str(HERE / "ext")]
3226
import anndata2ri.scipy2ri # noqa
3327

3428

@@ -58,6 +52,7 @@
5852
"sphinx.ext.autosummary",
5953
"sphinx_autodoc_typehints",
6054
"scanpydoc",
55+
*[p.stem for p in (HERE / "ext").glob("*.py")],
6156
]
6257

6358
# Generate the API documentation when building
@@ -85,6 +80,8 @@
8580

8681
html_theme = "scanpydoc"
8782
html_theme_options = dict(collapse_navigation=True)
83+
html_static_path = ["_static"]
84+
html_css_files = ["css/custom.css"]
8885
html_context = dict(
8986
display_github=True,
9087
github_user="theislab",
@@ -94,51 +91,6 @@
9491
)
9592

9693

97-
# -- Add R links ----------------------------------------------------------
98-
99-
100-
class RManRefRole(XRefRole):
101-
nodeclass = nodes.reference
102-
103-
def __init__(self, *a, cls: bool = False, **kw):
104-
super().__init__(*a, **kw)
105-
self.cls = cls
106-
107-
def process_link(
108-
self, env: BuildEnvironment, refnode: nodes.reference, has_explicit_title: bool, title: str, target: str
109-
) -> Tuple[str, str]:
110-
qualified = not target.startswith("~")
111-
if not qualified:
112-
target = target[1:]
113-
package, symbol = target.split("::")
114-
title = target if qualified else symbol
115-
topic = symbol
116-
if self.cls:
117-
topic += "-class"
118-
target = f"https://www.rdocumentation.org/packages/{package}/topics/{topic}"
119-
refnode["refuri"] = target
120-
return title, target
121-
122-
# def result_nodes(self, document: nodes.document, env: BuildEnvironment, node: nodes.reference, is_ref: bool):
123-
# target = node.get('reftarget')
124-
# if target:
125-
# node.attributes['refuri'] = target
126-
# return [node], []
127-
128-
129-
def setup(app: Sphinx):
130-
app.add_role("rman", RManRefRole())
131-
app.add_role("rcls", RManRefRole(cls=True))
132-
133-
134-
# -- Quick fixes ---------------------------------------------------------------
135-
136-
137-
from rpy2.rinterface import Sexp
138-
139-
Sexp.__module__ = "rpy2.rinterface"
140-
141-
14294
# -- Options for other output formats ------------------------------------------
14395

14496

docs/ext/r_links.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import logging
2+
from typing import Tuple
3+
4+
from docutils import nodes
5+
from sphinx.application import Sphinx
6+
from sphinx.environment import BuildEnvironment
7+
from sphinx.roles import XRefRole
8+
9+
10+
class RManRefRole(XRefRole):
11+
nodeclass = nodes.reference
12+
13+
topic_cache = {}
14+
15+
def __init__(self, *a, cls: bool = False, **kw):
16+
super().__init__(*a, **kw)
17+
self.cls = cls
18+
19+
def _get_man(self, pkg: str, alias: str):
20+
from urllib.error import HTTPError
21+
22+
pkg_cache = type(self).topic_cache.setdefault(pkg)
23+
if not pkg_cache:
24+
for repo in ["R-patched", "cran", "bioc"]:
25+
try:
26+
pkg_cache = self._fetch_cache(repo, pkg)
27+
break
28+
except HTTPError:
29+
pass
30+
else:
31+
return None
32+
type(self).topic_cache[pkg] = pkg_cache
33+
return pkg_cache.get(alias)
34+
35+
def _fetch_cache(self, repo: str, pkg: str):
36+
from lxml import html
37+
from urllib.request import urlopen
38+
from urllib.parse import urljoin
39+
40+
if repo.startswith("R"):
41+
url = f"https://stat.ethz.ch/R-manual/{repo}/library/{pkg}/html/00Index.html"
42+
tr_xpath = "//tr"
43+
get = lambda tr: (tr[0][0].text, tr[0][0].attrib["href"])
44+
else:
45+
url = f"https://rdrr.io/{repo}/{pkg}/api/"
46+
tr_xpath = "//div[@id='body-content']//tr[./td]"
47+
get = lambda tr: (tr[0].text, tr[1][0].attrib["href"])
48+
49+
with urlopen(url) as con:
50+
txt = con.read().decode(con.headers.get_content_charset())
51+
doc = html.fromstring(txt)
52+
cache = {}
53+
for tr in doc.xpath(tr_xpath):
54+
topic, href = get(tr)
55+
cache[topic] = urljoin(url, href)
56+
return cache
57+
58+
def process_link(
59+
self, env: BuildEnvironment, refnode: nodes.reference, has_explicit_title: bool, title: str, target: str
60+
) -> Tuple[str, str]:
61+
qualified = not target.startswith("~")
62+
if not qualified:
63+
target = target[1:]
64+
package, symbol = target.split("::")
65+
title = target if qualified else symbol
66+
topic = symbol
67+
if self.cls:
68+
topic += "-class"
69+
url = self._get_man(package, topic)
70+
refnode["refuri"] = url
71+
if not url:
72+
logging.warning(f"R topic {target} not found.")
73+
return title, url
74+
75+
# def result_nodes(self, document: nodes.document, env: BuildEnvironment, node: nodes.reference, is_ref: bool):
76+
# target = node.get('reftarget')
77+
# if target:
78+
# node.attributes['refuri'] = target
79+
# return [node], []
80+
81+
82+
def setup(app: Sphinx):
83+
app.add_role("rman", RManRefRole())
84+
app.add_role("rcls", RManRefRole(cls=True))

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ doc = [
3535
'sphinx>=3.0',
3636
'sphinx-autodoc-typehints',
3737
'scanpydoc',
38+
'lxml', # For scraping the R link info
3839
]
3940

4041
[tool.flit.metadata.urls]

0 commit comments

Comments
 (0)