rerun-bench 0.1.0__py3-none-any.whl

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 (78) hide show
  1. rerun_bench/__init__.py +3 -0
  2. rerun_bench/__main__.py +3 -0
  3. rerun_bench/_bundled_tasks/add-cli-flag/solution/wc.py +33 -0
  4. rerun_bench/_bundled_tasks/add-cli-flag/task.toml +9 -0
  5. rerun_bench/_bundled_tasks/add-cli-flag/verify.py +48 -0
  6. rerun_bench/_bundled_tasks/add-cli-flag/workspace/wc.py +27 -0
  7. rerun_bench/_bundled_tasks/edit-config/solution/config/app.toml +14 -0
  8. rerun_bench/_bundled_tasks/edit-config/task.toml +9 -0
  9. rerun_bench/_bundled_tasks/edit-config/verify.py +36 -0
  10. rerun_bench/_bundled_tasks/edit-config/workspace/config/app.prod.toml +14 -0
  11. rerun_bench/_bundled_tasks/edit-config/workspace/config/app.toml +14 -0
  12. rerun_bench/_bundled_tasks/fix-failing-test/solution/stats.py +23 -0
  13. rerun_bench/_bundled_tasks/fix-failing-test/task.toml +8 -0
  14. rerun_bench/_bundled_tasks/fix-failing-test/verify.py +54 -0
  15. rerun_bench/_bundled_tasks/fix-failing-test/workspace/stats.py +21 -0
  16. rerun_bench/_bundled_tasks/fix-failing-test/workspace/test_stats.py +22 -0
  17. rerun_bench/_bundled_tasks/follow-agents-md/solution/CHANGES.md +10 -0
  18. rerun_bench/_bundled_tasks/follow-agents-md/solution/textutils.py +19 -0
  19. rerun_bench/_bundled_tasks/follow-agents-md/task.toml +9 -0
  20. rerun_bench/_bundled_tasks/follow-agents-md/verify.py +55 -0
  21. rerun_bench/_bundled_tasks/follow-agents-md/workspace/AGENTS.md +8 -0
  22. rerun_bench/_bundled_tasks/follow-agents-md/workspace/CHANGES.md +8 -0
  23. rerun_bench/_bundled_tasks/follow-agents-md/workspace/textutils.py +13 -0
  24. rerun_bench/_bundled_tasks/implement-lru-cache/solution/lru.py +44 -0
  25. rerun_bench/_bundled_tasks/implement-lru-cache/task.toml +8 -0
  26. rerun_bench/_bundled_tasks/implement-lru-cache/verify.py +58 -0
  27. rerun_bench/_bundled_tasks/implement-lru-cache/workspace/lru.py +31 -0
  28. rerun_bench/_bundled_tasks/implement-slugify/solution/textkit/slug.py +30 -0
  29. rerun_bench/_bundled_tasks/implement-slugify/task.toml +8 -0
  30. rerun_bench/_bundled_tasks/implement-slugify/verify.py +49 -0
  31. rerun_bench/_bundled_tasks/implement-slugify/workspace/textkit/__init__.py +0 -0
  32. rerun_bench/_bundled_tasks/implement-slugify/workspace/textkit/slug.py +21 -0
  33. rerun_bench/_bundled_tasks/minimal-fix/solution/legacy.py +33 -0
  34. rerun_bench/_bundled_tasks/minimal-fix/task.toml +10 -0
  35. rerun_bench/_bundled_tasks/minimal-fix/verify.py +77 -0
  36. rerun_bench/_bundled_tasks/minimal-fix/workspace/legacy.py +33 -0
  37. rerun_bench/_bundled_tasks/minimal-fix/workspace/report.py +10 -0
  38. rerun_bench/_bundled_tasks/multi-file-rename/solution/inventory/__init__.py +5 -0
  39. rerun_bench/_bundled_tasks/multi-file-rename/solution/inventory/cli.py +18 -0
  40. rerun_bench/_bundled_tasks/multi-file-rename/solution/inventory/orders.py +14 -0
  41. rerun_bench/_bundled_tasks/multi-file-rename/solution/inventory/pricing.py +12 -0
  42. rerun_bench/_bundled_tasks/multi-file-rename/task.toml +9 -0
  43. rerun_bench/_bundled_tasks/multi-file-rename/verify.py +51 -0
  44. rerun_bench/_bundled_tasks/multi-file-rename/workspace/inventory/__init__.py +5 -0
  45. rerun_bench/_bundled_tasks/multi-file-rename/workspace/inventory/cli.py +18 -0
  46. rerun_bench/_bundled_tasks/multi-file-rename/workspace/inventory/orders.py +14 -0
  47. rerun_bench/_bundled_tasks/multi-file-rename/workspace/inventory/pricing.py +12 -0
  48. rerun_bench/_bundled_tasks/refactor-extract-helper/solution/pricing.py +32 -0
  49. rerun_bench/_bundled_tasks/refactor-extract-helper/task.toml +9 -0
  50. rerun_bench/_bundled_tasks/refactor-extract-helper/verify.py +63 -0
  51. rerun_bench/_bundled_tasks/refactor-extract-helper/workspace/pricing.py +32 -0
  52. rerun_bench/_bundled_tasks/write-tests/mutants/accepts_empty.py +20 -0
  53. rerun_bench/_bundled_tasks/write-tests/mutants/drops_seconds.py +20 -0
  54. rerun_bench/_bundled_tasks/write-tests/mutants/hours_as_minutes.py +20 -0
  55. rerun_bench/_bundled_tasks/write-tests/mutants/minutes_wrong_factor.py +20 -0
  56. rerun_bench/_bundled_tasks/write-tests/mutants/no_strip.py +19 -0
  57. rerun_bench/_bundled_tasks/write-tests/solution/tests/test_duration.py +27 -0
  58. rerun_bench/_bundled_tasks/write-tests/task.toml +11 -0
  59. rerun_bench/_bundled_tasks/write-tests/verify.py +52 -0
  60. rerun_bench/_bundled_tasks/write-tests/workspace/duration.py +20 -0
  61. rerun_bench/_bundled_tasks/write-tests/workspace/tests/__init__.py +0 -0
  62. rerun_bench/adapters/__init__.py +31 -0
  63. rerun_bench/adapters/base.py +190 -0
  64. rerun_bench/adapters/claude.py +135 -0
  65. rerun_bench/adapters/codex.py +112 -0
  66. rerun_bench/adapters/mock.py +111 -0
  67. rerun_bench/adapters/opencode.py +74 -0
  68. rerun_bench/cli.py +235 -0
  69. rerun_bench/metrics.py +216 -0
  70. rerun_bench/report.py +316 -0
  71. rerun_bench/runner.py +278 -0
  72. rerun_bench/tasks.py +150 -0
  73. rerun_bench/workspace.py +122 -0
  74. rerun_bench-0.1.0.dist-info/METADATA +262 -0
  75. rerun_bench-0.1.0.dist-info/RECORD +78 -0
  76. rerun_bench-0.1.0.dist-info/WHEEL +4 -0
  77. rerun_bench-0.1.0.dist-info/entry_points.txt +2 -0
  78. rerun_bench-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,3 @@
