algobattle-base 4.3.2__tar.gz → 4.3.3__tar.gz

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 (90) hide show
  1. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/PKG-INFO +2 -4
  2. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/battle.py +25 -35
  3. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/cli.py +105 -97
  4. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/match.py +31 -30
  5. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/problem.py +16 -26
  6. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/program.py +49 -51
  7. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/__init__.py +5 -5
  8. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/types.py +43 -53
  9. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/util.py +11 -16
  10. algobattle_base-4.3.3/pyproject.toml +156 -0
  11. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/test_battles.py +7 -4
  12. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/test_docker.py +16 -3
  13. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/test_match.py +7 -6
  14. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/test_types.py +2 -2
  15. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/test_util.py +4 -4
  16. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/problem.py +2 -1
  17. algobattle_base-4.3.2/pyproject.toml +0 -63
  18. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/LICENSE +0 -0
  19. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/README.md +0 -0
  20. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/__init__.py +0 -0
  21. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/py.typed +0 -0
  22. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/c/.gitignore +0 -0
  23. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/c/Dockerfile +0 -0
  24. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/c/src/CMakeLists.txt +0 -0
  25. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/c/src/main.c +0 -0
  26. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/cpp/.gitignore +0 -0
  27. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/cpp/Dockerfile +0 -0
  28. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/cpp/src/CMakeLists.txt +0 -0
  29. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/cpp/src/main.cpp +0 -0
  30. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/csharp/Dockerfile.jinja +0 -0
  31. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/csharp/Program.cs +0 -0
  32. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/csharp/{{program.capitalize()}}.csproj +0 -0
  33. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/go/.gitignore +0 -0
  34. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/go/Dockerfile +0 -0
  35. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/go/go.mod.jinja +0 -0
  36. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/go/main.go +0 -0
  37. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/java/.dockerignore +0 -0
  38. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/java/.gitignore +0 -0
  39. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/java/Dockerfile.jinja +0 -0
  40. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/java/pom.xml.jinja +0 -0
  41. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/java/src/main/java/com/{{team_normalized}}/{{program.capitalize()}}.java.jinja +0 -0
  42. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/javascript/.gitignore +0 -0
  43. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/javascript/Dockerfile.jinja +0 -0
  44. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/javascript/package-lock.json.jinja +0 -0
  45. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/javascript/package.json.jinja +0 -0
  46. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/javascript/{{program}}.mjs.jinja +0 -0
  47. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/plain/Dockerfile.jinja +0 -0
  48. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/problem.py.jinja +0 -0
  49. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/python/.gitignore +0 -0
  50. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/python/Dockerfile.jinja +0 -0
  51. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/python/pyproject.toml.jinja +0 -0
  52. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/python/{{program}}.py.jinja +0 -0
  53. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/rust/.gitignore +0 -0
  54. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/rust/Cargo.toml.jinja +0 -0
  55. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/rust/Dockerfile.jinja +0 -0
  56. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/rust/src/main.rs.jinja +0 -0
  57. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/typescript/.gitignore +0 -0
  58. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/typescript/Dockerfile +0 -0
  59. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/typescript/package-lock.json.jinja +0 -0
  60. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/typescript/package.json.jinja +0 -0
  61. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/typescript/src/main.ts +0 -0
  62. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/algobattle/templates/typescript/tsconfig.json +0 -0
  63. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/README.md +0 -0
  64. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/__init__.py +0 -0
  65. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/configs/empty.toml +0 -0
  66. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/configs/teams.toml +0 -0
  67. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/configs/teams_incorrect.toml +0 -0
  68. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/configs/test.toml +0 -0
  69. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/README.md +0 -0
  70. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/__init__.py +0 -0
  71. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/build_error/Dockerfile +0 -0
  72. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/build_timeout/Dockerfile +0 -0
  73. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/generator/Dockerfile +0 -0
  74. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/generator/instance.json +0 -0
  75. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/generator_execution_error/Dockerfile +0 -0
  76. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/generator_semantics_error/Dockerfile +0 -0
  77. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/generator_semantics_error/instance.json +0 -0
  78. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/generator_syntax_error/Dockerfile +0 -0
  79. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/generator_syntax_error/instance.json +0 -0
  80. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/generator_timeout/Dockerfile +0 -0
  81. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/generator_timeout/instance.json +0 -0
  82. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/solver/Dockerfile +0 -0
  83. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/solver/solution.json +0 -0
  84. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/solver_execution_error/Dockerfile +0 -0
  85. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/solver_semantics_error/Dockerfile +0 -0
  86. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/solver_semantics_error/solution.json +0 -0
  87. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/solver_syntax_error/Dockerfile +0 -0
  88. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/solver_syntax_error/solution.json +0 -0
  89. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/solver_timeout/Dockerfile +0 -0
  90. {algobattle_base-4.3.2 → algobattle_base-4.3.3}/tests/testsproblem/solver_timeout/solution.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: algobattle-base
