multidog 0.1.0a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ # SPDX-FileCopyrightText: © 2026 scy
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ [*]
6
+ end_of_line = lf
7
+ insert_final_newline = true
8
+ charset = utf-8
9
+ trim_trailing_whitespace = true
10
+
11
+ [*.{bash,sh}]
12
+ indent_style = tab
13
+
14
+ [*.{css,html,js,json,md,scss,ts,yaml}]
15
+ indent_size = 2
16
+ indent_style = space
17
+
18
+ [*.{py,toml}]
19
+ indent_size = 4
20
+ indent_style = space
21
+
22
+ [*.py]
23
+ max_line_length = 79
24
+
25
+ [Makefile]
26
+ indent_style = tab
@@ -0,0 +1,13 @@
1
+ # SPDX-FileCopyrightText: © 2026 scy
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ /.envrc
6
+
7
+ __pycache__/
8
+ *.egg-info/
9
+
10
+ /.coverage
11
+ /coverage.lcov
12
+ /coverage.xml
13
+ /report.xml
@@ -0,0 +1,18 @@
1
+ MIT License
2
+
3
+ Copyright (c) <year> <copyright holders>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6
+ associated documentation files (the "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
9
+ following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or substantial
12
+ portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
15
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
18
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # SPDX-FileCopyrightText: © 2026 scy
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ .PHONY: all fmt noqa qa test reuse
6
+
7
+
8
+ all: fmt qa reuse test
9
+
10
+
11
+ fmt:
12
+ ruff format
13
+ ruff check --fix
14
+
15
+
16
+ noqa:
17
+ ruff check --add-noqa
18
+
19
+
20
+ qa:
21
+ mypy
22
+
23
+
24
+ reuse:
25
+ reuse lint --lines
26
+
27
+
28
+ test:
29
+ pytest
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: multidog
3
+ Version: 0.1.0a1
4
+ Summary: A SIGALRM-based watchdog that can handle multiple timeouts.
5
+ Project-URL: Source, https://codeberg.org/scy/multidog
6
+ Project-URL: Documentation, https://codeberg.org/scy/multidog
7
+ Project-URL: Issues, https://codeberg.org/scy/multidog/issues
8
+ Author: scy
9
+ Maintainer: scy
10
+ License-Expression: MIT
11
+ License-File: LICENSES/MIT.txt
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: POSIX
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: <4,>=3.12
22
+ Description-Content-Type: text/markdown
23
+
24
+ <!--
25
+ SPDX-FileCopyrightText: © 2026 scy
26
+
27
+ SPDX-License-Identifier: MIT
28
+ -->
29
+
30
+ # Multidog
31
+
32
+ _A SIGALRM-based watchdog that can handle multiple timeouts._
33
+
34
+ ➡️ **Quick Links:** [Repository](https://codeberg.org/scy/multidog) · [Issues](https://codeberg.org/scy/multidog/issues)
35
+
36
+ Multidog works by keeping track of one or more timeouts and setting up an [`alarm`](https://docs.python.org/3/library/signal.html#signal.alarm) signal that will cause the OS to terminate your process when the timeout occurs.
37
+
38
+
39
+ ## 🌱 Status
40
+
41
+ Multidog has recently been extracted from another project of mine.
42
+ It kind of works, but I still have to fix some issues.
43
+
44
+ Things that are not yet implemented:
45
+
46
+ - [ ] Timeouts are only checked with the granularity of the shortest timeout. See the example below for what this means.
47
+ - [ ] You may only use one running instance of Multidog per process, because a process can only have one handler per signal. There is no safeguard against this at the moment.
48
+ - [ ] Adding more timeouts after creating the `Multidog` instance is not possible yet.
49
+
50
+
51
+ ## 📚 Usage
52
+
53
+ Example usage:
54
+
55
+ ```python
56
+ from time import sleep
57
+ from multidog import Multidog
58
+
59
+ dog = Multidog({"a": 5, "b": 10})
60
+
61
+ dog.start()
62
+ # This will call `sys.exit()` in 5 seconds unless you reset the timeouts regularly.
63
+
64
+ sleep(4)
65
+ dog.reset("a")
66
+ # `sys.exit()` will be deferred for another 5 seconds.
67
+
68
+ sleep(4)
69
+ dog.reset("a")
70
+ # `sys.exit()` will be deferred for another 5 seconds.
71
+ # This is a bug actually: The "b" timeout has never been reset and only has two
72
+ # seconds left. Multidog should defer for only 2 seconds, but right now it doesn't.
73
+
74
+ sleep(4)
75
+ dog.reset("a")
76
+ # Multidog finally notices that "b" is overdue and will refuse to reset the alarm,
77
+ # causing your application to exit in one second.
78
+
79
+ dog.stop()
80
+ # Stops the watchdog before it kills your app.
81
+ ```
82
+
83
+ ### Anonymous timeout
84
+
85
+ If you only have a single timeout to track, you can simplify your usage:
86
+
87
+ ```python
88
+ dog = Multidog(5) # equivalent to Multidog({"": 5})
89
+ dog.start()
90
+ dog.reset() # equivalent to dog.reset("")
91
+ ```
92
+
93
+ ### Exit timeout
94
+
95
+ Your Python application might not shut down fast enough (or at all) on [`sys.exit()`](https://docs.python.org/3/library/sys.html#sys.exit) (because yeah, that's something that can be suppressed).
96
+ Therefore, Multidog will set up a second `SIGALRM` after calling `sys.exit()`, with the signal handler reset to the default, which _should_ kill your process when the timeout expires.
97
+
98
+ The default timeout for this is 5 seconds, you can choose a different one like so:
99
+
100
+ ```python
101
+ dog = Multidog({"a": 5, "b": 10}, exit_timeout=120)
102
+ ```
103
+
104
+
105
+ ## 🗃️ Installation
106
+
107
+ Simply install the `multidog` package from PyPI via your preferred package manager.
108
+
109
+ For example, to add it as a dependency to your [uv](https://docs.astral.sh/uv/)-managed project, use this:
110
+
111
+ ```sh
112
+ uv add multidog
113
+ ```
114
+
115
+
116
+ ## 🧑‍💻 Development
117
+
118
+ ### Preparation
119
+
120
+ We're using [uv](https://docs.astral.sh/uv/) to manage this project and its dependencies.
121
+ After cloning the repository using Git, a simple `uv sync` should get everything you need.
122
+
123
+ ```sh
124
+ git clone https://codeberg.org/scy/multidog.git multidog
125
+ cd multidog
126
+ uv sync
127
+ ```
128
+
129
+ ### direnv
130
+
131
+ We recommend using [direnv](https://direnv.net/) to add installed dependencies to your `$PATH`, so that you don't need to prepend `uv run` to every command.
132
+ For example, after installing direnv, you can use `direnv edit` in this repository's directory and add the following line:
133
+
134
+ ```sh
135
+ PATH_add .venv/bin
136
+ ```
137
+
138
+ **The rest of this readme assumes that you have `.venv/bin` in your `$PATH`.**
139
+
140
+ ### EditorConfig
141
+
142
+ Make sure your editor or IDE supports the [EditorConfig](https://editorconfig.org/) standard, so that your code adheres to the project's [preferred](.editorconfig) indentation, line lengths, etc.
143
+
144
+ ### Makefile
145
+
146
+ There is a [`Makefile`](Makefile) that contains frequently used commands to speed up development, but it's optional to use.
147
+ The following targets exist:
148
+
149
+ - `fmt`: Use [Ruff](https://docs.astral.sh/ruff/) to format and lint the code.
150
+ - `qa`: Use [mypy](https://mypy.readthedocs.io/) for static type analysis.
151
+ - `reuse`: Use [`reuse lint`](https://codeberg.org/fsfe/reuse-tool) to make sure every file contains licensing information.
152
+ - `test`: Use [pytest](https://pytest.org/) for automated testing and code coverage.
153
+ - `noqa`: Add `noqa` statements to ignore everything Ruff complains about. Only use this after fixing everything that needs fixing.
154
+
155
+ `make all`, or simply `make`, will run `fmt`, `qa`, `reuse`, and `test`.
156
+ You should do this before every commit and fix all issues that are reported.
157
+
158
+
159
+ ## 📃 License
160
+
161
+ This project is licensed under the terms of the [MIT License](https://spdx.org/licenses/MIT.html).
162
+
163
+ The project also conforms to the [REUSE Specification, version 3.3](https://reuse.software/spec-3.3/).
164
+ You can use [the `reuse` tool](https://codeberg.org/fsfe/reuse-tool) to interpret the machine-readable licensing information.
@@ -0,0 +1,141 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: © 2026 scy
3
+
4
+ SPDX-License-Identifier: MIT
5
+ -->
6
+
7
+ # Multidog
8
+
9
+ _A SIGALRM-based watchdog that can handle multiple timeouts._
10
+
11
+ ➡️ **Quick Links:** [Repository](https://codeberg.org/scy/multidog) · [Issues](https://codeberg.org/scy/multidog/issues)
12
+
13
+ Multidog works by keeping track of one or more timeouts and setting up an [`alarm`](https://docs.python.org/3/library/signal.html#signal.alarm) signal that will cause the OS to terminate your process when the timeout occurs.
14
+
15
+
16
+ ## 🌱 Status
17
+
18
+ Multidog has recently been extracted from another project of mine.
19
+ It kind of works, but I still have to fix some issues.
20
+
21
+ Things that are not yet implemented:
22
+
23
+ - [ ] Timeouts are only checked with the granularity of the shortest timeout. See the example below for what this means.
24
+ - [ ] You may only use one running instance of Multidog per process, because a process can only have one handler per signal. There is no safeguard against this at the moment.
25
+ - [ ] Adding more timeouts after creating the `Multidog` instance is not possible yet.
26
+
27
+
28
+ ## 📚 Usage
29
+
30
+ Example usage:
31
+
32
+ ```python
33
+ from time import sleep
34
+ from multidog import Multidog
35
+
36
+ dog = Multidog({"a": 5, "b": 10})
37
+
38
+ dog.start()
39
+ # This will call `sys.exit()` in 5 seconds unless you reset the timeouts regularly.
40
+
41
+ sleep(4)
42
+ dog.reset("a")
43
+ # `sys.exit()` will be deferred for another 5 seconds.
44
+
45
+ sleep(4)
46
+ dog.reset("a")
47
+ # `sys.exit()` will be deferred for another 5 seconds.
48
+ # This is a bug actually: The "b" timeout has never been reset and only has two
49
+ # seconds left. Multidog should defer for only 2 seconds, but right now it doesn't.
50
+
51
+ sleep(4)
52
+ dog.reset("a")
53
+ # Multidog finally notices that "b" is overdue and will refuse to reset the alarm,
54
+ # causing your application to exit in one second.
55
+
56
+ dog.stop()
57
+ # Stops the watchdog before it kills your app.
58
+ ```
59
+
60
+ ### Anonymous timeout
61
+
62
+ If you only have a single timeout to track, you can simplify your usage:
63
+
64
+ ```python
65
+ dog = Multidog(5) # equivalent to Multidog({"": 5})
66
+ dog.start()
67
+ dog.reset() # equivalent to dog.reset("")
68
+ ```
69
+
70
+ ### Exit timeout
71
+
72
+ Your Python application might not shut down fast enough (or at all) on [`sys.exit()`](https://docs.python.org/3/library/sys.html#sys.exit) (because yeah, that's something that can be suppressed).
73
+ Therefore, Multidog will set up a second `SIGALRM` after calling `sys.exit()`, with the signal handler reset to the default, which _should_ kill your process when the timeout expires.
74
+
75
+ The default timeout for this is 5 seconds, you can choose a different one like so:
76
+
77
+ ```python
78
+ dog = Multidog({"a": 5, "b": 10}, exit_timeout=120)
79
+ ```
80
+
81
+
82
+ ## 🗃️ Installation
83
+
84
+ Simply install the `multidog` package from PyPI via your preferred package manager.
85
+
86
+ For example, to add it as a dependency to your [uv](https://docs.astral.sh/uv/)-managed project, use this:
87
+
88
+ ```sh
89
+ uv add multidog
90
+ ```
91
+
92
+
93
+ ## 🧑‍💻 Development
94
+
95
+ ### Preparation
96
+
97
+ We're using [uv](https://docs.astral.sh/uv/) to manage this project and its dependencies.
98
+ After cloning the repository using Git, a simple `uv sync` should get everything you need.
99
+
100
+ ```sh
101
+ git clone https://codeberg.org/scy/multidog.git multidog
102
+ cd multidog
103
+ uv sync
104
+ ```
105
+
106
+ ### direnv
107
+
108
+ We recommend using [direnv](https://direnv.net/) to add installed dependencies to your `$PATH`, so that you don't need to prepend `uv run` to every command.
109
+ For example, after installing direnv, you can use `direnv edit` in this repository's directory and add the following line:
110
+
111
+ ```sh
112
+ PATH_add .venv/bin
113
+ ```
114
+
115
+ **The rest of this readme assumes that you have `.venv/bin` in your `$PATH`.**
116
+
117
+ ### EditorConfig
118
+
119
+ Make sure your editor or IDE supports the [EditorConfig](https://editorconfig.org/) standard, so that your code adheres to the project's [preferred](.editorconfig) indentation, line lengths, etc.
120
+
121
+ ### Makefile
122
+
123
+ There is a [`Makefile`](Makefile) that contains frequently used commands to speed up development, but it's optional to use.
124
+ The following targets exist:
125
+
126
+ - `fmt`: Use [Ruff](https://docs.astral.sh/ruff/) to format and lint the code.
127
+ - `qa`: Use [mypy](https://mypy.readthedocs.io/) for static type analysis.
128
+ - `reuse`: Use [`reuse lint`](https://codeberg.org/fsfe/reuse-tool) to make sure every file contains licensing information.
129
+ - `test`: Use [pytest](https://pytest.org/) for automated testing and code coverage.
130
+ - `noqa`: Add `noqa` statements to ignore everything Ruff complains about. Only use this after fixing everything that needs fixing.
131
+
132
+ `make all`, or simply `make`, will run `fmt`, `qa`, `reuse`, and `test`.
133
+ You should do this before every commit and fix all issues that are reported.
134
+
135
+
136
+ ## 📃 License
137
+
138
+ This project is licensed under the terms of the [MIT License](https://spdx.org/licenses/MIT.html).
139
+
140
+ The project also conforms to the [REUSE Specification, version 3.3](https://reuse.software/spec-3.3/).
141
+ You can use [the `reuse` tool](https://codeberg.org/fsfe/reuse-tool) to interpret the machine-readable licensing information.
@@ -0,0 +1,9 @@
1
+ version = 1
2
+
3
+ # Generated files.
4
+ [[annotations]]
5
+ path = [
6
+ "uv.lock",
7
+ ]
8
+ "SPDX-FileCopyrightText" = "NONE"
9
+ "SPDX-License-Identifier" = "MIT"
@@ -0,0 +1,121 @@
1
+ # SPDX-FileCopyrightText: © 2026 scy
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ import logging
6
+ import signal
7
+ import sys
8
+ from collections.abc import Callable, Mapping
9
+ from datetime import UTC, datetime, timedelta
10
+ from importlib import metadata
11
+
12
+
13
+ PKG_NAME = "multidog"
14
+ PROJECT_URL = "https://codeberg.org/scy/multidog"
15
+
16
+ __version__ = metadata.version(PKG_NAME)
17
+
18
+
19
+ _log = logging.getLogger(__name__)
20
+
21
+
22
+ Timeout = timedelta | int
23
+
24
+
25
+ class Multidog:
26
+ def __init__(
27
+ self,
28
+ timeout: Mapping[str, Timeout] | Timeout,
29
+ *,
30
+ exit_timeout: Timeout = 5,
31
+ ) -> None:
32
+ # Create our dict of timeouts. If we just got a single timeout value,
33
+ # use the empty string as its name.
34
+ self._timeouts = (
35
+ {name: self.to_timedelta(v) for name, v in timeout.items()}
36
+ if isinstance(timeout, Mapping)
37
+ else {"": self.to_timedelta(timeout)}
38
+ )
39
+
40
+ self._exit_timeout = self.to_timedelta(exit_timeout)
41
+ self._reset_all()
42
+
43
+ def _alarm(self, _signal, _stack) -> None:
44
+ _log.critical("watchdog timeout, shutting down")
45
+
46
+ # Remove existing signal handler and schedule another alarm that will
47
+ # kill us if for some reason the sys.exit() doesn't work.
48
+ signal.signal(signal.SIGALRM, signal.SIG_DFL)
49
+ signal.alarm(int(self._exit_timeout.total_seconds()))
50
+
51
+ sys.exit(128 + signal.SIGALRM)
52
+
53
+ def _reset_all(self) -> None:
54
+ """Reset all timers to now, i.e. not yet expired."""
55
+ now = datetime.now(UTC)
56
+ self._resets = dict.fromkeys(self._timeouts, now)
57
+
58
+ @staticmethod
59
+ def to_timedelta(timeout: Timeout) -> timedelta:
60
+ if isinstance(timeout, int):
61
+ return timedelta(seconds=timeout)
62
+ if isinstance(timeout, timedelta):
63
+ return timeout
64
+ raise TypeError(f"invalid timeout type: {type(timeout)}")
65
+
66
+ def alarm_interval(self) -> int:
67
+ """Return the number of seconds of the shortest configured timeout."""
68
+ return min(
69
+ int(timeout.total_seconds()) for timeout in self._timeouts.values()
70
+ )
71
+
72
+ def reset(self, key: str = "") -> bool:
73
+ """Reset the given timeout."""
74
+ now = datetime.now(UTC)
75
+ if key not in self._timeouts:
76
+ raise KeyError(f"unknown watchdog key: {key}")
77
+ self._resets[key] = now
78
+
79
+ # If one of the configured timers has not been reset in its expected
80
+ # time frame, we don't reset the alarm just yet. In other words, all
81
+ # expected timers need to have been reset before their timeout for us
82
+ # to reset the alarm signal and thus not get killed.
83
+ if any(
84
+ name
85
+ for name, timeout in self._timeouts.items()
86
+ if now > self._resets[name] + timeout
87
+ ):
88
+ _log.debug(
89
+ "not resetting watchdog, some expected resets are missing"
90
+ )
91
+ return False
92
+
93
+ _log.debug("resetting watchdog")
94
+ signal.alarm(self.alarm_interval())
95
+ return True
96
+
97
+ def resetfunc(self, key: str = "") -> Callable[[], bool]:
98
+ """Get a function to reset the given timeout."""
99
+
100
+ def reset_func() -> bool:
101
+ return self.reset(key)
102
+
103
+ return reset_func
104
+
105
+ def start(self) -> None:
106
+ """Start watchdog operation."""
107
+ # Reset all timers.
108
+ self._reset_all()
109
+ # Set our signal handler.
110
+ signal.signal(signal.SIGALRM, self._alarm)
111
+ # Have the signal fire when the shortest timeout has been reached.
112
+ signal.alarm(self.alarm_interval())
113
+
114
+ def stop(self) -> None:
115
+ """Stop watchdog operation."""
116
+ # Disable the signal.
117
+ signal.alarm(0)
118
+ # Reset all timers.
119
+ self._reset_all()
120
+ # Restore the default signal handler.
121
+ signal.signal(signal.SIGALRM, signal.SIG_DFL)
File without changes
@@ -0,0 +1,144 @@
1
+ # SPDX-FileCopyrightText: © 2026 scy
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ [project]
6
+ name = "multidog"
7
+ version = "0.1.0a1"
8
+ description = "A SIGALRM-based watchdog that can handle multiple timeouts."
9
+ readme = "README.md"
10
+ requires-python = ">=3.12,<4"
11
+ license = "MIT"
12
+ license-files = ["LICENSES/*.txt"]
13
+ authors = [
14
+ {name = "scy"},
15
+ ]
16
+ maintainers = [
17
+ {name = "scy"},
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 4 - Beta",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Programming Language :: Python :: 3.14",
26
+ "Operating System :: POSIX",
27
+ "Intended Audience :: Developers",
28
+ "Typing :: Typed",
29
+ ]
30
+ dependencies = [
31
+ ]
32
+
33
+ [project.urls]
34
+ Source = "https://codeberg.org/scy/multidog"
35
+ Documentation = "https://codeberg.org/scy/multidog"
36
+ Issues = "https://codeberg.org/scy/multidog/issues"
37
+
38
+ [dependency-groups]
39
+ dev = [
40
+ "mypy>=1.19",
41
+ "pytest>=9",
42
+ "pytest-cov>=7",
43
+ "reuse>=6.2",
44
+ "ruff<=0.15.6", # last version before Astral joined OpenAI
45
+ ]
46
+
47
+ [build-system]
48
+ requires = ["hatchling"]
49
+ build-backend = "hatchling.build"
50
+
51
+ [tool.uv]
52
+ package = true
53
+
54
+ [tool.mypy]
55
+ packages = [
56
+ "multidog",
57
+ "tests",
58
+ ]
59
+ python_version = "3.12"
60
+ warn_return_any = true
61
+
62
+ [tool.pytest]
63
+ addopts = [
64
+ "--strict-markers",
65
+ "--junit-xml=report.xml",
66
+ "--cov-report=term",
67
+ "--cov-report=lcov",
68
+ "--cov-report=xml",
69
+ "--cov=multidog",
70
+ ]
71
+
72
+ [tool.ruff]
73
+ line-length = 79
74
+
75
+ [tool.ruff.format]
76
+ quote-style = "double"
77
+ indent-style = "space"
78
+
79
+ [tool.ruff.lint]
80
+ preview = true
81
+ select = [
82
+ "ANN", # flake8-annotations
83
+ "ARG", # flake8-unused-arguments
84
+ "ASYNC", # flake8-async
85
+ "B", # flake8-bugbear
86
+ "C4", # flake8-comprehensions
87
+ "C90", # mccabe complexity
88
+ "DTZ", # flake8-datetimez (naive datetimes)
89
+ "E", # pycodestyle errors
90
+ "ERA", # eradicate (commented-out code)
91
+ "EXE", # flake8-executable (shebang lines)
92
+ "F", # pyflakes
93
+ "FA", # flake8-future-annotations
94
+ "FURB", # refurb
95
+ "I", # isort
96
+ "INP", # flake8-no-pep420 (implicit namespace package)
97
+ "ISC", # flake8-implicit-str-concat
98
+ "LOG", # flake8-logging
99
+ "N", # pep8-naming
100
+ "PL", # pylint
101
+ "PT", # flake8-pytest-style
102
+ "PTH", # flake8-use-pathlib
103
+ "Q", # flake8-quotes
104
+ "RET", # flake8-return
105
+ "RSE", # flake8-raise
106
+ "RUF", # Ruff-specific rules
107
+ "S", # flake8-bandit (security)
108
+ "SIM", # flake8-simplify
109
+ "SLF", # flake8-self (private member access)
110
+ "SLOT", # flake8-slots
111
+ "TCH", # flake8-type-checking
112
+ "TRY", # tryceratops
113
+ "T20", # flake8-print
114
+ "UP", # pyupgrade
115
+ "W", # pycodestyle warnings
116
+ ]
117
+ ignore = [
118
+ "C408", # micro-optimization; I'd rather get rid of too many quotes and use dict(x=…)
119
+ "ISC001", # conflict w/ formatter, see https://github.com/astral-sh/ruff/issues/8272
120
+ "PLC0415", # forbids dynamic imports
121
+ "RUF067", # disallows constants in __init__.py
122
+ "RUF200", # requires a name for projects
123
+ "TRY003", # complains about simple ValueError messages
124
+ ]
125
+ allowed-confusables = [ # exceptions to RUF002, characters in docstrings
126
+ " ", # non-breaking space
127
+ ]
128
+
129
+ [tool.ruff.lint.flake8-annotations]
130
+ mypy-init-return = true # no return annotation required for __init__
131
+ suppress-dummy-args = true # don't require annotations for _foo variables
132
+
133
+ [tool.ruff.lint.per-file-ignores]
134
+ "tests/*" = [
135
+ "ANN001", # missing function argument annotation
136
+ "ANN201", # missing return type annotation
137
+ "ARG001", # unused function argument (e.g. require-only fixtures)
138
+ "INP001", # implicit namespace package
139
+ "S101", # use of assert
140
+ ]
141
+
142
+ [tool.ruff.lint.isort]
143
+ # Two blank lines after the imports.
144
+ lines-after-imports = 2