Skip to content

Commit 40c2419

Browse files
authored
Moving linting to ruff (#121)
* feat: Add ruff configuration * Deactivate flake8 in precommit * chore: Fixing ruff warnings * ci: Minimal ruff workflow * ci: Replace isort and black by ruff * ci: Disable checks for python 3.7 and 3.8 which are end-of-life * doc: Fix dead link * ci: fix codecov upload
1 parent 28faec5 commit 40c2419

15 files changed

+165
-104
lines changed

.github/workflows/ruff.yml

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Ruff
2+
on: push
3+
jobs:
4+
build:
5+
runs-on: ubuntu-latest
6+
steps:
7+
- uses: actions/checkout@v4
8+
- name: Install Python
9+
uses: actions/setup-python@v5
10+
with:
11+
python-version: "3.11"
12+
- name: Install dependencies
13+
run: |
14+
python -m pip install --upgrade pip
15+
pip install ruff
16+
# Update output format to enable automatic inline annotations.
17+
- name: Run Ruff
18+
run: ruff check --output-format=github .

.github/workflows/test.yml

+4-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
strategy:
2323
matrix:
2424
os: [ubuntu-latest, macos-latest, windows-latest]
25-
python-version: ["3.7", "3.8", "3.9", "3.10"]
25+
python-version: ["3.9", "3.10"]
2626
exclude:
2727
# No numpy wheels yet:
2828
- os: windows-latest
@@ -62,7 +62,7 @@ jobs:
6262
pip install poetry tox tox-gh-actions
6363
poetry install --with test,dev,doc
6464
65-
- name: Lint and test with tox
65+
- name: Test with tox
6666
if: ${{ matrix.tox-full-run }}
6767
run:
6868
poetry run tox
@@ -77,10 +77,11 @@ jobs:
7777
with:
7878
junit_files: pytest.xml
7979

80-
- uses: codecov/codecov-action@v3
80+
- uses: codecov/codecov-action@v4
8181
if: ${{ matrix.publish-results && always() }}
8282
with:
8383
fail_ci_if_error: true
84+
token: ${{ secrets.CODECOV_TOKEN }}
8485
files: coverage.xml
8586

8687
publish_dev_build:

.pre-commit-config.yaml

+6-14
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ ci:
55
submodules: false
66
repos:
77
- repo: https://github.com/pre-commit/pre-commit-hooks
8-
rev: v4.4.0
8+
rev: v5.0.0
99
hooks:
1010
- id: check-merge-conflict
1111
- id: check-json
@@ -14,17 +14,9 @@ repos:
1414
args: [--unsafe]
1515
- id: debug-statements
1616
- id: end-of-file-fixer
17-
- repo: https://github.com/pre-commit/mirrors-isort
18-
rev: v5.10.1
17+
- repo: https://github.com/astral-sh/ruff-pre-commit
18+
rev: v0.6.9
1919
hooks:
20-
- id: isort
21-
- repo: https://github.com/ambv/black
22-
rev: 22.10.0
23-
hooks:
24-
- id: black
25-
language_version: python3.9
26-
- repo: https://github.com/pycqa/flake8
27-
rev: 6.0.0
28-
hooks:
29-
- id: flake8
30-
additional_dependencies: [flake8-typing-imports==1.14.0]
20+
- id: ruff
21+
args: [ --fix ]
22+
- id: ruff-format

dike/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
"""Decorator library"""
1+
"""Decorator library."""
2+
23
import functools
34
import inspect
45
from typing import Callable

dike/_batch.py

+22-16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
"""Implementation of the @dike.batch decorator"""
1+
"""Implementation of the @dike.batch decorator."""
2+
23
import asyncio
34
import functools
45
from contextlib import contextmanager
@@ -11,18 +12,18 @@
1112

1213

1314
# Deactivate mccabe's complexity warnings which doesn't like closures
14-
# flake8: noqa: C901
15-
def batch(
15+
def batch( # noqa: PLR0915, C901
1616
*,
1717
target_batch_size: int,
1818
max_waiting_time: float,
1919
max_processing_time: float = 10.0,
2020
argument_type: str = "list",
2121
) -> Callable[[Callable[..., Coroutine[Any, Any, Any]]], Callable[..., Coroutine[Any, Any, Any]]]:
2222
"""@batch is a decorator to cumulate function calls and process them in batches.
23-
Not thread-safe.
2423
25-
The function to wrap must have arguments of type list or numpy.array which can be aggregated.
24+
Not thread-safe.
25+
26+
The function to wrap must have arguments of type list or `numpy.array` which can be aggregated.
2627
It must return just a single value of the same type. The type has to be specified with the
2728
`argument_type` parameter of the decorator.
2829
@@ -102,7 +103,7 @@ def batch(
102103
if argument_type == "numpy" and np is None:
103104
raise ValueError('Unable to use "numpy" as argument_type because numpy is not available')
104105

105-
def decorator(func):
106+
def decorator(func): # noqa: C901, PLR0915
106107
next_free_batch: int = 0
107108
call_args_queue: List[Tuple[List, Dict]] = []
108109
n_rows_in_queue: int = 0
@@ -112,16 +113,21 @@ def decorator(func):
112113

113114
@functools.wraps(func)
114115
async def batching_call(*args, **kwargs):
115-
"""This is the actual wrapper function which controls the process"""
116-
nonlocal results, num_results_ready, result_ready_events, call_args_queue, n_rows_in_queue
116+
"""This is the actual wrapper function which controls the process."""
117+
nonlocal \
118+
results, \
119+
num_results_ready, \
120+
result_ready_events, \
121+
call_args_queue, \
122+
n_rows_in_queue
117123

118124
with enqueue(args, kwargs) as (my_batch_no, start_index, stop_index):
119125
await wait_for_calculation(my_batch_no)
120126
return get_results(start_index, stop_index, my_batch_no)
121127

122128
@contextmanager
123129
def enqueue(args, kwargs) -> (int, int, int):
124-
"""Add call arguments to queue and get the batch number and result indices"""
130+
"""Add call arguments to queue and get the batch number and result indices."""
125131
batch_no = next_free_batch
126132
if batch_no not in result_ready_events:
127133
result_ready_events[batch_no] = asyncio.Event()
@@ -132,7 +138,7 @@ def enqueue(args, kwargs) -> (int, int, int):
132138
remove_result(batch_no)
133139

134140
def add_args_to_queue(args, kwargs):
135-
"""Add a new argument vector to the queue and return result indices"""
141+
"""Add a new argument vector to the queue and return result indices."""
136142
nonlocal call_args_queue, n_rows_in_queue
137143

138144
if call_args_queue and (
@@ -155,7 +161,7 @@ def add_args_to_queue(args, kwargs):
155161
return offset, n_rows_in_queue
156162

157163
async def wait_for_calculation(batch_no_to_calculate):
158-
"""Pause until the result becomes available or trigger the calculation on timeout"""
164+
"""Pause until the result becomes available or trigger the calculation on timeout."""
159165
if n_rows_in_queue >= target_batch_size:
160166
await calculate(batch_no_to_calculate)
161167
else:
@@ -173,20 +179,20 @@ async def wait_for_calculation(batch_no_to_calculate):
173179
)
174180

175181
async def calculate(batch_no_to_calculate):
176-
"""Call the decorated coroutine with batched arguments"""
182+
"""Call the decorated coroutine with batched arguments."""
177183
nonlocal results, call_args_queue, num_results_ready
178184
if next_free_batch == batch_no_to_calculate:
179185
n_results = len(call_args_queue)
180186
args, kwargs = pop_args_from_queue()
181187
try:
182188
results[batch_no_to_calculate] = await func(*args, **kwargs)
183-
except Exception as e: # pylint: disable=broad-except
189+
except Exception as e: # pylint: disable=broad-except # noqa: BLE001
184190
results[batch_no_to_calculate] = e
185191
num_results_ready[batch_no_to_calculate] = n_results
186192
result_ready_events[batch_no_to_calculate].set()
187193

188194
def pop_args_from_queue():
189-
"""Get all collected arguments from the queue as batch"""
195+
"""Get all collected arguments from the queue as batch."""
190196
nonlocal next_free_batch, call_args_queue, n_rows_in_queue
191197

192198
n_args = len(call_args_queue[0][0])
@@ -215,7 +221,7 @@ def pop_args_from_queue():
215221
return args, kwargs
216222

217223
def get_results(start_index: int, stop_index: int, batch_no):
218-
"""Pop the results for a certain index range from the output buffer"""
224+
"""Pop the results for a certain index range from the output buffer."""
219225
nonlocal results
220226

221227
if isinstance(results[batch_no], Exception):
@@ -225,7 +231,7 @@ def get_results(start_index: int, stop_index: int, batch_no):
225231
return results_to_return
226232

227233
def remove_result(batch_no):
228-
"""Reduce reference count to output buffer and eventually delete it"""
234+
"""Reduce reference count to output buffer and eventually delete it."""
229235
nonlocal num_results_ready, result_ready_events, results
230236

231237
if num_results_ready[batch_no] == 1:

dike/_limit_jobs.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
"""Implementation of the @dike.limit_jobs decorator"""
1+
"""Implementation of the @dike.limit_jobs decorator."""
2+
23
import functools
34
import inspect
45
from typing import Any, Callable, Coroutine
56

67

7-
class TooManyCalls(Exception):
8-
"""Error raised by @limit_jobs when a call exceeds the preset limit"""
8+
class TooManyCalls(Exception): # noqa: N818
9+
"""Error raised by @limit_jobs when a call exceeds the preset limit."""
910

1011

1112
def limit_jobs(*, limit: int) -> Callable[..., Coroutine[Any, Any, Any]]:
@@ -63,7 +64,7 @@ def limit_jobs(*, limit: int) -> Callable[..., Coroutine[Any, Any, Any]]:
6364

6465
def decorator(func):
6566
if not inspect.iscoroutinefunction(func):
66-
raise ValueError(f"Error when wrapping {str(func)}. Only coroutines can be wrapped!")
67+
raise ValueError(f"Error when wrapping {func!s}. Only coroutines can be wrapped!")
6768

6869
@functools.wraps(func)
6970
async def limited_call(*args, **kwargs):

dike/_retry.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1-
"""Implementation of the @dike.retry decorator"""
1+
"""Implementation of the @dike.retry decorator."""
2+
23
import asyncio
34
import datetime
45
import functools
56
import inspect
67
import logging
7-
from typing import Awaitable, Callable, Tuple, Type, Union
8+
from typing import Awaitable, Callable, Optional, Tuple, Type, Union
89

910
logger = logging.getLogger("dike")
1011

1112

1213
def retry(
1314
*,
14-
attempts: int = None,
15+
attempts: Optional[int] = None,
1516
exception_types: Union[Type[BaseException], Tuple[Type[BaseException]]] = Exception,
16-
delay: datetime.timedelta = None,
17+
delay: Optional[datetime.timedelta] = None,
1718
backoff: int = 1,
1819
log_exception_info: bool = True,
1920
) -> Callable[[Callable[..., Awaitable]], Callable[..., Awaitable]]:
@@ -70,7 +71,7 @@ def retry(
7071

7172
def decorator(func: Callable[..., Awaitable]) -> Callable[..., Awaitable]:
7273
if not inspect.iscoroutinefunction(func):
73-
raise ValueError(f"Error when wrapping {str(func)}. Only coroutines can be wrapped!")
74+
raise ValueError(f"Error when wrapping {func!s}. Only coroutines can be wrapped!")
7475

7576
@functools.wraps(func)
7677
async def guarded_call(*args, **kwargs):
@@ -86,7 +87,7 @@ async def guarded_call(*args, **kwargs):
8687
if not counter:
8788
raise
8889
logger.warning(
89-
f"Caught exception {repr(e)}. Retrying in {next_delay:.3g}s ...",
90+
f"Caught exception {e!r}. Retrying in {next_delay:.3g}s ...",
9091
exc_info=log_exception_info,
9192
)
9293
await asyncio.sleep(next_delay)

docs/contributing.md

-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
11
{%
22
include-markdown "../CONTRIBUTING.md"
33
%}
4-
5-
{%
6-
include-markdown "../AUTHORS.md"
7-
%}

examples/ml_prediction_service/api.py

+14-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
# Dependencies: fastapi, uvicorn
2-
# Run with: python api.py
3-
#
4-
# Simple load test with github.com/codesenberg/bombardier:
5-
# bombardier -c 25 -r 300 -d 10s -l 'localhost:8000/predict?number=5'
6-
#
1+
"""Minimal example API using dike for microbatching.
2+
3+
Dependencies: fastapi, uvicorn
4+
Run with: python api.py
5+
6+
Simple load test with github.com/codesenberg/bombardier:
7+
8+
bombardier -c 25 -r 300 -d 10s -l 'localhost:8000/predict?number=5'
9+
"""
10+
711
import asyncio
812
import concurrent
913
import random
@@ -18,7 +22,7 @@
1822

1923

2024
def predict(numbers: List[float]):
21-
"""Dummy machine learning operation"""
25+
"""Dummy machine learning operation."""
2226
arr = np.array(numbers)
2327
for _ in range(10000):
2428
arr = np.sqrt(arr + random.random() * 2)
@@ -28,17 +32,19 @@ def predict(numbers: List[float]):
2832
@dike.limit_jobs(limit=20)
2933
@dike.batch(target_batch_size=10, max_waiting_time=0.1)
3034
async def predict_in_pool(numbers: List[float]):
35+
"""Wrapper function for the predictions to allow batching."""
3136
loop = asyncio.get_running_loop()
3237
return await loop.run_in_executor(app.state.pool, predict, numbers)
3338

3439

3540
@app.on_event("startup")
36-
async def on_startup():
41+
async def on_startup(): # noqa: D103
3742
app.state.pool = concurrent.futures.ProcessPoolExecutor(max_workers=2)
3843

3944

4045
@app.get("/predict")
4146
async def get_predict(number: float, response: Response):
47+
"""API endpoint for machine learning inference."""
4248
try:
4349
x = await predict_in_pool([number])
4450
return {"result": x}

ruff.toml

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
line-length = 100
2+
##extend-exclude = ["*.ipynb", "**/*_pb2.py"]
3+
4+
[lint]
5+
ignore = [
6+
"PLR0913", # Too many arguments
7+
"S311", # suspicious-non-cryptographic-random-usage
8+
"G004" # Logging statement uses f-string
9+
]
10+
select = [
11+
"A", # flake8-builtins
12+
"C4", # flake8-comprehensions
13+
"DTZ", # flake8-datetimez
14+
"B",
15+
"B9",
16+
"C",
17+
"G", # flake8-logging-format
18+
"PYI", # flake8-pyi
19+
"PT", # flake8-pytest-style
20+
"TCH", # flake8-type-checking
21+
"C90", # MCCabe
22+
"D", # Pydocstyle
23+
"E", # Pycodestyle error
24+
"F", # Pyflakes
25+
"I", # Isort
26+
"N", # pep8-naming
27+
"W", # Pycodestyle warning
28+
# "ANN", # flake8-annotations
29+
"S", # flake8-bandit
30+
"BLE", # flake8-blind-except
31+
"PD", # pandas-vet
32+
"PL", # Pylint
33+
"RUF", # Ruff
34+
]
35+
36+
[lint.per-file-ignores]
37+
"__init__.py" = ["F401"]
38+
"tests/*" = ["ANN", "D", "S101", "PT", "PLR", "PD901"]
39+
"integration_tests/*" = ["ANN", "D", "S101", "PT", "PLR", "PD901"]
40+
41+
[lint.pydocstyle]
42+
convention = "google"
43+
44+
#[mccabe]
45+
#max-complexity = 18
46+
#
47+
#

tasks.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
"""
2-
Tasks for maintaining the project.
1+
"""Tasks for maintaining the project.
32
43
Execute 'invoke --list' for guidance on using Invoke
54
"""
5+
66
import platform
77
import webbrowser
88
from pathlib import Path

0 commit comments

Comments
 (0)