Skip to content

Commit eb50cfd

Browse files
committed
Add support for multiple years
1 parent 861adb5 commit eb50cfd

File tree

7 files changed

+106
-50
lines changed

7 files changed

+106
-50
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
77
### Added
88
- Extend cli with possibility to specify directories for: templates, challenges solutions, and data
99
- Extend cli and `BaseChallenge` interface with possibility to specify custom input file
10+
- Add support for multiple years
1011

1112
### Fixed
1213
- Fixed typing in cli module

src/aoc/day_00/__init__.py

Lines changed: 0 additions & 16 deletions
This file was deleted.

src/aoc/input_providers.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import abc
2-
from dataclasses import dataclass
2+
from dataclasses import InitVar, dataclass
33
from pathlib import Path
44

55
from aoc.logger import logger
66

77

88
@dataclass
99
class InputProvider(abc.ABC):
10+
year: int
1011
day: int
1112

1213
@abc.abstractmethod
@@ -26,9 +27,10 @@ def _day(self) -> int:
2627
@dataclass
2728
class SmartFileInputProvider(InputProvider):
2829
use_test_data: bool = False
30+
data_dir: InitVar[Path | None] = None
2931

30-
def __post_init__(self):
31-
self._data_dir = Path(__file__).parents[2].joinpath("data")
32+
def __post_init__(self, data_dir: Path | None):
33+
self._data_dir = data_dir or Path(__file__).parents[2].joinpath("data")
3234

