summaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/black_linter/wrapper.py104
-rw-r--r--tools/black_linter/ya.make13
-rw-r--r--tools/cpp_style_checker/wrapper.py (renamed from tools/cpp_style_checker/__main__.py)16
-rw-r--r--tools/cpp_style_checker/ya.make16
-rw-r--r--tools/enum_parser/enum_parser/main.cpp4
-rw-r--r--tools/enum_parser/enum_parser/ya.make3
-rw-r--r--tools/enum_parser/enum_serialization_runtime/README.md2
-rw-r--r--tools/enum_parser/enum_serialization_runtime/enum_runtime.h12
-rw-r--r--tools/enum_parser/parse_enum/parse_enum_ut.cpp3
-rw-r--r--tools/enum_parser/parse_enum/ut/digit_separator.h3
-rw-r--r--tools/flake8_linter/__main__.py193
-rw-r--r--tools/flake8_linter/ya.make17
-rwxr-xr-xtools/py2cc/__main__.py19
-rw-r--r--tools/py2cc/bin/ya.make29
-rw-r--r--tools/py2cc/stage0pycc/main.cpp60
-rw-r--r--tools/py2cc/stage0pycc/ya.make11
-rw-r--r--tools/py2cc/ya.make11
-rw-r--r--tools/py3cc/bin/ya.make45
-rw-r--r--tools/py3cc/main.cpp83
-rw-r--r--tools/py3cc/slow/bin/ya.make28
-rwxr-xr-xtools/py3cc/slow/main.py (renamed from tools/py3cc/main.py)0
-rw-r--r--tools/py3cc/slow/ya.make11
-rw-r--r--tools/py3cc/ya.make3
-rw-r--r--tools/ruff_linter/wrapper.py157
24 files changed, 758 insertions, 85 deletions
diff --git a/tools/black_linter/wrapper.py b/tools/black_linter/wrapper.py
new file mode 100644
index 00000000000..6f11136d356
--- /dev/null
+++ b/tools/black_linter/wrapper.py
@@ -0,0 +1,104 @@
+import logging
+import os
+import subprocess
+import sys
+import time
+from pathlib import Path
+
+from build.plugins.lib.test_const import BLACK_RESOURCE
+from library.python.testing.custom_linter_util import linter_params, reporter
+from library.python.testing.style import rules
+
+logger = logging.getLogger(__name__)
+
+SNIPPET_LINES_LIMIT = 20
+
+
+def get_black_bin(params) -> str:
+ black_root = params.global_resources[BLACK_RESOURCE]
+ return os.path.join(black_root, 'black')
+
+
+def run_black(black_bin, filename, args):
+ cmd = [black_bin, filename, *args]
+
+ res = subprocess.run(
+ cmd,
+ capture_output=True,
+ encoding='utf8',
+ errors='replace',
+ )
+
+ return res.returncode, res.stdout if res.returncode else ''
+
+
+def run_black_safe(black_bin, filename, args):
+ try:
+ return run_black(black_bin, filename, args)
+ except Exception:
+ # fast mode failed - retry
+ return run_black(black_bin, filename, args + ['--fast'])
+
+
+def process_file(black_bin, filename, config):
+ logger.debug("Check %s", filename)
+ args = ['--quiet', '--check', '--config', config]
+
+ # Fast path for runs with fix_style option or without errors.
+ rc, out = run_black_safe(black_bin, filename, args)
+ if rc == 1:
+ # black runs 15x+ slower if diff is requested, even for files w/o actual diff.
+ # Rerun black in case of found error.
+ rc, out = run_black_safe(black_bin, filename, args + ['--diff'])
+
+ if out:
+ sys.stdout.write(out)
+ lines = out.splitlines(keepends=True)
+ # strip diff header with "+++" "---" lines
+ lines = lines[2:]
+ if len(lines) > SNIPPET_LINES_LIMIT:
+ lines = lines[:SNIPPET_LINES_LIMIT]
+ lines += ["[[rst]]..[truncated].. see full diff in the stdout file in the logsdir"]
+ out = ''.join(lines)
+
+ return out
+
+
+def main():
+ params = linter_params.get_params()
+
+ black_bin = get_black_bin(params)
+ style_config_path = Path(params.source_root, params.configs[0])
+
+ report = reporter.LintReport()
+ for file_name in params.files:
+ start_time = time.perf_counter()
+
+ skip_reason = rules.get_skip_reason(file_name, Path(file_name).read_text(), skip_links=False)
+ if skip_reason:
+ elapsed = time.perf_counter() - start_time
+ report.add(
+ file_name,
+ reporter.LintStatus.SKIPPED,
+ f"Style check is omitted: {skip_reason}",
+ elapsed=elapsed,
+ )
+ continue
+
+ error = process_file(black_bin, file_name, style_config_path)
+ elapsed = time.perf_counter() - start_time
+
+ if error:
+ rel_file_name = os.path.relpath(file_name, params.source_root)
+ message = "Run [[imp]]ya style {}[[rst]] to fix format\n".format(rel_file_name) + error
+ status = reporter.LintStatus.FAIL
+ else:
+ message = ""
+ status = reporter.LintStatus.GOOD
+ report.add(file_name, status, message, elapsed=elapsed)
+
+ report.dump(params.report_file)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/black_linter/ya.make b/tools/black_linter/ya.make
deleted file mode 100644
index ed2d6421328..00000000000
--- a/tools/black_linter/ya.make
+++ /dev/null
@@ -1,13 +0,0 @@
-IF (USE_PREBUILT_TOOLS OR OPENSOURCE)
- INCLUDE(${ARCADIA_ROOT}/build/prebuilt/tools/black_linter/ya.make.prebuilt)
-ENDIF()
-
-IF (NOT PREBUILT)
- INCLUDE(${ARCADIA_ROOT}/tools/black_linter/bin/ya.make)
-ENDIF()
-
-IF (NOT OPENSOURCE)
- RECURSE(
- bin
- )
-ENDIF()
diff --git a/tools/cpp_style_checker/__main__.py b/tools/cpp_style_checker/wrapper.py
index 4ca1bc3a0f9..fe09bc6cb3c 100644
--- a/tools/cpp_style_checker/__main__.py
+++ b/tools/cpp_style_checker/wrapper.py
@@ -1,9 +1,7 @@
import difflib
-import json
import os
import subprocess
import time
-import yaml
from pathlib import PurePath
from build.plugins.lib.test_const import CLANG_FORMAT_RESOURCE
@@ -37,21 +35,17 @@ def main():
style_config_path = params.configs[0]
- with open(style_config_path) as f:
- style_config = yaml.safe_load(f)
- style_config_json = json.dumps(style_config)
-
report = reporter.LintReport()
for file_name in params.files:
- start_time = time.time()
- status, message = check_file(clang_format_binary, style_config_json, file_name)
- elapsed = time.time() - start_time
+ start_time = time.perf_counter()
+ status, message = check_file(clang_format_binary, style_config_path, file_name)
+ elapsed = time.perf_counter() - start_time
report.add(file_name, status, message, elapsed=elapsed)
report.dump(params.report_file)
-def check_file(clang_format_binary, style_config_json, filename):
+def check_file(clang_format_binary, style_config_path, filename):
with open(filename, "rb") as f:
actual_source = f.read()
@@ -59,7 +53,7 @@ def check_file(clang_format_binary, style_config_json, filename):
if skip_reason:
return reporter.LintStatus.SKIPPED, "Style check is omitted: {}".format(skip_reason)
- command = [clang_format_binary, '-assume-filename=' + filename, '-style=' + style_config_json]
+ command = [clang_format_binary, '-assume-filename=' + filename, '-style=file:' + style_config_path]
styled_source = subprocess.check_output(command, input=actual_source)
if styled_source == actual_source:
diff --git a/tools/cpp_style_checker/ya.make b/tools/cpp_style_checker/ya.make
deleted file mode 100644
index e762be1a1fc..00000000000
--- a/tools/cpp_style_checker/ya.make
+++ /dev/null
@@ -1,16 +0,0 @@
-PY3_PROGRAM()
-
-PEERDIR(
- build/plugins/lib/test_const
- contrib/python/PyYAML
- library/python/testing/custom_linter_util
- library/python/testing/style
-)
-
-PY_SRCS(
- __main__.py
-)
-
-STYLE_PYTHON()
-
-END()
diff --git a/tools/enum_parser/enum_parser/main.cpp b/tools/enum_parser/enum_parser/main.cpp
index 433a61a5db9..c87f76e98dc 100644
--- a/tools/enum_parser/enum_parser/main.cpp
+++ b/tools/enum_parser/enum_parser/main.cpp
@@ -269,8 +269,8 @@ void GenerateEnum(
out << " NAMES_INITIALIZATION_PAIRS,\n";
out << " VALUES_INITIALIZATION_PAIRS,\n";
out << " CPP_NAMES_INITIALIZATION_ARRAY,\n";
- out << " " << WrapStringBuf(outerScopeStr) << ",\n";
- out << " " << WrapStringBuf(name) << "\n";
+ out << " ::NEnumSerializationRuntime::CommonStringBuffer(" << WrapStringBuf(outerScopeStr) << ", " << WrapStringBuf(name) << ").first,\n";
+ out << " ::NEnumSerializationRuntime::CommonStringBuffer(" << WrapStringBuf(outerScopeStr) << ", " << WrapStringBuf(name) << ").second,\n";
out << " };\n\n";
// Properties
diff --git a/tools/enum_parser/enum_parser/ya.make b/tools/enum_parser/enum_parser/ya.make
index 1bd2356ff15..a0608e4e30a 100644
--- a/tools/enum_parser/enum_parser/ya.make
+++ b/tools/enum_parser/enum_parser/ya.make
@@ -1,5 +1,8 @@
PROGRAM(enum_parser)
+NO_PROFILE_RUNTIME()
+NO_CLANG_COVERAGE()
+
SRCS(
main.cpp
)
diff --git a/tools/enum_parser/enum_serialization_runtime/README.md b/tools/enum_parser/enum_serialization_runtime/README.md
index b8b4dd92cf6..05c30f3ec41 100644
--- a/tools/enum_parser/enum_serialization_runtime/README.md
+++ b/tools/enum_parser/enum_serialization_runtime/README.md
@@ -22,5 +22,5 @@ Use `GENERATE_ENUM_SERIALIZATION_WITH_HEADER` and `GENERATE_ENUM_SERIALIZATION`
3) как следствие, он меньше засоряет во время исполнения кеш инструкций процессора одинаковыми или очень похожими специализациями функций.
-Преобразование между `enum` и `int` выносится в пользовательский код (в шаблонные `inline` функции), и производится только в момент непосредственного использования (первым действием в семействе функций `ToString`, последним действием в семуйстве функций `FromString`), где оптимизирующий компилятор обычно может заменить их на no-op или на простые операции со значениями в регистрах.
+Преобразование между `enum` и `int` выносится в пользовательский код (в шаблонные `inline` функции), и производится только в момент непосредственного использования (первым действием в семействе функций `ToString`, последним действием в семействе функций `FromString`), где оптимизирующий компилятор обычно может заменить их на no-op или на простые операции со значениями в регистрах.
А контейнеры вида `TVector<EEnum>` и `TMap<EEnum, ...>`, которые возвращаются из функций `util/generic/serialized_enum.h`, заменяются на специальные классы `TArrayView` и `TMappedDictView`. Они также поддерживают быстрое и преобразование из перечислений в целочисленные типы и обратно в момент использования, и не требуют создавать специализации для каждого из возможных типов-перечислений.
diff --git a/tools/enum_parser/enum_serialization_runtime/enum_runtime.h b/tools/enum_parser/enum_serialization_runtime/enum_runtime.h
index 7c85ed3934a..7362c3d243b 100644
--- a/tools/enum_parser/enum_serialization_runtime/enum_runtime.h
+++ b/tools/enum_parser/enum_serialization_runtime/enum_runtime.h
@@ -173,4 +173,16 @@ namespace NEnumSerializationRuntime {
return {TCast::CastToRepresentationType(key), name};
}
};
+
+ constexpr std::pair<TStringBuf, TStringBuf> CommonStringBuffer(const TStringBuf a, const TStringBuf b) {
+ if (!a.empty() && !b.empty()) {
+ if (a.StartsWith(b)) {
+ return {a, TStringBuf{a.data(), b.size()}};
+ }
+ if (b.StartsWith(a)) {
+ return {TStringBuf{b.data(), a.size()}, b};
+ }
+ }
+ return {a, b};
+ }
}
diff --git a/tools/enum_parser/parse_enum/parse_enum_ut.cpp b/tools/enum_parser/parse_enum/parse_enum_ut.cpp
index e979f5119a8..5f7972d9a74 100644
--- a/tools/enum_parser/parse_enum/parse_enum_ut.cpp
+++ b/tools/enum_parser/parse_enum/parse_enum_ut.cpp
@@ -343,7 +343,8 @@ Y_UNIT_TEST_SUITE(TEnumParserTest) {
const TEnum& e = enums[0];
UNIT_ASSERT_VALUES_EQUAL(e.CppName, "ELiterals");
static constexpr TNameValuePair ref[]{
- {"Char", "sizeof(u8'.')"},
+ {"Char1", "sizeof(u8'.')"},
+ {"Char2", "sizeof(u8'0')"},
{"Int", "123'456'789"},
{"Float1", "int(456'789.123'456)"},
{"Float2", "int(1'2e0'1)"},
diff --git a/tools/enum_parser/parse_enum/ut/digit_separator.h b/tools/enum_parser/parse_enum/ut/digit_separator.h
index c7c8f526acc..d9d4a0ef38a 100644
--- a/tools/enum_parser/parse_enum/ut/digit_separator.h
+++ b/tools/enum_parser/parse_enum/ut/digit_separator.h
@@ -1,7 +1,8 @@
#pragma once
enum class ELiterals {
- Char = sizeof(u8'.'),
+ Char1 = sizeof(u8'.'),
+ Char2 = sizeof(u8'0'),
Int = 123'456'789,
Float1 = int(456'789.123'456),
Float2 = int(1'2e0'1),
diff --git a/tools/flake8_linter/__main__.py b/tools/flake8_linter/__main__.py
new file mode 100644
index 00000000000..26e94564b1d
--- /dev/null
+++ b/tools/flake8_linter/__main__.py
@@ -0,0 +1,193 @@
+import configparser
+import hashlib
+import itertools
+import io
+import logging
+import os
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+from collections import defaultdict
+from typing import Generator
+
+from devtools.ya.test.programs.test_tool.lib.migrations_config import load_yaml_config, MigrationsConfig
+from library.python.testing.custom_linter_util import linter_params, reporter
+from build.plugins.lib.test_const import FLAKE8_PY2_RESOURCE, FLAKE8_PY3_RESOURCE
+
+logger = logging.getLogger(__name__)
+
+ALLOWED_IGNORES = {"F401"}
+# Supports both default and pylint formats
+FLAKE_LINE_RE = r"^(.*?):(\d+):(\d+:)? \[(\w+\d+)\] (.*)"
+FLAKE8_CONFIG_INDEX = 0
+MIGRATIONS_CONFIG_INDEX = 1
+
+
+def get_flake8_bin(params) -> str:
+ if params.lint_name == "py2_flake8":
+ flake8_root = params.global_resources[FLAKE8_PY2_RESOURCE]
+ elif params.lint_name == "flake8":
+ flake8_root = params.global_resources[FLAKE8_PY3_RESOURCE]
+ else:
+ raise RuntimeError("Unexpected lint name: {}".format(params.lint_name))
+ return os.path.join(flake8_root, "flake8")
+
+
+def get_migrations_config(params) -> MigrationsConfig:
+ if params.extra_params.get("DISABLE_FLAKE8_MIGRATIONS", "no") == "yes":
+ return MigrationsConfig()
+ config_path = os.getenv("_YA_TEST_FLAKE8_CONFIG")
+ if config_path is None and len(params.configs) > 1:
+ config_path = params.configs[MIGRATIONS_CONFIG_INDEX]
+
+ if not config_path:
+ return MigrationsConfig()
+ else:
+ logger.debug("Loading flake8 migrations: %s", config_path)
+ migrations = load_yaml_config(config_path)
+ logger.debug("Building migration config")
+ return MigrationsConfig(migrations)
+
+
+def get_flake8_config(
+ flake8_config: str, migrations_config: MigrationsConfig, source_root: str, file_path: str
+) -> str | None:
+ arc_rel_file_path = os.path.relpath(file_path, source_root)
+ if migrations_config.is_skipped(arc_rel_file_path):
+ return None
+ exceptions = migrations_config.get_exceptions(arc_rel_file_path)
+ if exceptions:
+ logger.info("Ignore flake8 exceptions %s for file %s", str(list(exceptions)), arc_rel_file_path)
+
+ if os.path.basename(file_path) == "__init__.py":
+ exceptions |= get_noqa_exceptions(file_path)
+
+ if exceptions:
+ new_config = configparser.ConfigParser()
+ new_config.read(flake8_config) # https://bugs.python.org/issue16058 Why don't use deepcopy
+ new_config["flake8"]["ignore"] += "\n" + "\n".join(x + "," for x in sorted(exceptions))
+
+ config_stream = io.StringIO()
+ new_config.write(config_stream)
+ config_hash = hashlib.md5(config_stream.getvalue().encode()).hexdigest()
+ config_path = config_hash + ".config"
+ if not os.path.exists(config_path):
+ with open(config_path, "w") as f:
+ f.write(config_stream.getvalue())
+ return config_path
+ else:
+ return flake8_config
+
+
+def get_noqa_exceptions(file_path: str) -> set:
+ additional_exceptions = get_file_ignores(file_path)
+ validate_exceptions(additional_exceptions)
+ return additional_exceptions & ALLOWED_IGNORES
+
+
+def get_file_ignores(file_path: str) -> set:
+ file_ignore_regex = re.compile(r"#\s*flake8\s+noqa:\s*(.*)")
+ with open(file_path) as afile:
+ # looking for ignores only in the first 3 lines
+ for line in itertools.islice(afile, 3):
+ if match := file_ignore_regex.search(line):
+ ignores = match.group(1).strip()
+ if ignores:
+ ignores = re.split(r"\s*,\s*", ignores)
+ return set(ignores)
+ return set()
+
+
+def validate_exceptions(exceptions: set) -> None:
+ if exceptions - ALLOWED_IGNORES:
+ logger.error(
+ "Disabling %s checks. Only %s can be suppressed in the __init__.py files using # flake8 noqa",
+ str(list(exceptions - ALLOWED_IGNORES)),
+ str(list(ALLOWED_IGNORES)),
+ )
+
+
+def run_flake8_for_dir(flake8_bin: str, source_root: str, config: str, check_files: list[str]) -> dict[str, list[str]]:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ logger.debug("flake8 temp dir: %s", temp_dir)
+ for f in check_files:
+ copy_file_path = os.path.join(temp_dir, os.path.relpath(f, source_root))
+ os.makedirs(os.path.dirname(copy_file_path), exist_ok=True)
+ shutil.copyfile(f, copy_file_path)
+ flake8_res = run_flake8(flake8_bin, temp_dir, config)
+ return get_flake8_results(flake8_res, source_root, temp_dir)
+
+
+def run_flake8(flake8_bin: str, dir_path: str, config: str) -> list[str]:
+ cmd = [flake8_bin, dir_path, config]
+ res = subprocess.run(cmd, capture_output=True, encoding="utf8", errors="replace")
+ if res.stderr:
+ logger.debug("flake8 stderr: %s", res.stderr)
+ return res.stdout.split("\n") if res.returncode else []
+
+
+def get_flake8_results(flake8_res: list[str], source_root: str, temp_dir: str) -> dict[str, list[str]]:
+ flake8_errors_map = defaultdict(list)
+ for line in iterate_over_results(flake8_res):
+ match = re.match(FLAKE_LINE_RE, line)
+ if not match:
+ raise RuntimeError("Cannot parse flake8 output line: '{}'".format(line))
+ file_path, row, col_with_sep, code, text = match.groups()
+ file_path = file_path.replace(temp_dir, source_root)
+ if col_with_sep is None:
+ col_with_sep = ""
+ colorized_line = f"[[unimp]]{file_path}[[rst]]:[[alt2]]{row}[[rst]]:[[alt2]]{col_with_sep}[[rst]] [[[alt1]]{code}[[rst]]] [[bad]]{text}[[rst]]"
+ flake8_errors_map[file_path].append(colorized_line)
+ return flake8_errors_map
+
+
+def iterate_over_results(flake8_res: list[str]) -> Generator[str, None, None]:
+ to_skip = {"[[bad]]", "[[rst]]"}
+ for line in flake8_res:
+ if line and line not in to_skip:
+ yield line
+
+
+def main():
+ params = linter_params.get_params()
+ logging.basicConfig(level=logging.DEBUG, stream=sys.stdout, format="%(asctime)s: %(levelname)s: %(message)s")
+
+ flake8_bin = get_flake8_bin(params)
+ flake8_config = params.configs[FLAKE8_CONFIG_INDEX]
+ migrations_config = get_migrations_config(params)
+ source_root = params.source_root
+
+ logger.debug("Constructing flake8 config")
+ config_map = defaultdict(list)
+ report = reporter.LintReport()
+
+ skipped_files = set()
+ for file_path in params.files:
+ config_path = get_flake8_config(flake8_config, migrations_config, source_root, file_path)
+ if config_path:
+ config_map[config_path].append(file_path)
+ else:
+ skipped_files.add(file_path)
+
+ logger.debug("Configuration:\n%s", str(config_map))
+
+ flake8_errors_map = {}
+ for config_path, check_files in config_map.items():
+ flake8_errors_map.update(run_flake8_for_dir(flake8_bin, source_root, config_path, check_files))
+
+ report = reporter.LintReport()
+ for file_path in params.files:
+ if file_path in skipped_files:
+ report.add(file_path, reporter.LintStatus.SKIPPED, "Skipped by config")
+ elif file_path in flake8_errors_map:
+ message = "\n".join(flake8_errors_map[file_path])
+ report.add(file_path, reporter.LintStatus.FAIL, message)
+ else:
+ report.add(file_path, reporter.LintStatus.GOOD)
+ report.dump(params.report_file)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/flake8_linter/ya.make b/tools/flake8_linter/ya.make
deleted file mode 100644
index cd0469d8b62..00000000000
--- a/tools/flake8_linter/ya.make
+++ /dev/null
@@ -1,17 +0,0 @@
-IF (USE_PREBUILT_TOOLS OR OPENSOURCE)
- INCLUDE(${ARCADIA_ROOT}/build/prebuilt/tools/flake8_linter/ya.make.prebuilt)
-ENDIF()
-
-IF (NOT PREBUILT)
- INCLUDE(${ARCADIA_ROOT}/tools/flake8_linter/bin/ya.make)
-ENDIF()
-
-IF (NOT OPENSOURCE)
- RECURSE(
- bin
- )
-
- RECURSE_FOR_TESTS(
- bin/tests
- )
-ENDIF()
diff --git a/tools/py2cc/__main__.py b/tools/py2cc/__main__.py
new file mode 100755
index 00000000000..54f87baa5ea
--- /dev/null
+++ b/tools/py2cc/__main__.py
@@ -0,0 +1,19 @@
+import marshal
+import sys
+
+
+def main():
+ srcpathx, in_fname, out_fname = sys.argv[1:]
+ srcpath = srcpathx[:-1]
+
+ with open(in_fname, 'r') as in_file:
+ source = in_file.read()
+
+ code = compile(source, srcpath, 'exec', dont_inherit=True)
+
+ with open(out_fname, 'wb') as out_file:
+ marshal.dump(code, out_file)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/py2cc/bin/ya.make b/tools/py2cc/bin/ya.make
new file mode 100644
index 00000000000..bbf86739459
--- /dev/null
+++ b/tools/py2cc/bin/ya.make
@@ -0,0 +1,29 @@
+PY2_PROGRAM(py2cc)
+
+#ENABLE(PYBUILD_NO_PYC)
+
+ENABLE(USE_LIGHT_PY2CC)
+DISABLE(PYTHON_SQLITE3)
+
+PEERDIR(
+ library/python/runtime
+ library/python/runtime/main
+)
+
+NO_CHECK_IMPORTS()
+
+NO_PYTHON_INCLUDES()
+
+NO_PYTHON_COVERAGE()
+
+NO_IMPORT_TRACING()
+
+SRCDIR(
+ tools/py2cc
+)
+
+PY_SRCS(
+ __main__.py
+)
+
+END()
diff --git a/tools/py2cc/stage0pycc/main.cpp b/tools/py2cc/stage0pycc/main.cpp
new file mode 100644
index 00000000000..1c296267f99
--- /dev/null
+++ b/tools/py2cc/stage0pycc/main.cpp
@@ -0,0 +1,60 @@
+#include <util/folder/path.h>
+#include <util/generic/scope.h>
+#include <util/generic/string.h>
+#include <util/stream/file.h>
+#include <util/stream/output.h>
+
+#include <Python.h>
+#include <marshal.h>
+
+#include <cstdio>
+#include <system_error>
+
+struct TPyObjDeleter {
+ static void Destroy(PyObject* o) noexcept {
+ Py_XDECREF(o);
+ }
+};
+using TPyObject = THolder<PyObject, TPyObjDeleter>;
+
+int main(int argc, char** argv) {
+ if (argc < 4) {
+ Cerr << "Usage: " << argv[0] << " SRC_PATH_X SRC OUT" << Endl;
+ return 1;
+ }
+
+ TString srcpath{argv[1]};
+ srcpath.pop_back();
+ const TFsPath inPath{argv[2]};
+ const char* outPath = argv[3];
+
+ Py_Initialize();
+ Y_SCOPE_EXIT() {
+ Py_Finalize();
+ };
+
+ TPyObject bytecode{Py_CompileString(
+ TFileInput{inPath}.ReadAll().c_str(),
+ srcpath.c_str(),
+ Py_file_input)};
+ if (!bytecode) {
+ Cerr << "Failed to compile " << outPath << Endl;
+ PyErr_Print();
+ return 1;
+ }
+
+ if (FILE* out = fopen(outPath, "wb")) {
+ PyMarshal_WriteObjectToFile(bytecode.Get(), out, Py_MARSHAL_VERSION);
+ fclose(out);
+ if (PyErr_Occurred()) {
+ Cerr << "Failed to marshal " << outPath << Endl;
+ PyErr_Print();
+ return 1;
+ }
+ } else {
+ Cerr << "Failed to write " << outPath << ": " << std::error_code{errno, std::system_category()}.message() << Endl;
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/tools/py2cc/stage0pycc/ya.make b/tools/py2cc/stage0pycc/ya.make
new file mode 100644
index 00000000000..4ac109f0f2b
--- /dev/null
+++ b/tools/py2cc/stage0pycc/ya.make
@@ -0,0 +1,11 @@
+PROGRAM(py2cc)
+
+PYTHON2_ADDINCL()
+
+PEERDIR(
+ contrib/tools/python/lib
+)
+
+SRCS(main.cpp)
+
+END()
diff --git a/tools/py2cc/ya.make b/tools/py2cc/ya.make
new file mode 100644
index 00000000000..b6f5ce202eb
--- /dev/null
+++ b/tools/py2cc/ya.make
@@ -0,0 +1,11 @@
+IF (USE_PREBUILT_TOOLS)
+ INCLUDE(ya.make.prebuilt)
+ENDIF()
+
+IF (NOT PREBUILT)
+ INCLUDE(bin/ya.make)
+ENDIF()
+
+RECURSE(
+ bin
+)
diff --git a/tools/py3cc/bin/ya.make b/tools/py3cc/bin/ya.make
index 40e76e93705..555cee8b99e 100644
--- a/tools/py3cc/bin/ya.make
+++ b/tools/py3cc/bin/ya.make
@@ -1,28 +1,29 @@
-PY3_PROGRAM_BIN(py3cc)
+PROGRAM(py3cc)
-ENABLE(PYBUILD_NO_PYC)
+IF (NOT USE_ARCADIA_PYTHON)
+ PYTHON3_ADDINCL()
-DISABLE(PYTHON_SQLITE3)
+ PEERDIR(
+ contrib/tools/python3
+ )
+ELSEIF (USE_PYTHON3_PREV)
+ PEERDIR(
+ contrib/tools/python3_prev
+ )
+ ADDINCL(
+ contrib/tools/python3_prev/Include
+ )
+ELSE()
+ PEERDIR(
+ contrib/tools/python3
+ )
+ ADDINCL(
+ contrib/tools/python3/Include
+ )
+ENDIF()
-PEERDIR(
- library/python/runtime_py3
- library/python/runtime_py3/main
-)
+SRCDIR(tools/py3cc)
-NO_CHECK_IMPORTS()
-
-NO_PYTHON_INCLUDES()
-
-NO_PYTHON_COVERAGE()
-
-NO_IMPORT_TRACING()
-
-SRCDIR(
- tools/py3cc
-)
-
-PY_SRCS(
- MAIN main.py
-)
+SRCS(main.cpp)
END()
diff --git a/tools/py3cc/main.cpp b/tools/py3cc/main.cpp
new file mode 100644
index 00000000000..a70a5ac48ec
--- /dev/null
+++ b/tools/py3cc/main.cpp
@@ -0,0 +1,83 @@
+#include <util/folder/path.h>
+#include <util/generic/scope.h>
+#include <util/generic/string.h>
+#include <util/stream/file.h>
+#include <util/stream/output.h>
+#include <util/system/shellcommand.h>
+
+#include <Python.h>
+#include <marshal.h>
+
+#include <cstdio>
+#include <system_error>
+
+struct TPyObjDeleter {
+ static void Destroy(PyObject* o) noexcept {
+ Py_XDECREF(o);
+ }
+};
+using TPyObject = THolder<PyObject, TPyObjDeleter>;
+
+int runSlowPy3cc(const char* slowPy3cc, const char* srcpath, const char* inPath, const char* outPath) {
+ TShellCommandOptions opts;
+ opts.SetUseShell(false);
+ opts.SetOutputStream(&Cout);
+ opts.SetErrorStream(&Cerr);
+
+ TShellCommand cmd(slowPy3cc, {srcpath, inPath, outPath}, opts);
+ cmd.Run().Wait();
+
+ if (auto rc = cmd.GetExitCode(); rc.Defined()) {
+ return *rc;
+ }
+ return 1;
+}
+
+int main(int argc, char** argv) {
+ if (argc != 6) {
+ Cerr << "Usage:\n\t" << argv[0] << " --slow-py3cc <slow-py3cc> SRC_PATH_X- SRC OUT" << Endl;
+ return 1;
+ }
+
+ PyConfig cfg{};
+ PyConfig_InitIsolatedConfig(&cfg);
+ cfg._install_importlib = 0;
+ Y_SCOPE_EXIT(&cfg) {PyConfig_Clear(&cfg);};
+
+ const char* slowPy3cc{argv[2]};
+ TString srcpath{argv[3]};
+ srcpath.pop_back();
+ const TFsPath inPath{argv[4]};
+ const char* outPath = argv[5];
+
+ const auto status = Py_InitializeFromConfig(&cfg);
+ if (PyStatus_Exception(status)) {
+ Py_ExitStatusException(status);
+ }
+ Y_SCOPE_EXIT() {Py_Finalize();};
+
+ TPyObject bytecode{Py_CompileString(
+ TFileInput{inPath}.ReadAll().c_str(),
+ srcpath.c_str(),
+ Py_file_input
+ )};
+ if (!bytecode) {
+ int rc = runSlowPy3cc(slowPy3cc, argv[3], argv[4], argv[5]);
+ return rc;
+ }
+
+ if (FILE* out = fopen(outPath, "wb")) {
+ PyMarshal_WriteObjectToFile(bytecode.Get(), out, Py_MARSHAL_VERSION);
+ fclose(out);
+ if (PyErr_Occurred()) {
+ Cerr << "Failed to marshal " << outPath << Endl;
+ PyErr_Print();
+ return 1;
+ }
+ } else {
+ Cerr << "Failed to write " << outPath << ": " << std::error_code{errno, std::system_category()}.message() << Endl;
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/tools/py3cc/slow/bin/ya.make b/tools/py3cc/slow/bin/ya.make
new file mode 100644
index 00000000000..c8c0c771214
--- /dev/null
+++ b/tools/py3cc/slow/bin/ya.make
@@ -0,0 +1,28 @@
+PY3_PROGRAM_BIN(py3cc)
+
+ENABLE(PYBUILD_NO_PYC)
+
+DISABLE(PYTHON_SQLITE3)
+
+PEERDIR(
+ library/python/runtime_py3
+ library/python/runtime_py3/main
+)
+
+NO_CHECK_IMPORTS()
+
+NO_PYTHON_INCLUDES()
+
+NO_PYTHON_COVERAGE()
+
+NO_IMPORT_TRACING()
+
+SRCDIR(
+ tools/py3cc/slow
+)
+
+PY_SRCS(
+ MAIN main.py
+)
+
+END()
diff --git a/tools/py3cc/main.py b/tools/py3cc/slow/main.py
index 996edb2bdda..996edb2bdda 100755
--- a/tools/py3cc/main.py
+++ b/tools/py3cc/slow/main.py
diff --git a/tools/py3cc/slow/ya.make b/tools/py3cc/slow/ya.make
new file mode 100644
index 00000000000..283a03dbee1
--- /dev/null
+++ b/tools/py3cc/slow/ya.make
@@ -0,0 +1,11 @@
+IF (USE_PREBUILT_TOOLS AND NOT USE_PYTHON3_PREV)
+ INCLUDE(ya.make.prebuilt)
+ENDIF()
+
+IF (NOT PREBUILT)
+ INCLUDE(bin/ya.make)
+ENDIF()
+
+RECURSE(
+ bin
+)
diff --git a/tools/py3cc/ya.make b/tools/py3cc/ya.make
index b6f5ce202eb..ea4ff598d05 100644
--- a/tools/py3cc/ya.make
+++ b/tools/py3cc/ya.make
@@ -1,4 +1,4 @@
-IF (USE_PREBUILT_TOOLS)
+IF (USE_PREBUILT_TOOLS AND NOT USE_PYTHON3_PREV)
INCLUDE(ya.make.prebuilt)
ENDIF()
@@ -8,4 +8,5 @@ ENDIF()
RECURSE(
bin
+ slow
)
diff --git a/tools/ruff_linter/wrapper.py b/tools/ruff_linter/wrapper.py
new file mode 100644
index 00000000000..f21fa737812
--- /dev/null
+++ b/tools/ruff_linter/wrapper.py
@@ -0,0 +1,157 @@
+import logging
+import os
+import subprocess
+import time
+import tomllib
+from pathlib import Path
+
+from build.plugins.lib.test_const import RUFF_RESOURCE
+from library.python.testing.custom_linter_util import linter_params, reporter
+from library.python.testing.style import rules
+
+logger = logging.getLogger(__name__)
+
+FORMAT_SNIPPET_LINES_LIMIT = 100
+
+
+def get_ruff_bin(params) -> str:
+ ruff_root = params.global_resources[RUFF_RESOURCE]
+ return os.path.join(ruff_root, 'bin', 'ruff')
+
+
+def check_extend_option_present(config: Path) -> bool:
+ with config.open('rb') as afile:
+ cfg = tomllib.load(afile)
+ if config.name == 'pyproject.toml' and cfg.get('tool', {}).get('ruff'):
+ return 'extend' in cfg['tool']['ruff']
+ elif config.name == 'ruff.toml':
+ return 'extend' in cfg
+ raise RuntimeError(f'Unknown config type: {config.name}')
+
+
+def run_ruff(ruff_bin: str, cmd_args: list[str], filename: str, config: Path) -> list[str]:
+ # XXX: `--no-cache` is important when we run ruff in source root and don't want to pollute arcadia
+ cmd = [ruff_bin, *cmd_args, '--no-cache', '--config', config, filename]
+ res = subprocess.run(
+ cmd,
+ capture_output=True,
+ encoding='utf8',
+ errors='replace',
+ env=dict(os.environ.copy(), RUFF_OUTPUT_FORMAT='concise', RAYON_NUM_THREADS='1'),
+ # When config is passed through `--config`, `exclude` starts searching from cwd
+ # so set cwd to config cwd to mimic behavior of autodiscovery.
+ # Note that it stops being accurate when `extend` is used.
+ # https://docs.astral.sh/ruff/configuration/#config-file-discovery
+ cwd=os.path.dirname(config),
+ )
+ return res.stdout.splitlines(keepends=True) if res.returncode else []
+
+
+def process_file(
+ orig_filename: str, ruff_bin: str, orig_config: Path, source_root: str, check_format: bool, run_in_source_root: bool
+) -> str:
+ logger.debug('Check %s with config %s', orig_filename, orig_config)
+
+ file_path = os.path.relpath(orig_filename, source_root)
+
+ if run_in_source_root:
+ filename = os.path.realpath(orig_filename) if os.path.islink(orig_filename) else orig_filename
+ config = orig_config.resolve() if orig_config.is_symlink() else orig_config
+ else:
+ filename = orig_filename
+ config = orig_config
+
+ if check_format:
+ ruff_format_check_out = run_ruff(ruff_bin, ['format', '--diff'], filename, config)
+ if len(ruff_format_check_out) > FORMAT_SNIPPET_LINES_LIMIT:
+ ruff_format_check_out = ruff_format_check_out[:FORMAT_SNIPPET_LINES_LIMIT]
+ ruff_format_check_out.append('[truncated]...\n')
+ # first two lines are absolute file paths, replace with relative ones
+ if ruff_format_check_out:
+ ruff_format_check_out[0] = f'--- [[imp]]{file_path}[[rst]]\n'
+ ruff_format_check_out[1] = f'+++ [[imp]]{file_path}[[rst]]\n'
+ else:
+ ruff_format_check_out = []
+
+ ruff_check_out = run_ruff(ruff_bin, ['check', '-q'], filename, config)
+ # Every line starts with an absolute path to a file, replace with relative one
+ for idx, line in enumerate(ruff_check_out):
+ ruff_check_out[idx] = f'[[imp]]{file_path}[[rst]]:{line.split(':', 1)[-1]}'
+
+ msg = ''
+ if ruff_format_check_out:
+ msg += '[[bad]]Formatting errors[[rst]]:\n'
+ msg += ''.join(ruff_format_check_out)
+
+ if ruff_format_check_out and ruff_check_out:
+ msg += '\n'
+
+ if ruff_check_out:
+ msg += '[[bad]]Linting errors[[rst]]:\n'
+ msg += ''.join(ruff_check_out)
+
+ return msg
+
+
+def main():
+ params = linter_params.get_params()
+
+ style_config_path = Path(params.configs[0])
+
+ # TODO: Ideally, to enable `extend` we first should move execution to build root (src files + configs)
+ # otherwise we risk allowing to steal from arcadia. To do that we need to mark modules 1st-party/3rd-party
+ # in pyproject.toml.
+ # UPD: it turned out to be more complicated for uservices-like projects because they use TOP_LEVEL / NAMESPACES
+ extend_option_present = check_extend_option_present(style_config_path)
+
+ ruff_bin = get_ruff_bin(params)
+
+ report = reporter.LintReport()
+ for file_name in params.files:
+ start_time = time.perf_counter()
+
+ if extend_option_present:
+ elapsed = time.perf_counter() - start_time
+ report.add(
+ file_name,
+ reporter.LintStatus.FAIL,
+ "`extend` option in not supported in ruff config files for now. Modify your configs not to have it.",
+ elapsed=elapsed,
+ )
+ continue
+
+ skip_reason = rules.get_skip_reason(file_name, Path(file_name).read_text(), skip_links=False)
+ if skip_reason:
+ elapsed = time.perf_counter() - start_time
+ report.add(
+ file_name,
+ reporter.LintStatus.SKIPPED,
+ f"Style check is omitted: {skip_reason}",
+ elapsed=elapsed,
+ )
+ continue
+
+ error = process_file(
+ file_name,
+ ruff_bin,
+ style_config_path,
+ params.source_root,
+ params.extra_params.get('check_format') == 'yes',
+ params.extra_params.get('run_in_source_root') == 'yes',
+ )
+ elapsed = time.perf_counter() - start_time
+
+ if error:
+ rel_file_name = os.path.relpath(file_name, params.source_root)
+ message = f'Run [[imp]]ya style --ruff {rel_file_name}[[rst]] to fix format\n{error}'
+ status = reporter.LintStatus.FAIL
+ else:
+ message = ''
+ status = reporter.LintStatus.GOOD
+ report.add(file_name, status, message, elapsed=elapsed)
+
+ report.dump(params.report_file)
+
+
+if __name__ == '__main__':
+ main()