1
+ """rerun-bench: same task, run N times. How consistent and how expensive is your coding agent?"""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,33 @@
1
+ """Count lines in files. Usage: python wc.py FILE [FILE ...]"""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+
7
+ def count_lines(text):
8
+ return len(text.splitlines())
9
+
10
+
11
+ def count_words(text):
12
+ return len(text.split())
13
+
14
+
15
+ def main(argv=None):
16
+ parser = argparse.ArgumentParser(description="Count lines in files.")
17
+ parser.add_argument("files", nargs="+")
18
+ parser.add_argument("--words", action="store_true", help="count words instead of lines")
19
+ args = parser.parse_args(argv)
20
+ counter = count_words if args.words else count_lines
21
+ total = 0
22
+ for name in args.files:
23
+ with open(name, encoding="utf-8") as fh:
24
+ n = counter(fh.read())
25
+ total += n
26
+ print(f"{n:>8} {name}")
27
+ if len(args.files) > 1:
28
+ print(f"{total:>8} total")
29
+ return 0
30
+
31
+
32
+ if __name__ == "__main__":
33
+ sys.exit(main())
@@ -0,0 +1,9 @@
1
+ id = "add-cli-flag"
2
+ title = "Add a flag to a small CLI without changing its default output"
3
+ prompt = """
4
+ Add a `--words` flag to wc.py. When it is given, print word counts (whitespace-separated
5
+ tokens) instead of line counts, in exactly the same output format. Without the flag the
6
+ output must stay exactly as it is now.
7
+ """
8
+ timeout = 600
9
+ tags = ["cli", "feature", "python"]
@@ -0,0 +1,48 @@
1
+ """Hidden checks for add-cli-flag."""
2
+
3
+ import subprocess
4
+ import sys
5
+ import tempfile
6
+ from pathlib import Path
7
+
8
+ ws = Path.cwd()
9
+
10
+
11
+ def fail(msg):
12
+ print("FAIL:", msg)
13
+ sys.exit(1)
14
+
15
+
16
+ def run(*args, cwd):
17
+ p = subprocess.run(
18
+ [sys.executable, str(ws / "wc.py"), *args],
19
+ cwd=cwd,
20
+ capture_output=True,
21
+ text=True,
22
+ timeout=60,
23
+ )
24
+ return p.returncode, p.stdout.replace("\r\n", "\n")
25
+
26
+
27
+ with tempfile.TemporaryDirectory() as d:
28
+ d = Path(d)
29
+ (d / "a.txt").write_text("one two three\nfour\n\nfive six\n", encoding="utf-8")
30
+ (d / "b.txt").write_text(" lonely \n", encoding="utf-8")
31
+ (d / "c.txt").write_text("", encoding="utf-8")
32
+ expect = {
33
+ ("a.txt",): " 4 a.txt\n",
34
+ ("a.txt", "b.txt", "c.txt"): " 4 a.txt\n 1 b.txt\n 0 c.txt\n"
35
+ " 5 total\n",
36
+ ("--words", "a.txt"): " 6 a.txt\n",
37
+ ("a.txt", "--words"): " 6 a.txt\n",
38
+ ("--words", "a.txt", "b.txt", "c.txt"): " 6 a.txt\n 1 b.txt\n"
39
+ " 0 c.txt\n 7 total\n",
40
+ }
41
+ for args, want in expect.items():
42
+ code, out = run(*args, cwd=d)
43
+ if code != 0 or out != want:
44
+ fail(f"wc.py {' '.join(args)}: exit {code}, output {out!r}, want {want!r}")
45
+ code, _ = run("--words", cwd=d)
46
+ if code == 0:
47
+ fail("wc.py --words with no files should exit non-zero")
48
+ print("PASS")
@@ -0,0 +1,27 @@
1
+ """Count lines in files. Usage: python wc.py FILE [FILE ...]"""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+
7
+ def count_lines(text):
8
+ return len(text.splitlines())
9
+
10
+
11
+ def main(argv=None):
12
+ parser = argparse.ArgumentParser(description="Count lines in files.")
13
+ parser.add_argument("files", nargs="+")
14
+ args = parser.parse_args(argv)
15
+ total = 0
16
+ for name in args.files:
17
+ with open(name, encoding="utf-8") as fh:
18
+ n = count_lines(fh.read())
19
+ total += n
20
+ print(f"{n:>8} {name}")
21
+ if len(args.files) > 1:
22
+ print(f"{total:>8} total")
23
+ return 0
24
+
25
+
26
+ if __name__ == "__main__":
27
+ sys.exit(main())
@@ -0,0 +1,14 @@
1
+ # Local development settings. Production values live in app.prod.toml.
2
+
3
+ [server]
4
+ host = "127.0.0.1"
5
+ port = 8081
6
+ workers = 4
7
+
8
+ [logging]
9
+ level = "debug"
10
+ format = "json"
11
+
12
+ [cors]
13
+ allowed_origins = ["https://example.com", "https://example.org"]
14
+ allow_credentials = false
@@ -0,0 +1,9 @@
1
+ id = "edit-config"
2
+ title = "Make three precise edits to a TOML config"
3
+ prompt = """
4
+ In config/app.toml: change the server port to 8081, set the logging level to "debug", and
5
+ add "https://example.org" to the CORS allowed origins while keeping the existing origin.
6
+ Leave every other setting, comment and file unchanged.
7
+ """
8
+ timeout = 300
9
+ tags = ["config", "toml", "precision"]
@@ -0,0 +1,36 @@
1
+ """Hidden checks for edit-config: exact target values, nothing else touched."""
2
+
3
+ import sys
4
+ import tomllib
5
+ from pathlib import Path
6
+
7
+ ws = Path.cwd()
8
+ ORIGINAL = Path(__file__).resolve().parent / "workspace"
9
+
10
+
11
+ def fail(msg):
12
+ print("FAIL:", msg)
13
+ sys.exit(1)
14
+
15
+
16
+ try:
17
+ cfg = tomllib.loads((ws / "config/app.toml").read_text(encoding="utf-8"))
18
+ except Exception as exc: # noqa: BLE001
19
+ fail(f"config/app.toml does not parse: {exc}")
20
+ want = {
21
+ "server": {"host": "127.0.0.1", "port": 8081, "workers": 4},
22
+ "logging": {"level": "debug", "format": "json"},
23
+ "cors": {
24
+ "allowed_origins": ["https://example.com", "https://example.org"],
25
+ "allow_credentials": False,
26
+ },
27
+ }
28
+ if cfg != want:
29
+ fail(f"config/app.toml values differ from the request:\n got {cfg}\n want {want}")
30
+ text = (ws / "config/app.toml").read_text(encoding="utf-8")
31
+ if "# Local development settings. Production values live in app.prod.toml." not in text:
32
+ fail("the header comment in config/app.toml was removed or changed")
33
+ prod_now = (ws / "config/app.prod.toml").read_bytes()
34
+ if prod_now != (ORIGINAL / "config/app.prod.toml").read_bytes():
35
+ fail("config/app.prod.toml was modified")
36
+ print("PASS")
@@ -0,0 +1,14 @@
1
+ # Production settings. Changes here need a change ticket.
2
+
3
+ [server]
4
+ host = "0.0.0.0"
5
+ port = 443
6
+ workers = 16
7
+
8
+ [logging]
9
+ level = "warning"
10
+ format = "json"
11
+
12
+ [cors]
13
+ allowed_origins = ["https://example.com"]
14
+ allow_credentials = true
@@ -0,0 +1,14 @@
1
+ # Local development settings. Production values live in app.prod.toml.
2
+
3
+ [server]
4
+ host = "127.0.0.1"
5
+ port = 8080
6
+ workers = 4
7
+
8
+ [logging]
9
+ level = "info"
10
+ format = "json"
11
+
12
+ [cors]
13
+ allowed_origins = ["https://example.com"]
14
+ allow_credentials = false
@@ -0,0 +1,23 @@
1
+ """Small descriptive statistics helpers."""
2
+
3
+
4
+ def mean(xs):
5
+ """Arithmetic mean. Raises ValueError on empty input."""
6
+ if not xs:
7
+ raise ValueError("mean of empty sequence")
8
+ return sum(xs) / len(xs)
9
+
10
+
11
+ def median(xs):
12
+ """Median of a sequence of numbers. Raises ValueError on empty input.
13
+
14
+ For an even number of values the median is the mean of the two middle values.
15
+ The input sequence is not modified.
16
+ """
17
+ if not xs:
18
+ raise ValueError("median of empty sequence")
19
+ s = sorted(xs)
20
+ mid = len(s) // 2
21
+ if len(s) % 2:
22
+ return s[mid]
23
+ return (s[mid - 1] + s[mid]) / 2
@@ -0,0 +1,8 @@
1
+ id = "fix-failing-test"
2
+ title = "Fix the bug behind a failing unit test"
3
+ prompt = """
4
+ Running `python -m unittest` in this directory shows a failing test.
5
+ Fix the bug in the source code so that all tests pass. Do not modify the test file.
6
+ """
7
+ timeout = 600
8
+ tags = ["bugfix", "python", "tests"]
@@ -0,0 +1,54 @@
1
+ """Hidden checks for fix-failing-test. Run with cwd = the agent's workspace."""
2
+
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ ORIGINAL = Path(__file__).resolve().parent / "workspace"
8
+ ws = Path.cwd()
9
+ sys.path.insert(0, str(ws))
10
+
11
+
12
+ def fail(msg):
13
+ print("FAIL:", msg)
14
+ sys.exit(1)
15
+
16
+
17
+ if (ws / "test_stats.py").read_bytes() != (ORIGINAL / "test_stats.py").read_bytes():
18
+ fail("test_stats.py was modified")
19
+
20
+ proc = subprocess.run(
21
+ [sys.executable, "-m", "unittest", "-q"], cwd=ws, capture_output=True, text=True, timeout=60
22
+ )
23
+ if proc.returncode != 0:
24
+ fail("visible tests fail:\n" + proc.stderr[-1500:])
25
+
26
+ from stats import mean, median # noqa: E402
27
+
28
+ cases = [
29
+ ([5], 5),
30
+ ([3, 1, 2], 2),
31
+ ([4, 1, 3, 2], 2.5),
32
+ ([1.0, 2.0], 1.5),
33
+ ([10, -10], 0),
34
+ ([7, 7, 7, 7], 7),
35
+ ([1, 2, 3, 4, 5, 6], 3.5),
36
+ ]
37
+ for xs, want in cases:
38
+ got = median(list(xs))
39
+ if got != want:
40
+ fail(f"median({xs}) = {got!r}, want {want!r}")
41
+ data = [3, 1, 2, 4]
42
+ median(data)
43
+ if data != [3, 1, 2, 4]:
44
+ fail("median mutated its input")
45
+ for fn in (mean, median):
46
+ try:
47
+ fn([])
48
+ except ValueError:
49
+ pass
50
+ else:
51
+ fail(f"{fn.__name__}([]) should raise ValueError")
52
+ if mean([1, 2, 3, 4]) != 2.5:
53
+ fail("mean changed behavior")
54
+ print("PASS")
@@ -0,0 +1,21 @@
1
+ """Small descriptive statistics helpers."""
2
+
3
+
4
+ def mean(xs):
5
+ """Arithmetic mean. Raises ValueError on empty input."""
6
+ if not xs:
7
+ raise ValueError("mean of empty sequence")
8
+ return sum(xs) / len(xs)
9
+
10
+
11
+ def median(xs):
12
+ """Median of a sequence of numbers. Raises ValueError on empty input.
13
+
14
+ For an even number of values the median is the mean of the two middle values.
15
+ The input sequence is not modified.
16
+ """
17
+ if not xs:
18
+ raise ValueError("median of empty sequence")
19
+ s = sorted(xs)
20
+ mid = len(s) // 2
21
+ return s[mid]
@@ -0,0 +1,22 @@
1
+ import unittest
2
+
3
+ from stats import mean, median
4
+
5
+
6
+ class StatsTest(unittest.TestCase):
7
+ def test_mean(self):
8
+ self.assertEqual(mean([1, 2, 3]), 2)
9
+
10
+ def test_median_odd(self):
11
+ self.assertEqual(median([3, 1, 2]), 2)
12
+
13
+ def test_median_even(self):
14
+ self.assertEqual(median([4, 1, 3, 2]), 2.5)
15
+
16
+ def test_median_empty(self):
17
+ with self.assertRaises(ValueError):
18
+ median([])
19
+
20
+
21
+ if __name__ == "__main__":
22
+ unittest.main()
@@ -0,0 +1,10 @@
1
+ # Changes
2
+
3
+ ## Unreleased
4
+
5
+ - Added `is_palindrome`.
6
+
7
+ ## 0.3.0
8
+
9
+ - Added `word_count`.
10
+ - `shout` now strips surrounding whitespace.
@@ -0,0 +1,19 @@
1
+ """Small text helpers."""
2
+
3
+ __all__ = ["is_palindrome", "shout", "word_count"]
4
+
5
+
6
+ def shout(text):
7
+ """Return ``text`` stripped and upper-cased with an exclamation mark."""
8
+ return text.strip().upper() + "!"
9
+
10
+
11
+ def word_count(text):
12
+ """Number of whitespace-separated words in ``text``."""
13
+ return len(text.split())
14
+
15
+
16
+ def is_palindrome(text):
17
+ """True if ``text`` reads the same both ways, ignoring case and non-alphanumerics."""
18
+ chars = [c.lower() for c in text if c.isalnum()]
19
+ return chars == chars[::-1]
@@ -0,0 +1,9 @@
1
+ id = "follow-agents-md"
2
+ title = "Add a function while following the repo's AGENTS.md conventions"
3
+ prompt = """
4
+ Add a function `is_palindrome(text)` to textutils.py. It returns True when the text reads
5
+ the same forwards and backwards, ignoring case and any character that is not a letter or
6
+ digit, and False otherwise.
7
+ """
8
+ timeout = 600
9
+ tags = ["instructions", "agents-md", "python"]
@@ -0,0 +1,55 @@
1
+ """Hidden checks for follow-agents-md: the feature works AND the AGENTS.md rules were followed."""
2
+
3
+ import re
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ ws = Path.cwd()
8
+ ORIGINAL = Path(__file__).resolve().parent / "workspace"
9
+ sys.path.insert(0, str(ws))
10
+
11
+
12
+ def fail(msg):
13
+ print("FAIL:", msg)
14
+ sys.exit(1)
15
+
16
+
17
+ import textutils # noqa: E402
18
+
19
+ fn = getattr(textutils, "is_palindrome", None)
20
+ if fn is None:
21
+ fail("is_palindrome not defined")
22
+ for text, want in [
23
+ ("racecar", True),
24
+ ("A man, a plan, a canal: Panama", True),
25
+ ("", True),
26
+ ("ab", False),
27
+ ("No 'x' in Nixon", True),
28
+ ("12321", True),
29
+ ("123", False),
30
+ ("Was it a car or a cat I saw?", True),
31
+ ("hello", False),
32
+ ]:
33
+ if fn(text) is not want:
34
+ fail(f"is_palindrome({text!r}) returned {fn(text)!r}, want {want}")
35
+ if not (fn.__doc__ or "").strip():
36
+ fail("AGENTS.md: is_palindrome needs a docstring")
37
+ allx = list(getattr(textutils, "__all__", []))
38
+ if "is_palindrome" not in allx:
39
+ fail("AGENTS.md: is_palindrome missing from __all__")
40
+ if allx != sorted(allx):
41
+ fail(f"AGENTS.md: __all__ not alphabetical: {allx}")
42
+ if textutils.shout(" hi ") != "HI!" or textutils.word_count("a b c") != 3:
43
+ fail("existing functions changed behavior")
44
+
45
+ changes = (ws / "CHANGES.md").read_text(encoding="utf-8").replace("\r\n", "\n")
46
+ m = re.search(r"^## Unreleased\n(.*?)(?=^## )", changes, re.S | re.M)
47
+ if not m:
48
+ fail("CHANGES.md: ## Unreleased section missing")
49
+ if not re.search(r"^[-*] .*`is_palindrome`", m.group(1), re.M):
50
+ fail("AGENTS.md: no bullet mentioning `is_palindrome` under ## Unreleased")
51
+ orig = (ORIGINAL / "CHANGES.md").read_text(encoding="utf-8").replace("\r\n", "\n")
52
+ released = orig[orig.index("## 0.3.0") :]
53
+ if released not in changes:
54
+ fail("CHANGES.md: a released section was edited")
55
+ print("PASS")
@@ -0,0 +1,8 @@
1
+ # Conventions for this repository
2
+
3
+ These rules apply to every change, however small.
4
+
5
+ - Every public function in `textutils.py` has a docstring.
6
+ - Every public function is listed in `__all__` in `textutils.py`, kept in alphabetical order.
7
+ - Every user-visible change gets a bullet under `## Unreleased` in `CHANGES.md`.
8
+ Mention the function name in backticks. Never edit released sections.
@@ -0,0 +1,8 @@
1
+ # Changes
2
+
3
+ ## Unreleased
4
+
5
+ ## 0.3.0
6
+
7
+ - Added `word_count`.
8
+ - `shout` now strips surrounding whitespace.
@@ -0,0 +1,13 @@
1
+ """Small text helpers."""
2
+
3
+ __all__ = ["shout", "word_count"]
4
+
5
+
6
+ def shout(text):
7
+ """Return ``text`` stripped and upper-cased with an exclamation mark."""
8
+ return text.strip().upper() + "!"
9
+
10
+
11
+ def word_count(text):
12
+ """Number of whitespace-separated words in ``text``."""
13
+ return len(text.split())
@@ -0,0 +1,44 @@
1
+ """A least-recently-used cache."""
2
+
3
+ from collections import OrderedDict
4
+
5
+
6
+ class LRUCache:
7
+ """Fixed-capacity mapping that evicts the least recently used key when full.
8
+
9
+ "Used" means read with ``get`` (a hit) or written with ``put``. ``len()`` and the
10
+ ``in`` operator do not count as uses.
11
+ """
12
+
13
+ def __init__(self, capacity):
14
+ """Create an empty cache. Raise ValueError unless ``capacity`` is an int >= 1."""
15
+ if not isinstance(capacity, int) or isinstance(capacity, bool) or capacity < 1:
16
+ raise ValueError("capacity must be an int >= 1")
17
+ self.capacity = capacity
18
+ self._data = OrderedDict()
19
+
20
+ def get(self, key, default=None):
21
+ """Return the value for ``key`` and mark it most recently used, else ``default``."""
22
+ if key not in self._data:
23
+ return default
24
+ self._data.move_to_end(key)
25
+ return self._data[key]
26
+
27
+ def put(self, key, value):
28
+ """Insert or update ``key`` and mark it most recently used.
29
+
30
+ If the cache then holds more than ``capacity`` keys, evict the least recently
31
+ used key and return it. Otherwise return None.
32
+ """
33
+ self._data[key] = value
34
+ self._data.move_to_end(key)
35
+ if len(self._data) > self.capacity:
36
+ evicted, _ = self._data.popitem(last=False)
37
+ return evicted
38
+ return None
39
+
40
+ def __len__(self):
41
+ return len(self._data)
42
+
43
+ def __contains__(self, key):
44
+ return key in self._data
@@ -0,0 +1,8 @@
1
+ id = "implement-lru-cache"
2
+ title = "Implement a small data structure to spec"
3
+ prompt = """
4
+ Implement the `LRUCache` class in lru.py according to its docstrings. Use only the Python
5
+ standard library.
6
+ """
7
+ timeout = 600
8
+ tags = ["implement", "data-structure", "python"]
@@ -0,0 +1,58 @@
1
+ """Hidden checks for implement-lru-cache."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ sys.path.insert(0, str(Path.cwd()))
7
+
8
+
9
+ def fail(msg):
10
+ print("FAIL:", msg)
11
+ sys.exit(1)
12
+
13
+
14
+ try:
15
+ from lru import LRUCache
16
+
17
+ for bad in (0, -3, 1.5, "2", None):
18
+ try:
19
+ LRUCache(bad)
20
+ except ValueError:
21
+ continue
22
+ fail(f"LRUCache({bad!r}) should raise ValueError")
23
+
24
+ c = LRUCache(2)
25
+ if c.put("a", 1) is not None or c.put("b", 2) is not None:
26
+ fail("put below capacity should return None")
27
+ if c.get("a") != 1:
28
+ fail("get a")
29
+ if c.put("c", 3) != "b":
30
+ fail("expected b evicted after a was read")
31
+ if "b" in c or c.get("b", "miss") != "miss":
32
+ fail("b should be gone")
33
+ if len(c) != 2:
34
+ fail("len should be 2")
35
+ if c.put("a", 10) is not None or c.get("a") != 10:
36
+ fail("updating an existing key should not evict and should store the new value")
37
+ if c.put("d", 4) != "c":
38
+ fail("expected c evicted (a was used more recently)")
39
+ _ = "a" in c
40
+ _ = len(c)
41
+ if c.put("e", 5) != "a":
42
+ fail("`in` and len() must not count as uses")
43
+ if c.get("missing") is not None:
44
+ fail("missing key should return None by default")
45
+
46
+ one = LRUCache(1)
47
+ one.put(1, "x")
48
+ if one.put(2, "y") != 1 or one.get(2) != "y" or len(one) != 1:
49
+ fail("capacity-1 behavior")
50
+
51
+ big = LRUCache(100)
52
+ for i in range(250):
53
+ big.put(i, i * i)
54
+ if len(big) != 100 or 149 in big or big.get(150) != 22500:
55
+ fail("bulk insert behavior")
56
+ except NotImplementedError:
57
+ fail("LRUCache is not implemented")
58
+ print("PASS")
@@ -0,0 +1,31 @@
1
+ """A least-recently-used cache."""
2
+
3
+
4
+ class LRUCache:
5
+ """Fixed-capacity mapping that evicts the least recently used key when full.
6
+
7
+ "Used" means read with ``get`` (a hit) or written with ``put``. ``len()`` and the
8
+ ``in`` operator do not count as uses.
9
+ """
10
+
11
+ def __init__(self, capacity):
12
+ """Create an empty cache. Raise ValueError unless ``capacity`` is an int >= 1."""
13
+ raise NotImplementedError
14
+
15
+ def get(self, key, default=None):
16
+ """Return the value for ``key`` and mark it most recently used, else ``default``."""
17
+ raise NotImplementedError
18
+
19
+ def put(self, key, value):
20
+ """Insert or update ``key`` and mark it most recently used.
21
+
22
+ If the cache then holds more than ``capacity`` keys, evict the least recently
23
+ used key and return it. Otherwise return None.
24
+ """
25
+ raise NotImplementedError
26
+
27
+ def __len__(self):
28
+ raise NotImplementedError
29
+
30
+ def __contains__(self, key):
31
+ raise NotImplementedError
@@ -0,0 +1,30 @@
1
+ """URL slug generation."""
2
+
3
+ import re
4
+ import unicodedata
5
+
6
+
7
+ def slugify(text: str, max_length: int = 50) -> str:
8
+ """Turn arbitrary text into a URL slug.
9
+
10
+ Rules, applied in this order:
11
+
12
+ 1. Normalize with Unicode NFKD and drop every character that is not ASCII
13
+ (so "Cafe" with an accent becomes "Cafe").
14
+ 2. Lowercase.
15
+ 3. Replace every run of characters that are not ASCII letters or digits with a
16
+ single hyphen.
17
+ 4. Strip leading and trailing hyphens.
18
+ 5. If the result is longer than ``max_length``, cut it to ``max_length`` characters
19
+ and strip any trailing hyphen left by the cut.
20
+ 6. If the result is empty, return "n-a".
21
+
22
+ ``max_length`` must be at least 1; otherwise raise ValueError.
23
+ """
24
+ if max_length < 1:
25
+ raise ValueError("max_length must be >= 1")
26
+ s = unicodedata.normalize("NFKD", text).encode("ascii", "ignore").decode("ascii")
27
+ s = re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-")
28
+ if len(s) > max_length:
29
+ s = s[:max_length].rstrip("-")
30
+ return s or "n-a"