Skip to content

Commit e81a3b3

Browse files
authored
Pivot to using ruff for all linting (#15603)
* Pivot to using ruff for all linting This commit switches us to fully using ruff for all the linting in CI and locally. The primary motivation for this change is to improve productivity because ruff is signficantly faster. Pylint is incredibly slow in general, but compared to ruff especially so. For example, on my laptop ruff takes 0.04 seconds to run on the qiskit/ subdirectory (after clearing the cache, with the cache populated it takes 0.025 sec) of the source tree while running pylint on the same path took 70 sec. This leads to people skipping lint locally and causes churn in CI becaus. We had started to experimenting with ruff in the past and used it for a some small set of rules but were still using pylint for the bulk of the linting in the repo. The concern at the time was a loss of lint coverage or a lot of code churn caused by migrating to a new tool. Specifically pylint does more type inference and checking that ruff doesn't. However since we started the experiment one major change in qiskit is how much work is happening in rust now vs Python. At this point any loss in lint coverage is unlikely to cause a significant problem in practice and we'll make real productivity gains by making this change. * Remove out of date comment * Update makefile * Enable more rules * Revert lambda autofixes * Fix new rules * Add bandit rules * Enable Ruff native rules * Remove pylint disable comments * Enable docstring rules This commit adds the docstring rules on the repo. This involves a few more changes than previous commits because there are a lot of formatting consistency rules that needed to be auto-applied. The checking also found several instances where there was missing documentation that should have been included. * Add categories from old ruff config * Fix from rebase * Add flake8-raise rule * Add flake8-pie rules * Add implicit namespace rules * Fix deprecation decorator error on import * Fix __init__ docstring for deprecated circuit library elements * Revert "Enable docstring rules" The formatting changes that ruff auto applied to the docstrings were not valid in many cases. The changeset for the commit was too large to systematically revert just the incorrect autoformatting and disabling those rules. Instead this reverts commit f928646 on the whole. We can circle back and add a reduced subset of the docstring rules that makes sense and a functional docs build after the revert. * Fix typo in latex circuit drawer changes * Fix csp tests for rng change * Remove unused typing imports * Bump to ruff 0.15.2 Since this PR was first opened there have been several new ruff releases. This updates to the latest release as of this commit. The 0.15.0 release included a new default rule in the RUF ruleset which triggered on the code base which this also fixes.
1 parent aac0b18 commit e81a3b3

737 files changed

Lines changed: 2552 additions & 2827 deletions

File tree

Some content is hidden

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

CONTRIBUTING.md

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -787,26 +787,18 @@ int test_FILE_NAME()
787787
Qiskit uses three tools for Python code formatting and lint checking. The
788788
first tool is [black](https://github.com/psf/black) which is a code formatting
789789
tool that will automatically update the code formatting to a consistent style.
790-
The second tool is [pylint](https://www.pylint.org/) which is a code linter
790+
The second tool is [ruff](https://docs.astral.sh/ruff/) which is a code linter
791791
which does a deeper analysis of the Python code to find both style issues and
792-
potential bugs and other common issues in Python. The third tool is the linter
793-
[ruff](https://github.com/charliermarsh/ruff), which has been recently
794-
introduced into Qiskit on an experimental basis. Only a very small number
795-
of rules are enabled.
792+
potential bugs and other common issues in Python.
796793

797794
You can check that your local modifications conform to the style rules by
798-
running `tox -elint` which will run `black`, `ruff`, and `pylint` to check the
795+
running `tox -elint` which will run `black` and `ruff` to check the
799796
local code formatting and lint. If black returns a code formatting error you can
800797
run `tox -eblack` to automatically update the code formatting to conform to the
801-
style. However, if `ruff` or `pylint` return any error you will have to fix
802-
these issues by manually updating your code.
803-
804-
Because `pylint` analysis can be slow, there is also a `tox -elint-incr` target,
805-
which runs `black` and `ruff` just as `tox -elint` does, but only applies
806-
`pylint` to files which have changed from the source github. On rare occasions
807-
this will miss some issues that would have been caught by checking the complete
808-
source tree, but makes up for this by being much faster (and those rare
809-
oversights will still be caught by the CI after you open a pull request).
798+
style. However, if `ruff` return any error you will have to fix these issues by
799+
manually updating your code. Sometimes `ruff` will be able to fix failures with
800+
the `--fix` flag. In these cases the output will tell you how many errors can be
801+
automatically fixed
810802

811803
Because they are so fast, it is sometimes convenient to run the tools `black` and `ruff` separately
812804
rather than via `tox`. You can install all the lint dependencies using the `lint` or `dev`

Makefile

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ endif
1616

1717
.PHONY: default ruff env lint lint-incr style black test test_randomized pytest pytest_randomized test_ci coverage coverage_erase clean
1818

19-
default: ruff style lint-incr test ;
19+
default: style lint-incr test ;
2020

2121
# Dependencies need to be installed on the Anaconda virtual environment.
2222
env:
@@ -27,18 +27,15 @@ env:
2727
bash -c "source activate Qiskitenv;pip install -r requirements.txt"; \
2828
fi;
2929

30-
# Ignoring generated ones with .py extension.
3130
lint:
32-
pylint -rn qiskit test tools
31+
ruff check qiskit test tools setup.py
3332
tools/verify_headers.py qiskit test tools
3433
tools/find_optional_imports.py
3534
tools/find_stray_release_notes.py
3635
tools/verify_images.py
3736

38-
# Only pylint on files that have changed from origin/main. Also parallelize (disables cyclic-import check)
3937
lint-incr:
40-
-git fetch -q https://github.com/Qiskit/qiskit-terra.git :lint_incr_latest
41-
tools/pylint_incr.py -j4 -rn -sn --paths :/qiskit/*.py :/test/*.py :/tools/*.py
38+
ruff check qiskit test tools setup.py
4239
tools/verify_headers.py qiskit test tools
4340
tools/find_optional_imports.py
4441
tools/verify_images.py

pyproject.toml

Lines changed: 174 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,7 @@ build = ["setuptools>=77.0", "setuptools-rust"]
188188
# We use quite tight pins on pylint and astroid because they have a habit of adding new
189189
# on-by-default lints that are harder to deal with.
190190
lint = [
191-
"pylint~=3.2.0",
192-
"astroid~=3.2.0",
193-
"ruff==0.0.267",
191+
"ruff==0.15.2",
194192
"reno>=4.1.0", # duplicated with `doc`
195193
"black[jupyter]~=25.1",
196194
]
@@ -312,17 +310,180 @@ test-command = "cp -r {project}/test . && stestr --test-path test/python run --a
312310
repair-wheel-command = "cp {wheel} {dest_dir}/. && pipx run abi3audit --strict --report {wheel}"
313311

314312
[tool.ruff]
313+
line-length = 110
314+
315+
[tool.ruff.lint]
315316
select = [
316-
# Rules in alphabetic order
317-
"C4", # category: flake8-comprehensions
318-
"EXE", # Category: flake8-executable
319-
"F631", # assert-tuple
320-
"F632", # is-literal
321-
"F634", # if-tuple
322-
"F823", # undefined-local
323-
"G", # flake8-logging-format
324-
"T10", # category: flake8-debugger
317+
# pycodestyle
318+
"E",
319+
# Pyflakes
320+
"F",
321+
# Pylint
322+
"PL",
323+
# pyupgrade
324+
"UP",
325+
# bandit
326+
"S",
327+
# Ruff native rules
328+
"RUF",
329+
# flake8-logging-format
330+
"G",
331+
# flake8-executable
332+
"EXE",
333+
# flake8-comprehensions
334+
"C4",
335+
# flake8-debugger
336+
"T10",
337+
# flake8-raise
338+
"RSE",
339+
# flake8-pie
340+
"PIE",
341+
# flake8-no-pep420
342+
"INP",
343+
]
344+
ignore = [
345+
"E402", # module-import-not-at-top-of-file false positives with module docs
346+
"F401", # unused-import false triggers on init re-exports with __all__
347+
"E501", # line-too-long rely on black for formatting
348+
"PLR2004", # magic-value-comparison overly pedantic especially in tests
349+
"PLR0911", # too-many-return-statements
350+
"PLR0912", # too-many-branches
351+
"PLR0904", # too-many-public-methods
352+
"PLR0914", # too-many-locals
353+
"PLR0915", # too-many-statements
354+
"PLR1702", # too-many-nested-blocks
355+
"PLR0904", # too-many-public-methods
356+
"PLC0415", # import-outside-top-level
357+
"E731", # lambda-assignment
358+
"PLW1514", # unspecified-encoding do not want to implement
359+
"PLR0913", # too-many-arguments
360+
"PLW1641", # eq-without-hash not everything has to be hashablie
361+
"PLW3301", # nested-min-max
362+
"PLW2901", # redefined-loop-variablke - TODO fix this
363+
"RUF003", # ambiguous-unicode-character-comment
364+
"RUF005", # collection-literal-concatenation
365+
"RUF012", # mutable-class-default"
366+
"RUF001", # ambiguous-unicode-character-string
367+
"RUF002", # ambiguous-unicode-character-docstring
368+
"RUF007", # zip-instead-of-pairwise overly opinionated
369+
]
370+
371+
[tool.ruff.lint.per-file-ignores]
372+
"*.ipynb" = ["F821"]
373+
# Security rules from bandit don't apply to tests and utils
374+
"tools/*" = [
375+
"S603",
376+
# Docstring rules don't apply to tools as they're not documented
377+
"D100",
378+
"D101",
379+
"D102",
380+
"D103",
381+
"D104",
382+
"D105",
383+
"D106",
384+
"D107",
385+
"D200",
386+
"D201",
387+
"D202",
388+
"D203",
389+
"D204",
390+
"D205",
391+
"D206",
392+
"D207",
393+
"D208",
394+
"D209",
395+
"D210",
396+
"D211",
397+
"D212",
398+
"D213",
399+
"D214",
400+
"D215",
401+
"D300",
402+
"D301",
403+
"D400",
404+
"D401",
405+
"D402",
406+
"D403",
407+
"D404",
408+
"D405",
409+
"D406",
410+
"D407",
411+
"D408",
412+
"D409",
413+
"D410",
414+
"D411",
415+
"D412",
416+
"D413",
417+
"D414",
418+
"D415",
419+
"D416",
420+
"D417",
421+
"D418",
422+
"D419",
423+
# Implicit namespace rules don't apply to tools because they're all standalone scripts
424+
"INP001",
425+
]
426+
"test/*" = [
427+
"S110",
428+
"S301",
429+
"S603",
430+
"S311",
431+
"S307",
432+
"RUF015",
433+
# Docstring rules don't apply to tests as they're not documented
434+
"D100",
435+
"D101",
436+
"D102",
437+
"D103",
438+
"D104",
439+
"D105",
440+
"D106",
441+
"D107",
442+
"D200",
443+
"D201",
444+
"D202",
445+
"D203",
446+
"D204",
447+
"D205",
448+
"D206",
449+
"D207",
450+
"D208",
451+
"D209",
452+
"D210",
453+
"D211",
454+
"D212",
455+
"D213",
456+
"D214",
457+
"D215",
458+
"D300",
459+
"D301",
460+
"D400",
461+
"D401",
462+
"D402",
463+
"D403",
464+
"D404",
465+
"D405",
466+
"D406",
467+
"D407",
468+
"D408",
469+
"D409",
470+
"D410",
471+
"D411",
472+
"D412",
473+
"D413",
474+
"D414",
475+
"D415",
476+
"D416",
477+
"D417",
478+
"D418",
479+
"D419",
325480
]
481+
"test/randomized/*" = ["S101"]
482+
"test/qpy_compat/*" = ["S101", "INP001"]
483+
"test/benchmarks/*" = ["S101"]
484+
# Visualization functions often subprocess to visualization tools
485+
"qiskit/visualization/*" = ["S603", "S607"]
486+
326487

327488
[tool.pylint.main]
328489
extension-pkg-allow-list = [
@@ -354,22 +515,19 @@ disable = [
354515
"protected-access", # disabled as we don't follow the public vs private convention strictly
355516
"duplicate-code", # disabled as it is too verbose
356517
"redundant-returns-doc", # for @abstractmethod, it cannot interpret "pass"
357-
"too-many-lines", "too-many-branches", "too-many-locals", "too-many-nested-blocks", "too-many-statements",
358-
"too-many-instance-attributes", "too-many-arguments", "too-many-public-methods", "too-few-public-methods", "too-many-ancestors",
518+
"too-many-instance-attributes", "too-many-arguments", "too-few-public-methods", "too-many-ancestors",
359519
"unnecessary-pass", # allow for methods with just "pass", for clarity
360520
"unnecessary-dunder-call", # do not want to implement
361521
"no-else-return", # relax "elif" after a clause with a return
362522
"docstring-first-line-empty", # relax docstring style
363523
"import-outside-toplevel", "import-error", # overzealous with our optionals/dynamic packages
364-
"nested-min-max", # this gives false equivalencies if implemented for the current lint version
365524
"consider-using-max-builtin", "consider-using-min-builtin", # unnecessary stylistic opinion
366525
# TODO(#9614): these were added in modern Pylint. Decide if we want to enable them. If so,
367526
# remove from here and fix the issues. Else, move it above this section and add a comment
368527
# with the rationale
369528
"no-member", # for dynamically created members
370529
"not-context-manager",
371530
"unnecessary-lambda-assignment", # do not want to implement
372-
"unspecified-encoding", # do not want to implement
373531
]
374532

375533
enable = [

qiskit/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
# copyright notice, and modified files need to carry a notice indicating
1111
# that they have been altered from the originals.
1212

13-
# pylint: disable=wrong-import-position,wrong-import-order
1413

1514
# The documentation of the root namespace is manual in `docs/apidoc/root.rst`, so that the
1615
# :mod:`qiskit` Sphinx cross-reference can more easily point to the top-level API table in our
@@ -34,7 +33,7 @@
3433
# `qiskit/tools` folder in their path, which will appear as a "namespace package" with no valid
3534
# location. We catch that case as "not actually having Qiskit 0.x" as a convenience to devs.
3635
_has_tools = getattr(importlib.util.find_spec("qiskit.tools"), "has_location", False)
37-
_suppress_error = os.environ.get("QISKIT_SUPPRESS_1_0_IMPORT_ERROR", False) == "1"
36+
_suppress_error = os.environ.get("QISKIT_SUPPRESS_1_0_IMPORT_ERROR", "") == "1"
3837
if not _suppress_error and _has_tools:
3938
raise ImportError(
4039
"Qiskit is installed in an invalid environment that has both Qiskit >=1.0"
@@ -172,6 +171,6 @@
172171
"QiskitError",
173172
"QuantumCircuit",
174173
"QuantumRegister",
175-
"transpile",
176174
"generate_preset_pass_manager",
175+
"transpile",
177176
]

qiskit/_numpy_compat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"""
5454

5555
VERSION = np.lib.NumpyVersion(np.__version__)
56-
VERSION_PARTS: typing.Tuple[int, ...]
56+
VERSION_PARTS: tuple[int, ...]
5757
"""The numeric parts of the Numpy release version, e.g. ``(2, 0, 0)``. Does not include pre- or
5858
post-release markers (e.g. ``rc1``)."""
5959
if match := re.fullmatch(_VERSION_PATTERN, np.__version__, flags=re.VERBOSE | re.IGNORECASE):

qiskit/circuit/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,7 +1374,7 @@ def __array__(self, dtype=None, copy=None):
13741374
\end{pmatrix}
13751375
"""
13761376

1377-
from qiskit._accelerate.circuit import ( # pylint: disable=unused-import
1377+
from qiskit._accelerate.circuit import (
13781378
Bit,
13791379
Qubit,
13801380
AncillaQubit,
@@ -1391,7 +1391,7 @@ def __array__(self, dtype=None, copy=None):
13911391
from .quantumcircuit import QuantumCircuit
13921392
from .gate import Gate
13931393

1394-
# pylint: disable=cyclic-import
1394+
13951395
from . import annotation
13961396
from .annotation import Annotation
13971397
from .controlledgate import ControlledGate

qiskit/circuit/_add_control.py

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ def control(
127127
CircuitError: gate contains non-gate in definition
128128
"""
129129

130-
# pylint: disable=cyclic-import
131130
from qiskit.circuit import controlledgate
132131

133132
ctrl_state = _ctrl_state_to_int(ctrl_state, num_ctrl_qubits)
@@ -262,23 +261,22 @@ def apply_basic_controlled_gate(circuit, gate, controls, target):
262261
circuit.cp(lamb, controls[0], target)
263262
else:
264263
circuit.cu(theta, phi, lamb, 0, controls[0], target)
264+
elif phi == -pi / 2 and lamb == pi / 2:
265+
circuit.mcrx(theta, controls, target, use_basis_gates=False)
266+
elif phi == 0 and lamb == 0:
267+
circuit.mcry(
268+
theta,
269+
controls,
270+
target,
271+
use_basis_gates=False,
272+
)
273+
elif theta == 0 and phi == 0:
274+
circuit.mcp(lamb, controls, target)
265275
else:
266-
if phi == -pi / 2 and lamb == pi / 2:
267-
circuit.mcrx(theta, controls, target, use_basis_gates=False)
268-
elif phi == 0 and lamb == 0:
269-
circuit.mcry(
270-
theta,
271-
controls,
272-
target,
273-
use_basis_gates=False,
274-
)
275-
elif theta == 0 and phi == 0:
276-
circuit.mcp(lamb, controls, target)
277-
else:
278-
circuit.mcrz(lamb, controls, target, use_basis_gates=False)
279-
circuit.mcry(theta, controls, target, use_basis_gates=False)
280-
circuit.mcrz(phi, controls, target, use_basis_gates=False)
281-
circuit.mcp((phi + lamb) / 2, controls[1:], controls[0])
276+
circuit.mcrz(lamb, controls, target, use_basis_gates=False)
277+
circuit.mcry(theta, controls, target, use_basis_gates=False)
278+
circuit.mcrz(phi, controls, target, use_basis_gates=False)
279+
circuit.mcp((phi + lamb) / 2, controls[1:], controls[0])
282280

283281
elif gate.name == "z":
284282
circuit.h(target)

0 commit comments

Comments
 (0)