Skip to content

Commit

Permalink
Ruff (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
flying-sheep authored Jul 14, 2023
1 parent 637e30f commit 7cf91ed
Show file tree
Hide file tree
Showing 22 changed files with 428 additions and 239 deletions.
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: double-quote-string-fixer
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
language_version: python3
- repo: https://github.com/pycqa/isort
rev: 5.12.0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.278
hooks:
- id: isort
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
7 changes: 4 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "ms-python.black-formatter",
//"editor.codeActionsOnSave": {
// "source.fixAll.ruff": true,
//},
"editor.codeActionsOnSave": {
"source.fixAll.ruff": true,
"source.organizeImports.ruff": true,
},
},
"python.testing.pytestArgs": [],
"python.testing.unittestEnabled": false,
Expand Down
13 changes: 7 additions & 6 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Sphinx configuration."""

import sys
from abc import ABC
from datetime import datetime
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import MagicMock, patch

Expand All @@ -11,8 +13,8 @@
from importlib_metadata import metadata


def mock_rpy2():
# Can’t use autodoc_mock_imports as we import anndata2ri
def mock_rpy2() -> None:
"""Can’t use autodoc_mock_imports as we import anndata2ri."""
patch('rpy2.situation.get_r_home', lambda: None).start()
sys.modules['rpy2.rinterface_lib'] = MagicMock()
submods = ['embedded', 'conversion', 'memorymanagement', 'sexp', 'bufferprotocol', 'callbacks', '_rinterface_capi']
Expand All @@ -27,7 +29,7 @@ def mock_rpy2():
import rpy2.rinterface_lib.sexp

rpy2.rinterface_lib = sys.modules['rpy2.rinterface_lib']
rpy2.rinterface._MissingArgType = object
rpy2.rinterface._MissingArgType = object # noqa: SLF001
rpy2.rinterface.initr_simple = lambda *_, **__: None

assert rpy2.rinterface_lib.sexp is sexp
Expand All @@ -48,15 +50,14 @@ def mock_rpy2():
project = 'anndata2ri'
meta = metadata(project)
author = meta['author-email'].split('"')[1]
copyright = f'{datetime.now():%Y}, {author}.'
copyright = f'{datetime.now(tz=timezone.utc):%Y}, {author}.' # noqa: A001
version = meta['version']
release = version

# default settings
templates_path = ['_templates']
source_suffix = '.rst'
master_doc = 'index'
# default_role = '?'
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
pygments_style = 'sphinx'

Expand Down
62 changes: 40 additions & 22 deletions docs/ext/r_links.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
"""Sphinx extension for links to R documentation."""

from __future__ import annotations

import logging
from typing import Tuple
from typing import TYPE_CHECKING, ClassVar

from docutils import nodes
from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment
from sphinx.roles import XRefRole


if TYPE_CHECKING:
from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment


class RManRefRole(XRefRole):
nodeclass = nodes.reference
"""R reference role."""

nodeclass: ClassVar[nodes.Node] = nodes.reference
topic_cache: ClassVar[dict[str, dict[str, str]]] = {}
"""pkg → alias → url"""

topic_cache = {}
cls: bool

def __init__(self, *a, cls: bool = False, **kw):
def __init__(self, *a, cls: bool = False, **kw) -> None: # noqa: ANN002, ANN003
"""Set self.cls."""
super().__init__(*a, **kw)
self.cls = cls

def _get_man(self, pkg: str, alias: str):
def _get_man(self, pkg: str, alias: str) -> str:
from urllib.error import HTTPError

pkg_cache = type(self).topic_cache.setdefault(pkg)
Expand All @@ -25,14 +37,14 @@ def _get_man(self, pkg: str, alias: str):
try:
pkg_cache = self._fetch_cache(repo, pkg)
break
except HTTPError:
except HTTPError: # noqa: PERF203
pass
else:
return None
type(self).topic_cache[pkg] = pkg_cache
return pkg_cache.get(alias)

def _fetch_cache(self, repo: str, pkg: str):
def _fetch_cache(self, repo: str, pkg: str) -> dict[str, str]:
from urllib.parse import urljoin
from urllib.request import urlopen

Expand All @@ -41,13 +53,18 @@ def _fetch_cache(self, repo: str, pkg: str):
if repo.startswith('R'):
url = f'https://stat.ethz.ch/R-manual/{repo}/library/{pkg}/html/00Index.html'
tr_xpath = '//tr'
get = lambda tr: (tr[0][0].text, tr[0][0].attrib['href'])

def get(tr: html.HtmlElement) -> tuple[str, str]:
return tr[0][0].text, tr[0][0].attrib['href']

else:
url = f'https://rdrr.io/{repo}/{pkg}/api/'
tr_xpath = "//div[@id='body-content']//tr[./td]"
get = lambda tr: (tr[0].text, tr[1][0].attrib['href'])

with urlopen(url) as con:
def get(tr: html.HtmlElement) -> tuple[str, str]:
return tr[0].text, tr[1][0].attrib['href']

with urlopen(url) as con: # noqa: S310
txt = con.read().decode(con.headers.get_content_charset())
doc = html.fromstring(txt)
cache = {}
Expand All @@ -57,8 +74,14 @@ def _fetch_cache(self, repo: str, pkg: str):
return cache

def process_link(
self, env: BuildEnvironment, refnode: nodes.reference, has_explicit_title: bool, title: str, target: str
) -> Tuple[str, str]:
self,
env: BuildEnvironment, # noqa: ARG002
refnode: nodes.reference,
has_explicit_title: bool, # noqa: ARG002, FBT001
title: str,
target: str,
) -> tuple[str, str]:
"""Derive link title and URL from target."""
qualified = not target.startswith('~')
if not qualified:
target = target[1:]
Expand All @@ -70,16 +93,11 @@ def process_link(
url = self._get_man(package, topic)
refnode['refuri'] = url
if not url:
logging.warning(f'R topic {target} not found.')
logging.warning('R topic %s not found.', target)
return title, url

# def result_nodes(self, document: nodes.document, env: BuildEnvironment, node: nodes.reference, is_ref: bool):
# target = node.get('reftarget')
# if target:
# node.attributes['refuri'] = target
# return [node], []


def setup(app: Sphinx):
def setup(app: Sphinx) -> None:
"""Set Sphinx extension up."""
app.add_role('rman', RManRefRole())
app.add_role('rcls', RManRefRole(cls=True))
34 changes: 29 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,35 @@ filterwarnings = [
line-length = 120
skip-string-normalization = true

[tool.isort]
profile = 'black'
line_length = 120
lines_after_imports = 2
length_sort_straight = true
[tool.ruff]
line-length = 120
select = ['ALL']
ignore = [
'ANN101', # self type doesn’t need to be annotated
'C408', # dict() calls are nice
'COM812', # trailing commas handled by black
'D203', # prefer 0 to 1 blank line before class members
'D213', # prefer docstring summary on first line
'FIX002', # “TODO” comments
'PLR0913', # having many (kw)args is fine
'S101', # asserts are fine
]
allowed-confusables = ['', '×']
[tool.ruff.per-file-ignores]
'src/**/*.py' = ['PT'] # No Pytest checks
'docs/**/*.py' = ['INP001'] # No __init__.py in docs
'tests/**/*.py' = [
'INP001', # No __init__.py in tests
'D100', # test modules don’t need docstrings
'D103', # tests don’t need docstrings
'PD901', # “df” is a fine var name in tests
'PLR2004', # magic numbers are fine in tests
]
[tool.ruff.isort]
known-first-party = ['anndata2ri']
lines-after-imports = 2
[tool.ruff.flake8-quotes]
inline-quotes = 'single'

[build-system]
requires = ['hatchling', 'hatch-vcs']
Expand Down
39 changes: 23 additions & 16 deletions src/anndata2ri/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
r"""
Converter between Python’s AnnData and R’s SingleCellExperiment.
r"""Converter between Python’s AnnData and R’s SingleCellExperiment.
========================================================== = ========================================================
:rcls:`~SingleCellExperiment::SingleCellExperiment` :class:`~anndata.AnnData`
Expand All @@ -14,17 +12,23 @@
:rman:`~SingleCellExperiment::reducedDim`\ ``(d, 'DM')`` ⇄ ``d.``\ :attr:`~anndata.AnnData.obsm`\ ``['X_diffmap']``
========================================================== = ========================================================
"""
__all__ = ['activate', 'deactivate', 'py2rpy', 'rpy2py', 'converter']
from __future__ import annotations