3335
def provide_input(self, part: int | None) -> str:
3436
filename = self.get_input_filename(part)
@@ -45,7 +47,7 @@ def get_input_filename(self, part: int | None = None) -> str:
4547
default_filename = f"{base_filename}.txt"
4648
if part is not None:
4749
filename = f"{base_filename}_part_{part}.txt"
48-
if self._data_dir.joinpath(filename).exists():
50+
if self._data_dir.joinpath(str(self.year), filename).exists():
4951
return filename
5052
else:
5153
logger.info(
@@ -57,7 +59,7 @@ def get_input_filename(self, part: int | None = None) -> str:
5759

5860
def get_input_file_path(self, filename: str) -> Path:
5961
"""Return the input filename for this challenge."""
60-
return self._data_dir.joinpath(filename)
62+
return self._data_dir.joinpath(str(self.year), filename)
6163

6264

6365
@dataclass

src/aoc/main.py

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import importlib
22
import shutil
3+
import typing
34
from datetime import datetime
45
from pathlib import Path
56
from typing import Annotated, Literal
@@ -26,6 +27,9 @@ def get_current_aoc_year():
2627
return now.year if now.month == 12 else now.year - 1
2728

2829

30+
current_aoc_year = get_current_aoc_year()
31+
32+
2933
def echo(text, fg=typer.colors.GREEN):
3034
typer.echo(
3135
typer.style(
@@ -35,16 +39,29 @@ def echo(text, fg=typer.colors.GREEN):
3539
)
3640

3741

38-
def import_challenge_module(day: int):
39-
module_name = f"aoc.day_{day:02}"
42+
year_option = Annotated[
43+
int,
44+
typer.Option(
45+
"-y",
46+
"--year",
47+
help="Year for which to get the puzzle data",
48+
),
49+
]
50+
51+
52+
def get_challenge_module_name(year: int, day: int) -> str:
53+
return f"aoc_solutions.{year}.day_{day:02}"
54+
55+
56+
def import_challenge_module(year, day: int):
4057
try:
41-
module = importlib.import_module(module_name)
58+
module = importlib.import_module(get_challenge_module_name(year, day))
4259
except ModuleNotFoundError as e:
4360
echo(
4461
f'Could not import "{e.name}"',
4562
fg=typer.colors.RED,
4663
)
47-
if e.name == module_name:
64+
if e.name == get_challenge_module_name:
4865
echo(
4966
f"You have not solved day {day} yet. Start it using 'new-day' command",
5067
fg=typer.colors.RED,
@@ -55,12 +72,20 @@ def import_challenge_module(day: int):
5572
return module
5673

5774

58-
def run_challenge(day: int, test_data: bool, input_path: Path | None = None):
59-
module = import_challenge_module(day)
75+
def run_challenge(
76+
year: int,
77+
day: int,
78+
test_data: bool,
79+
data_dir: Path | None,
80+
input_path: Path | None = None,
81+
):
82+
module = import_challenge_module(year, day)
6083
input_provider = (
61-
SingleFileInputProvider(day=day, input_path=input_path)
84+
SingleFileInputProvider(year=year, day=day, input_path=input_path)
6285
if input_path
63-
else SmartFileInputProvider(day=day, use_test_data=test_data)
86+
else SmartFileInputProvider(
87+
year=year, day=day, data_dir=data_dir, use_test_data=test_data
88+
)
6489
)
6590
if test_data:
6691
module.Challenge(input_provider=input_provider).run()
@@ -100,20 +125,31 @@ def write_input_data(puzzle, real_input_data):
100125
@app.command()
101126
def run(
102127
day: Annotated[int, typer.Argument(..., help="Day of the challenge to run.")],
128+
year: year_option = current_aoc_year,
129+
data_directory: Annotated[
130+
Path,
131+
typer.Option(
132+
help="Path to a directory with data. Will be used if you won't provide"
133+
" --file/-f option"
134+
),
135+
] = Path("data"),
103136
file: Annotated[
104-
Path | None, typer.Option(..., "--file", "-f", help="File to run.")
137+
typing.Optional[Path], typer.Option(..., "--file", "-f", help="File to run.") # noqa
105138
] = None,
106139
test_data: bool = typer.Option(
107140
False, "--test-data", "-t", help="Run challenge also for test data."
108141
),
109142
):
110143
"""Run the challenge."""
111-
run_challenge(day, test_data, file)
144+
run_challenge(year, day, test_data, data_directory, file)
112145

113146

114147
@app.command()
115148
def verify(
116-
day: int | None = typer.Argument(None, help="Day of the challenge to verify."),
149+
day: typing.Optional[int] = typer.Argument( # noqa: UP007
150+
None, help="Day of the challenge to verify."
151+
),
152+
year: year_option = current_aoc_year,
117153
part_one_only: bool = typer.Option(
118154
False, "--part-one", "-1", help="Verify only part one of the solution."
119155
),
@@ -135,7 +171,8 @@ def verify(
135171
if sum([part_one_only, part_two_only]) == 1:
136172
pytest_args.extend(["-k", "part_1" if part_one_only else "part_2"])
137173
if day is not None:
138-
module = import_challenge_module(day)
174+
module = import_challenge_module(year, day)
175+
print(module.__file__)
139176
if not module.__file__:
140177
typer.echo(
141178
typer.style(
@@ -192,7 +229,7 @@ def submit(
192229
),
193230
):
194231
validated_part = _full_validate(part)
195-
module = import_challenge_module(day)
232+
module = import_challenge_module(year, day)
196233
if validated_part == "a":
197234
solution = module.Challenge(use_test_data=False).part_1()
198235
else:
@@ -203,14 +240,7 @@ def submit(
203240
@app.command()
204241
def new_day(
205242
day: Annotated[int, typer.Argument(help="Day for which to create a directory.")],
206-
year: Annotated[
207-
int,
208-
typer.Option(
209-
"-y",
210-
"--year",
211-
help="Year for which to get the puzzle data",
212-
),
213-
] = get_current_aoc_year(),
243+
year: year_option = current_aoc_year,
214244
directory: Annotated[
215245
Path,
216246
typer.Option(
@@ -219,7 +249,7 @@ def new_day(
219249
"--directory",
220250
help="Path to a directory with challenges",
221251
),
222-
] = Path("src/aoc"),
252+
] = Path("."),
223253
data_directory: Annotated[
224254
Path,
225255
typer.Option(help="Path to a directory with data."),
@@ -245,7 +275,7 @@ def new_day(
245275
if day < 1 or day > 25:
246276
typer.echo(typer.style("Day must be between 1 and 25.", fg=typer.colors.RED))
247277
raise typer.Exit(1)
248-
destination = directory.joinpath(f"day_{day:02}")
278+
destination = directory.joinpath(f"aoc_solutions/{year}/day_{day:02}")
249279
if destination.exists() and not force:
250280
typer.echo(
251281
typer.style(
@@ -258,9 +288,9 @@ def new_day(
258288
typer.echo(
259289
typer.style(f"Created solution directory for day {day}.", fg=typer.colors.GREEN)
260290
)
261-
data_directory.mkdir(exist_ok=True)
291+
data_directory.joinpath(str(year)).mkdir(parents=True, exist_ok=True)
262292
if (
263-
real_input_data := data_directory.joinpath(f"{day:02}_input.txt")
293+
real_input_data := data_directory.joinpath(f"{year}/{day:02}_input.txt")
264294
).exists() and real_input_data.stat().st_size:
265295
typer.echo(
266296
typer.style(
@@ -275,7 +305,9 @@ def new_day(
275305
typer.style(f"Created input data for day {day}.", fg=typer.colors.GREEN)
276306
)
277307
if (
278-
test_input_data := data_directory.joinpath(Path(f"{day:02}_test_input.txt"))
308+
test_input_data := data_directory.joinpath(
309+
Path(f"{year}/{day:02}_test_input.txt")
310+
)
279311
).exists() and test_input_data.stat().st_size:
280312
typer.echo(
281313
typer.style(
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import sys
2+
from pathlib import Path
3+
4+
from aoc.base import BaseChallenge
5+
from aoc.input_providers import SingleFileInputProvider, SmartFileInputProvider
6+
7+
8+
class Challenge(BaseChallenge):
9+
"""Base class for all challenges."""
10+
11+
def part_1(self, input_lines: list[str]) -> int | str:
12+
return int(all(input_lines))
13+
14+
def part_2(self, input_lines: list[str]) -> int | str:
15+
print(input_lines)
16+
return sum(map(int, input_lines))
17+
18+
19+
if __name__ == "__main__":
20+
if len(sys.argv) > 1:
21+
input_provider = SingleFileInputProvider(
22+
Challenge.day, input_path=Path(sys.argv[1])
23+
)
24+
Challenge(input_provider).run()
25+
else:
26+
Challenge(SmartFileInputProvider(Challenge.day, use_test_data=True)).run()
27+
Challenge(SmartFileInputProvider(Challenge.day)).run()
File renamed without changes.

tests/test_cli.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ def test_setting_up_new_day_creates_directories_in_configured_places(
2727
[
2828
"new-day",
2929
"11",
30+
"--year",
31+
"2023",
3032
"--data-directory",
3133
str(tmp_path / "data"),
3234
"--directory",
@@ -37,9 +39,9 @@ def test_setting_up_new_day_creates_directories_in_configured_places(
3739
raise result.exception
3840
assert result.exit_code == 0, result.exc_info
3941

40-
assert (tmp_path / "data" / "11_input.txt").read_text() == "1"
41-
assert (tmp_path / "data" / "11_test_input.txt").read_text() == "2"
42-
assert (tmp_path / "src" / "day_11").exists()
42+
assert (tmp_path / "data" / "2023" / "11_input.txt").read_text() == "1"
43+
assert (tmp_path / "data" / "2023" / "11_test_input.txt").read_text() == "2"
44+
assert (tmp_path / "src" / "aoc_solutions" / "2023" / "day_11").exists()
4345

4446

4547
class TestRunningSolution:
@@ -48,6 +50,10 @@ def test_run_for_a_specific_day_with_default_data(self, tmp_path: Path):
4850
app,
4951
[
5052
"run",
53+
"--data-directory",
54+
str(Path(__file__).parent.parent / "data"),
55+
"--year",
56+
"2023",
5157
"0",
5258
],
5359
)
@@ -62,6 +68,8 @@ def test_run_for_a_specific_day_with_test_data(self, tmp_path: Path):
6268
app,
6369
[
6470
"run",
71+
"--data-directory",
72+
str(Path(__file__).parent.parent / "data"),
6573
"0",
6674
"-t",
6775
],
@@ -78,6 +86,8 @@ def test_run_for_a_specific_day_with_a_custom_file(self, tmp_path: Path):
7886
app,
7987
[
8088
"run",
89+
"--data-directory",
90+
str(Path(__file__).parent / "data"),
8191
"0",
8292
"-f",
8393
Path(__file__).parent / "data/custom_input.txt",

0 commit comments

Comments
 (0)