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))