Skip to content

Commit 075138a

Browse files
authored
Ruff rules: Stricter code quality rules (#879)
The boosts linting stringency and includes sorting of imports. Docs: - https://beta.ruff.rs/docs/rules/ - https://beta.ruff.rs/docs/settings/ See also: tmux-python/libtmux#488
2 parents 4140006 + 08a9c03 commit 075138a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+586
-411
lines changed

CHANGES

+8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ $ pipx install --suffix=@next 'tmuxp' --pip-args '\--pre' --force
1919

2020
<!-- Maintainers, insert changes / features for the next release here -->
2121

22+
### Development
23+
24+
- Code quality improved via [ruff] rules (#879)
25+
26+
This includes fixes made by hand alongside ruff's automated fixes. The more
27+
stringent rules include import sorting, and still runs almost instantly
28+
against the whole codebase.
29+
2230
## tmuxp 1.29.1 (2023-09-02)
2331

2432
### Development

conftest.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@
1010
import typing as t
1111

1212
import pytest
13-
1413
from _pytest.doctest import DoctestItem
15-
1614
from libtmux.test import namer
15+
1716
from tests.fixtures import utils as test_utils
1817
from tmuxp.workspace.finders import get_workspace_dir
1918

@@ -24,7 +23,7 @@
2423
USING_ZSH = "zsh" in os.getenv("SHELL", "")
2524

2625

27-
@pytest.mark.skipif(USING_ZSH, reason="Using ZSH")
26+
@pytest.mark.skipif(not USING_ZSH, reason="Using ZSH")
2827
@pytest.fixture(autouse=USING_ZSH, scope="session")
2928
def zshrc(user_path: pathlib.Path) -> pathlib.Path:
3029
"""This quiets ZSH default message.
@@ -70,7 +69,7 @@ def monkeypatch_plugin_test_packages(monkeypatch: pytest.MonkeyPatch) -> None:
7069
"tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_fail/",
7170
]
7271
for path in paths:
73-
monkeypatch.syspath_prepend(os.path.abspath(os.path.relpath(path)))
72+
monkeypatch.syspath_prepend(str(pathlib.Path(path).resolve()))
7473

7574

7675
@pytest.fixture(scope="function")

docs/_ext/aafig.py

+30-25
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"""
1313
import logging
1414
import posixpath
15+
import typing as t
1516
from hashlib import sha1 as sha
1617
from os import path
1718

@@ -28,11 +29,11 @@
2829

2930
logger = logging.getLogger(__name__)
3031

31-
DEFAULT_FORMATS = dict(html="svg", latex="pdf", text=None)
32+
DEFAULT_FORMATS = {"html": "svg", "latex": "pdf", "text": None}
3233

3334

3435
def merge_dict(dst, src):
35-
for (k, v) in src.items():
36+
for k, v in src.items():
3637
if k not in dst:
3738
dst[k] = v
3839
return dst
@@ -58,22 +59,22 @@ class AafigDirective(images.Image):
5859

5960
has_content = True
6061
required_arguments = 0
61-
own_option_spec = dict(
62-
line_width=float,
63-
background=str,
64-
foreground=str,
65-
fill=str,
66-
aspect=nonnegative_int,
67-
textual=flag,
68-
proportional=flag,
69-
)
62+
own_option_spec: t.ClassVar = {
63+
"line_width": float,
64+
"background": str,
65+
"foreground": str,
66+
"fill": str,
67+
"aspect": nonnegative_int,
68+
"textual": flag,
69+
"proportional": flag,
70+
}
7071
option_spec = images.Image.option_spec.copy()
7172
option_spec.update(own_option_spec)
7273

7374
def run(self):
74-
aafig_options = dict()
75-
own_options_keys = [self.own_option_spec.keys()] + ["scale"]
76-
for (k, v) in self.options.items():
75+
aafig_options = {}
76+
own_options_keys = [self.own_option_spec.keys(), "scale"]
77+
for k, v in self.options.items():
7778
if k in own_options_keys:
7879
# convert flags to booleans
7980
if v is None:
@@ -88,7 +89,7 @@ def run(self):
8889
if isinstance(image_node, nodes.system_message):
8990
return [image_node]
9091
text = "\n".join(self.content)
91-
image_node.aafig = dict(options=aafig_options, text=text)
92+
image_node.aafig = {"options": aafig_options, "text": text}
9293
return [image_node]
9394

9495

@@ -138,13 +139,18 @@ def render_aafig_images(app, doctree):
138139
img["height"] = height
139140

140141

142+
class AafigureNotInstalled(AafigError):
143+
def __init__(self, *args: object, **kwargs: object) -> None:
144+
return super().__init__("aafigure module not installed", *args, **kwargs)
145+
146+
141147
def render_aafigure(app, text, options):
142148
"""
143149
Render an ASCII art figure into the requested format output file.
144150
"""
145151

146152
if aafigure is None:
147-
raise AafigError("aafigure module not installed")
153+
raise AafigureNotInstalled()
148154

149155
fname = get_basename(text, options)
150156
fname = "{}.{}".format(get_basename(text, options), options["format"])
@@ -173,10 +179,10 @@ def render_aafigure(app, text, options):
173179
f = None
174180
try:
175181
try:
176-
f = open(metadata_fname)
177-
extra = f.read()
178-
except Exception:
179-
raise AafigError()
182+
with open(metadata_fname) as f:
183+
extra = f.read()
184+
except Exception as e:
185+
raise AafigError() from e
180186
finally:
181187
if f is not None:
182188
f.close()
@@ -190,14 +196,13 @@ def render_aafigure(app, text, options):
190196
(visitor, output) = aafigure.render(text, outfn, options)
191197
output.close()
192198
except aafigure.UnsupportedFormatError as e:
193-
raise AafigError(str(e))
199+
raise AafigError(str(e)) from e
194200

195201
extra = None
196202
if options["format"].lower() == "svg":
197203
extra = visitor.get_size_attrs()
198-
f = open(metadata_fname, "w")
199-
f.write(extra)
200-
f.close()
204+
with open(metadata_fname, "w") as f:
205+
f.write(extra)
201206

202207
return relfn, outfn, id, extra
203208

@@ -206,7 +211,7 @@ def setup(app):
206211
app.add_directive("aafig", AafigDirective)
207212
app.connect("doctree-read", render_aafig_images)
208213
app.add_config_value("aafig_format", DEFAULT_FORMATS, "html")
209-
app.add_config_value("aafig_default_options", dict(), "html")
214+
app.add_config_value("aafig_default_options", {}, "html")
210215

211216

212217
# vim: set expandtab shiftwidth=4 softtabstop=4 :

docs/conf.py

+6-9
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# flake8: NOQA: E501
22
import contextlib
33
import inspect
4+
import pathlib
45
import sys
56
import typing as t
67
from os.path import relpath
7-
import pathlib
88

99
import tmuxp
1010

@@ -172,12 +172,12 @@
172172
}
173173

174174
# aafig format, try to get working with pdf
175-
aafig_format = dict(latex="pdf", html="gif")
175+
aafig_format = {"latex": "pdf", "html": "gif"}
176176

177-
aafig_default_options = dict(scale=0.75, aspect=0.5, proportional=True)
177+
aafig_default_options = {"scale": 0.75, "aspect": 0.5, "proportional": True}
178178

179179

180-
def linkcode_resolve(domain, info): # NOQA: C901
180+
def linkcode_resolve(domain, info):
181181
"""
182182
Determine the URL corresponding to Python object
183183
@@ -200,7 +200,7 @@ def linkcode_resolve(domain, info): # NOQA: C901
200200
for part in fullname.split("."):
201201
try:
202202
obj = getattr(obj, part)
203-
except Exception:
203+
except Exception: # NOQA: PERF203
204204
return None
205205

206206
# strip decorators, which would resolve to the source of the decorator
@@ -224,10 +224,7 @@ def linkcode_resolve(domain, info): # NOQA: C901
224224
except Exception:
225225
lineno = None
226226

227-
if lineno:
228-
linespec = "#L%d-L%d" % (lineno, lineno + len(source) - 1)
229-
else:
230-
linespec = ""
227+
linespec = "#L%d-L%d" % (lineno, lineno + len(source) - 1) if lineno else ""
231228

232229
fn = relpath(fn, start=pathlib.Path(tmuxp.__file__).parent)
233230

pyproject.toml

+29
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,35 @@ module = [
155155
]
156156
ignore_missing_imports = true
157157

158+
[tool.ruff]
159+
target-version = "py37"
160+
select = [
161+
"E", # pycodestyle
162+
"F", # pyflakes
163+
"I", # isort
164+
"UP", # pyupgrade
165+
"B", # flake8-bugbear
166+
"C4", # flake8-comprehensions
167+
"Q", # flake8-quotes
168+
"PTH", # flake8-use-pathlib
169+
"SIM", # flake8-simplify
170+
"TRY", # Trycertatops
171+
"PERF", # Perflint
172+
"RUF" # Ruff-specific rules
173+
]
174+
175+
[tool.ruff.isort]
176+
known-first-party = [
177+
"tmuxp"
178+
]
179+
combine-as-imports = true
180+
181+
[tool.ruff.per-file-ignores]
182+
"*/__init__.py" = ["F401"]
183+
"src/tmuxp/workspace/finders.py" = ["PTH"]
184+
"src/tmuxp/cli/*.py" = ["PTH"]
185+
"docs/_ext/aafig.py" = ["PTH"]
186+
158187
[build-system]
159188
requires = ["poetry_core>=1.0.0"]
160189
build-backend = "poetry.core.masonry.api"

src/tmuxp/cli/convert.py

+17-8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from tmuxp.config_reader import ConfigReader
77
from tmuxp.workspace.finders import find_workspace_file, get_workspace_dir
88

9+
from .. import exc
910
from .utils import prompt_yes_no
1011

1112
if t.TYPE_CHECKING:
@@ -40,6 +41,13 @@ def create_convert_subparser(
4041
return parser
4142

4243

44+
class ConvertUnknownFileType(exc.TmuxpException):
45+
def __init__(self, ext: str, *args: object, **kwargs: object) -> None:
46+
return super().__init__(
47+
f"Unknown filetype: {ext} (valid: [.json, .yaml, .yml])"
48+
)
49+
50+
4351
def command_convert(
4452
workspace_file: t.Union[str, pathlib.Path],
4553
answer_yes: bool,
@@ -61,7 +69,7 @@ def command_convert(
6169
elif ext in [".yaml", ".yml"]:
6270
to_filetype = "json"
6371
else:
64-
raise Exception(f"Unknown filetype: {ext} (valid: [.json, .yaml, .yml])")
72+
raise ConvertUnknownFileType(ext)
6573

6674
configparser = ConfigReader.from_file(workspace_file)
6775
newfile = workspace_file.parent / (str(workspace_file.stem) + f".{to_filetype}")
@@ -72,13 +80,14 @@ def command_convert(
7280
**{"default_flow_style": False} if to_filetype == "yaml" else {},
7381
)
7482

75-
if not answer_yes:
76-
if prompt_yes_no(f"Convert to <{workspace_file}> to {to_filetype}?"):
77-
if prompt_yes_no("Save workspace to %s?" % newfile):
78-
answer_yes = True
83+
if (
84+
not answer_yes
85+
and prompt_yes_no(f"Convert to <{workspace_file}> to {to_filetype}?")
86+
and prompt_yes_no("Save workspace to %s?" % newfile)
87+
):
88+
answer_yes = True
7989

8090
if answer_yes:
81-
buf = open(newfile, "w")
82-
buf.write(new_workspace)
83-
buf.close()
91+
with open(newfile, "w") as buf:
92+
buf.write(new_workspace)
8493
print(f"New workspace file saved to <{newfile}>.")

src/tmuxp/cli/debug_info.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import typing as t
88

99
from colorama import Fore
10-
1110
from libtmux.__about__ import __version__ as libtmux_version
1211
from libtmux.common import get_version, tmux_cmd
1312

@@ -34,7 +33,7 @@ def prepend_tab(strings):
3433
"""
3534
Prepend tab to strings in list.
3635
"""
37-
return list(map(lambda x: "\t%s" % x, strings))
36+
return ["\t%s" % x for x in strings]
3837

3938
def output_break():
4039
"""

src/tmuxp/cli/freeze.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
import typing as t
66

77
from libtmux.server import Server
8+
89
from tmuxp.config_reader import ConfigReader
910
from tmuxp.exc import TmuxpException
1011
from tmuxp.workspace.finders import get_workspace_dir
1112

12-
from .. import util
13+
from .. import exc, util
1314
from ..workspace import freezer
1415
from .utils import prompt, prompt_choices, prompt_yes_no
1516

@@ -105,7 +106,7 @@ def command_freeze(
105106
session = util.get_session(server)
106107

107108
if not session:
108-
raise TmuxpException("Session not found.")
109+
raise exc.SessionNotFound()
109110
except TmuxpException as e:
110111
print(e)
111112
return
@@ -194,9 +195,8 @@ def extract_workspace_format(
194195
destdir = os.path.dirname(dest)
195196
if not os.path.isdir(destdir):
196197
os.makedirs(destdir)
197-
buf = open(dest, "w")
198-
buf.write(workspace)
199-
buf.close()
198+
with open(dest, "w") as buf:
199+
buf.write(workspace)
200200

201201
if not args.quiet:
202202
print("Saved to %s." % dest)

0 commit comments

Comments
 (0)