3
- Version: 4.3.2
3
+ Version: 4.3.3
4
4
  Summary: The Algobattle lab course package.
5
5
  Author: Imogen Hergeth, Jan Dreier, Henri Lotze
6
6
  License: MIT
@@ -22,9 +22,7 @@ Requires-Dist: typing-extensions>=4.15.0
22
22
  Requires-Dist: tomlkit>=0.13.3
23
23
  Requires-Dist: jinja2>=3.1.6
24
24
  Provides-Extra: dev
25
- Requires-Dist: black>=23.12.1; extra == "dev"
26
- Requires-Dist: flake8>=6.1.0; extra == "dev"
27
- Requires-Dist: flake8-docstrings>=1.7.0; extra == "dev"
25
+ Requires-Dist: ruff>=0.13.2; extra == "dev"
28
26
  Requires-Dist: mkdocs>=1.5.3; extra == "dev"
29
27
  Requires-Dist: mkdocs-material>=9.5.3; extra == "dev"
30
28
  Requires-Dist: pymdown-extensions>=10.7; extra == "dev"
@@ -3,10 +3,11 @@
3
3
  This module contains the :class:`Battle` class, which speciefies how each type of battle is fought and scored,
4
4
  some basic battle types, and related classed.
5
5
  """
6
+ from abc import abstractmethod
7
+ from collections.abc import Iterable
6
8
  from dataclasses import dataclass, field
7
9
  from enum import StrEnum
8
10
  from importlib.metadata import entry_points
9
- from abc import abstractmethod
10
11
  from inspect import isclass
11
12
  from itertools import count
12
13
  from pathlib import Path
@@ -16,7 +17,6 @@ from typing import (
16
17
  Annotated,
17
18
  Any,
18
19
  ClassVar,
19
- Iterable,
20
20
  Literal,
21
21
  ParamSpec,
22
22
  Protocol,
@@ -26,9 +26,8 @@ from typing import (
26
26
  Unpack,
27
27
  overload,
28
28
  )
29
- from typing_extensions import TypedDict
30
- from annotated_types import Ge
31
29
 
30
+ from annotated_types import Ge
32
31
  from pydantic import (
33
32
  ConfigDict,
34
33
  Field,
@@ -39,11 +38,10 @@ from pydantic import (
39
38
  ValidatorFunctionWrapHandler,
40
39
  )
41
40
  from pydantic_core import CoreSchema
42
- from pydantic_core.core_schema import (
43
- tagged_union_schema,
44
- with_info_wrap_validator_function,
45
- )
41
+ from pydantic_core.core_schema import tagged_union_schema, with_info_wrap_validator_function
42
+ from typing_extensions import TypedDict
46
43
 
44
+ from algobattle.problem import InstanceModel, Problem, SolutionModel
47
45
  from algobattle.program import (
48
46
  Generator,
49
47
  GeneratorResult,
@@ -53,15 +51,7 @@ from algobattle.program import (
53
51
  Solver,
54
52
  SolverResult,
55
53
  )
56
- from algobattle.problem import InstanceModel, Problem, SolutionModel
57
- from algobattle.util import (
58
- Encodable,
59
- EncodableModel,
60
- ExceptionInfo,
61
- BaseModel,
62
- Role,
63
- )
64
-
54
+ from algobattle.util import BaseModel, Encodable, EncodableModel, ExceptionInfo, Role
65
55
 
66
56
  _BattleConfig: TypeAlias = Any
67
57
  """Type alias used to generate correct typings when subclassing :class:`Battle`.
