@owenlamont/ryl 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/README.md +13 -0
  2. package/bin/ryl.js +195 -1
  3. package/package.json +35 -13
  4. package/.github/CODEOWNERS +0 -1
  5. package/.github/dependabot.yml +0 -13
  6. package/.github/workflows/ci.yml +0 -107
  7. package/.github/workflows/release.yml +0 -613
  8. package/.github/workflows/update_dependencies.yml +0 -61
  9. package/.github/workflows/update_linters.yml +0 -56
  10. package/.pre-commit-config.yaml +0 -87
  11. package/.yamllint +0 -4
  12. package/AGENTS.md +0 -200
  13. package/Cargo.lock +0 -908
  14. package/Cargo.toml +0 -32
  15. package/clippy.toml +0 -1
  16. package/docs/config-presets.md +0 -100
  17. package/img/benchmark-5x5-5runs.svg +0 -2176
  18. package/pyproject.toml +0 -42
  19. package/ruff.toml +0 -107
  20. package/rumdl.toml +0 -20
  21. package/rust-toolchain.toml +0 -3
  22. package/rustfmt.toml +0 -3
  23. package/scripts/benchmark_perf_vs_yamllint.py +0 -400
  24. package/scripts/coverage-missing.ps1 +0 -80
  25. package/scripts/coverage-missing.sh +0 -60
  26. package/src/bin/discover_config_bin.rs +0 -24
  27. package/src/cli_support.rs +0 -33
  28. package/src/conf/mod.rs +0 -85
  29. package/src/config.rs +0 -2099
  30. package/src/decoder.rs +0 -326
  31. package/src/discover.rs +0 -31
  32. package/src/lib.rs +0 -19
  33. package/src/lint.rs +0 -558
  34. package/src/main.rs +0 -535
  35. package/src/migrate.rs +0 -233
  36. package/src/rules/anchors.rs +0 -517
  37. package/src/rules/braces.rs +0 -77
  38. package/src/rules/brackets.rs +0 -77
  39. package/src/rules/colons.rs +0 -475
  40. package/src/rules/commas.rs +0 -372
  41. package/src/rules/comments.rs +0 -299
  42. package/src/rules/comments_indentation.rs +0 -243
  43. package/src/rules/document_end.rs +0 -175
  44. package/src/rules/document_start.rs +0 -84
  45. package/src/rules/empty_lines.rs +0 -152
  46. package/src/rules/empty_values.rs +0 -255
  47. package/src/rules/float_values.rs +0 -259
  48. package/src/rules/flow_collection.rs +0 -562
  49. package/src/rules/hyphens.rs +0 -104
  50. package/src/rules/indentation.rs +0 -803
  51. package/src/rules/key_duplicates.rs +0 -218
  52. package/src/rules/key_ordering.rs +0 -303
  53. package/src/rules/line_length.rs +0 -326
  54. package/src/rules/mod.rs +0 -25
  55. package/src/rules/new_line_at_end_of_file.rs +0 -23
  56. package/src/rules/new_lines.rs +0 -95
  57. package/src/rules/octal_values.rs +0 -121
  58. package/src/rules/quoted_strings.rs +0 -577
  59. package/src/rules/span_utils.rs +0 -37
  60. package/src/rules/trailing_spaces.rs +0 -65
  61. package/src/rules/truthy.rs +0 -420
  62. package/tests/brackets_carriage_return.rs +0 -114
  63. package/tests/build_global_cfg_error.rs +0 -23
  64. package/tests/cli_anchors_rule.rs +0 -143
  65. package/tests/cli_braces_rule.rs +0 -104
  66. package/tests/cli_brackets_rule.rs +0 -104
  67. package/tests/cli_colons_rule.rs +0 -65
  68. package/tests/cli_commas_rule.rs +0 -104
  69. package/tests/cli_comments_indentation_rule.rs +0 -61
  70. package/tests/cli_comments_rule.rs +0 -67
  71. package/tests/cli_config_data_error.rs +0 -30
  72. package/tests/cli_config_flags.rs +0 -66
  73. package/tests/cli_config_migrate.rs +0 -229
  74. package/tests/cli_document_end_rule.rs +0 -92
  75. package/tests/cli_document_start_rule.rs +0 -92
  76. package/tests/cli_empty_lines_rule.rs +0 -87
  77. package/tests/cli_empty_values_rule.rs +0 -68
  78. package/tests/cli_env_config.rs +0 -34
  79. package/tests/cli_exit_and_errors.rs +0 -41
  80. package/tests/cli_file_encoding.rs +0 -203
  81. package/tests/cli_float_values_rule.rs +0 -64
  82. package/tests/cli_format_options.rs +0 -316
  83. package/tests/cli_global_cfg_relaxed.rs +0 -20
  84. package/tests/cli_hyphens_rule.rs +0 -104
  85. package/tests/cli_indentation_rule.rs +0 -65
  86. package/tests/cli_invalid_project_config.rs +0 -39
  87. package/tests/cli_key_duplicates_rule.rs +0 -104
  88. package/tests/cli_key_ordering_rule.rs +0 -59
  89. package/tests/cli_line_length_rule.rs +0 -85
  90. package/tests/cli_list_files.rs +0 -29
  91. package/tests/cli_new_line_rule.rs +0 -141
  92. package/tests/cli_new_lines_rule.rs +0 -119
  93. package/tests/cli_octal_values_rule.rs +0 -60
  94. package/tests/cli_quoted_strings_rule.rs +0 -47
  95. package/tests/cli_toml_config.rs +0 -119
  96. package/tests/cli_trailing_spaces_rule.rs +0 -77
  97. package/tests/cli_truthy_rule.rs +0 -83
  98. package/tests/cli_yaml_files_negation.rs +0 -45
  99. package/tests/colons_rule.rs +0 -303
  100. package/tests/common/compat.rs +0 -114
  101. package/tests/common/fake_env.rs +0 -93
  102. package/tests/common/mod.rs +0 -1
  103. package/tests/conf_builtin.rs +0 -9
  104. package/tests/config_anchors.rs +0 -84
  105. package/tests/config_braces.rs +0 -121
  106. package/tests/config_brackets.rs +0 -127
  107. package/tests/config_commas.rs +0 -79
  108. package/tests/config_comments.rs +0 -65
  109. package/tests/config_comments_indentation.rs +0 -20
  110. package/tests/config_deep_merge_nonstring_key.rs +0 -24
  111. package/tests/config_document_end.rs +0 -54
  112. package/tests/config_document_start.rs +0 -55
  113. package/tests/config_empty_lines.rs +0 -48
  114. package/tests/config_empty_values.rs +0 -35
  115. package/tests/config_env_errors.rs +0 -23
  116. package/tests/config_env_invalid_inline.rs +0 -15
  117. package/tests/config_env_missing.rs +0 -63
  118. package/tests/config_env_shim.rs +0 -301
  119. package/tests/config_explicit_file_parse_error.rs +0 -55
  120. package/tests/config_extended_features.rs +0 -225
  121. package/tests/config_extends_inline.rs +0 -185
  122. package/tests/config_extends_sequence.rs +0 -18
  123. package/tests/config_find_project_home_boundary.rs +0 -54
  124. package/tests/config_find_project_two_files_in_cwd.rs +0 -47
  125. package/tests/config_float_values.rs +0 -34
  126. package/tests/config_from_yaml_paths.rs +0 -32
  127. package/tests/config_hyphens.rs +0 -51
  128. package/tests/config_ignore_errors.rs +0 -243
  129. package/tests/config_ignore_overrides.rs +0 -83
  130. package/tests/config_indentation.rs +0 -65
  131. package/tests/config_invalid_globs.rs +0 -16
  132. package/tests/config_invalid_types.rs +0 -19
  133. package/tests/config_key_duplicates.rs +0 -34
  134. package/tests/config_key_ordering.rs +0 -70
  135. package/tests/config_line_length.rs +0 -65
  136. package/tests/config_locale.rs +0 -111
  137. package/tests/config_merge.rs +0 -26
  138. package/tests/config_new_lines.rs +0 -89
  139. package/tests/config_octal_values.rs +0 -33
  140. package/tests/config_quoted_strings.rs +0 -195
  141. package/tests/config_rule_level.rs +0 -147
  142. package/tests/config_rules_non_string_keys.rs +0 -23
  143. package/tests/config_scalar_overrides.rs +0 -27
  144. package/tests/config_to_toml.rs +0 -110
  145. package/tests/config_toml_coverage.rs +0 -80
  146. package/tests/config_toml_discovery.rs +0 -304
  147. package/tests/config_trailing_spaces.rs +0 -152
  148. package/tests/config_truthy.rs +0 -77
  149. package/tests/config_yaml_files.rs +0 -62
  150. package/tests/config_yaml_files_all_non_string.rs +0 -15
  151. package/tests/config_yaml_files_empty.rs +0 -30
  152. package/tests/coverage_commas.rs +0 -46
  153. package/tests/decoder_decode.rs +0 -338
  154. package/tests/discover_config_bin_all.rs +0 -66
  155. package/tests/discover_config_bin_env_invalid_yaml.rs +0 -26
  156. package/tests/discover_config_bin_project_config_parse_error.rs +0 -24
  157. package/tests/discover_config_bin_user_global_error.rs +0 -26
  158. package/tests/discover_module.rs +0 -30
  159. package/tests/discover_per_file_dir.rs +0 -10
  160. package/tests/discover_per_file_project_config_error.rs +0 -21
  161. package/tests/float_values.rs +0 -43
  162. package/tests/lint_multi_errors.rs +0 -32
  163. package/tests/main_yaml_ok_filtering.rs +0 -30
  164. package/tests/migrate_module.rs +0 -259
  165. package/tests/resolve_ctx_empty_parent.rs +0 -16
  166. package/tests/rule_anchors.rs +0 -442
  167. package/tests/rule_braces.rs +0 -258
  168. package/tests/rule_brackets.rs +0 -217
  169. package/tests/rule_commas.rs +0 -205
  170. package/tests/rule_comments.rs +0 -197
  171. package/tests/rule_comments_indentation.rs +0 -127
  172. package/tests/rule_document_end.rs +0 -118
  173. package/tests/rule_document_start.rs +0 -60
  174. package/tests/rule_empty_lines.rs +0 -96
  175. package/tests/rule_empty_values.rs +0 -102
  176. package/tests/rule_float_values.rs +0 -109
  177. package/tests/rule_hyphens.rs +0 -65
  178. package/tests/rule_indentation.rs +0 -455
  179. package/tests/rule_key_duplicates.rs +0 -76
  180. package/tests/rule_key_ordering.rs +0 -207
  181. package/tests/rule_line_length.rs +0 -200
  182. package/tests/rule_new_lines.rs +0 -51
  183. package/tests/rule_octal_values.rs +0 -53
  184. package/tests/rule_quoted_strings.rs +0 -290
  185. package/tests/rule_trailing_spaces.rs +0 -41
  186. package/tests/rule_truthy.rs +0 -236
  187. package/tests/user_global_invalid_yaml.rs +0 -32
  188. package/tests/yamllint_compat_anchors.rs +0 -280
  189. package/tests/yamllint_compat_braces.rs +0 -411
  190. package/tests/yamllint_compat_brackets.rs +0 -364
  191. package/tests/yamllint_compat_colons.rs +0 -298
  192. package/tests/yamllint_compat_colors.rs +0 -80
  193. package/tests/yamllint_compat_commas.rs +0 -375
  194. package/tests/yamllint_compat_comments.rs +0 -167
  195. package/tests/yamllint_compat_comments_indentation.rs +0 -281
  196. package/tests/yamllint_compat_config.rs +0 -170
  197. package/tests/yamllint_compat_document_end.rs +0 -243
  198. package/tests/yamllint_compat_document_start.rs +0 -136
  199. package/tests/yamllint_compat_empty_lines.rs +0 -117
  200. package/tests/yamllint_compat_empty_values.rs +0 -179
  201. package/tests/yamllint_compat_float_values.rs +0 -216
  202. package/tests/yamllint_compat_hyphens.rs +0 -223
  203. package/tests/yamllint_compat_indentation.rs +0 -398
  204. package/tests/yamllint_compat_key_duplicates.rs +0 -139
  205. package/tests/yamllint_compat_key_ordering.rs +0 -170
  206. package/tests/yamllint_compat_line_length.rs +0 -375
  207. package/tests/yamllint_compat_list.rs +0 -127
  208. package/tests/yamllint_compat_new_line.rs +0 -133
  209. package/tests/yamllint_compat_newline_types.rs +0 -185
  210. package/tests/yamllint_compat_octal_values.rs +0 -172
  211. package/tests/yamllint_compat_quoted_strings.rs +0 -154
  212. package/tests/yamllint_compat_syntax.rs +0 -200
  213. package/tests/yamllint_compat_trailing_spaces.rs +0 -162
  214. package/tests/yamllint_compat_truthy.rs +0 -130
  215. package/tests/yamllint_compat_yaml_files.rs +0 -81
  216. package/typos.toml +0 -2
