Skip to content

Commit 3e0fa69

Browse files
authored
Enable ruff (#143)
1 parent 6d1b13c commit 3e0fa69

File tree

6 files changed

+110
-68
lines changed

6 files changed

+110
-68
lines changed

.pre-commit-config.yaml

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,6 @@ exclude: |
44
^docs/conf.py$
55
)
66
repos:
7-
- repo: https://github.com/PyCQA/isort
8-
rev: 5.13.2
9-
hooks:
10-
- id: isort
11-
- repo: https://github.com/psf/black
12-
rev: 24.10.0
13-
hooks:
14-
- id: black
15-
language_version: python3
167
- repo: https://github.com/pre-commit/pre-commit-hooks.git
178
rev: v5.0.0
189
hooks:
@@ -45,6 +36,20 @@ repos:
4536
rev: v0.24.2
4637
hooks:
4738
- id: toml-sort-fix
39+
- repo: https://github.com/astral-sh/ruff-pre-commit
40+
rev: v0.8.1
41+
hooks:
42+
- id: ruff
43+
args:
44+
- --fix
45+
- --exit-non-zero-on-fix
46+
types_or: [python, pyi]
47+
# - id: ruff-format # must be after ruff
48+
# types_or: [python, pyi]
49+
- repo: https://github.com/psf/black # must be after ruff
50+
rev: 24.10.0
51+
hooks:
52+
- id: black
4853
- repo: https://github.com/pre-commit/mirrors-mypy
4954
rev: v1.13.0
5055
hooks:

pyproject.toml

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,8 @@ documentation = "https://subprocess-tee.readthedocs.io"
7373
homepage = "https://github.com/pycontribs/subprocess-tee"
7474
repository = "https://github.com/pycontribs/subprocess-tee"
7575

76-
[tool.isort]
77-
known_first_party = "subprocess_tee"
78-
profile = "black"
76+
[tool.black]
77+
target-version = ["py39"]
7978

8079
[tool.mypy]
8180
color_output = true
@@ -89,6 +88,45 @@ warn_redundant_casts = true
8988
warn_return_any = true
9089
warn_unused_configs = true
9190

91+
[tool.pyright]
92+
pythonVersion = "3.9"
93+
94+
[tool.ruff]
95+
cache-dir = "./.cache/.ruff"
96+
fix = true
97+
# Same as Black.
98+
line-length = 88
99+
preview = true
100+
target-version = "py39"
101+
102+
[tool.ruff.lint]
103+
ignore = [
104+
"COM812", # conflicts with ISC001 on format
105+
"CPY001", # missing-copyright-notice
106+
"D203", # incompatible with D211
107+
"D213", # incompatible with D212
108+
"E501", # we use black
109+
"ERA001", # auto-removal of commented out code affects development and vscode integration
110+
"INP001", # "is part of an implicit namespace package", all false positives
111+
"ISC001", # conflicts with COM812 on format
112+
"PLW2901", # PLW2901: Redefined loop variable
113+
"RET504", # Unnecessary variable assignment before `return` statement
114+
# temporary disabled until we fix them:
115+
"ANN",
116+
"PLR",
117+
"ASYNC230"
118+
]
119+
select = ["ALL"]
120+
121+
[tool.ruff.lint.isort]
122+
known-first-party = ["src"]
123+
124+
[tool.ruff.lint.per-file-ignores]
125+
"test/*.py" = ["S", "T201"]
126+
127+
[tool.ruff.lint.pydocstyle]
128+
convention = "google"
129+
92130
[tool.setuptools_scm]
93131
local_scheme = "no-local-version"
94132

src/subprocess_tee/__init__.py

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,31 @@
11
"""tee like run implementation."""
22

3+
from __future__ import annotations
4+
35
import asyncio
46
import os
57
import platform
6-
import subprocess
8+
import subprocess # noqa: S404
79
import sys
810
from asyncio import StreamReader
9-
from importlib.metadata import PackageNotFoundError, version # type: ignore
11+
from importlib.metadata import PackageNotFoundError, version
12+
from pathlib import Path
1013
from shlex import join
11-
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
14+
from typing import TYPE_CHECKING, Any
1215

1316
try:
1417
__version__ = version("subprocess-tee")
1518
except PackageNotFoundError: # pragma: no branch
1619
__version__ = "0.1.dev1"
1720

18-
__all__ = ["run", "CompletedProcess", "__version__"]
21+
__all__ = ["CompletedProcess", "__version__", "run"]
1922

2023
if TYPE_CHECKING:
2124
CompletedProcess = subprocess.CompletedProcess[Any] # pylint: disable=E1136
25+
from collections.abc import Callable
2226
else:
2327
CompletedProcess = subprocess.CompletedProcess
2428

25-
2629
STREAM_LIMIT = 2**23 # 8MB instead of default 64kb, override it if you need
2730

2831