@@ -73,7 +63,7 @@ the new battle type directly.
73
63
  """
74
64
  T = TypeVar("T")
75
65
  P = ParamSpec("P")
76
- Type = type
66
+ _type = type
77
67
 
78
68
 
79
69
  class ProgramLogConfigTime(StrEnum):
@@ -91,7 +81,7 @@ class ProgramLogConfigLocation(StrEnum):
91
81
  inline = "inline"
92
82
 
93
83
 
94
- class ProgramLogConfigView(Protocol): # noqa: D101
84
+ class ProgramLogConfigView(Protocol):
95
85
  when: ProgramLogConfigTime = ProgramLogConfigTime.error
96
86
  output: ProgramLogConfigLocation = ProgramLogConfigLocation.inline
97
87
 
@@ -100,7 +90,7 @@ class ProgramRunInfo(BaseModel):
100
90
  """Data about a program's execution."""
101
91
 
102
92
  runtime: float = 0
103
- overriden: RunConfigOverride = Field(default_factory=dict)
93
+ overriden: RunConfigOverride = Field(default_factory=RunConfigOverride, validate_default=True)
104
94
  error: ExceptionInfo | None = None
105
95
  battle_data: SerializeAsAny[EncodableModel] | None = None
106
96
  instance: SerializeAsAny[InstanceModel] | None = None
@@ -177,7 +167,7 @@ class FightUi(ProgramUi, Protocol):
177
167
  """Informs the ui that the fight has finished running and has been added to the battle's `.fight_results`."""
178
168
 
179
169
 
180
- class RunKwargs(TypedDict, total=False):
170
+ class RunKwargs(TypedDict, total=False, closed=True):
181
171
  """The keyword arguments used by the FightHandler.run family of functions."""
182
172
 
183
173
  timeout_generator: float | None
@@ -394,7 +384,7 @@ class Battle(BaseModel):
394
384
  """Type of battle that will be used."""
395
385
 
396
386
  @classmethod
397
- def __get_pydantic_core_schema__(cls, source: Type, handler: GetCoreSchemaHandler) -> CoreSchema:
387
+ def __get_pydantic_core_schema__(cls, source: _type, handler: GetCoreSchemaHandler) -> CoreSchema:
398
388
  # there's two bugs we need to catch:
399
389
  # 1. this function is called during the pydantic BaseModel metaclass's __new__, so the BattleConfig class
400
390
  # won't be ready at that point and be missing in the namespace
@@ -446,7 +436,7 @@ class Battle(BaseModel):
446
436
  installed = ", ".join(b.name() for b in Battle._battle_types.values())
447
437
  raise ValueError(
448
438
  f"The specified battle type '{passed}' is not installed. Installed types are: {installed}"
449
- )
439
+ ) from e
450
440
 
451
441
  return with_info_wrap_validator_function(check_installed, subclass_schema)
452
442
 
@@ -459,7 +449,7 @@ class Battle(BaseModel):
459
449
 
460
450
  if TYPE_CHECKING:
461
451
  # to hint that we're gonna fill this with arbitrary data belonging to some supposed battle type
462
- def __getattr__(self, __attr: str) -> Any:
452
+ def __getattr__(self, attr: str, /) -> Any:
463
453
  ...
464
454
 
465
455
  class UiData(BaseModel):
@@ -560,7 +550,7 @@ class Iterated(Battle):
560
550
  exit after that many failures, or `"unlimited"` to never exit early.
