summaryrefslogtreecommitdiffstats
path: root/tools/lint/python
diff options
context:
space:
mode:
Diffstat (limited to 'tools/lint/python')
-rw-r--r--tools/lint/python/__init__.py3
-rw-r--r--tools/lint/python/black.py179
-rw-r--r--tools/lint/python/black_requirements.in4
-rw-r--r--tools/lint/python/black_requirements.txt118
-rw-r--r--tools/lint/python/flake8.py215
-rw-r--r--tools/lint/python/flake8_requirements.in4
-rw-r--r--tools/lint/python/flake8_requirements.txt41
-rw-r--r--tools/lint/python/isort.py138
-rw-r--r--tools/lint/python/isort_requirements.in1
-rw-r--r--tools/lint/python/isort_requirements.txt10
-rw-r--r--tools/lint/python/l10n_lint.py171
-rw-r--r--tools/lint/python/pylint.py133
-rw-r--r--tools/lint/python/pylint_requirements.in5
-rw-r--r--tools/lint/python/pylint_requirements.txt136
14 files changed, 1158 insertions, 0 deletions
diff --git a/tools/lint/python/__init__.py b/tools/lint/python/__init__.py
new file mode 100644
index 0000000000..c580d191c1
--- /dev/null
+++ b/tools/lint/python/__init__.py
@@ -0,0 +1,3 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
diff --git a/tools/lint/python/black.py b/tools/lint/python/black.py
new file mode 100644
index 0000000000..8c44a56951
--- /dev/null
+++ b/tools/lint/python/black.py
@@ -0,0 +1,179 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import platform
+import re
+import signal
+import subprocess
+import sys
+
+import mozpack.path as mozpath
+from mozfile import which
+from mozlint import result
+from mozlint.pathutils import expand_exclusions
+from mozprocess import ProcessHandler
+
+here = os.path.abspath(os.path.dirname(__file__))
+BLACK_REQUIREMENTS_PATH = os.path.join(here, "black_requirements.txt")
+
+BLACK_INSTALL_ERROR = """
+Unable to install correct version of black
+Try to install it manually with:
+ $ pip install -U --require-hashes -r {}
+""".strip().format(
+ BLACK_REQUIREMENTS_PATH
+)
+
+
+def default_bindir():
+ # We use sys.prefix to find executables as that gets modified with
+ # virtualenv's activate_this.py, whereas sys.executable doesn't.
+ if platform.system() == "Windows":
+ return os.path.join(sys.prefix, "Scripts")
+ else:
+ return os.path.join(sys.prefix, "bin")
+
+
+def get_black_version(binary):
+ """
+ Returns found binary's version
+ """
+ try:
+ output = subprocess.check_output(
+ [binary, "--version"],
+ stderr=subprocess.STDOUT,
+ universal_newlines=True,
+ )
+ except subprocess.CalledProcessError as e:
+ output = e.output
+ try:
+ # Accept `black.EXE, version ...` on Windows.
+ # for old version of black, the output is
+ # black, version 21.4b2
+ # From black 21.11b1, the output is like
+ # black, 21.11b1 (compiled: no)
+ return re.match(r"black.*,( version)? (\S+)", output)[2]
+ except TypeError as e:
+ print("Could not parse the version '{}'".format(output))
+ print("Error: {}".format(e))
+
+
+def parse_issues(config, output, paths, *, log):
+ would_reformat = re.compile("^would reformat (.*)$", re.I)
+ reformatted = re.compile("^reformatted (.*)$", re.I)
+ cannot_reformat = re.compile("^error: cannot format (.*?): (.*)$", re.I)
+ results = []
+ for line in output:
+ line = line.decode("utf-8")
+ if line.startswith("All done!") or line.startswith("Oh no!"):
+ break
+
+ match = would_reformat.match(line)
+ if match:
+ res = {"path": match.group(1), "level": "error"}
+ results.append(result.from_config(config, **res))
+ continue
+
+ match = reformatted.match(line)
+ if match:
+ res = {"path": match.group(1), "level": "warning", "message": "reformatted"}
+ results.append(result.from_config(config, **res))
+ continue
+
+ match = cannot_reformat.match(line)
+ if match:
+ res = {"path": match.group(1), "level": "error", "message": match.group(2)}
+ results.append(result.from_config(config, **res))
+ continue
+
+ log.debug("Unhandled line", line)
+ return results
+
+
+class BlackProcess(ProcessHandler):
+ def __init__(self, config, *args, **kwargs):
+ self.config = config
+ kwargs["stream"] = False
+ ProcessHandler.__init__(self, *args, **kwargs)
+
+ def run(self, *args, **kwargs):
+ orig = signal.signal(signal.SIGINT, signal.SIG_IGN)
+ ProcessHandler.run(self, *args, **kwargs)
+ signal.signal(signal.SIGINT, orig)
+
+
+def run_process(config, cmd):
+ proc = BlackProcess(config, cmd)
+ proc.run()
+ try:
+ proc.wait()
+ except KeyboardInterrupt:
+ proc.kill()
+
+ return proc.output
+
+
+def setup(root, **lintargs):
+ log = lintargs["log"]
+ virtualenv_bin_path = lintargs.get("virtualenv_bin_path")
+ # Using `which` searches multiple directories and handles `.exe` on Windows.
+ binary = which("black", path=(virtualenv_bin_path, default_bindir()))
+
+ if binary and os.path.exists(binary):
+ binary = mozpath.normsep(binary)
+ log.debug("Looking for black at {}".format(binary))
+ version = get_black_version(binary)
+ versions = [
+ line.split()[0].strip()
+ for line in open(BLACK_REQUIREMENTS_PATH).readlines()
+ if line.startswith("black==")
+ ]
+ if ["black=={}".format(version)] == versions:
+ log.debug("Black is present with expected version {}".format(version))
+ return 0
+ else:
+ log.debug("Black is present but unexpected version {}".format(version))
+
+ log.debug("Black needs to be installed or updated")
+ virtualenv_manager = lintargs["virtualenv_manager"]
+ try:
+ virtualenv_manager.install_pip_requirements(BLACK_REQUIREMENTS_PATH, quiet=True)
+ except subprocess.CalledProcessError:
+ print(BLACK_INSTALL_ERROR)
+ return 1
+
+
+def run_black(config, paths, fix=None, *, log, virtualenv_bin_path):
+ fixed = 0
+ binary = os.path.join(virtualenv_bin_path or default_bindir(), "black")
+
+ log.debug("Black version {}".format(get_black_version(binary)))
+
+ cmd_args = [binary]
+ if not fix:
+ cmd_args.append("--check")
+
+ base_command = cmd_args + paths
+ log.debug("Command: {}".format(" ".join(base_command)))
+ output = parse_issues(config, run_process(config, base_command), paths, log=log)
+
+ # black returns an issue for fixed files as well
+ for eachIssue in output:
+ if eachIssue.message == "reformatted":
+ fixed += 1
+
+ return {"results": output, "fixed": fixed}
+
+
+def lint(paths, config, fix=None, **lintargs):
+ files = list(expand_exclusions(paths, config, lintargs["root"]))
+
+ return run_black(
+ config,
+ files,
+ fix=fix,
+ log=lintargs["log"],
+ virtualenv_bin_path=lintargs.get("virtualenv_bin_path"),
+ )
diff --git a/tools/lint/python/black_requirements.in b/tools/lint/python/black_requirements.in
new file mode 100644
index 0000000000..e5efa47492
--- /dev/null
+++ b/tools/lint/python/black_requirements.in
@@ -0,0 +1,4 @@
+black==21.11b1
+typing-extensions==3.10.0.2
+dataclasses==0.6
+
diff --git a/tools/lint/python/black_requirements.txt b/tools/lint/python/black_requirements.txt
new file mode 100644
index 0000000000..944e5a83ec
--- /dev/null
+++ b/tools/lint/python/black_requirements.txt
@@ -0,0 +1,118 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+# pip-compile --generate-hashes --output-file=tools/lint/python/black_requirements.txt tools/lint/python/black_requirements.in
+#
+black==21.11b1 \
+ --hash=sha256:802c6c30b637b28645b7fde282ed2569c0cd777dbe493a41b6a03c1d903f99ac \
+ --hash=sha256:a042adbb18b3262faad5aff4e834ff186bb893f95ba3a8013f09de1e5569def2
+ # via -r tools/lint/python/black_requirements.in
+click==8.0.3 \
+ --hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \
+ --hash=sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b
+ # via black
+dataclasses==0.6 \
+ --hash=sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f \
+ --hash=sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84
+ # via -r tools/lint/python/black_requirements.in
+mypy-extensions==0.4.3 \
+ --hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \
+ --hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8
+ # via black
+pathspec==0.9.0 \
+ --hash=sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a \
+ --hash=sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1
+ # via black
+platformdirs==2.4.0 \
+ --hash=sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2 \
+ --hash=sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d
+ # via black
+regex==2021.11.10 \
+ --hash=sha256:05b7d6d7e64efe309972adab77fc2af8907bb93217ec60aa9fe12a0dad35874f \
+ --hash=sha256:0617383e2fe465732af4509e61648b77cbe3aee68b6ac8c0b6fe934db90be5cc \
+ --hash=sha256:07856afef5ffcc052e7eccf3213317fbb94e4a5cd8177a2caa69c980657b3cb4 \
+ --hash=sha256:162abfd74e88001d20cb73ceaffbfe601469923e875caf9118333b1a4aaafdc4 \
+ --hash=sha256:2207ae4f64ad3af399e2d30dde66f0b36ae5c3129b52885f1bffc2f05ec505c8 \
+ --hash=sha256:30ab804ea73972049b7a2a5c62d97687d69b5a60a67adca07eb73a0ddbc9e29f \
+ --hash=sha256:3b5df18db1fccd66de15aa59c41e4f853b5df7550723d26aa6cb7f40e5d9da5a \
+ --hash=sha256:3c5fb32cc6077abad3bbf0323067636d93307c9fa93e072771cf9a64d1c0f3ef \
+ --hash=sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f \
+ --hash=sha256:432bd15d40ed835a51617521d60d0125867f7b88acf653e4ed994a1f8e4995dc \
+ --hash=sha256:4aaa4e0705ef2b73dd8e36eeb4c868f80f8393f5f4d855e94025ce7ad8525f50 \
+ --hash=sha256:537ca6a3586931b16a85ac38c08cc48f10fc870a5b25e51794c74df843e9966d \
+ --hash=sha256:53db2c6be8a2710b359bfd3d3aa17ba38f8aa72a82309a12ae99d3c0c3dcd74d \
+ --hash=sha256:5537f71b6d646f7f5f340562ec4c77b6e1c915f8baae822ea0b7e46c1f09b733 \
+ --hash=sha256:6650f16365f1924d6014d2ea770bde8555b4a39dc9576abb95e3cd1ff0263b36 \
+ --hash=sha256:666abff54e474d28ff42756d94544cdfd42e2ee97065857413b72e8a2d6a6345 \
+ --hash=sha256:68a067c11463de2a37157930d8b153005085e42bcb7ad9ca562d77ba7d1404e0 \
+ --hash=sha256:780b48456a0f0ba4d390e8b5f7c661fdd218934388cde1a974010a965e200e12 \
+ --hash=sha256:788aef3549f1924d5c38263104dae7395bf020a42776d5ec5ea2b0d3d85d6646 \
+ --hash=sha256:7ee1227cf08b6716c85504aebc49ac827eb88fcc6e51564f010f11a406c0a667 \
+ --hash=sha256:7f301b11b9d214f83ddaf689181051e7f48905568b0c7017c04c06dfd065e244 \
+ --hash=sha256:83ee89483672b11f8952b158640d0c0ff02dc43d9cb1b70c1564b49abe92ce29 \
+ --hash=sha256:85bfa6a5413be0ee6c5c4a663668a2cad2cbecdee367630d097d7823041bdeec \
+ --hash=sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf \
+ --hash=sha256:93a5051fcf5fad72de73b96f07d30bc29665697fb8ecdfbc474f3452c78adcf4 \
+ --hash=sha256:962b9a917dd7ceacbe5cd424556914cb0d636001e393b43dc886ba31d2a1e449 \
+ --hash=sha256:98ba568e8ae26beb726aeea2273053c717641933836568c2a0278a84987b2a1a \
+ --hash=sha256:a3feefd5e95871872673b08636f96b61ebef62971eab044f5124fb4dea39919d \
+ --hash=sha256:b43c2b8a330a490daaef5a47ab114935002b13b3f9dc5da56d5322ff218eeadb \
+ --hash=sha256:b483c9d00a565633c87abd0aaf27eb5016de23fed952e054ecc19ce32f6a9e7e \
+ --hash=sha256:ba05430e819e58544e840a68b03b28b6d328aff2e41579037e8bab7653b37d83 \
+ --hash=sha256:ca5f18a75e1256ce07494e245cdb146f5a9267d3c702ebf9b65c7f8bd843431e \
+ --hash=sha256:d5ca078bb666c4a9d1287a379fe617a6dccd18c3e8a7e6c7e1eb8974330c626a \
+ --hash=sha256:da1a90c1ddb7531b1d5ff1e171b4ee61f6345119be7351104b67ff413843fe94 \
+ --hash=sha256:dba70f30fd81f8ce6d32ddeef37d91c8948e5d5a4c63242d16a2b2df8143aafc \
+ --hash=sha256:dd33eb9bdcfbabab3459c9ee651d94c842bc8a05fabc95edf4ee0c15a072495e \
+ --hash=sha256:e0538c43565ee6e703d3a7c3bdfe4037a5209250e8502c98f20fea6f5fdf2965 \
+ --hash=sha256:e1f54b9b4b6c53369f40028d2dd07a8c374583417ee6ec0ea304e710a20f80a0 \
+ --hash=sha256:e32d2a2b02ccbef10145df9135751abea1f9f076e67a4e261b05f24b94219e36 \
+ --hash=sha256:e71255ba42567d34a13c03968736c5d39bb4a97ce98188fafb27ce981115beec \
+ --hash=sha256:ed2e07c6a26ed4bea91b897ee2b0835c21716d9a469a96c3e878dc5f8c55bb23 \
+ --hash=sha256:eef2afb0fd1747f33f1ee3e209bce1ed582d1896b240ccc5e2697e3275f037c7 \
+ --hash=sha256:f23222527b307970e383433daec128d769ff778d9b29343fb3496472dc20dabe \
+ --hash=sha256:f341ee2df0999bfdf7a95e448075effe0db212a59387de1a70690e4acb03d4c6 \
+ --hash=sha256:f7f325be2804246a75a4f45c72d4ce80d2443ab815063cdf70ee8fb2ca59ee1b \
+ --hash=sha256:f8af619e3be812a2059b212064ea7a640aff0568d972cd1b9e920837469eb3cb \
+ --hash=sha256:fa8c626d6441e2d04b6ee703ef2d1e17608ad44c7cb75258c09dd42bacdfc64b \
+ --hash=sha256:fbb9dc00e39f3e6c0ef48edee202f9520dafb233e8b51b06b8428cfcb92abd30 \
+ --hash=sha256:fff55f3ce50a3ff63ec8e2a8d3dd924f1941b250b0aac3d3d42b687eeff07a8e
+ # via black
+tomli==1.2.2 \
+ --hash=sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee \
+ --hash=sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade
+ # via black
+typed-ast==1.5.2 \
+ --hash=sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e \
+ --hash=sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344 \
+ --hash=sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266 \
+ --hash=sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a \
+ --hash=sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd \
+ --hash=sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d \
+ --hash=sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837 \
+ --hash=sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098 \
+ --hash=sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e \
+ --hash=sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27 \
+ --hash=sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b \
+ --hash=sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596 \
+ --hash=sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76 \
+ --hash=sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30 \
+ --hash=sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4 \
+ --hash=sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78 \
+ --hash=sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca \
+ --hash=sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985 \
+ --hash=sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb \
+ --hash=sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88 \
+ --hash=sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7 \
+ --hash=sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5 \
+ --hash=sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e \
+ --hash=sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7
+ # via black
+typing-extensions==3.10.0.2 \
+ --hash=sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e \
+ --hash=sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7 \
+ --hash=sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34
+ # via
+ # -r tools/lint/python/black_requirements.in
+ # black
diff --git a/tools/lint/python/flake8.py b/tools/lint/python/flake8.py
new file mode 100644
index 0000000000..88fec87822
--- /dev/null
+++ b/tools/lint/python/flake8.py
@@ -0,0 +1,215 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import json
+import os
+import platform
+import subprocess
+import sys
+
+import mozfile
+import mozpack.path as mozpath
+from mozlint import result
+from mozlint.pathutils import expand_exclusions
+
+here = os.path.abspath(os.path.dirname(__file__))
+FLAKE8_REQUIREMENTS_PATH = os.path.join(here, "flake8_requirements.txt")
+
+FLAKE8_NOT_FOUND = """
+Could not find flake8! Install flake8 and try again.
+
+ $ pip install -U --require-hashes -r {}
+""".strip().format(
+ FLAKE8_REQUIREMENTS_PATH
+)
+
+
+FLAKE8_INSTALL_ERROR = """
+Unable to install correct version of flake8
+Try to install it manually with:
+ $ pip install -U --require-hashes -r {}
+""".strip().format(
+ FLAKE8_REQUIREMENTS_PATH
+)
+
+LINE_OFFSETS = {
+ # continuation line under-indented for hanging indent
+ "E121": (-1, 2),
+ # continuation line missing indentation or outdented
+ "E122": (-1, 2),
+ # continuation line over-indented for hanging indent
+ "E126": (-1, 2),
+ # continuation line over-indented for visual indent
+ "E127": (-1, 2),
+ # continuation line under-indented for visual indent
+ "E128": (-1, 2),
+ # continuation line unaligned for hanging indend
+ "E131": (-1, 2),
+ # expected 1 blank line, found 0
+ "E301": (-1, 2),
+ # expected 2 blank lines, found 1
+ "E302": (-2, 3),
+}
+"""Maps a flake8 error to a lineoffset tuple.
+
+The offset is of the form (lineno_offset, num_lines) and is passed
+to the lineoffset property of an `Issue`.
+"""
+
+
+def default_bindir():
+ # We use sys.prefix to find executables as that gets modified with
+ # virtualenv's activate_this.py, whereas sys.executable doesn't.
+ if platform.system() == "Windows":
+ return os.path.join(sys.prefix, "Scripts")
+ else:
+ return os.path.join(sys.prefix, "bin")
+
+
+class NothingToLint(Exception):
+ """Exception used to bail out of flake8's internals if all the specified
+ files were excluded.
+ """
+
+
+def setup(root, **lintargs):
+ virtualenv_manager = lintargs["virtualenv_manager"]
+ try:
+ virtualenv_manager.install_pip_requirements(
+ FLAKE8_REQUIREMENTS_PATH, quiet=True
+ )
+ except subprocess.CalledProcessError:
+ print(FLAKE8_INSTALL_ERROR)
+ return 1
+
+
+def lint(paths, config, **lintargs):
+
+ root = lintargs["root"]
+ virtualenv_bin_path = lintargs.get("virtualenv_bin_path")
+ config_path = os.path.join(root, ".flake8")
+
+ results = run(paths, config, **lintargs)
+ fixed = 0
+
+ if lintargs.get("fix"):
+ # fix and run again to count remaining issues
+ fixed = len(results)
+ fix_cmd = [
+ os.path.join(virtualenv_bin_path or default_bindir(), "autopep8"),
+ "--global-config",
+ config_path,
+ "--in-place",
+ "--recursive",
+ ]
+
+ if config.get("exclude"):
+ fix_cmd.extend(["--exclude", ",".join(config["exclude"])])
+
+ subprocess.call(fix_cmd + paths)
+
+ results = run(paths, config, **lintargs)
+
+ fixed = fixed - len(results)
+
+ return {"results": results, "fixed": fixed}
+
+
+def run(paths, config, **lintargs):
+ from flake8 import __version__ as flake8_version
+ from flake8.main.application import Application
+
+ log = lintargs["log"]
+ root = lintargs["root"]
+ config_path = os.path.join(root, ".flake8")
+
+ # Run flake8.
+ app = Application()
+ log.debug("flake8 version={}".format(flake8_version))
+
+ output_file = mozfile.NamedTemporaryFile(mode="r")
+ flake8_cmd = [
+ "--config",
+ config_path,
+ "--output-file",
+ output_file.name,
+ "--format",
+ '{"path":"%(path)s","lineno":%(row)s,'
+ '"column":%(col)s,"rule":"%(code)s","message":"%(text)s"}',
+ "--filename",
+ ",".join(["*.{}".format(e) for e in config["extensions"]]),
+ ]
+ log.debug("Command: {}".format(" ".join(flake8_cmd)))
+
+ orig_make_file_checker_manager = app.make_file_checker_manager
+
+ def wrap_make_file_checker_manager(self):
+ """Flake8 is very inefficient when it comes to applying exclusion
+ rules, using `expand_exclusions` to turn directories into a list of
+ relevant python files is an order of magnitude faster.
+
+ Hooking into flake8 here also gives us a convenient place to merge the
+ `exclude` rules specified in the root .flake8 with the ones added by
+ tools/lint/mach_commands.py.
+ """
+ # Ignore exclude rules if `--no-filter` was passed in.
+ config.setdefault("exclude", [])
+ if lintargs.get("use_filters", True):
+ config["exclude"].extend(map(mozpath.normpath, self.options.exclude))
+
+ # Since we use the root .flake8 file to store exclusions, we haven't
+ # properly filtered the paths through mozlint's `filterpaths` function
+ # yet. This mimics that though there could be other edge cases that are
+ # different. Maybe we should call `filterpaths` directly, though for
+ # now that doesn't appear to be necessary.
+ filtered = [
+ p for p in paths if not any(p.startswith(e) for e in config["exclude"])
+ ]
+
+ self.options.filenames = self.options.filenames + list(
+ expand_exclusions(filtered, config, root)
+ )
+
+ if not self.options.filenames:
+ raise NothingToLint
+ return orig_make_file_checker_manager()
+
+ app.make_file_checker_manager = wrap_make_file_checker_manager.__get__(
+ app, Application
+ )
+
+ # Make sure to run from repository root so exclusions are joined to the
+ # repository root and not the current working directory.
+ oldcwd = os.getcwd()
+ os.chdir(root)
+ try:
+ app.run(flake8_cmd)
+ except NothingToLint:
+ pass
+ finally:
+ os.chdir(oldcwd)
+
+ results = []
+
+ WARNING_RULES = set(config.get("warning-rules", []))
+
+ def process_line(line):
+ # Escape slashes otherwise JSON conversion will not work
+ line = line.replace("\\", "\\\\")
+ try:
+ res = json.loads(line)
+ except ValueError:
+ print("Non JSON output from linter, will not be processed: {}".format(line))
+ return
+
+ if res.get("code") in LINE_OFFSETS:
+ res["lineoffset"] = LINE_OFFSETS[res["code"]]
+
+ if res["rule"] in WARNING_RULES:
+ res["level"] = "warning"
+
+ results.append(result.from_config(config, **res))
+
+ list(map(process_line, output_file.readlines()))
+ return results
diff --git a/tools/lint/python/flake8_requirements.in b/tools/lint/python/flake8_requirements.in
new file mode 100644
index 0000000000..0a9262b9c6
--- /dev/null
+++ b/tools/lint/python/flake8_requirements.in
@@ -0,0 +1,4 @@
+flake8==5.0.4
+zipp==0.5
+autopep8==1.7.0
+typing-extensions==3.10.0.2
diff --git a/tools/lint/python/flake8_requirements.txt b/tools/lint/python/flake8_requirements.txt
new file mode 100644
index 0000000000..36879d20c8
--- /dev/null
+++ b/tools/lint/python/flake8_requirements.txt
@@ -0,0 +1,41 @@
+#
+# This file is autogenerated by pip-compile with python 3.10
+# To update, run:
+#
+# pip-compile --generate-hashes tools/lint/python/flake8_requirements.in
+#
+autopep8==1.7.0 \
+ --hash=sha256:6f09e90a2be784317e84dc1add17ebfc7abe3924239957a37e5040e27d812087 \
+ --hash=sha256:ca9b1a83e53a7fad65d731dc7a2a2d50aa48f43850407c59f6a1a306c4201142
+ # via -r tools/lint/python/flake8_requirements.in
+flake8==5.0.4 \
+ --hash=sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db \
+ --hash=sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248
+ # via -r tools/lint/python/flake8_requirements.in
+mccabe==0.7.0 \
+ --hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \
+ --hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e
+ # via flake8
+pycodestyle==2.9.1 \
+ --hash=sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785 \
+ --hash=sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b
+ # via
+ # autopep8
+ # flake8
+pyflakes==2.5.0 \
+ --hash=sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2 \
+ --hash=sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3
+ # via flake8
+toml==0.10.2 \
+ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
+ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f
+ # via autopep8
+typing-extensions==3.10.0.2 \
+ --hash=sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e \
+ --hash=sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7 \
+ --hash=sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34
+ # via -r tools/lint/python/flake8_requirements.in
+zipp==0.5 \
+ --hash=sha256:46dfd547d9ccbf8bdc26ecea52818046bb28509f12bb6a0de1cd66ab06e9a9be \
+ --hash=sha256:d7ac25f895fb65bff937b381353c14eb1fa23d35f40abd72a5342cd57eb57fd1
+ # via -r tools/lint/python/flake8_requirements.in
diff --git a/tools/lint/python/isort.py b/tools/lint/python/isort.py
new file mode 100644
index 0000000000..9af0e42d10
--- /dev/null
+++ b/tools/lint/python/isort.py
@@ -0,0 +1,138 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import configparser
+import os
+import platform
+import re
+import signal
+import subprocess
+import sys
+
+import mozpack.path as mozpath
+from mozlint import result
+from mozlint.pathutils import expand_exclusions
+from mozprocess import ProcessHandler
+
+here = os.path.abspath(os.path.dirname(__file__))
+ISORT_REQUIREMENTS_PATH = os.path.join(here, "isort_requirements.txt")
+
+ISORT_INSTALL_ERROR = """
+Unable to install correct version of isort
+Try to install it manually with:
+ $ pip install -U --require-hashes -r {}
+""".strip().format(
+ ISORT_REQUIREMENTS_PATH
+)
+
+
+def default_bindir():
+ # We use sys.prefix to find executables as that gets modified with
+ # virtualenv's activate_this.py, whereas sys.executable doesn't.
+ if platform.system() == "Windows":
+ return os.path.join(sys.prefix, "Scripts")
+ return os.path.join(sys.prefix, "bin")
+
+
+def parse_issues(config, output, *, log):
+ would_sort = re.compile(
+ "^ERROR: (.*?) Imports are incorrectly sorted and/or formatted.$", re.I
+ )
+ sorted = re.compile("^Fixing (.*)$", re.I)
+ results = []
+ for line in output:
+ line = line.decode("utf-8")
+
+ match = would_sort.match(line)
+ if match:
+ res = {"path": match.group(1)}
+ results.append(result.from_config(config, **res))
+ continue
+
+ match = sorted.match(line)
+ if match:
+ res = {"path": match.group(1), "message": "sorted"}
+ results.append(result.from_config(config, **res))
+ continue
+
+ log.debug("Unhandled line", line)
+ return results
+
+
+class IsortProcess(ProcessHandler):
+ def __init__(self, config, *args, **kwargs):
+ self.config = config
+ kwargs["stream"] = False
+ ProcessHandler.__init__(self, *args, **kwargs)
+
+ def run(self, *args, **kwargs):
+ orig = signal.signal(signal.SIGINT, signal.SIG_IGN)
+ ProcessHandler.run(self, *args, **kwargs)
+ signal.signal(signal.SIGINT, orig)
+
+
+def run_process(config, cmd):
+ proc = IsortProcess(config, cmd)
+ proc.run()
+ try:
+ proc.wait()
+ except KeyboardInterrupt:
+ proc.kill()
+
+ return proc.output
+
+
+def setup(root, **lintargs):
+ virtualenv_manager = lintargs["virtualenv_manager"]
+ try:
+ virtualenv_manager.install_pip_requirements(ISORT_REQUIREMENTS_PATH, quiet=True)
+ except subprocess.CalledProcessError:
+ print(ISORT_INSTALL_ERROR)
+ return 1
+
+
+def lint(paths, config, **lintargs):
+ from isort import __version__ as isort_version
+
+ binary = os.path.join(
+ lintargs.get("virtualenv_bin_path") or default_bindir(), "isort"
+ )
+
+ log = lintargs["log"]
+ root = lintargs["root"]
+
+ log.debug("isort version {}".format(isort_version))
+
+ cmd_args = [
+ binary,
+ "--resolve-all-configs",
+ "--config-root",
+ root,
+ ]
+ if not lintargs.get("fix"):
+ cmd_args.append("--check-only")
+
+ # We merge exclusion rules from .flake8 to avoid having to repeat the same exclusions twice.
+ flake8_config_path = os.path.join(root, ".flake8")
+ flake8_config = configparser.ConfigParser()
+ flake8_config.read(flake8_config_path)
+ config["exclude"].extend(
+ mozpath.normpath(p.strip())
+ for p in flake8_config.get("flake8", "exclude").split(",")
+ )
+
+ paths = list(expand_exclusions(paths, config, lintargs["root"]))
+ if len(paths) == 0:
+ return {"results": [], "fixed": 0}
+
+ base_command = cmd_args + paths
+ log.debug("Command: {}".format(" ".join(base_command)))
+
+ output = run_process(config, base_command)
+
+ results = parse_issues(config, output, log=log)
+
+ fixed = sum(1 for issue in results if issue.message == "sorted")
+
+ return {"results": results, "fixed": fixed}
diff --git a/tools/lint/python/isort_requirements.in b/tools/lint/python/isort_requirements.in
new file mode 100644
index 0000000000..8eeb146b1a
--- /dev/null
+++ b/tools/lint/python/isort_requirements.in
@@ -0,0 +1 @@
+isort==5.10.1
diff --git a/tools/lint/python/isort_requirements.txt b/tools/lint/python/isort_requirements.txt
new file mode 100644
index 0000000000..84df6d2093
--- /dev/null
+++ b/tools/lint/python/isort_requirements.txt
@@ -0,0 +1,10 @@
+#
+# This file is autogenerated by pip-compile with python 3.10
+# To update, run:
+#
+# pip-compile --generate-hashes --output-file=tools/lint/python/isort_requirements.txt tools/lint/python/isort_requirements.in
+#
+isort==5.10.1 \
+ --hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
+ --hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951
+ # via -r tools/lint/python/isort_requirements.in
diff --git a/tools/lint/python/l10n_lint.py b/tools/lint/python/l10n_lint.py
new file mode 100644
index 0000000000..ef3269ef2a
--- /dev/null
+++ b/tools/lint/python/l10n_lint.py
@@ -0,0 +1,171 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+from datetime import datetime, timedelta
+
+import mozversioncontrol.repoupdate
+from compare_locales import parser
+from compare_locales.lint.linter import L10nLinter
+from compare_locales.lint.util import l10n_base_reference_and_tests
+from compare_locales.paths import ProjectFiles, TOMLParser
+from mach import util as mach_util
+from mozlint import pathutils, result
+from mozpack import path as mozpath
+
+LOCALE = "gecko-strings"
+STRINGS_REPO = "https://hg.mozilla.org/l10n/gecko-strings"
+
+PULL_AFTER = timedelta(days=2)
+
+# Wrapper to call lint_strings with mozilla-central configuration
+# comm-central defines its own wrapper since comm-central strings are
+# in separate repositories
+def lint(paths, lintconfig, **lintargs):
+ return lint_strings(LOCALE, paths, lintconfig, **lintargs)
+
+
+def lint_strings(locale, paths, lintconfig, **lintargs):
+ l10n_base = mach_util.get_state_dir()
+ root = lintargs["root"]
+ exclude = lintconfig.get("exclude")
+ extensions = lintconfig.get("extensions")
+
+ # Load l10n.toml configs
+ l10nconfigs = load_configs(lintconfig, root, l10n_base, locale)
+
+ # Check include paths in l10n.yml if it's in our given paths
+ # Only the l10n.yml will show up here, but if the l10n.toml files
+ # change, we also get the l10n.yml as the toml files are listed as
+ # support files.
+ if lintconfig["path"] in paths:
+ results = validate_linter_includes(lintconfig, l10nconfigs, lintargs)
+ paths.remove(lintconfig["path"])
+ else:
+ results = []
+
+ all_files = []
+ for p in paths:
+ fp = pathutils.FilterPath(p)
+ if fp.isdir:
+ for _, fileobj in fp.finder:
+ all_files.append(fileobj.path)
+ if fp.isfile:
+ all_files.append(p)
+ # Filter again, our directories might have picked up files the
+ # explicitly excluded in the l10n.yml configuration.
+ # `browser/locales/en-US/firefox-l10n.js` is a good example.
+ all_files, _ = pathutils.filterpaths(
+ lintargs["root"],
+ all_files,
+ lintconfig["include"],
+ exclude=exclude,
+ extensions=extensions,
+ )
+ # These should be excluded in l10n.yml
+ skips = {p for p in all_files if not parser.hasParser(p)}
+ results.extend(
+ result.from_config(
+ lintconfig,
+ level="warning",
+ path=path,
+ message="file format not supported in compare-locales",
+ )
+ for path in skips
+ )
+ all_files = [p for p in all_files if p not in skips]
+ files = ProjectFiles(locale, l10nconfigs)
+
+ get_reference_and_tests = l10n_base_reference_and_tests(files)
+
+ linter = MozL10nLinter(lintconfig)
+ results += linter.lint(all_files, get_reference_and_tests)
+ return results
+
+
+# Similar to the lint/lint_strings wrapper setup, for comm-central support.
+def gecko_strings_setup(**lint_args):
+ return strings_repo_setup(STRINGS_REPO, LOCALE)
+
+
+def strings_repo_setup(repo, locale):
+ gs = mozpath.join(mach_util.get_state_dir(), locale)
+ marker = mozpath.join(gs, ".hg", "l10n_pull_marker")
+ try:
+ last_pull = datetime.fromtimestamp(os.stat(marker).st_mtime)
+ skip_clone = datetime.now() < last_pull + PULL_AFTER
+ except OSError:
+ skip_clone = False
+ if skip_clone:
+ return
+ try:
+ hg = mozversioncontrol.get_tool_path("hg")
+ except mozversioncontrol.MissingVCSTool:
+ if os.environ.get("MOZ_AUTOMATION"):
+ raise
+ print("warning: l10n linter requires Mercurial but was unable to find 'hg'")
+ return 1
+ mozversioncontrol.repoupdate.update_mercurial_repo(hg, repo, gs)
+ with open(marker, "w") as fh:
+ fh.flush()
+
+
+def load_configs(lintconfig, root, l10n_base, locale):
+ """Load l10n configuration files specified in the linter configuration."""
+ configs = []
+ env = {"l10n_base": l10n_base}
+ for toml in lintconfig["l10n_configs"]:
+ cfg = TOMLParser().parse(
+ mozpath.join(root, toml), env=env, ignore_missing_includes=True
+ )
+ cfg.set_locales([locale], deep=True)
+ configs.append(cfg)
+ return configs
+
+
+def validate_linter_includes(lintconfig, l10nconfigs, lintargs):
+ """Check l10n.yml config against l10n.toml configs."""
+ reference_paths = set(
+ mozpath.relpath(p["reference"].prefix, lintargs["root"])
+ for project in l10nconfigs
+ for config in project.configs
+ for p in config.paths
+ )
+ # Just check for directories
+ reference_dirs = sorted(p for p in reference_paths if os.path.isdir(p))
+ missing_in_yml = [
+ refd for refd in reference_dirs if refd not in lintconfig["include"]
+ ]
+ # These might be subdirectories in the config, though
+ missing_in_yml = [
+ d
+ for d in missing_in_yml
+ if not any(d.startswith(parent + "/") for parent in lintconfig["include"])
+ ]
+ if missing_in_yml:
+ dirs = ", ".join(missing_in_yml)
+ return [
+ result.from_config(
+ lintconfig,
+ path=lintconfig["path"],
+ message="l10n.yml out of sync with l10n.toml, add: " + dirs,
+ )
+ ]
+ return []
+
+
+class MozL10nLinter(L10nLinter):
+ """Subclass linter to generate the right result type."""
+
+ def __init__(self, lintconfig):
+ super(MozL10nLinter, self).__init__()
+ self.lintconfig = lintconfig
+
+ def lint(self, files, get_reference_and_tests):
+ return [
+ result.from_config(self.lintconfig, **result_data)
+ for result_data in super(MozL10nLinter, self).lint(
+ files, get_reference_and_tests
+ )
+ ]
diff --git a/tools/lint/python/pylint.py b/tools/lint/python/pylint.py
new file mode 100644
index 0000000000..8bb0c68b87
--- /dev/null
+++ b/tools/lint/python/pylint.py
@@ -0,0 +1,133 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import json
+import os
+import signal
+import subprocess
+
+from mach.site import InstallPipRequirementsException
+from mozlint import result
+from mozlint.pathutils import expand_exclusions
+from mozprocess import ProcessHandler
+
+here = os.path.abspath(os.path.dirname(__file__))
+PYLINT_REQUIREMENTS_PATH = os.path.join(here, "pylint_requirements.txt")
+
+PYLINT_NOT_FOUND = """
+Could not find pylint! Install pylint and try again.
+
+ $ pip install -U --require-hashes -r {}
+""".strip().format(
+ PYLINT_REQUIREMENTS_PATH
+)
+
+
+PYLINT_INSTALL_ERROR = """
+Unable to install correct version of pylint
+Try to install it manually with:
+ $ pip install -U --require-hashes -r {}
+""".strip().format(
+ PYLINT_REQUIREMENTS_PATH
+)
+
+
+class PylintProcess(ProcessHandler):
+ def __init__(self, config, *args, **kwargs):
+ self.config = config
+ kwargs["stream"] = False
+ kwargs["universal_newlines"] = True
+ ProcessHandler.__init__(self, *args, **kwargs)
+
+ def run(self, *args, **kwargs):
+ orig = signal.signal(signal.SIGINT, signal.SIG_IGN)
+ ProcessHandler.run(self, *args, **kwargs)
+ signal.signal(signal.SIGINT, orig)
+
+
+def setup(root, **lintargs):
+ virtualenv_manager = lintargs["virtualenv_manager"]
+ try:
+ virtualenv_manager.install_pip_requirements(
+ PYLINT_REQUIREMENTS_PATH,
+ quiet=True,
+ )
+ except (subprocess.CalledProcessError, InstallPipRequirementsException):
+ print(PYLINT_INSTALL_ERROR)
+ return 1
+
+
+def get_pylint_binary():
+ return "pylint"
+
+
+def run_process(config, cmd):
+ proc = PylintProcess(config, cmd)
+ proc.run()
+ try:
+ proc.wait()
+ except KeyboardInterrupt:
+ proc.kill()
+
+ return proc.output
+
+
+def parse_issues(log, config, issues_json, path):
+ results = []
+
+ try:
+ issues = json.loads(issues_json)
+ except json.decoder.JSONDecodeError:
+ log.debug("Could not parse the output:")
+ log.debug("pylint output: {}".format(issues_json))
+ return []
+
+ for issue in issues:
+ res = {
+ "path": issue["path"],
+ "level": issue["type"],
+ "lineno": issue["line"],
+ "column": issue["column"],
+ "message": issue["message"],
+ "rule": issue["message-id"],
+ }
+ results.append(result.from_config(config, **res))
+ return results
+
+
+def get_pylint_version(binary):
+ return subprocess.check_output(
+ [binary, "--version"],
+ universal_newlines=True,
+ stderr=subprocess.STDOUT,
+ )
+
+
+def lint(paths, config, **lintargs):
+ log = lintargs["log"]
+
+ binary = get_pylint_binary()
+
+ log = lintargs["log"]
+ paths = list(expand_exclusions(paths, config, lintargs["root"]))
+
+ cmd_args = [binary]
+ results = []
+
+ # list from https://code.visualstudio.com/docs/python/linting#_pylint
+ # And ignore a bit more elements
+ cmd_args += [
+ "-fjson",
+ "--disable=all",
+ "--enable=F,E,unreachable,duplicate-key,unnecessary-semicolon,global-variable-not-assigned,unused-variable,binary-op-exception,bad-format-string,anomalous-backslash-in-string,bad-open-mode,no-else-return", # NOQA: E501
+ "--disable=import-error,no-member",
+ ]
+
+ base_command = cmd_args + paths
+ log.debug("Command: {}".format(" ".join(cmd_args)))
+ log.debug("pylint version: {}".format(get_pylint_version(binary)))
+ output = " ".join(run_process(config, base_command))
+ results = parse_issues(log, config, str(output), [])
+
+ return results
diff --git a/tools/lint/python/pylint_requirements.in b/tools/lint/python/pylint_requirements.in
new file mode 100644
index 0000000000..c584cdf2a6
--- /dev/null
+++ b/tools/lint/python/pylint_requirements.in
@@ -0,0 +1,5 @@
+pylint==2.15.8
+dill==0.3.4
+tomli==1.2.2
+typing-extensions==3.10.0.2
+tomlkit==0.10.1
diff --git a/tools/lint/python/pylint_requirements.txt b/tools/lint/python/pylint_requirements.txt
new file mode 100644
index 0000000000..093b1ce402
--- /dev/null
+++ b/tools/lint/python/pylint_requirements.txt
@@ -0,0 +1,136 @@
+#
+# This file is autogenerated by pip-compile with python 3.10
+# To update, run:
+#
+# pip-compile --generate-hashes tools/lint/python/pylint_requirements.in
+#
+astroid==2.12.13 \
+ --hash=sha256:10e0ad5f7b79c435179d0d0f0df69998c4eef4597534aae44910db060baeb907 \
+ --hash=sha256:1493fe8bd3dfd73dc35bd53c9d5b6e49ead98497c47b2307662556a5692d29d7
+ # via pylint
+dill==0.3.4 \
+ --hash=sha256:7e40e4a70304fd9ceab3535d36e58791d9c4a776b38ec7f7ec9afc8d3dca4d4f \
+ --hash=sha256:9f9734205146b2b353ab3fec9af0070237b6ddae78452af83d2fca84d739e675
+ # via
+ # -r tools/lint/python/pylint_requirements.in
+ # pylint
+isort==5.11.2 \
+ --hash=sha256:dd8bbc5c0990f2a095d754e50360915f73b4c26fc82733eb5bfc6b48396af4d2 \
+ --hash=sha256:e486966fba83f25b8045f8dd7455b0a0d1e4de481e1d7ce4669902d9fb85e622
+ # via pylint
+lazy-object-proxy==1.8.0 \
+ --hash=sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada \
+ --hash=sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d \
+ --hash=sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7 \
+ --hash=sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe \
+ --hash=sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd \
+ --hash=sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c \
+ --hash=sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858 \
+ --hash=sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288 \
+ --hash=sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec \
+ --hash=sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f \
+ --hash=sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891 \
+ --hash=sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c \
+ --hash=sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25 \
+ --hash=sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156 \
+ --hash=sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8 \
+ --hash=sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f \
+ --hash=sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e \
+ --hash=sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0 \
+ --hash=sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b
+ # via astroid
+mccabe==0.7.0 \
+ --hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \
+ --hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e
+ # via pylint
+platformdirs==2.6.0 \
+ --hash=sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca \
+ --hash=sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e
+ # via pylint
+pylint==2.15.8 \
+ --hash=sha256:ea82cd6a1e11062dc86d555d07c021b0fb65afe39becbe6fe692efd6c4a67443 \
+ --hash=sha256:ec4a87c33da054ab86a6c79afa6771dc8765cb5631620053e727fcf3ef8cbed7
+ # via -r tools/lint/python/pylint_requirements.in
+tomli==1.2.2 \
+ --hash=sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee \
+ --hash=sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade
+ # via
+ # -r tools/lint/python/pylint_requirements.in
+ # pylint
+tomlkit==0.10.1 \
+ --hash=sha256:3c517894eadef53e9072d343d37e4427b8f0b6200a70b7c9a19b2ebd1f53b951 \
+ --hash=sha256:3eba517439dcb2f84cf39f4f85fd2c3398309823a3c75ac3e73003638daf7915
+ # via
+ # -r tools/lint/python/pylint_requirements.in
+ # pylint
+typing-extensions==3.10.0.2 \
+ --hash=sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e \
+ --hash=sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7 \
+ --hash=sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34
+ # via -r tools/lint/python/pylint_requirements.in
+wrapt==1.14.1 \
+ --hash=sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3 \
+ --hash=sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b \
+ --hash=sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4 \
+ --hash=sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2 \
+ --hash=sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656 \
+ --hash=sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3 \
+ --hash=sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff \
+ --hash=sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310 \
+ --hash=sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a \
+ --hash=sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57 \
+ --hash=sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069 \
+ --hash=sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383 \
+ --hash=sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe \
+ --hash=sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87 \
+ --hash=sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d \
+ --hash=sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b \
+ --hash=sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907 \
+ --hash=sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f \
+ --hash=sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0 \
+ --hash=sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28 \
+ --hash=sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1 \
+ --hash=sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853 \
+ --hash=sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc \
+ --hash=sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3 \
+ --hash=sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3 \
+ --hash=sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164 \
+ --hash=sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1 \
+ --hash=sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c \
+ --hash=sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1 \
+ --hash=sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7 \
+ --hash=sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1 \
+ --hash=sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320 \
+ --hash=sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed \
+ --hash=sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1 \
+ --hash=sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248 \
+ --hash=sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c \
+ --hash=sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456 \
+ --hash=sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77 \
+ --hash=sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef \
+ --hash=sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1 \
+ --hash=sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7 \
+ --hash=sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86 \
+ --hash=sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4 \
+ --hash=sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d \
+ --hash=sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d \
+ --hash=sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8 \
+ --hash=sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5 \
+ --hash=sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471 \
+ --hash=sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00 \
+ --hash=sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68 \
+ --hash=sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3 \
+ --hash=sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d \
+ --hash=sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735 \
+ --hash=sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d \
+ --hash=sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569 \
+ --hash=sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7 \
+ --hash=sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59 \
+ --hash=sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5 \
+ --hash=sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb \
+ --hash=sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b \
+ --hash=sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f \
+ --hash=sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462 \
+ --hash=sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015 \
+ --hash=sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af
+ # via astroid