@@ -35,18 +38,19 @@ async def _read_stream(stream: StreamReader, callback: Callable[..., Any]) -> No
3538
break
3639

3740

38-
async def _stream_subprocess(
39-
args: Union[str, List[str]], **kwargs: Any
41+
async def _stream_subprocess( # noqa: C901
42+
args: str | list[str],
43+
**kwargs: Any,
4044
) -> CompletedProcess:
41-
platform_settings: Dict[str, Any] = {}
45+
platform_settings: dict[str, Any] = {}
4246
if platform.system() == "Windows":
4347
platform_settings["env"] = os.environ
4448

4549
# this part keeps behavior backwards compatible with subprocess.run
4650
tee = kwargs.get("tee", True)
4751
stdout = kwargs.get("stdout", sys.stdout)
4852

49-
with open(os.devnull, "w", encoding="UTF-8") as devnull:
53+
with Path(os.devnull).open("w", encoding="UTF-8") as devnull:
5054
if stdout == subprocess.DEVNULL or not tee:
5155
stdout = devnull
5256
stderr = kwargs.get("stderr", sys.stderr)
@@ -85,31 +89,31 @@ async def _stream_subprocess(
8589
stderr=asyncio.subprocess.PIPE,
8690
**platform_settings,
8791
)
88-
out: List[str] = []
89-
err: List[str] = []
92+
out: list[str] = []
93+
err: list[str] = []
9094

91-
def tee_func(line: bytes, sink: List[str], pipe: Optional[Any]) -> None:
95+
def tee_func(line: bytes, sink: list[str], pipe: Any | None) -> None:
9296
line_str = line.decode("utf-8").rstrip()
9397
sink.append(line_str)
9498
if not kwargs.get("quiet", False):
9599
if pipe and hasattr(pipe, "write"):
96100
print(line_str, file=pipe)
97101
else:
98-
print(line_str)
102+
print(line_str) # noqa: T201
99103

100104
loop = asyncio.get_running_loop()
101105
tasks = []
102106
if process.stdout:
103107
tasks.append(
104108
loop.create_task(
105-
_read_stream(process.stdout, lambda x: tee_func(x, out, stdout))
106-
)
109+
_read_stream(process.stdout, lambda x: tee_func(x, out, stdout)),
110+
),
107111
)
108112
if process.stderr:
109113
tasks.append(
110114
loop.create_task(
111-
_read_stream(process.stderr, lambda x: tee_func(x, err, stderr))
112-
)
115+
_read_stream(process.stderr, lambda x: tee_func(x, err, stderr)),
116+
),
113117
)
114118

115119
await asyncio.wait(set(tasks))
@@ -132,32 +136,39 @@ def tee_func(line: bytes, sink: List[str], pipe: Optional[Any]) -> None:
132136
)
133137

134138

135-
def run(args: Union[str, List[str]], **kwargs: Any) -> CompletedProcess:
139+
def run(args: str | list[str], **kwargs: Any) -> CompletedProcess:
136140
"""Drop-in replacement for subprocess.run that behaves like tee.
137141
138142
Extra arguments added by our version:
139143
echo: False - Prints command before executing it.
140144
quiet: False - Avoid printing output
145+
146+
Returns:
147+
CompletedProcess: ...
148+
149+
Raises:
150+
CalledProcessError: ...
151+
141152
"""
142-
if isinstance(args, str):
143-
cmd = args
144-
else:
145-
# run was called with a list instead of a single item but asyncio
146-
# create_subprocess_shell requires command as a single string, so
147-
# we need to convert it to string
148-
cmd = join(args)
153+
# run was called with a list instead of a single item but asyncio
154+
# create_subprocess_shell requires command as a single string, so
155+
# we need to convert it to string
156+
cmd = args if isinstance(args, str) else join(args)
149157

150158
check = kwargs.get("check", False)
151159

152160
if kwargs.get("echo", False):
153-
print(f"COMMAND: {cmd}")
161+
print(f"COMMAND: {cmd}") # noqa: T201
154162

155163
result = asyncio.run(_stream_subprocess(args, **kwargs))
156164
# we restore original args to mimic subproces.run()
157165
result.args = args
158166

159167
if check and result.returncode != 0:
160168
raise subprocess.CalledProcessError(
161-
result.returncode, args, output=result.stdout, stderr=result.stderr
169+
result.returncode,
170+
args,
171+
output=result.stdout,
172+
stderr=result.stderr,
162173
)
163174
return result

test/test_func.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,15 @@
11
"""Functional tests for subprocess-tee library."""
22

33
import subprocess
4-
import sys
54

6-
import pytest
75

8-
9-
@pytest.mark.skipif(
10-
sys.version_info < (3, 9), reason="molecule test requires python 3.9+"
11-
)
126
def test_molecule() -> None:
137
"""Ensures molecule does display output of its subprocesses."""
148
result = subprocess.run(
159
["molecule", "test"],
16-
stdout=subprocess.PIPE,
17-
stderr=subprocess.PIPE,
18-
universal_newlines=True,
10+
capture_output=True,
11+
text=True,
1912
check=False,
20-
) # type: ignore
13+
)
2114
assert result.returncode == 0
2215
assert "Past glories are poor feeding." in result.stdout