561
551
  """
562
552
 
563
- class UiData(Battle.UiData): # noqa: D106
553
+ class UiData(Battle.UiData):
564
554
  reached: list[int]
565
555
  cap: int
566
556
  note: str
@@ -620,7 +610,7 @@ class Iterated(Battle):
620
610
  return 0 if len(self.results) == 0 else sum(self.results) / len(self.results)
621
611
 
622
612
  @staticmethod
623
- def format_score(score: float) -> str: # noqa: D102
613
+ def format_score(score: float) -> str:
624
614
  return str(int(score))
625
615
 
626
616
 
@@ -637,7 +627,7 @@ class Averaged(Battle):
637
627
  num_fights: int = 10
638
628
  """Number of iterations in each round."""
639
629
 
640
- class UiData(Battle.UiData): # noqa: D106
630
+ class UiData(Battle.UiData):
641
631
  round: int
642
632
 
643
633
  async def run_battle(self, fight: FightHandler, config: Config, min_size: int, ui: BattleUi) -> None:
@@ -659,7 +649,7 @@ class Averaged(Battle):
659
649
  return sum(f.score for f in self.fights) / len(self.fights)
660
650
 
661
651
  @staticmethod
662
- def format_score(score: float) -> str: # noqa: D102
652
+ def format_score(score: float) -> str:
663
653
  return format(score, ".0%")
664
654
 
665
655
 
@@ -681,7 +671,7 @@ class FightHistory(Encodable):
681
671
  gen_sols: set[Role]
682
672
  sol_sols: set[Role]
683
673
 
684
- def encode(self, target: Path, role: Role) -> None: # noqa: D102
674
+ def encode(self, target: Path, role: Role) -> None:
685
675
  target.mkdir()
686
676
  for i, fight in enumerate(self.history):
687
677
  fight_dir = target / str(i)
@@ -715,16 +705,16 @@ class Improving(Battle):
715
705
  """Number of fights that will be fought."""
716
706
  weighting: Annotated[float, Ge(0)] = 1.1
717
707
  """How much each successive fight should be weighted more than the previous."""
718
- scores: set[Role] = {Role.generator, Role.solver}
708
+ scores: set[Role] = {Role.generator, Role.solver} # noqa: RUF012
719
709
  """Who to show each fight's scores to."""
720
- instances: set[Role] = {Role.generator, Role.solver}
710
+ instances: set[Role] = {Role.generator, Role.solver} # noqa: RUF012
721
711
  """Who to show the instances to."""
722
- generator_solutions: set[Role] = {Role.generator}
712
+ generator_solutions: set[Role] = {Role.generator} # noqa: RUF012
723
713
  """Who to show the generator's solutions to, if the problem requires them."""
724
- solver_solutions: set[Role] = {Role.solver}
714
+ solver_solutions: set[Role] = {Role.solver} # noqa: RUF012
725
715
  """Who to show the solver's solutions to."""
726
716
 
727
- class UiData(Battle.UiData): # noqa: D106
717
+ class UiData(Battle.UiData):
728
718
  round: int
729
719
 
730
720
  async def run_battle(self, fight: FightHandler, config: Config, min_size: int, ui: BattleUi) -> None:
@@ -760,5 +750,5 @@ class Improving(Battle):
760
750
  return total / quotient
761
751
 
762
752
  @staticmethod
763
- def format_score(score: float) -> str: # noqa: D102
753
+ def format_score(score: float) -> str:
764
754
  return format(score, ".0%")
@@ -2,68 +2,69 @@
2
2
 
3
3
  Provides a command line interface to start matches and observe them. See `battle --help` for further options.