from pathlib import Path
from typing import Any
from typing import TYPE_CHECKING

from . import _py2r, _r2py # noqa: F401
from ._conv import activate, converter, deactivate

from rpy2.rinterface import Sexp

from . import py2r, r2py
from .conv import activate, converter, deactivate
if TYPE_CHECKING:
from anndata import AnnData
from pandas import DataFrame
from rpy2.rinterface import Sexp


__all__ = ['__version__', 'activate', 'deactivate', 'py2rpy', 'rpy2py', 'converter']

HERE = Path(__file__).parent

__author__ = 'Philipp Angerer'
Expand All @@ -35,22 +39,25 @@
except (ImportError, LookupError):
try:
from ._version import __version__
except ImportError:
raise ImportError('Cannot infer version. Make sure to `pip install` the project or install `setuptools-scm`.')
except ImportError as e:
msg = 'Cannot infer version. Make sure to `pip install` the project or install `setuptools-scm`.'
raise ImportError(msg) from e


def py2rpy(obj: Any) -> Sexp:
"""
Convert Python objects to R interface objects. Supports:
def py2rpy(obj: AnnData) -> Sexp:
"""Convert Python objects to R interface objects.
Supports:
- :class:`~anndata.AnnData` → :rcls:`~SingleCellExperiment::SingleCellExperiment`
"""
return converter.py2rpy(obj)


def rpy2py(obj: Any) -> Sexp:
"""
Convert R interface objects to Python objects. Supports:
def rpy2py(obj: Sexp) -> AnnData | DataFrame:
"""Convert R interface objects to Python objects.
Supports:
- :rcls:`~SingleCellExperiment::SingleCellExperiment` → :class:`~anndata.AnnData`
- :rcls:`S4Vectors::DataFrame` → :class:`pandas.DataFrame`
Expand Down
26 changes: 18 additions & 8 deletions src/anndata2ri/conv.py → src/anndata2ri/_conv.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np
import pandas as pd
from rpy2.robjects import conversion, numpy2ri, pandas2ri
Expand All @@ -8,13 +10,20 @@
from . import scipy2ri


if TYPE_CHECKING:
from collections.abc import Callable

from rpy2.rinterface import Sexp
from scipy.sparse import spmatrix


original_converter: conversion.Converter | None = None
converter = conversion.Converter('original anndata conversion')

_mat_converter = numpy2ri.converter + scipy2ri.converter


def mat_py2rpy(obj: np.ndarray) -> np.ndarray:
def mat_py2rpy(obj: np.ndarray | spmatrix | pd.DataFrame) -> Sexp:
if isinstance(obj, pd.DataFrame):
numeric_cols = obj.dtypes <= np.number
if not numeric_cols.all():
Expand All @@ -25,7 +34,7 @@ def mat_py2rpy(obj: np.ndarray) -> np.ndarray:
return _mat_converter.py2rpy(obj)


mat_rpy2py = _mat_converter.rpy2py
mat_rpy2py: Callable[[Sexp], np.ndarray | spmatrix | Sexp] = _mat_converter.rpy2py


def full_converter() -> conversion.Converter:
Expand All @@ -40,15 +49,16 @@ def full_converter() -> conversion.Converter:
return new_converter


def activate():
r"""
Activate conversion for :class:`~anndata.AnnData` objects
def activate() -> None:
r"""Activate conversion for supported objects.
This includes :class:`~anndata.AnnData` objects
as well as :ref:`numpy:arrays` and :class:`pandas.DataFrame`\ s
via ``rpy2.robjects.numpy2ri`` and ``rpy2.robjects.pandas2ri``.
Does nothing if this is the active converter.
"""
global original_converter
global original_converter # noqa: PLW0603

if original_converter is not None:
return
Expand All @@ -58,9 +68,9 @@ def activate():
conversion.set_conversion(new_converter)


def deactivate():
def deactivate() -> None:
"""Deactivate the conversion described above if it is active."""
global original_converter
global original_converter # noqa: PLW0603

if original_converter is None:
return
Expand Down
3 changes: 3 additions & 0 deletions src/anndata2ri/conv_name.py → src/anndata2ri/_conv_name.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from __future__ import annotations


dimension_reductions = {
'pca',
'dca',
Expand Down
Loading

0 comments on commit 7cf91ed

Please sign in to comment.