test/test_rich.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def test_rich_console_ex() -> None:
1919
# While not supposed to happen we want to be sure that this will not raise
2020
# an exception. Some libraries may still sometimes send bytes to the
2121
# streams, notable example being click.
22-
# sys.stdout.write(b"epsilon\n") # type: ignore
22+
# sys.stdout.write(b"epsilon\n")
2323
proc = run("echo 123")
2424
assert proc.stdout == "123\n"
2525
text = console.export_text()

test/test_unit.py

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import subprocess
44
import sys
5-
from typing import Dict
65

76
import pytest
87
from _pytest.capture import CaptureFixture
@@ -16,9 +15,8 @@ def test_run_string() -> None:
1615
old_result = subprocess.run(
1716
cmd,
1817
shell=True,
19-
universal_newlines=True,
20-
stdout=subprocess.PIPE,
21-
stderr=subprocess.PIPE,
18+
text=True,
19+
capture_output=True,
2220
check=False,
2321
)
2422
result = run(cmd)
@@ -36,9 +34,8 @@ def test_run_list() -> None:
3634
old_result = subprocess.run(
3735
cmd,
3836
# shell=True,
39-
universal_newlines=True,
40-
stdout=subprocess.PIPE,
41-
stderr=subprocess.PIPE,
37+
text=True,
38+
capture_output=True,
4239
check=False,
4340
)
4441
result = run(cmd)
@@ -53,9 +50,8 @@ def test_run_echo(capsys: CaptureFixture[str]) -> None:
5350
old_result = subprocess.run(
5451
cmd,
5552
# shell=True,
56-
universal_newlines=True,
57-
stdout=subprocess.PIPE,
58-
stderr=subprocess.PIPE,
53+
text=True,
54+
capture_output=True,
5955
check=False,
6056
)
6157
result = run(cmd, echo=True)
@@ -64,15 +60,15 @@ def test_run_echo(capsys: CaptureFixture[str]) -> None:
6460
assert result.stderr == old_result.stderr
6561
out, err = capsys.readouterr()
6662
assert out.startswith("COMMAND:")
67-
assert err == ""
63+
assert not err
6864

6965

7066
@pytest.mark.parametrize(
7167
"env",
7268
[{}, {"SHELL": "/bin/sh"}, {"SHELL": "/bin/bash"}, {"SHELL": "/bin/zsh"}],
7369
ids=["auto", "sh", "bash", "zsh"],
7470
)
75-
def test_run_with_env(env: Dict[str, str]) -> None:
71+
def test_run_with_env(env: dict[str, str]) -> None:
7672
"""Validate that passing custom env to run() works."""
7773
env["FOO"] = "BAR"
7874
result = run("echo $FOO", env=env, echo=True)
@@ -110,7 +106,7 @@ def test_run_with_check_raise() -> None:
110106
with pytest.raises(subprocess.CalledProcessError) as ours:
111107
run("false", check=True)
112108
with pytest.raises(subprocess.CalledProcessError) as original:
113-
subprocess.run("false", check=True, universal_newlines=True)
109+
subprocess.run("false", check=True, text=True)
114110
assert ours.value.returncode == original.value.returncode
115111
assert ours.value.cmd == original.value.cmd
116112
assert ours.value.output == original.value.output
@@ -121,7 +117,7 @@ def test_run_with_check_raise() -> None:
121117
def test_run_with_check_pass() -> None:
122118
"""Asure compatibility with subprocess.run when using check (return 0)."""
123119
ours = run("true", check=True)
124-
original = subprocess.run("true", check=True, universal_newlines=True)
120+
original = subprocess.run("true", check=True, text=True)
125121
assert ours.returncode == original.returncode
126122
assert ours.args == original.args
127123
assert ours.stdout == original.stdout
@@ -134,9 +130,8 @@ def test_run_compat() -> None:
134130
ours = run(cmd)
135131
original = subprocess.run(
136132
cmd,
137-
stdout=subprocess.PIPE,
138-
stderr=subprocess.PIPE,
139-
universal_newlines=True,
133+
capture_output=True,
134+
text=True,
140135
check=False,
141136
)
142137
assert ours.returncode == original.returncode
@@ -148,5 +143,5 @@ def test_run_compat() -> None:
148143
def test_run_waits_for_completion(tmp_path):
149144
"""run() should always wait for the process to complete."""
150145
tmpfile = tmp_path / "output.txt"
151-
run(f"sleep 0.1 && echo 42 > {str(tmpfile)}")
146+
run(f"sleep 0.1 && echo 42 > {tmpfile!s}")
152147
assert tmpfile.read_text() == "42\n"

0 commit comments

Comments
 (0)