4
4
  """
5
+
6
+ import operator
7
+ import shutil
8
+ import sys
9
+ from collections.abc import Iterable
5
10
  from enum import StrEnum
6
11
  from functools import cached_property
7
- import operator
12
+ from importlib.metadata import version as pkg_version
8
13
  from os import environ
9
14
  from pathlib import Path
10
15
  from random import choice
11
16
  from shutil import rmtree
12
17
  from subprocess import PIPE, Popen
13
- import sys
14
- from typing import Annotated, Any, ClassVar, Iterable, Literal, Optional, Self, cast
15
- from typing_extensions import override
16
- from importlib.metadata import version as pkg_version
18
+ from typing import Annotated, Any, ClassVar, Literal, Self, cast
17
19
  from zipfile import ZipFile
18
- import shutil
19
20
 
20
21
  from anyio import run as run_async_fn
21
22
  from click import Choice
22
- from click.core import Parameter
23
+ from click.core import Context, Parameter
23
24
  from pydantic import Field, TypeAdapter, ValidationError
24
- from typer import Typer, Argument, Option, Abort, get_app_dir, launch
25
- from rich.console import Group, RenderableType, Console
25
+ from rich.columns import Columns
26
+ from rich.console import Console, Group, RenderableType
26
27
  from rich.live import Live
27
- from rich.table import Table, Column
28
+ from rich.padding import Padding
29
+ from rich.panel import Panel
28
30
  from rich.progress import (
29
- Progress,
30
- TextColumn,
31
- SpinnerColumn,
32
31
  BarColumn,
33
32
  MofNCompleteColumn,
34
- TimeElapsedColumn,
33
+ Progress,
35
34
  ProgressColumn,
35
+ SpinnerColumn,
36
36
  Task,
37
+ TextColumn,
38
+ TimeElapsedColumn,
37
39
  )
38
- from rich.panel import Panel
40
+ from rich.prompt import Confirm, Prompt
41
+ from rich.rule import Rule
42
+ from rich.table import Column, Table
39
43
  from rich.text import Text
40
- from rich.columns import Columns
41
- from rich.prompt import Prompt, Confirm
42
44
  from rich.theme import Theme
43
- from rich.rule import Rule
44
- from rich.padding import Padding
45
45
  from rich.traceback import Traceback
46
- from tomlkit import TOMLDocument, comment, parse as parse_toml, dumps as dumps_toml, table, nl as toml_newline
46
+ from tomlkit import TOMLDocument, comment, dumps as dumps_toml, nl as toml_newline, parse as parse_toml, table
47
47
  from tomlkit.exceptions import ParseError
48
48
  from tomlkit.items import Table as TomlTable
49
+ from typer import Abort, Argument, Option, Typer, get_app_dir, launch
50
+ from typing_extensions import override
49
51
 
50
52
  from algobattle.battle import Battle
51
- from algobattle.match import AlgobattleConfig, EmptyUi, Match, MatchConfig, MatchupStr, TeamInfo, Ui, ProjectConfig
53
+ from algobattle.match import AlgobattleConfig, EmptyUi, Match, MatchConfig, MatchupStr, ProjectConfig, TeamInfo, Ui
52
54
  from algobattle.problem import Instance, Problem, Solution
53
55
  from algobattle.program import Generator, Matchup, Solver
56
+ from algobattle.templates import Language, PartialTemplateArgs, TemplateArgs, write_problem_template, write_templates
54
57
  from algobattle.util import (
58
+ BaseModel,
55
59
  BuildError,
56
60
  DockerNotRunning,
57
61
  EncodableModel,
58
62
  ExceptionInfo,
59
63
  Role,
60
64
  RunningTimer,
61
- BaseModel,
62
65
  TempDir,
63
66
  timestamp,
64
67
  )
65
- from algobattle.templates import Language, PartialTemplateArgs, TemplateArgs, write_problem_template, write_templates
66
-
67
68
 
68
69
  __all__ = ("app",)
69
70
 
@@ -75,16 +76,14 @@ For more detailed documentation, visit our website at http://algobattle.org/docs
75
76
  app = Typer(pretty_exceptions_show_locals=True, help=help_message)
76
77
  packager = Typer(help="Subcommands to package problems and programs into `.algo` files.")
77
78
  app.add_typer(packager, name="package")
78
- theme = Theme(
79
- {
80
- "success": "green",
81
- "warning": "orange3",
82
- "error": "red",
83
- "attention": "magenta2",
84
- "heading": "blue",
85
- "info": "dim cyan",
86
- }
87
- )
79
+ theme = Theme({
80
+ "success": "green",
81
+ "warning": "orange3",
82
+ "error": "red",
83
+ "attention": "magenta2",
84
+ "heading": "blue",
85
+ "info": "dim cyan",
86
+ })
88
87
  console = Console(theme=theme)
89
88
 
90
89
 
@@ -101,7 +100,7 @@ class _General(BaseModel):
101
100
 
102
101
 
103
102
  class CliConfig(BaseModel):
104
- general: _General = Field(default_factory=dict, validate_default=True)
103
+ general: _General = Field(default_factory=_General, validate_default=True)
105
104
  default_project_table: ProjectConfig | None = Field(default=None)
106
105
 
107
106
  _doc: TOMLDocument
@@ -138,8 +137,7 @@ class CliConfig(BaseModel):
138
137
  @property
139
138
  def default_project_doc(self) -> TomlTable | None:
140
139
  """The default exec config for each problem."""
141
- exec: Any = self._doc.get("default_project_table", None)
142
- return exec
140
+ return self._doc.get("default_project_table", None)
143
141
 
144
142
  @cached_property
145
143
  def install_cmd(self) -> list[str]:
@@ -167,6 +165,7 @@ class CliConfig(BaseModel):
167
165
 
168
166
  @app.command("run")
169
167
  def run_match(
168
+ *,
170
169
  path: Annotated[
171
170
  Path, Argument(exists=True, help="Path to either a config file or a directory containing one.")
172
171
  ] = Path(),
@@ -184,27 +183,25 @@ def run_match(
184
183
  save = False
185
184
  except KeyboardInterrupt:
186
185
  console.print("[error]Stopping match execution")
187
- finally:
188
- try:
189
- if config.project.points > 0 and result.active_teams:
190
- points = result.calculate_points()
191
- leaderboard = Table(
192
- Column("Team", justify="center"),
193
- Column("Points", justify="right"),
194
- title="[heading]Leaderboard",
195
- )
196
- for team, pts in sorted(points.items(), key=operator.itemgetter(1)):
197
- leaderboard.add_row(team, f"{pts:.1f}")
198
- console.print(Padding(leaderboard, (1, 0, 0, 0)))
199
-
200
- if save:
201
- out_path = config.project.results.joinpath(f"match-{timestamp()}.json")
202
- config.project.results.mkdir(parents=True, exist_ok=True)
203
- out_path.write_text(result.format(error_detail=config.project.error_detail))
204
- console.print("Saved match result to ", out_path)
205
- return result
206
- except KeyboardInterrupt:
207
- raise Abort
186
+ try:
187
+ if config.project.points > 0 and result.active_teams:
188
+ points = result.calculate_points()
189
+ leaderboard = Table(
190
+ Column("Team", justify="center"),
191
+ Column("Points", justify="right"),
192
+ title="[heading]Leaderboard",
193
+ )
194
+ for team, pts in sorted(points.items(), key=operator.itemgetter(1)):
195
+ leaderboard.add_row(team, f"{pts:.1f}")
196
+ console.print(Padding(leaderboard, (1, 0, 0, 0)))
197
+ if save:
198
+ out_path = config.project.results.joinpath(f"match-{timestamp()}.json")
199
+ config.project.results.mkdir(parents=True, exist_ok=True)
200
+ out_path.write_text(result.format(error_detail=config.project.error_detail))
201
+ console.print("Saved match result to ", out_path)
202
+ except KeyboardInterrupt as e:
203
+ raise Abort from e
204
+ return result
208
205
 
209
206
 
210
207
  def _init_program(target: Path, lang: Language, args: PartialTemplateArgs, role: Role) -> None:
@@ -233,17 +230,15 @@ class ClickLanguage(Choice):
233
230
  def __init__(self, case_sensitive: bool = True) -> None:
234
231
  super().__init__([lang.value for lang in Language], case_sensitive)
235
232
 
236
- def get_metavar(self, param: Parameter) -> str:
233
+ def get_metavar(self, param: Parameter, ctx: Context) -> str:
237
234
  return "LANGUAGE"
238
235
 
239
236
 
240
237
  @app.command(epilog=f"Supported languages are: {', '.join(Language)}.")
241
238
  def init(
242
- target: Annotated[
243
- Optional[Path], Argument(file_okay=False, writable=True, help="The folder to initialize.")
244
- ] = None,
239
+ target: Annotated[Path | None, Argument(file_okay=False, writable=True, help="The folder to initialize.")] = None,
245
240
  problem_: Annotated[
246
- Optional[str],
241
+ str | None,
247
242
  Option(
248
243
  "--problem",
249
244
  "-p",
@@ -251,15 +246,15 @@ def init(
251
246
  ),
252
247
  ] = None,
253
248
  language: Annotated[
254
- Optional[Language],
249
+ Language | None,
255
250
  Option("--language", "-l", help="The language to use for the programs.", click_type=ClickLanguage()),
256
251
  ] = None,
257
252
  generator: Annotated[
258
- Optional[Language],
253
+ Language | None,
259
254
  Option("--generator", "-g", help="The language to use for the generator.", click_type=ClickLanguage()),
260
255
  ] = None,
261
256
  solver: Annotated[
262
- Optional[Language],
257
+ Language | None,
263
258
  Option("--solver", "-s", help="The language to use for the solver.", click_type=ClickLanguage()),
264
259
  ] = None,
265
260
  schemas: Annotated[bool, Option(help="Whether to also save the problem's IO schemas.")] = False,
@@ -282,10 +277,28 @@ def init(
282
277
  if language:
283
278
  generator = solver = language
284
279
  config = CliConfig.load()
285
- team_name = config.general.team_name or choice(
286
- ("Dogs", "Cats", "Otters", "Red Pandas", "Crows", "Rats", "Cockatoos", "Dingos", "Penguins", "Kiwis", "Orcas")
287
- + ("Bearded Dragons", "Macaws", "Wombats", "Wallabies", "Owls", "Seals", "Octopuses", "Frogs", "Jellyfish")
288
- )
280
+ team_name = config.general.team_name or choice((
281
+ "Dogs",
282
+ "Cats",
283
+ "Otters",
284
+ "Red Pandas",
285
+ "Crows",
286
+ "Rats",
287
+ "Cockatoos",
288
+ "Dingos",
289
+ "Penguins",
290
+ "Kiwis",
291
+ "Orcas",
292
+ "Bearded Dragons",
293
+ "Macaws",
294
+ "Wombats",
295
+ "Wallabies",
296
+ "Owls",
297
+ "Seals",
298
+ "Octopuses",
299
+ "Frogs",
300
+ "Jellyfish",
301
+ ))
289
302
 
290
303
  if new: # create a new problem
291
304
  if problem_ is None:
@@ -304,12 +317,12 @@ def init(
304
317
  target = Path()
305
318
  try:
306
319
  parsed_config = AlgobattleConfig.from_file(target, relativize_paths=False)
307
- except FileNotFoundError:
320
+ except FileNotFoundError as e:
308
321
  console.print("[error]You must use a problem spec file or target a directory with an existing config.")
309
- raise Abort
322
+ raise Abort from e
310
323
  except ValueError as e:
311
324
  console.print("[error]The Algobattle config file is not formatted properly\n", e)
312
- raise Abort
325
+ raise Abort from e
313
326
  console.print("Using existing project data")
314
327
  if len(parsed_config.teams) == 1:
315
328
  team_name = next(iter(parsed_config.teams.keys()))
@@ -323,9 +336,8 @@ def init(
323
336
 
324
337
  elif (problem := Path(problem_)).is_file(): # use a problem spec file
325
338
  with TempDir() as unpack_dir:
326
- with console.status("Extracting problem data"):
327
- with ZipFile(problem) as problem_zip:
328
- problem_zip.extractall(unpack_dir)
339
+ with console.status("Extracting problem data"), ZipFile(problem) as problem_zip:
340
+ problem_zip.extractall(unpack_dir)
329
341
 
330
342
  parsed_config = AlgobattleConfig.from_file(unpack_dir, relativize_paths=False)
331
343
  if target is None:
@@ -333,7 +345,7 @@ def init(
333
345
 
334
346
  target.mkdir(parents=True, exist_ok=True)
335
347
  problem_data = list(unpack_dir.iterdir())
336
- if any(((target / path.name).exists() for path in problem_data)):
348
+ if any(target.joinpath(path.name).exists() for path in problem_data):
337
349
  copy_problem_data = Confirm.ask(
338
350
  "[attention]The target directory already contains an algobattle project, "
339
351
  "do you want to replace it?",
@@ -363,9 +375,10 @@ def init(
363
375
  problem_name = parsed_config.match.problem
364
376
  if deps := parsed_config.problem.dependencies:
365
377
  cmd = config.install_cmd
366
- with console.status(f"Installing {problem_name}'s dependencies"), Popen(
367
- cmd + deps, env=environ.copy(), stdout=PIPE, stderr=PIPE, text=True
368
- ) as installer:
378
+ with (
379
+ console.status(f"Installing {problem_name}'s dependencies"),
380
+ Popen(cmd + deps, env=environ.copy(), stdout=PIPE, stderr=PIPE, text=True) as installer,
381
+ ):
369
382
  assert installer.stdout is not None
370
383
  assert installer.stderr is not None
371
384
  for line in installer.stdout:
@@ -506,7 +519,7 @@ def test_team(config: AlgobattleConfig, team: str, size: int | None = None) -> T
506
519
  @app.command()
507
520
  def test(
508
521
  project: Annotated[Path, Argument(help="The project folder to use.")] = Path(),
509
- size: Annotated[Optional[int], Option(help="The size of instance the generator will be asked to create.")] = None,
522
+ size: Annotated[int | None, Option(help="The size of instance the generator will be asked to create.")] = None,
510
523
  ) -> Literal["success", "error"]:
511
524
  """Tests whether the programs install successfully and run on dummy instances without crashing."""
512
525
  if not (project.is_file() or project.joinpath("algobattle.toml").is_file()):
@@ -542,10 +555,10 @@ def config() -> None:
542
555
  def package_problem(
543
556
  project: Annotated[Path, Argument(exists=True, resolve_path=True, help="Path to the project directory.")] = Path(),
544
557
  description: Annotated[
545
- Optional[Path], Option(exists=True, dir_okay=False, help="Path to a problem description file.")
558
+ Path | None, Option(exists=True, dir_okay=False, help="Path to a problem description file.")
546
559
  ] = None,
547
560
  out: Annotated[
548
- Optional[Path], Option("--out", "-o", dir_okay=False, file_okay=False, help="Location of the output.")
561
+ Path | None, Option("--out", "-o", dir_okay=False, file_okay=False, help="Location of the output.")
549
562
  ] = None,
550
563
  ) -> None:
551
564
  """Packages problem data into an `.algo` file."""
@@ -571,22 +584,23 @@ def package_problem(
571
584
  parsed_config = AlgobattleConfig.from_file(config)
572
585
  except (ValidationError, ParseError) as e:
573
586
  console.print(f"[error]Improperly formatted config file[/]\nError: {e}")
574
- raise Abort
587
+ raise Abort from e
575
588
  problem_name = parsed_config.match.problem
576
589
  try:
577
590
  with console.status("Loading problem"):
578
- parsed_config.loaded_problem
591
+ # we need to access the property so that it gets loaded
592
+ parsed_config.loaded_problem # noqa: B018
579
593
  except ValueError as e:
580
594
  console.print(f"[error]Couldn't load the problem file[/]\nError: {e}")
581
- raise Abort
595
+ raise Abort from e
582
596
  except RuntimeError as e:
583
597
  error = e.__cause__
584
598
  if error is None:
585
599
  console.print(f"[error]Couldn't load the problem file[/]\nError: {e}")
586
- raise Abort
600
+ raise Abort from e
587
601
  trace = Traceback.from_exception(error.__class__, error, error.__traceback__)
588
602
  console.print("[error]Couldn't execute the problem file[/]\nError:", trace)
589
- raise Abort
603
+ raise Abort from e
590
604
 
591
605
  if "project" in config_doc:
592
606
  config_doc.remove("project")
@@ -613,7 +627,7 @@ def package_problem(
613
627
  def package_programs(
614
628
  project: Annotated[Path, Argument(help="The project folder to use.")] = Path(),
615
629
  team: Annotated[
616
- Optional[str],
630
+ str | None,
617
631
  Option(
618
632
  help="Name of team whose programs should be packaged. If None are specified, every team's are packaged."
619
633
  ),
@@ -789,9 +803,6 @@ class CliUi(Live, Ui):
789
803
  self.config = config
790
804
  super().__init__(None, refresh_per_second=10, transient=True, console=console)
791
805
 
792
- def __enter__(self) -> Self:
793
- return cast(Self, super().__enter__())
794
-
795
806
  def _update_renderable(self) -> None:
796
807
  if self.build is None:
797
808
  renderable = Group(self.display_match(self.match, self.config.match), *self.battle_panels.values())
@@ -809,10 +820,7 @@ class CliUi(Live, Ui):
809
820
  title="[heading]Match overview",
810
821
  )
811
822
  for matchup, battle in match.battles.items():
812
- if battle.runtime_error is None:
813
- res = battle.format_score(battle.score(config.battle))
814
- else:
815
- res = ":warning:"
823
+ res = battle.format_score(battle.score(config.battle)) if battle.runtime_error is None else ":warning:"
816
824
  table.add_row(matchup.generator, matchup.solver, res)
817
825
  return Padding(table, pad=(1, 0, 0, 0))
818
826
 
@@ -868,7 +876,7 @@ class CliUi(Live, Ui):
868
876
  fights = battle.fights[-1:-6:-1]
869
877
  panel = self.battle_panels[matchup]
870
878
  table = panel._fights_table()
871
- for i, fight in zip(range(len(battle.fights), len(battle.fights) - len(fights), -1), fights):
879
+ for i, fight in zip(range(len(battle.fights), len(battle.fights) - len(fights), -1), fights, strict=True):
872
880
  if fight.generator.error:
873
881
  info = f"[error]Generator failed[/]: {fight.generator.error.message}"
874
882
  elif fight.solver and fight.solver.error: