Skip to content

Commit 469fdcf

Browse files
committed
testing for various python versions, removing .python-version, testing for installation, some mypy fixes, pyproject.toml updates
1 parent 46738f8 commit 469fdcf

File tree

9 files changed

+837
-451
lines changed

9 files changed

+837
-451
lines changed

.github/workflows/ci.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@ on:
99
jobs:
1010
lint-and-test:
1111
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ['3.10', '3.11', '3.12']
1215
steps:
1316
- uses: actions/checkout@v4
1417
- uses: actions/setup-python@v5
1518
with:
16-
python-version: '3.12'
19+
python-version: ${{ matrix.python-version }}
1720

1821
- name: Install uv
1922
run: pip install uv
@@ -22,13 +25,17 @@ jobs:
2225
uses: actions/cache@v3
2326
with:
2427
path: ~/.cache/uv
25-
key: ${{ runner.os }}-uv-${{ hashFiles('pyproject.toml') }}
28+
key: ${{ runner.os }}-uv-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }}
29+
restore-keys: |
30+
${{ runner.os }}-uv-${{ matrix.python-version }}-
2631
2732
- name: Install dependencies
2833
run: uv pip install --system -e . --group dev
2934

3035
- name: Lint with ruff
36+
# Run linting only on one Python version to avoid redundant checks
37+
if: matrix.python-version == '3.12'
3138
run: ruff check .
3239

3340
- name: Run tests with coverage
34-
run: pytest --cov=loupepy -vv
41+
run: pytest --cov=loupepy -vv

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ build/
55
dist/
66
wheels/
77
*.egg-info
8+
.python-version
89
#cache
910
*.mypy_cache
1011
*.pytest_cache
1112
# Virtual environments
12-
random_testing*
1313
.venv/
1414
.git/
1515
.idea/
16+
# internal testing files
17+
random_testing*
1618
create_test_file.py
1719
test.h5ad

.python-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.12
1+
3.10

pyproject.toml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
11
[project]
22
name = "loupepy"
33
version = "0.1.0"
4-
description = "Add your description here"
4+
authors = [
5+
{ name="Alexei Martsinkovskiy", email="[email protected]" },
6+
]
7+
description = "A python package for generating 10x cloupe files from anndata objects"
58
readme = "README.md"
6-
requires-python = ">=3.12"
9+
requires-python = ">=3.10"
710
dependencies = [
811
"anndata>=0.11.4",
912
"certifi>=2025.4.26",
1013
]
14+
classifiers = [
15+
"Programming Language :: Python :: 3",
16+
"Operating System :: OS Independent",
17+
]
18+
license = "MIT"
19+
license-files = ["LICEN[CS]E*"]
20+
[project.urls]
21+
Homepage = "https://github.com/linearparadox/loupepy"
22+
Issues = "https://github.com/linearparadox/loupepy/issues"
1123

1224
[tool.ruff.lint.per-file-ignores]
1325
"__init__.py" = ["F401"]
@@ -29,4 +41,4 @@ dev = [
2941
"ruff>=0.11.5",
3042
"scanpy>=1.11.1",
3143
"scipy-stubs>=1.15.2.1",
32-
]
44+
]

src/loupepy/convert.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import os
22
from os import PathLike
3+
4+
import numpy as np
35
from anndata import AnnData # type: ignore
46
import pandas as pd
57
from scipy.sparse import csc_matrix
@@ -74,7 +76,7 @@ def _write_clusters(f: h5py.File, obs: pd.DataFrame) -> None:
7476
group.create_dataset(name="score", shape=(1,), data=[0.0])
7577
_create_string_dataset(group, "clustering_type", "unknown")
7678