package/pyproject.toml DELETED
@@ -1,42 +0,0 @@
1
- [build-system]
2
- requires = ["maturin>=1.4,<2.0"]
3
- build-backend = "maturin"
4
-
5
- [project]
6
- name = "ryl"
7
- version = "0.4.1"
8
- description = "Fast YAML linter inspired by yamllint"
9
- requires-python = ">=3.10"
10
- readme = "README.md"
11
- classifiers = [
12
- "Development Status :: 3 - Alpha",
13
- "Environment :: Console",
14
- "Intended Audience :: Developers",
15
- "License :: OSI Approved :: MIT License",
16
- "Operating System :: MacOS :: MacOS X",
17
- "Operating System :: Microsoft :: Windows",
18
- "Operating System :: POSIX :: Linux",
19
- "Programming Language :: Python",
20
- "Programming Language :: Python :: 3",
21
- "Programming Language :: Python :: 3 :: Only",
22
- "Programming Language :: Python :: 3.10",
23
- "Programming Language :: Python :: 3.11",
24
- "Programming Language :: Python :: 3.12",
25
- "Programming Language :: Python :: 3.13",
26
- "Programming Language :: Python :: 3.14",
27
- "Programming Language :: Rust",
28
- "Topic :: File Formats",
29
- "Topic :: Text Processing :: Markup",
30
- "Topic :: Software Development :: Quality Assurance",
31
- "Topic :: Utilities",
32
- ]
33
- keywords = ["yaml", "linter", "lint", "cli", "rust"]
34
-
35
- [project.urls]
36
- Homepage = "https://github.com/owenlamont/ryl"
37
- Repository = "https://github.com/owenlamont/ryl"
38
- Issues = "https://github.com/owenlamont/ryl/issues"
39
-
40
- [tool.maturin]
41
- bindings = "bin"
42
- sdist = true
package/ruff.toml DELETED
@@ -1,107 +0,0 @@
1
- line-length = 88
2
- indent-width = 4
3
-
4
- [format]
5
- quote-style = "double"
6
- indent-style = "space"
7
- skip-magic-trailing-comma = true
8
- line-ending = "lf"
9
-
10
- [lint]
11
- # See https://docs.astral.sh/ruff/rules/
12
- select = [
13
- "A",
14
- "ASYNC",
15
- "B",
16
- "C4",
17
- "D",
18
- "DOC",
19
- "E",
20
- "ERA",
21
- "F",
22
- "FURB",
23
- "I",
24
- "ISC",
25
- "N",
26
- "NPY",
27
- "PD",
28
- "PERF",
29
- "PT",
30
- "PTH",
31
- "Q",
32
- "S",
33
- "RET",
34
- "RUF",
35
- "SIM",
36
- "TID",
37
- "UP",
38
- ]
39
- ignore = [
40
- "D100", # Missing docstring in public module
41
- "D101", # Missing docstring in public class
42
- "D104", # Missing docstring in public package
43
- "D105", # Missing docstring in magic method
44
- "D106", # Missing docstring in public nested class
45
- "D107", # Missing docstring in __init__
46
- "D202", # No blank lines allowed after function docstring
47
- "D213", # Multi-line docstring summary should start at the second line
48
- "D214", # Section is over-indented
49
- "D215", # Section underline is over-indented
50
- "D400", # First line should end with a period
51
- "D401", # First line of docstring should be in imperative mood
52
- "D415", # First line should end with a period, question mark, or exclamation
53
- "D416", # Section name should end with a colon
54
- "D417", # Missing argument descriptions in the docstring
55
- "D418", # Function/ Method decorated with @overload shouldn't contain a docstring
56
- "E203", # Whitespace before ':' (fights ruff format)
57
- "ISC001", # Implicitly concatenated string literals on one line
58
- ]
59
- extend-select = ["RUF027"]
60
- preview = true
61
-
62
- [lint.per-file-ignores]
63
- "tests/**" = ["D102", "D103", "S101"]
64
- "tests/uv_secure/package_info/test_dependency_file_parser.py" = ["E501"]
65
- "scripts/benchmark_perf_vs_yamllint.py" = [
66
- "B008",
67
- "B904",
68
- "B905",
69
- "D103",
70
- "E501",
71
- "S404",
72
- "S603",
73
- ]
74
-
75
- # Allow fix for all enabled rules (when `--fix`) is provided.
76
- fixable = ["ALL"]
77
- unfixable = [
78
- "B905", # Enforce strict argument on zip - but don't autofix as strict=False
79
- ]
80
-
81
- [lint.flake8-pytest-style]
82
- mark-parentheses = false
83
-
84
- [lint.flake8-tidy-imports]
85
- # Disallow all relative imports.
86
- ban-relative-imports = "all"
87
-
88
- [lint.flake8-tidy-imports.banned-api]
89
- "typing.cast".msg = "Type casts are not allowed"
90
-
91
- [lint.isort]
92
- case-sensitive = false
93
- combine-as-imports = true
94
- force-sort-within-sections = true
95
- lines-after-imports = 2
96
- order-by-type = false
97
- section-order = [
98
- "future",
99
- "standard-library",
100
- "third-party",
101
- "first-party",
102
- "local-folder",
103
- ]
104
- split-on-trailing-comma = false
105
-
106
- [lint.pydocstyle]
107
- convention = "google"
package/rumdl.toml DELETED
@@ -1,20 +0,0 @@
1
- line-length = 88
2
- disable = []
3
- exclude = []
4
- respect-gitignore = true
5
-
6
- [MD004]
7
- style = "dash"
8
-
9
- [MD013]
10
- line_length = 88
11
- code_blocks = false
12
- tables = true
13
- headings = true
14
- strict = false
15
-
16
- [MD029]
17
- style = "ordered"
18
-
19
- [MD035]
20
- style = "---"
@@ -1,3 +0,0 @@
1
- [toolchain]
2
- channel = "1.93.1"
3
- components = ["llvm-tools-preview"]
package/rustfmt.toml DELETED
@@ -1,3 +0,0 @@
1
- edition = "2024"
2
- style_edition = "2024"
3
- max_width = 88
@@ -1,400 +0,0 @@
1
- #!/usr/bin/env -S uv run
2
- # /// script
3
- # requires-python = ">=3.14"
4
- # dependencies = [
5
- # "matplotlib>=3.9,<4",
6
- # "orjson>=3.11,<4",
7
- # "polars>=1.30,<2",
8
- # "ryl",
9
- # "tqdm>=4.67,<5",
10
- # "typer>=0.16,<1",
11
- # "yamllint",
12
- # ]
13
- # ///
14
-
15
- from __future__ import annotations
16
-
17
- from collections.abc import Iterable
18
- from dataclasses import dataclass
19
- from datetime import datetime, timezone
20
- import os
21
- from pathlib import Path
22
- import shutil
23
- import string
24
- import subprocess
25
- import sys
26
-
27
- import matplotlib.pyplot as plt
28
- import orjson
29
- import polars as pl
30
- from tqdm import tqdm
31
- import typer
32
-
33
-
34
- @dataclass(frozen=True)
35
- class Case:
36
- file_count: int
37
- file_size_kib: int
38
- dataset_dir: Path
39
-
40
-
41
- app = typer.Typer(add_completion=False)
42
-
43
-
44
- def parse_int_list(raw: str) -> list[int]:
45
- values = [int(part.strip()) for part in raw.split(",") if part.strip()]
46
- if not values:
47
- raise ValueError("expected at least one integer")
48
- if any(value <= 0 for value in values):
49
- raise ValueError("all values must be > 0")
50
- return values
51
-
52
-
53
- def quote_shell(text: str) -> str:
54
- if os.name == "nt":
55
- return subprocess.list2cmdline([text])
56
- import shlex
57
-
58
- return shlex.quote(text)
59
-
60
-
61
- def run_checked(
62
- args: list[str], *, cwd: Path | None = None
63
- ) -> subprocess.CompletedProcess[str]:
64
- return subprocess.run(args, cwd=cwd, check=True, text=True, capture_output=True)
65
-
66
-
67
- def require_command(name: str) -> None:
68
- if shutil.which(name) is None:
69
- raise RuntimeError(f"required command is not installed or not on PATH: {name}")
70
-
71
-
72
- def resolve_tool_path(name: str) -> Path:
73
- path = shutil.which(name)
74
- if path is None:
75
- raise RuntimeError(
76
- f"{name} executable not found on PATH in this uv environment"
77
- )
78
- return Path(path)
79
-
80
-
81
- def build_yaml_blob(target_bytes: int, seed: int) -> str:
82
- lines = ["root:"]
83
- alphabet = string.ascii_lowercase + string.digits
84
- index = 0
85
- while len(("\n".join(lines) + "\n").encode("utf-8")) < target_bytes:
86
- offset = (seed + index) % len(alphabet)
87
- token = "".join(alphabet[(offset + i) % len(alphabet)] for i in range(28))
88
- lines.append(f' key_{index:06d}: "{token}"')
89
- index += 1
90
- return "\n".join(lines) + "\n"
91
-
92
-
93
- def materialize_case(case: Case, seed: int) -> None:
94
- case.dataset_dir.mkdir(parents=True, exist_ok=True)
95
- target_bytes = case.file_size_kib * 1024
96
- for file_index in range(case.file_count):
97
- payload = build_yaml_blob(target_bytes=target_bytes, seed=seed + file_index)
98
- file_path = case.dataset_dir / f"file_{file_index:05d}.yaml"
99
- file_path.write_text(payload, encoding="utf-8")
100
-
101
-
102
- def iter_cases(
103
- file_counts: Iterable[int], file_sizes_kib: Iterable[int], base_dir: Path
104
- ) -> list[Case]:
105
- cases: list[Case] = []
106
- for size_kib in file_sizes_kib:
107
- for count in file_counts:
108
- case_dir = base_dir / f"files_{count:05d}__size_{size_kib:04d}kib"
109
- cases.append(
110
- Case(file_count=count, file_size_kib=size_kib, dataset_dir=case_dir)
111
- )
112
- return cases
113
-
114
-
115
- def expand_range(
116
- *, start: int | None, end: int | None, step: int | None, value_name: str
117
- ) -> list[int] | None:
118
- values = (start, end, step)
119
- if all(value is None for value in values):
120
- return None
121
- if any(value is None for value in values):
122
- raise typer.BadParameter(
123
- f"{value_name} range requires start, end, and step together."
124
- )
125
- if start <= 0 or end <= 0 or step <= 0:
126
- raise typer.BadParameter(f"{value_name} range values must be > 0.")
127
- if end < start:
128
- raise typer.BadParameter(f"{value_name} range end must be >= start.")
129
- return list(range(start, end + 1, step))
130
-
131
-
132
- def benchmark_case(
133
- case: Case,
134
- *,
135
- ryl_bin: Path,
136
- yamllint_bin: Path,
137
- runs: int,
138
- warmup: int,
139
- output_json_path: Path,
140
- ) -> dict[str, dict[str, float | list[float] | str]]:
141
- cfg = "extends: relaxed"
142
- ryl_cmd = f"{quote_shell(str(ryl_bin))} -d {quote_shell(cfg)} {quote_shell(str(case.dataset_dir))}"
143
- yamllint_cmd = f"{quote_shell(str(yamllint_bin))} -d {quote_shell(cfg)} {quote_shell(str(case.dataset_dir))}"
144
- run_checked(
145
- [
146
- "hyperfine",
147
- "--runs",
148
- str(runs),
149
- "--warmup",
150
- str(warmup),
151
- "--export-json",
152
- str(output_json_path),
153
- "-n",
154
- "ryl",
155
- ryl_cmd,
156
- "-n",
157
- "yamllint",
158
- yamllint_cmd,
159
- ]
160
- )
161
- raw = orjson.loads(output_json_path.read_bytes())
162
- parsed: dict[str, dict[str, float | list[float] | str]] = {}
163
- for result in raw["results"]:
164
- parsed[str(result["command"])] = {
165
- "mean": float(result["mean"]),
166
- "stddev": float(result["stddev"] or 0.0),
167
- "min": float(result["min"]),
168
- "max": float(result["max"]),
169
- "times": [float(value) for value in result["times"]],
170
- }
171
- return parsed
172
-
173
-
174
- def plot_results(
175
- df: pl.DataFrame,
176
- out_png: Path,
177
- out_svg: Path,
178
- *,
179
- runs: int,
180
- ryl_version: str,
181
- yamllint_version: str,
182
- ) -> None:
183
- plt.style.use("seaborn-v0_8-whitegrid")
184
- tools = ["ryl", "yamllint"]
185
- sizes = sorted(int(value) for value in df["file_size_kib"].unique().to_list())
186
- cmap = plt.cm.Blues
187
- min_tone = 0.35
188
- max_tone = 0.95
189
- tones = [
190
- min_tone + (max_tone - min_tone) * idx / max(len(sizes) - 1, 1)
191
- for idx in range(len(sizes))
192
- ]
193
- fig, axes = plt.subplots(1, len(tools), figsize=(14, 5.5), sharey=True)
194
- if len(tools) == 1:
195
- axes = [axes]
196
- version_map = {"ryl": ryl_version, "yamllint": yamllint_version}
197
- for axis, tool in zip(axes, tools):
198
- tool_df = df.filter(pl.col("tool") == tool)
199
- for idx, size in enumerate(sizes):
200
- size_df = tool_df.filter(pl.col("file_size_kib") == size).sort("file_count")
201
- x_values = [int(value) for value in size_df["file_count"].to_list()]
202
- y_values = [float(value) for value in size_df["mean_seconds"].to_list()]
203
- y_stddev = [float(value) for value in size_df["stddev_seconds"].to_list()]
204
- color = cmap(tones[idx])
205
- axis.plot(
206
- x_values,
207
- y_values,
208
- color=color,
209
- linewidth=2,
210
- marker="o",
211
- label=f"{size} KiB",
212
- )
213
- axis.fill_between(
214
- x_values,
215
- [value - std for value, std in zip(y_values, y_stddev)],
216
- [value + std for value, std in zip(y_values, y_stddev)],
217
- color=color,
218
- alpha=0.16,
219
- )
220
- axis.set_title(version_map.get(tool, tool))
221
- axis.set_xlabel("Number of YAML files")
222
- axes[0].set_ylabel("Mean runtime (seconds)")
223
- axes[-1].legend(title="File size", loc="upper left")
224
- fig.suptitle(f"ryl vs yamllint (hyperfine, {runs} runs per point)", fontsize=13)
225
- fig.tight_layout()
226
- out_png.parent.mkdir(parents=True, exist_ok=True)
227
- fig.savefig(out_png, dpi=170)
228
- fig.savefig(out_svg)
229
- plt.close(fig)
230
-
231
-
232
- @app.command()
233
- def main(
234
- file_counts: str = typer.Option(
235
- "25,100,400,1000",
236
- help="Comma-separated file counts. Ignored when --file-count-start/end/step are set.",
237
- ),
238
- file_count_start: int | None = typer.Option(
239
- None, help="Start of file-count range (inclusive)."
240
- ),
241
- file_count_end: int | None = typer.Option(
242
- None, help="End of file-count range (inclusive)."
243
- ),
244
- file_count_step: int | None = typer.Option(
245
- None, help="Increment for file-count range."
246
- ),
247
- file_sizes_kib: str = typer.Option(
248
- "1,8,32,128",
249
- help="Comma-separated file sizes in KiB. Ignored when --file-size-start-kib/end/step are set.",
250
- ),
251
- file_size_start_kib: int | None = typer.Option(
252
- None, help="Start of file-size range in KiB (inclusive)."
253
- ),
254
- file_size_end_kib: int | None = typer.Option(
255
- None, help="End of file-size range in KiB (inclusive)."
256
- ),
257
- file_size_step_kib: int | None = typer.Option(
258
- None, help="Increment for file-size range in KiB."
259
- ),
260
- runs: int = typer.Option(10, help="Number of hyperfine runs per point."),
261
- warmup: int = typer.Option(2, help="Number of warmup runs per point."),
262
- seed: int = typer.Option(7331, help="Base RNG seed for synthetic YAML generation."),
263
- output_dir: Path = typer.Option(
264
- Path("manual_outputs") / "benchmarks",
265
- help="Directory where all artifacts are written.",
266
- ),
267
- keep_datasets: bool = typer.Option(
268
- False, help="Keep generated YAML datasets on disk instead of deleting them."
269
- ),
270
- ) -> None:
271
- if runs <= 0:
272
- raise typer.BadParameter("--runs must be > 0")
273
- if warmup < 0:
274
- raise typer.BadParameter("--warmup must be >= 0")
275
-
276
- file_counts_range = expand_range(
277
- start=file_count_start,
278
- end=file_count_end,
279
- step=file_count_step,
280
- value_name="file count",
281
- )
282
- file_sizes_range = expand_range(
283
- start=file_size_start_kib,
284
- end=file_size_end_kib,
285
- step=file_size_step_kib,
286
- value_name="file size",
287
- )
288
- try:
289
- file_counts_values = (
290
- file_counts_range
291
- if file_counts_range is not None
292
- else parse_int_list(file_counts)
293
- )
294
- file_size_values = (
295
- file_sizes_range
296
- if file_sizes_range is not None
297
- else parse_int_list(file_sizes_kib)
298
- )
299
- except ValueError as err:
300
- raise typer.BadParameter(str(err)) from err
301
-
302
- require_command("uv")
303
- require_command("hyperfine")
304
-
305
- timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
306
- run_dir = output_dir / timestamp
307
- run_dir.mkdir(parents=True, exist_ok=True)
308
- dataset_root = run_dir / "datasets"
309
- dataset_root.mkdir(parents=True, exist_ok=True)
310
- raw_dir = run_dir / "hyperfine-json"
311
- raw_dir.mkdir(parents=True, exist_ok=True)
312
-
313
- ryl_bin = resolve_tool_path("ryl")
314
- yamllint_bin = resolve_tool_path("yamllint")
315
- ryl_version = run_checked([str(ryl_bin), "--version"]).stdout.strip()
316
- yamllint_version = run_checked([str(yamllint_bin), "--version"]).stdout.strip()
317
-
318
- cases = iter_cases(file_counts_values, file_size_values, dataset_root)
319
- rows: list[dict[str, float | int | str]] = []
320
- for case_index, case in enumerate(tqdm(cases, desc="Benchmark cases", unit="case")):
321
- materialize_case(case, seed=seed + case_index * 100_000)
322
- case_json = (
323
- raw_dir / f"files_{case.file_count}__size_{case.file_size_kib}kib.json"
324
- )
325
- results = benchmark_case(
326
- case,
327
- ryl_bin=ryl_bin,
328
- yamllint_bin=yamllint_bin,
329
- runs=runs,
330
- warmup=warmup,
331
- output_json_path=case_json,
332
- )
333
- for tool in ("ryl", "yamllint"):
334
- row = {
335
- "tool": tool,
336
- "file_count": case.file_count,
337
- "file_size_kib": case.file_size_kib,
338
- "mean_seconds": float(results[tool]["mean"]),
339
- "stddev_seconds": float(results[tool]["stddev"]),
340
- "min_seconds": float(results[tool]["min"]),
341
- "max_seconds": float(results[tool]["max"]),
342
- }
343
- rows.append(row)
344
-
345
- results_df = pl.DataFrame(rows).select(
346
- [
347
- "tool",
348
- "file_count",
349
- "file_size_kib",
350
- "mean_seconds",
351
- "stddev_seconds",
352
- "min_seconds",
353
- "max_seconds",
354
- ]
355
- )
356
- csv_path = run_dir / "summary.csv"
357
- results_df.write_csv(csv_path)
358
-
359
- meta_path = run_dir / "meta.json"
360
- meta_path.write_bytes(
361
- orjson.dumps(
362
- {
363
- "generated_at_utc": timestamp,
364
- "ryl_version": ryl_version,
365
- "yamllint_version": yamllint_version,
366
- "runs": runs,
367
- "warmup": warmup,
368
- "file_counts": file_counts_values,
369
- "file_sizes_kib": file_size_values,
370
- },
371
- option=orjson.OPT_INDENT_2,
372
- )
373
- + b"\n"
374
- )
375
-
376
- plot_png = run_dir / "benchmark.png"
377
- plot_svg = run_dir / "benchmark.svg"
378
- plot_results(
379
- results_df,
380
- plot_png,
381
- plot_svg,
382
- runs=runs,
383
- ryl_version=ryl_version,
384
- yamllint_version=yamllint_version,
385
- )
386
-
387
- if not keep_datasets:
388
- shutil.rmtree(dataset_root)
389
-
390
- print(f"Benchmark complete. Artifacts: {run_dir}")
391
- print(f"Versions: {ryl_version}; {yamllint_version}")
392
- print(f"Plot: {plot_png}")
393
-
394
-
395
- if __name__ == "__main__":
396
- try:
397
- app()
398
- except KeyboardInterrupt:
399
- print("Interrupted.", file=sys.stderr)
400
- raise SystemExit(130)
@@ -1,80 +0,0 @@
1
- #!/usr/bin/env pwsh
2
- Set-StrictMode -Version Latest
3
- $ErrorActionPreference = 'Stop'
4
-
5
- function Fail([string]$Message) {
6
- [Console]::Error.WriteLine($Message)
7
- exit 1
8
- }
9
-
10
- function Require-Command([string]$Name) {
11
- if (-not (Get-Command -Name $Name -ErrorAction SilentlyContinue)) {
12
- Fail "$Name is required to run this script"
13
- }
14
- }
15
-
16
- $scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
17
- if (-not $scriptRoot) { $scriptRoot = Get-Location }
18
- $projectRoot = (Resolve-Path (Join-Path $scriptRoot '..')).Path
19
- $normalizedRoot = $projectRoot -replace '\\', '/'
20
- if (-not $normalizedRoot.EndsWith('/')) { $normalizedRoot += '/' }
21
-
22
- Require-Command 'cargo'
23
- Require-Command 'jq'
24
-
25
- $tmp = $null
26
- $locationPushed = $false
27
- try {
28
- Push-Location $projectRoot
29
- $locationPushed = $true
30
-
31
- & cargo llvm-cov nextest --summary-only *> $null
32
- if ($LASTEXITCODE -ne 0) {
33
- Fail 'cargo llvm-cov nextest --summary-only failed; inspect the output above for details.'
34
- }
35
-
36
- $tmp = [System.IO.Path]::GetTempFileName()
37
-
38
- & cargo llvm-cov report --json --output-path $tmp *> $null
39
- if ($LASTEXITCODE -ne 0) {
40
- Fail 'Failed to generate coverage report.'
41
- }
42
-
43
- $jqFilter = @'
44
- def ranges:
45
- sort
46
- | unique
47
- | reduce .[] as $line ([];
48
- if length > 0 and $line == (.[-1][1] + 1) then
49
- (.[-1] = [.[-1][0], $line])
50
- else
51
- . + [[ $line, $line ]]
52
- end)
53
- | map(if .[0] == .[1] then (.[0] | tostring) else "\(.[0])-\(.[1])" end);
54
-
55
- .data[].files[]
56
- | select(.summary.regions.percent < 100)
57
- | {file: (.filename | gsub("\\\\"; "/") | sub("^" + $prefix; "")),
58
- uncovered: [ .segments[]
59
- | select(.[2] == 0 and .[3] == true and .[5] == false)
60
- | .[0]
61
- ] }
62
- | select(.uncovered | length > 0)
63
- | "\(.file):\(.uncovered | ranges | join(","))"
64
- '@
65
-
66
- $report = (& jq -r --arg prefix $normalizedRoot $jqFilter $tmp)
67
-
68
- if ([string]::IsNullOrWhiteSpace($report)) {
69
- Write-Output 'Coverage OK: no uncovered regions.'
70
- } else {
71
- Write-Output 'Uncovered regions (file:path line ranges):'
72
- $report | ForEach-Object { Write-Output $_ }
73
- }
74
- }
75
- finally {
76
- if ($tmp -and (Test-Path -LiteralPath $tmp)) {
77
- Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue
78
- }
79
- if ($locationPushed) { Pop-Location }
80
- }