diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..ee7178fb --- /dev/null +++ b/.flake8 @@ -0,0 +1,20 @@ +[flake8] +max-line-length = 100 +ignore = + # line break before a binary operator -> black does not adhere to PEP8 + W503 + # line break occured after a binary operator -> black does not adhere to PEP8 + W504 + # line too long -> we accept long comment lines; black gets rid of long code lines + E501 + # whitespace before : -> black does not adhere to PEP8 + E203 + # leading '#' are fine if not used too often + E266 + # it can be more convenient to use a bare 'except' + E722 +exclude = .git,__pycache__ +per-file-ignores = + tests/*: D + */__init__.py: F401 + sopa/main.py: F401 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..3c877354 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,30 @@ +fail_fast: false +default_language_version: + python: python3 +default_stages: + - commit + - push +minimum_pre_commit_version: 2.16.0 +repos: + - repo: https://github.com/psf/black + rev: 24.1.1 + hooks: + - id: black + - repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort + - repo: https://github.com/PyCQA/flake8 + rev: 7.0.0 + hooks: + - id: flake8 + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: debug-statements + - id: double-quote-string-fixer + - id: name-tests-test + - id: requirements-txt-fixer diff --git a/poetry.lock b/poetry.lock index d35aedfd..e1c6ef3a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1200,6 +1200,23 @@ calc = ["shapely"] s3 = ["boto3 (>=1.3.1)"] test = ["Fiona[s3]", "pytest (>=7)", "pytest-cov", "pytz"] +[[package]] +name = "flake8" +version = "7.0.0" +description = "the modular source code checker: pep8 pyflakes and co" +category = "main" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"}, + {file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.2.0,<3.3.0" + [[package]] name = "fonttools" version = "4.47.2" @@ -2308,6 +2325,18 @@ files = [ [package.dependencies] traitlets = "*" +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + [[package]] name = "mdit-py-plugins" version = "0.4.0" @@ -3714,6 +3743,18 @@ files = [ [package.dependencies] numpy = ">=1.16.6" +[[package]] +name = "pycodestyle" +version = "2.11.1" +description = "Python style guide checker" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, +] + [[package]] name = "pycparser" version = "2.21" @@ -3747,6 +3788,18 @@ cmd = ["pyyaml", "requests"] doc = ["nbsite", "sphinx-ioam-theme"] tests = ["flake8", "pytest"] +[[package]] +name = "pyflakes" +version = "3.2.0" +description = "passive checker of Python programs" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, +] + [[package]] name = "pygeos" version = "0.14" @@ -4018,6 +4071,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -5950,4 +6004,4 @@ tangram = ["tangram-sc"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.11" -content-hash = "977234c9fae0a78884783b221a10679ce689f35298d003a6142de30e9bf38b4a" +content-hash = "66c7434863f556e32d1f70c0d2be68d10be74014707ea1c4143c627d1c195eeb" diff --git a/pyproject.toml b/pyproject.toml index a84b1695..0e816cda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ tangram-sc = { version = "^1.0.4", optional = true } snakemake = { version = "^7.32.4,<8.1.3", optional = true } pulp = { version = ">=2.3.1,<2.8", optional = true } +flake8 = {version = "^7.0.0", extras = ["dev"]} [tool.poetry.extras] cellpose = ["cellpose", "opencv-python", "torch"] diff --git a/sopa/annotation/tangram/run.py b/sopa/annotation/tangram/run.py index d1df98f6..c96414f6 100644 --- a/sopa/annotation/tangram/run.py +++ b/sopa/annotation/tangram/run.py @@ -166,7 +166,7 @@ def pp_adata(self, ad_sp_: AnnData, ad_sc_: AnnData, split: np.ndarray) -> AnnDa assert len( selection - ), f"No gene in common between the reference and the spatial adata object. Have you run transcript aggregation?" + ), "No gene in common between the reference and the spatial adata object. Have you run transcript aggregation?" log.info(f"Keeping {len(selection)} shared genes") for ad_ in [ad_sp_split, ad_sc_]: diff --git a/sopa/cli/annotate.py b/sopa/cli/annotate.py index 9733fbfe..8774e02a 100644 --- a/sopa/cli/annotate.py +++ b/sopa/cli/annotate.py @@ -26,7 +26,7 @@ def fluorescence( sdata = read_zarr_standardized(sdata_path) - assert sdata.table is not None, f"Annotation requires `sdata.table` to be not None" + assert sdata.table is not None, "Annotation requires `sdata.table` to be not None" higher_z_score(sdata.table, marker_cell_dict, cell_type_key) sdata.table.write_zarr(Path(sdata_path) / "table" / "table") diff --git a/sopa/cli/check.py b/sopa/cli/check.py index 2442e38b..35ac084e 100644 --- a/sopa/cli/check.py +++ b/sopa/cli/check.py @@ -77,7 +77,7 @@ def reference( def _get(config, *args): for arg in args: - if not arg in config: + if arg not in config: return False config = config[arg] return config @@ -85,7 +85,7 @@ def _get(config, *args): def _check_dict(config: dict, d: dict, log, prefix: str = "config"): for key, values in d.items(): - if not key in config: + if key not in config: log.warn(f"Required config key {prefix}['{key}'] not found") elif isinstance(values, dict): _check_dict(config[key], values, log, f"{prefix}['{key}']") diff --git a/sopa/cli/patchify.py b/sopa/cli/patchify.py index 3f2ff4b2..54523e8c 100644 --- a/sopa/cli/patchify.py +++ b/sopa/cli/patchify.py @@ -69,8 +69,6 @@ def baysor( ), ): """Prepare the patches for Baysor segmentation""" - from pathlib import Path - from sopa._constants import SopaFiles, SopaKeys from sopa._sdata import get_key from sopa.io.standardize import read_zarr_standardized, sanity_check diff --git a/sopa/cli/utils.py b/sopa/cli/utils.py index d84bfd5a..523696f0 100644 --- a/sopa/cli/utils.py +++ b/sopa/cli/utils.py @@ -1,7 +1,7 @@ def _check_zip(names: list[str]): for name in names: if isinstance(name, str): - assert name.endswith(".zip"), f"Intermediate files names must end with .zip" + assert name.endswith(".zip"), "Intermediate files names must end with .zip" def _default_boundary_dir(sdata_path: str, directory_name: str): diff --git a/sopa/io/explorer/_constants.py b/sopa/io/explorer/_constants.py index 91fd77f6..5c5d421f 100644 --- a/sopa/io/explorer/_constants.py +++ b/sopa/io/explorer/_constants.py @@ -97,7 +97,6 @@ def experiment_dict(run_name: str, region_name: str, num_cells: int, pixel_size: "instrument_sn": "N/A", "instrument_sw_version": "N/A", "analysis_sw_version": "xenium-1.3.0.5", - "experiment_uuid": "", "cassette_uuid": "", "roi_uuid": "", "z_step_size": 3.0, diff --git a/sopa/io/explorer/converter.py b/sopa/io/explorer/converter.py index faf216f1..5555563b 100644 --- a/sopa/io/explorer/converter.py +++ b/sopa/io/explorer/converter.py @@ -22,7 +22,7 @@ def _check_explorer_directory(path: Path): assert ( not path.exists() or path.is_dir() - ), f"A path to an existing file was provided. It should be a path to a directory." + ), "A path to an existing file was provided. It should be a path to a directory." path.mkdir(parents=True, exist_ok=True) @@ -33,7 +33,7 @@ def _should_save(mode: str | None, character: str): assert len(mode) and mode[0] in [ "-", "+", - ], f"Mode should be a string that starts with '+' or '-'" + ], "Mode should be a string that starts with '+' or '-'" return character in mode if mode[0] == "+" else character not in mode diff --git a/sopa/io/explorer/images.py b/sopa/io/explorer/images.py index 269b8089..bc6c12ca 100644 --- a/sopa/io/explorer/images.py +++ b/sopa/io/explorer/images.py @@ -144,7 +144,7 @@ def _set_colors(channel_names: list[str]) -> list[str]: ] valid_colors = [c for c in ExplorerConstants.COLORS if c != ExplorerConstants.NUCLEUS_COLOR] n_missing = sum( - not is_wavelength and not c in ExplorerConstants.KNOWN_CHANNELS + not is_wavelength and c not in ExplorerConstants.KNOWN_CHANNELS for c, is_wavelength in zip(channel_names, existing_wavelength) ) colors_iterator: list = np.repeat(valid_colors, ceil(n_missing / len(valid_colors))).tolist() diff --git a/sopa/io/explorer/shapes.py b/sopa/io/explorer/shapes.py index 26f803cd..26fb7273 100644 --- a/sopa/io/explorer/shapes.py +++ b/sopa/io/explorer/shapes.py @@ -7,7 +7,7 @@ import zarr from shapely.geometry import Polygon -from ._constants import ExplorerConstants, FileNames, cell_summary_attrs, group_attrs +from ._constants import FileNames, cell_summary_attrs, group_attrs from .utils import explorer_file_path log = logging.getLogger(__name__) diff --git a/sopa/io/imaging.py b/sopa/io/imaging.py index c9f232e2..88a8839a 100644 --- a/sopa/io/imaging.py +++ b/sopa/io/imaging.py @@ -272,7 +272,7 @@ def ome_tif(path: Path, as_image: bool = False) -> SpatialImage | SpatialData: image: da.Array = imread(path) if image.ndim == 4: - assert image.shape[0] == 1, f"4D images not supported" + assert image.shape[0] == 1, "4D images not supported" image = da.moveaxis(image[0], 2, 0) log.info(f"Transformed 4D image into a 3D image of shape (c, y, x) = {image.shape}") elif image.ndim != 3: diff --git a/sopa/io/report/generate.py b/sopa/io/report/generate.py index 2d65a07b..b43e57a1 100644 --- a/sopa/io/report/generate.py +++ b/sopa/io/report/generate.py @@ -56,7 +56,7 @@ def __init__(self, sdata: SpatialData): self.sdata = sdata def _table_has(self, key, default=False): - if not SopaKeys.UNS_KEY in self.sdata.table.uns: + if SopaKeys.UNS_KEY not in self.sdata.table.uns: return default return self.sdata.table.uns[SopaKeys.UNS_KEY].get(key, default) @@ -70,7 +70,7 @@ def general_section(self): "SpatialData information", [ Paragraph( - f"Sopa is using SpatialData under the hood. This is how the object looks like:" + "Sopa is using SpatialData under the hood. This is how the object looks like:" ), CodeBlock(str(self.sdata)), ], diff --git a/sopa/io/standardize.py b/sopa/io/standardize.py index 12489f92..fb422598 100644 --- a/sopa/io/standardize.py +++ b/sopa/io/standardize.py @@ -14,7 +14,7 @@ def sanity_check(sdata: SpatialData, delete_table: bool = False, warn: bool = False): assert ( len(sdata.images) > 0 - ), f"The spatialdata object has no image. Sopa is not designed for this." + ), "The spatialdata object has no image. Sopa is not designed for this." if len(sdata.images) != 1: message = f"The spatialdata object has {len(sdata.images)} images. We advise to run sopa on one image (which can have multiple channels and multiple scales)" diff --git a/sopa/io/transcriptomics.py b/sopa/io/transcriptomics.py index cb0fa9ba..60320567 100644 --- a/sopa/io/transcriptomics.py +++ b/sopa/io/transcriptomics.py @@ -21,8 +21,7 @@ from spatialdata import SpatialData from spatialdata._logging import logger from spatialdata.models import Image2DModel, PointsModel, ShapesModel, TableModel -from spatialdata.transformations import Affine, Identity -from spatialdata.transformations.transformations import Identity, Scale +from spatialdata.transformations import Affine, Identity, Scale from spatialdata_io._constants._constants import MerscopeKeys, XeniumKeys diff --git a/sopa/segmentation/aggregate.py b/sopa/segmentation/aggregate.py index 37dbb5a0..0c5f152b 100644 --- a/sopa/segmentation/aggregate.py +++ b/sopa/segmentation/aggregate.py @@ -130,7 +130,7 @@ def update_table( assert ( average_intensities or does_count - ), f"You must choose at least one aggregation: transcripts or fluorescence intensities" + ), "You must choose at least one aggregation: transcripts or fluorescence intensities" if gene_column is not None: if self.table is not None: diff --git a/sopa/segmentation/shapes.py b/sopa/segmentation/shapes.py index 8a2b77f7..cc87724d 100644 --- a/sopa/segmentation/shapes.py +++ b/sopa/segmentation/shapes.py @@ -31,7 +31,7 @@ def solve_conflicts( n_cells = len(cells) resolved_indices = np.arange(n_cells) - assert n_cells > 0, f"No cells was segmented, cannot continue" + assert n_cells > 0, "No cells was segmented, cannot continue" tree = shapely.STRtree(cells) conflicts = tree.query(cells, predicate="intersects") diff --git a/sopa/segmentation/stainings.py b/sopa/segmentation/stainings.py index cc0259da..71af765a 100644 --- a/sopa/segmentation/stainings.py +++ b/sopa/segmentation/stainings.py @@ -4,7 +4,6 @@ import geopandas as gpd import numpy as np -import zarr from scipy.ndimage import gaussian_filter from shapely import affinity from shapely.geometry import Polygon, box diff --git a/sopa/spatial/_build.py b/sopa/spatial/_build.py index 2b0c7322..b2caf921 100644 --- a/sopa/spatial/_build.py +++ b/sopa/spatial/_build.py @@ -42,7 +42,7 @@ def spatial_neighbors( assert ( radius is None or len(radius) == 2 - ), f"Radius is expected to be a tuple (min_radius, max_radius)" + ), "Radius is expected to be a tuple (min_radius, max_radius)" log.info("Computing delaunay graph") diff --git a/sopa/spatial/morpho.py b/sopa/spatial/morpho.py index f8402d02..166474f2 100644 --- a/sopa/spatial/morpho.py +++ b/sopa/spatial/morpho.py @@ -141,7 +141,7 @@ def niches_geometry_stats( gdf = geometrize_niches(adata, niche_key, **geometrize_niches_kwargs) value_counts = gdf[niche_key].value_counts() - assert len(gdf), f"No niche geometry found, stats can't be computed" + assert len(gdf), "No niche geometry found, stats can't be computed" log.info(f"Computing pairwise distances between {len(gdf)} components") pairwise_distances: pd.DataFrame = gdf.geometry.apply(lambda g: gdf.distance(g))