diff --git a/docs/source/conf.py b/docs/source/conf.py
index 99a9155..17474b3 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -14,20 +14,20 @@
import os.path
import sys
-sys.path.insert(0, os.path.abspath("../.."))
-sys.path.insert(0, os.path.abspath("../../../py-pde"))
+sys.path.insert(0, os.path.abspath("../..")) # noqa: PTH100
+sys.path.insert(0, os.path.abspath("../../../py-pde")) # noqa: PTH100
sys.path.insert(0, ".")
from datetime import date
-import sphinx_simplify_typehints # @UnresolvedImport @UnusedImport
+import sphinx_simplify_typehints
# -- Project information -----------------------------------------------------
project = "py-droplets"
module_name = "droplets"
author = "David Zwicker"
-copyright = f"{date.today().year}, {author}" # @ReservedAssignment
+copyright = f"{date.today().year}, {author}" # noqa: A001
# The short X.Y version
import droplets
diff --git a/docs/source/quickstart/contributing.rst b/docs/source/quickstart/contributing.rst
index 711f9f6..8882250 100644
--- a/docs/source/quickstart/contributing.rst
+++ b/docs/source/quickstart/contributing.rst
@@ -19,8 +19,9 @@ define a droplet class that stores additional information by subclassing
Coding style
""""""""""""
-The coding style is enforced using `isort `_
-and `black `_. Moreover, we use `Google Style docstrings
+The coding style is enforced using `ruff `_, based on the
+styles suggest by `isort `_ and
+`black `_. Moreover, we use `Google Style docstrings
`_,
which might be best `learned by example
`_.
diff --git a/docs/source/run_autodoc.py b/docs/source/run_autodoc.py
index 6e4bc04..d935598 100755
--- a/docs/source/run_autodoc.py
+++ b/docs/source/run_autodoc.py
@@ -4,10 +4,11 @@
import logging
import os
import subprocess as sp
+from pathlib import Path
logging.basicConfig(level=logging.INFO)
-OUTPUT_PATH = "packages"
+OUTPUT_PATH = Path("packages")
def replace_in_file(infile, replacements, outfile=None):
@@ -26,21 +27,21 @@ def replace_in_file(infile, replacements, outfile=None):
if outfile is None:
outfile = infile
- with open(infile) as fp:
+ with infile.open() as fp:
content = fp.read()
for key, value in replacements.items():
content = content.replace(key, value)
- with open(outfile, "w") as fp:
+ with outfile.open("w") as fp:
fp.write(content)
def main(folder="droplets"):
# remove old files
- for path in glob.glob(f"{OUTPUT_PATH}/*.rst"):
+ for path in OUTPUT_PATH.glob("*.rst"):
logging.info("Remove file `%s`", path)
- os.remove(path)
+ path.unlink()
# run sphinx-apidoc
sp.check_call(
@@ -50,7 +51,7 @@ def main(folder="droplets"):
"--maxdepth",
"4",
"--output-dir",
- OUTPUT_PATH,
+ str(OUTPUT_PATH),
"--module-first",
f"../../{folder}", # path of the package
f"../../{folder}/tests", # ignored path
@@ -59,7 +60,7 @@ def main(folder="droplets"):
)
# replace unwanted information
- for path in glob.glob(f"{OUTPUT_PATH}/*.rst"):
+ for path in OUTPUT_PATH.glob("*.rst"):
logging.info("Patch file `%s`", path)
replace_in_file(path, {"Submodules\n----------\n\n": ""})
diff --git a/droplets/droplet_tracks.py b/droplets/droplet_tracks.py
index 1e040ad..8029a6b 100644
--- a/droplets/droplet_tracks.py
+++ b/droplets/droplet_tracks.py
@@ -328,7 +328,7 @@ def _write_hdf_dataset(self, hdf_path, key: str = "droplet_track"):
else:
# create empty dataset to indicate empty emulsion
- dataset = hdf_path.create_dataset(key, shape=tuple())
+ dataset = hdf_path.create_dataset(key, shape=())
dataset.attrs["droplet_class"] = "None"
return dataset
@@ -549,7 +549,7 @@ def match_tracks(
tracks.append(DropletTrack(droplets=[droplet], times=[time]))
if found_multiple_overlap:
- logger.debug(f"Found multiple overlapping droplet(s) at t={time}")
+ logger.debug("Found multiple overlapping droplet(s) at t=%g", time)
elif method == "distance":
# track droplets by their physical distance
@@ -591,11 +591,11 @@ def match_tracks(
tracks.append(DropletTrack(droplets=[droplet], times=[time]))
else:
- raise ValueError(f"Unknown tracking method {method}")
+ raise ValueError("Unknown tracking method `%s`", method)
# check kwargs
if kwargs:
- logger.warning(f"Unused keyword arguments: {kwargs}")
+ logger.warning("Unused keyword arguments: %s", kwargs)
# add all emulsions successively using the given algorithm
t_last = None
diff --git a/droplets/droplets.py b/droplets/droplets.py
index 821aeaf..1ebdfe7 100644
--- a/droplets/droplets.py
+++ b/droplets/droplets.py
@@ -192,7 +192,6 @@ def __eq__(self, other):
def check_data(self):
"""Method that checks the validity and consistency of self.data."""
- pass
@property
def _args(self):
@@ -1092,8 +1091,10 @@ def __init__(
opt_modes = spherical.spherical_index_count(l) - 1
logger.warning(
"The length of `amplitudes` should be such that all orders are "
- f"captured for the perturbations with the highest degree ({l}). "
- f"Consider increasing the size of the array to {opt_modes}."
+ "captured for the perturbations with the highest degree (%d). "
+ "Consider increasing the size of the array to %d.",
+ l,
+ opt_modes,
)
@preserve_scalars
diff --git a/droplets/emulsions.py b/droplets/emulsions.py
index 7691130..e27e9e8 100644
--- a/droplets/emulsions.py
+++ b/droplets/emulsions.py
@@ -373,7 +373,7 @@ def _write_hdf_dataset(self, hdf_path, key: str = "emulsion"):
else:
# create empty dataset to indicate empty emulsion
- dataset = hdf_path.create_dataset(key, shape=tuple())
+ dataset = hdf_path.create_dataset(key, shape=())
dataset.attrs["droplet_class"] = "None"
return dataset
@@ -675,15 +675,15 @@ def plot(
if len(self) == 0:
# empty emulsions can be plotted in all dimensions :)
return PlotReference(ax, [], {})
+
if self.dim is None or self.dim <= 1:
raise NotImplementedError(
f"Plotting emulsions in {self.dim} dimensions is not implemented."
)
- elif self.dim > 2:
- if Emulsion._show_projection_warning:
- logger = logging.getLogger(self.__class__.__name__)
- logger.warning("A projection on the first two axes is shown.")
- Emulsion._show_projection_warning = False
+ elif self.dim > 2 and Emulsion._show_projection_warning:
+ logger = logging.getLogger(self.__class__.__name__)
+ logger.warning("A projection on the first two axes is shown.")
+ Emulsion._show_projection_warning = False
# plot background and determine bounds for the droplets
if field is not None:
diff --git a/droplets/image_analysis.py b/droplets/image_analysis.py
index e8f1165..4a38832 100644
--- a/droplets/image_analysis.py
+++ b/droplets/image_analysis.py
@@ -107,7 +107,7 @@ def _locate_droplets_in_mask_cartesian(mask: ScalarField) -> Emulsion:
# locate individual clusters in the padded image
labels, num_labels = ndimage.label(mask.data)
- grid._logger.info(f"Found {num_labels} cluster(s) in image")
+ grid._logger.info("Found %d cluster(s) in image", num_labels)
if num_labels == 0:
example_drop = SphericalDroplet(np.zeros(grid.dim), radius=0)
return Emulsion.empty(example_drop)
@@ -164,11 +164,11 @@ def _locate_droplets_in_mask_cartesian(mask: ScalarField) -> Emulsion:
emulsion = Emulsion(droplets)
num_candidates = len(emulsion)
if num_candidates < num_labels:
- grid._logger.info(f"Only {num_candidates} candidate(s) inside bounds")
+ grid._logger.info("Only %d candidate(s) inside bounds", num_candidates)
emulsion.remove_overlapping(grid=grid)
if len(emulsion) < num_candidates:
- grid._logger.info(f"Only {num_candidates} candidate(s) not overlapping")
+ grid._logger.info("Only %d candidate(s) not overlapping", num_candidates)
return emulsion
@@ -212,8 +212,6 @@ def _locate_droplets_in_mask_spherical(mask: ScalarField) -> Emulsion:
class _SpanningDropletSignal(RuntimeError):
"""Exception signaling that an untypical droplet spanning the system was found."""
- ...
-
def _locate_droplets_in_mask_cylindrical_single(
grid: CylindricalSymGrid, mask: np.ndarray
@@ -299,7 +297,7 @@ def _locate_droplets_in_mask_cylindrical(mask: ScalarField) -> Emulsion:
except _SpanningDropletSignal:
pass
else:
- grid._logger.info(f"Found {len(candidates)} droplet candidates.")
+ grid._logger.info("Found %d droplet candidates.", len(candidates))
# keep droplets that are inside the central area
droplets = Emulsion()
@@ -310,7 +308,7 @@ def _locate_droplets_in_mask_cylindrical(mask: ScalarField) -> Emulsion:
if z_min <= droplet.position[2] <= z_max:
droplets.append(droplet)
- grid._logger.info(f"Kept {len(droplets)} central droplets.")
+ grid._logger.info("Kept %d central droplets.", len(droplets))
# filter overlapping droplets (e.g. due to duplicates)
droplets.remove_overlapping()
@@ -373,7 +371,7 @@ def locate_droplets(
field,
threshold="auto",
refine=True,
- refine_args={'vmin': None, 'vmax': None},
+ refine_args={"vmin": None, "vmax": None},
)
:code:`field` is the scalar field, in which the droplets are located. The
@@ -840,8 +838,9 @@ def get_length_scale(
for window_size in [5, 1, 0.2]:
bracket = [max_est / window_size, max_est, max_est * window_size]
logger.debug(
- f"Search maximum of structure factor in interval {bracket} with "
- f"window_size={window_size}"
+ "Seek maximal structure factor in interval %s with window_size=%g",
+ bracket,
+ window_size,
)
try:
result = optimize.minimize_scalar(
@@ -854,7 +853,8 @@ def get_length_scale(
if not result.success:
logger.warning(
"Maximization of structure factor resulted in the "
- f"following message: {result.message}"
+ "following message: %s",
+ result.message,
)
length_scale = 2 * np.pi / result.x
break # found some answer, which we will use
diff --git a/droplets/resources/make_spheres_3d.py b/droplets/resources/make_spheres_3d.py
index 58658ac..e3fb899 100755
--- a/droplets/resources/make_spheres_3d.py
+++ b/droplets/resources/make_spheres_3d.py
@@ -54,4 +54,4 @@
num_list.add(num)
# store the number of generated spheres
- f.attrs["num_list"] = list(sorted(num_list))
+ f.attrs["num_list"] = sorted(num_list)
diff --git a/droplets/trackers.py b/droplets/trackers.py
index b594cdd..ffc4d28 100644
--- a/droplets/trackers.py
+++ b/droplets/trackers.py
@@ -12,6 +12,7 @@
from __future__ import annotations
import math
+from pathlib import Path
from typing import Any, Callable, Literal
from pde.fields.base import FieldBase
@@ -112,7 +113,7 @@ def finalize(self, info: InfoDict | None = None) -> None:
import json
data = {"times": self.times, "length_scales": self.length_scales}
- with open(self.filename, "w") as fp:
+ with Path(self.filename).open("w") as fp:
json.dump(data, fp)
@@ -153,7 +154,7 @@ def __init__(
.. code-block:: python
droplet_tracker = DropletTracker(
- 1, refine=True, refine_args={'vmin': None, 'vmax': None}
+ 1, refine=True, refine_args={"vmin": None, "vmax": None}
)
:code:`field` is the scalar field, in which the droplets are located. The
diff --git a/examples/tutorial/Using the py-droplets package.ipynb b/examples/tutorial/Using the py-droplets package.ipynb
index cc47b24..e15685f 100644
--- a/examples/tutorial/Using the py-droplets package.ipynb
+++ b/examples/tutorial/Using the py-droplets package.ipynb
@@ -19,6 +19,7 @@
"\n",
"# import necessary packages\n",
"import pde\n",
+ "\n",
"import droplets"
]
},
diff --git a/pyproject.toml b/pyproject.toml
index d844cd3..fb82667 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -59,6 +59,41 @@ namespaces = false
[tool.setuptools_scm]
write_to = "droplets/_version.py"
+[tool.ruff]
+target-version = "py38"
+exclude = ["scripts/templates"]
+
+[tool.ruff.format]
+docstring-code-format = true
+
+[tool.ruff.lint]
+select = [
+ "UP", # pyupgrade
+ "I", # isort
+ "A", # flake8-builtins
+ "B", # flake8-bugbear
+ "C4", # flake8-comprehensions
+ "FA", # flake8-future-annotations
+ "ISC", # flake8-implicit-str-concat
+ "ICN", # flake8-import-conventions
+ "LOG", # flake8-logging
+ "G", # flake8-logging-format
+ "PIE", # flake8-pie
+ "PT", # flake8-pytest-style
+ "Q", # flake8-quotes
+ "RSE", # flake8-raise
+ "RET", # flake8-return
+ "SIM", # flake8-simplify
+ "PTH", # flake8-use-pathlib
+]
+ignore = ["B007", "B027", "B028", "SIM108", "ISC001", "PT006", "PT011", "RET504", "RET505", "RET506"]
+
+[tool.ruff.lint.isort]
+section-order = ["future", "standard-library", "third-party", "first-party", "my-modules", "self", "local-folder"]
+
+[tool.ruff.lint.isort.sections]
+my-modules = ["pde"]
+self = ["droplets"]
[tool.black]
target_version = ["py39"]
diff --git a/scripts/format_code.sh b/scripts/format_code.sh
index 0d91fc6..8608a54 100755
--- a/scripts/format_code.sh
+++ b/scripts/format_code.sh
@@ -7,10 +7,10 @@ find . -name '*.py' -exec pyupgrade --py39-plus {} +
popd > /dev/null
echo "Formating import statements..."
-isort ..
+ruff check --fix --config=../pyproject.toml ..
echo "Formating docstrings..."
docformatter --in-place --black --recursive ..
echo "Formating source code..."
-black ..
\ No newline at end of file
+ruff format --config=../pyproject.toml ..
\ No newline at end of file
diff --git a/tests/conftest.py b/tests/conftest.py
index 7c93873..4573b9e 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -10,7 +10,7 @@
from pde.tools.numba import random_seed
-@pytest.fixture(scope="function", autouse=False, name="rng")
+@pytest.fixture(autouse=False, name="rng")
def init_random_number_generators():
"""Get a random number generator and set the seed of the random number generator.
@@ -21,8 +21,8 @@ def init_random_number_generators():
return np.random.default_rng(0)
-@pytest.fixture(scope="function", autouse=True)
-def setup_and_teardown():
+@pytest.fixture(autouse=True)
+def _setup_and_teardown():
"""Helper function adjusting environment before and after tests."""
# ensure we use the Agg backend, so figures are not displayed
plt.switch_backend("agg")
diff --git a/tests/requirements.txt b/tests/requirements.txt
index 8da3fa0..eee7d9c 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -1,11 +1,10 @@
-r ../requirements.txt
-black>=24
docformatter>=1.7
-isort>=5.1
pyupgrade>=3
pytest>=5.4
pytest-cov>=2.8
pytest-xdist>=1.30
mypy>=0.770
+ruff>=0.6
types-PyYAML
types-tqdm
\ No newline at end of file
diff --git a/tests/test_droplets.py b/tests/test_droplets.py
index fe0d449..255fd9c 100644
--- a/tests/test_droplets.py
+++ b/tests/test_droplets.py
@@ -57,7 +57,7 @@ def test_random_droplet(dim, rng):
def test_perturbed_droplet_2d():
"""Test methods of perturbed droplets in 2d."""
d = droplets.PerturbedDroplet2D([0, 1], 1, 0.1, [0.0, 0.1, 0.2])
- d.volume
+ assert d.volume > 0
d.interface_distance(0.1)
d.interface_position(0.1)
d.interface_curvature(0.1)
@@ -66,7 +66,7 @@ def test_perturbed_droplet_2d():
def test_perturbed_droplet_3d():
"""Test methods of perturbed droplets in 3d."""
d = droplets.PerturbedDroplet3D([0, 1, 2], 1, 0.1, [0.0, 0.1, 0.2, 0.3])
- d.volume_approx
+ assert d.volume_approx > 0
d.interface_distance(0.1, 0.2)
d.interface_position(0.1, 0.2)
d.interface_curvature(0.1, 0.2)
@@ -75,7 +75,7 @@ def test_perturbed_droplet_3d():
def test_perturbed_droplet_3d_axis_sym():
"""Test methods of axisymmetrically perturbed droplets in 3d."""
d = droplets.PerturbedDroplet3DAxisSym([0, 0, 0], 1, 0.1, [0.0, 0.1])
- d.volume_approx
+ assert d.volume_approx > 0
d.interface_distance(0.1)
d.interface_curvature(0.1)
@@ -251,7 +251,8 @@ def test_droplet_merge(cls, dim):
# test simple merge
d3 = d1.merge(d2, inplace=False)
- assert d3 is not d1 and d3 is not d2
+ assert d3 is not d1
+ assert d3 is not d2
np.testing.assert_allclose(d3.position, [1] * dim)
assert d3.volume == pytest.approx(d1.volume + d2.volume)
if cls == droplets.DiffuseDroplet:
@@ -268,7 +269,8 @@ def test_droplet_merge(cls, dim):
# test inplace merge
d4 = d1.merge(d2, inplace=True)
- assert d4 is d1 and d3 is not d2
+ assert d4 is d1
+ assert d3 is not d2
np.testing.assert_allclose(d4.position, [1] * dim)
assert d4.volume == pytest.approx(d3.volume)
if cls == droplets.DiffuseDroplet:
diff --git a/tests/test_emulsion.py b/tests/test_emulsion.py
index b2e0116..e221832 100644
--- a/tests/test_emulsion.py
+++ b/tests/test_emulsion.py
@@ -46,7 +46,7 @@ def test_empty_emulsion(caplog):
e = Emulsion([])
assert len(e) == 0
with pytest.raises(RuntimeError):
- e.data
+ print(e.data)
e.append(d1, force_consistency=True)
assert e.dtype == d1.data.dtype
@@ -73,7 +73,7 @@ def test_empty_emulsion(caplog):
e.append(d2)
assert len(e) == 1
- e.data # raises warning
+ print(e.data) # raises warning
assert "inconsistent" in caplog.text
@@ -136,7 +136,7 @@ def test_emulsion_incompatible():
e = Emulsion([d1, d2])
assert len(e) == 2
with pytest.raises(TypeError):
- e.data
+ print(e.data)
# same type
d1 = SphericalDroplet([1], 2)
@@ -293,8 +293,10 @@ def test_emulsion_random(dim, grid, rng):
em = Emulsion.from_random(10, bounds, radius=(1, 2), rng=rng)
assert 1 < len(em) <= 10
assert em.dim == dim
- assert np.all(em.data["position"] > 10) and np.all(em.data["position"] < 30)
- assert np.all(em.data["radius"] > 1) and np.all(em.data["radius"] < 2)
+ assert np.all(em.data["position"] > 10)
+ assert np.all(em.data["position"] < 30)
+ assert np.all(em.data["radius"] > 1)
+ assert np.all(em.data["radius"] < 2)
@pytest.mark.parametrize("proc", [1, 2])
diff --git a/tests/test_examples.py b/tests/test_examples.py
index 0dbf274..ed0a99a 100644
--- a/tests/test_examples.py
+++ b/tests/test_examples.py
@@ -2,6 +2,8 @@
.. codeauthor:: David Zwicker
"""
+from __future__ import annotations
+
import os
import subprocess as sp
import sys
@@ -39,7 +41,7 @@ def test_example(path):
proc.kill()
outs, errs = proc.communicate()
- msg = "Script `%s` failed with following output:" % path
+ msg = f"Script `{path}` failed with following output:"
if outs:
msg = f"{msg}\nSTDOUT:\n{outs}"
if errs:
diff --git a/tests/test_spherical.py b/tests/test_spherical.py
index b0e0c10..c98ddef 100644
--- a/tests/test_spherical.py
+++ b/tests/test_spherical.py
@@ -119,7 +119,7 @@ def test_spherical_harmonics_real():
for m2 in range(-deg, m1 + 1):
def integrand(t, p):
- return Ylm(deg, m1, t, p) * Ylm(deg, m2, t, p) * np.sin(t)
+ return Ylm(deg, m1, t, p) * Ylm(deg, m2, t, p) * np.sin(t) # noqa: B023
overlap = integrate.dblquad(
integrand, 0, 2 * np.pi, lambda _: 0, lambda _: np.pi