77-
def _write_projection(f: h5py.Group, dim: array, name: str) -> None:
79+
def _write_projection(f: h5py.Group, dim: ndarray, name: str) -> None:
7880
'''
7981
Writes the projections to the h5 file
8082

src/loupepy/setup.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
from urllib.request import urlretrieve
33
import hashlib
44
from pathlib import Path
5+
import logging
56
import os
7+
from typing import Union
68
OPERATING_SYSTEM = platform.system()
79

810
def _get_checksum() -> tuple[str, str]:
@@ -57,37 +59,45 @@ def _download_loupe_converter(path: Path) -> None:
5759
link = _md5_checksum()
5860
urlretrieve(link, path)
5961
path.chmod(0o755)
60-
print(path)
62+
logging.log(logging.INFO, f"loupe converter binary successfully downloaded to {path}")
6163

62-
def setup(path: Path | None | str = None) -> None:
64+
def setup(path: Union[os.PathLike[str], Path, None] = None) -> None:
6365
'''
64-
Downloads the loupe converter binary to the specified path.
66+
Downloads the loupe converter binary to the specified directory.
6567
If no path is specified, it will be downloaded to the default location for the operating system.
68+
Args:
69+
path (Union[os.PathLike[str], Path, None]): The path to download the loupe converter binary to.
70+
If None, it will be downloaded to the default location for the operating system.
6671
'''
6772
if path is None:
6873
path = _get_install_path()
6974
if path is str:
7075
path = Path(path)
71-
eula()
76+
eula(path)
7277
_download_loupe_converter(path) # type: ignore
7378

7479

7580

7681

7782

78-
def eula() -> None:
83+
def eula(path: Union[os.PathLike[str], Path, None] = None) -> None:
7984
'''
8085
Prompts the user to agree to the EULA, as it is in the R version of the tool
8186
'''
8287
print("This tool is independently maintained,"
8388
" but utilizes 10x genomics tools to perform conversions")
8489
print("By using this tool, you agree to the 10X Genomics End User License Agreement "
85-
"(https://www.10xgenomics.com/legal/end-user-software-license-agreement).")
86-
print("Do you agree to the terms of the EULA? (yes/no)")
87-
response = input()
88-
if response.lower() != 'yes':
90+
"(https://www.10xgenomics.com/legal/end-user-software-license-agreement ).")
91+
print("Do you agree to the terms of the EULA? (yes/y or no/n)")
92+
response = input("Do you agree to the terms of the EULA? (yes/y or no/n)")
93+
while response.lower() not in ["yes", "y", "no", "n"]:
94+
response = input("Do you agree to the terms of the EULA? (yes/y or no/n)")
95+
if response.lower() in ["no", "n"]:
8996
raise OSError('You must agree to the EULA to use this tool')
90-
path = _get_install_path()
97+
if path is None:
98+
path = _get_install_path()
99+
if not isinstance(path, Path):
100+
path = Path(path)
91101
path.mkdir(parents=False, exist_ok=True)
92102
path = path / 'eula'
93103
path.touch(exist_ok=True)
@@ -97,12 +107,16 @@ def eula() -> None:
97107

98108

99109

100-
def eula_reset() -> None:
110+
def eula_reset(path: Union[os.PathLike[str], Path, None] = None) -> None:
101111
'''
102112
Resets the EULA agreement
113+
"
103114
'''
104-
path = _get_install_path()
105-
path = path / 'loupepy' / 'eula'
115+
if path is None:
116+
path = _get_install_path()
117+
if not isinstance(path, Path):
118+
path = Path(path)
119+
path = path / 'eula'
106120
path.unlink()
107121
path=path.parent / 'loupe_converter'
108122
path.unlink(missing_ok=True)

src/loupepy/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def _validate_obs(obs: DataFrame, strict=False) -> DataFrame:
101101
warnings.warn(f'Column {col} has more than 32768 categories, skipping')
102102
obs.drop(col, axis=1, inplace=True)
103103

104-
def _validate_obsm(obsm: dict[str, ndarray], obsm_keys: str|None = None, strict: bool = False) -> list[str]:
104+
def _validate_obsm(obsm: dict[str, ndarray], obsm_keys: list[str]|None = None, strict: bool = False) -> list[str]:
105105
"""
106106
Validate the obsm dictionary.
107107
Args:

tests/test_setup.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import pytest
2+
from loupepy.setup import eula_reset, eula_reset, setup # type: ignore
3+
import os
4+
5+
def test_eula_and_reset(monkeypatch, tmp_path_factory):
6+
output_dir = tmp_path_factory.mktemp("fake_directory")
7+
monkeypatch.setattr('builtins.input', lambda _: "y")
8+
setup(output_dir)
9+
assert os.path.exists(output_dir / "eula")
10+
assert os.path.exists(output_dir / "loupe_converter")
11+
eula_reset(output_dir)
12+
assert not os.path.exists(output_dir / "eula")
13+
assert not os.path.exists(output_dir / "loupe_converter")

0 commit comments

Comments
 (0)