Skip to content

Commit 1de2184

Browse files
committed
copilot refactor
1 parent 31df024 commit 1de2184

File tree

5 files changed

+184
-1
lines changed

5 files changed

+184
-1
lines changed

.github/copilot-instructions.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<!-- Copilot / AI agent instructions for the advent-of-code-python repository -->
2+
3+
# How to be productive in this repository (short, specific)
4+
5+
This repo contains per-day Advent of Code solutions under `src/advent_of_code/year_<YYYY>/day_<NN>.py` with test cases under `tests/year_<YYYY>/test_day_<NN>.py` and input files in `inputs/year_<YYYY>/<NN>.dat`.
6+
7+
Follow these concrete rules and patterns when making changes or adding new solutions:
8+
9+
- Entrypoint contract: each day module should expose a function `main(input_file)` which accepts a path to the input file (string) and performs/prints the solution. `scripts/run_day.py` dynamically imports `advent_of_code.year_<year>.day_<NN>` and calls `main(input_file)`.
10+
- Input handling: use `src/advent_of_code/utils/input_handling.py::read_input(f)` to load inputs as a list of stripped lines. Input files live at `inputs/year_<YYYY>/<NN>.dat` and use zero-padded two-digit day names (e.g. `01.dat`).
11+
- Filenames & modules: code files are named `day_01.py`, `day_02.py`, etc. The module import path is `advent_of_code.year_2025.day_01` (note the zero padding in the module filename).
12+
- Tests & pytest: tests are under `tests/year_<YYYY>/test_day_<NN>.py`. The project config (pyproject.toml) sets `pythonpath = "src"` and `--import-mode=importlib`, so prefer importable modules and avoid relying on working-directory-only imports.
13+
14+
Quick commands (examples you can run locally):
15+
16+
- Run one day (example for year 2025 day 1):
17+
python3 scripts/run_day.py --year 2025 --day 1
18+
19+
- Run all discovered solutions (skips days without input files):
20+
python3 scripts/run_all_solutions.py
21+
22+
- Generate a new day + test from templates (templates in `scripts/templates`):
23+
python3 scripts/generate_new_day_skeleton_files_from_templates.py --year 2025 --day 1
24+
25+
- Run tests:
26+
pytest
27+
28+
Key files to inspect when changing behavior:
29+
30+
- `scripts/run_day.py` — dynamic import + `main(input_file)` invocation. If you change the `main` contract, update this script.
31+
- `scripts/run_all_solutions.py` — discovery logic: scans `src/advent_of_code` for `year_*` directories and filenames matching `day_\d{2}\.py`. It will skip running days with no corresponding `inputs/year_<YYYY>/<NN>.dat` file.
32+
- `src/advent_of_code/utils/input_handling.py` — canonical helpers for reading inputs and parsing simple formats used across days.
33+
- `scripts/generate_new_day_skeleton_files_from_templates.py` and `scripts/templates/*` — used to scaffold new day modules and tests; templates include `{day}` and `{year}` placeholders.
34+
- `pyproject.toml` — pytest configuration (`pythonpath=src`, `--import-mode=importlib`), black formatting, and build backend (hatchling). Use these settings when adding CI or packaging.
35+
36+
Project-specific conventions and gotchas (be precise):
37+
38+
- Zero-padding is significant: day files and input files are named with two-digit zero padding (e.g., `day_01.py``01.dat`). `run_day.py` zero-pads the provided `--day` argument.
39+
- `run_all_solutions.py` relies on simple `os.listdir` ordering; do not assume nested directories beyond `src/advent_of_code/year_<YYYY>/`.
40+
- The repo uses `python3` in scripts; local development should use a Python 3.8+ interpreter (pyproject says >=3.8). Black config targets Python 3.9 but code is compatible with 3.8+.
41+
- Tests import the package from `src/` via pytest config. When editing tests or modules, ensure the module path `advent_of_code.year_<YYYY>.day_<NN>` is importable.
42+
43+
Small implementation checklist for a new/updated day module:
44+
45+
1. Create `src/advent_of_code/year_<YYYY>/day_<NN>.py` using templates or follow the `main(input_file)` contract.
46+
2. Add `inputs/year_<YYYY>/<NN>.dat` (zero-padded).
47+
3. Add/verify `tests/year_<YYYY>/test_day_<NN>.py` follows existing test style.
48+
4. Use `utils.read_input` where appropriate to keep input parsing consistent.
49+
5. Run `python3 scripts/run_day.py --year <YYYY> --day <N>` and `pytest`.
50+
51+
If you need to change a project-wide behavior (import rules, test config, discovery), update `pyproject.toml` and the scripts above; prefer small, backward-compatible changes because many scripts dynamically import day modules.
52+
53+
If anything here is unclear or you'd like the instructions to be stricter (for example, enforcing a return value contract instead of printing), tell me which style you'd prefer and I will update this file.

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,8 @@ addopts = [
3131

3232
[tool.black]
3333
line-length = 88
34-
target-version = ['py39']
34+
target-version = ['py39']
35+
36+
[project.scripts]
37+
# Console script for running day solutions: after `pip install -e .` you can run `aoc --year 2025 --day 1`
38+
aoc = "advent_of_code.cli:main"

src/advent_of_code/__main__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""Allow running the package with `python -m advent_of_code`.
2+
3+
Example:
4+
python -m advent_of_code --year 2025 --day 1
5+
6+
This file delegates to `advent_of_code.cli:main`.
7+
"""
8+
from .cli import main
9+
10+
11+
if __name__ == "__main__":
12+
raise SystemExit(main())

src/advent_of_code/cli.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""CLI entrypoint for the advent_of_code package.
2+
3+
Provides a small, modern CLI so the project can be run as:
4+
5+
- `python -m advent_of_code --year 2025 --day 1`
6+
- or, after editable install: `aoc --year 2025 --day 1`
7+
8+
This duplicates the behavior of `scripts/run_day.py` but lives inside the package.
9+
"""
10+
from __future__ import annotations
11+
12+
import argparse
13+
import importlib
14+
import sys
15+
from typing import Sequence, Optional
16+
17+
18+
def main(argv: Optional[Sequence[str]] = None) -> int:
19+
"""Console entry point.
20+
21+
Args:
22+
argv: list of args (None -> sys.argv[1:])
23+
24+
Returns:
25+
exit code (0 on success).
26+
"""
27+
parser = argparse.ArgumentParser(prog="aoc", description="Run an Advent of Code solution module")
28+
parser.add_argument("--year", required=True, type=int, help="Year directory (e.g. 2025)")
29+
parser.add_argument("--day", required=True, type=str, help="Day number (1 or 01)")
30+
31+
args = parser.parse_args(argv)
32+
33+
day_zero_padded_str = str(args.day).zfill(2)
34+
input_file = f"inputs/year_{args.year}/{day_zero_padded_str}.dat"
35+
36+
day_module = f"advent_of_code.year_{args.year}.day_{day_zero_padded_str}"
37+
38+
try:
39+
module = importlib.import_module(day_module)
40+
except ModuleNotFoundError as exc:
41+
print(f"Could not find module: {day_module}")
42+
# Helpful hint: if running from the repository root, make sure `src` is on PYTHONPATH or install the package editable
43+
print("Hint: run with `PYTHONPATH=src python -m advent_of_code ...` or `pip install -e .` to make the package importable")
44+
if isinstance(exc, ModuleNotFoundError):
45+
# show original message
46+
print(str(exc))
47+
return 1
48+
49+
if hasattr(module, "main"):
50+
try:
51+
module.main(input_file)
52+
return 0
53+
except Exception:
54+
# Bubble up stack trace for debugging convenience
55+
raise
56+
else:
57+
print(f"The module {day_module} does not have a 'main(input_file)' function.")
58+
return 2
59+
60+
61+
if __name__ == "__main__":
62+
raise SystemExit(main())

tests/year_2025/test_day_01.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import pytest
2+
from advent_of_code.year_2025.day_01 import (
3+
solve,
4+
turn_dial,
5+
)
6+
7+
8+
@pytest.fixture
9+
def day_01_test_input():
10+
return [
11+
"L68",
12+
"L30",
13+
"R48",
14+
"L5",
15+
"R60",
16+
"L55",
17+
"L1",
18+
"L99",
19+
"R14",
20+
"L82",
21+
]
22+
23+
24+
@pytest.fixture
25+
def day_01_expected_output():
26+
return (3, None)
27+
28+
29+
def test_solver(day_01_test_input, day_01_expected_output):
30+
result = solve(day_01_test_input)
31+
assert result == day_01_expected_output
32+
33+
@pytest.mark.parametrize(
34+
"current_position, turn_direction, distance, expected_new_position",
35+
[
36+
(50, -1, 68, 82),
37+
(82, -1, 30, 52),
38+
(52, 1, 48, 0),
39+
(0, -1, 5, 95),
40+
(95, 1, 60, 55),
41+
(55, -1, 55, 0),
42+
(0, -1, 1, 99),
43+
(99, -1, 99, 0),
44+
(0, 1, 14, 14),
45+
(14, -1, 82, 32),
46+
],
47+
)
48+
def test_turn_dial(current_position, turn_direction, distance, expected_new_position):
49+
new_position = turn_dial(current_position, turn_direction, distance)
50+
assert new_position == expected_new_position
51+
52+
def

0 commit comments

Comments
 (0)