Skip to content
6 changes: 5 additions & 1 deletion bin/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,18 @@
# fmt: on


def set_default_args() -> None:
def set_default_args() -> list[str]:
# Set default argument values.
missing = []
for arg in ARGS_LIST:
if not hasattr(args, arg):
setattr(args, arg, None)
missing.append(arg)
for arg, value in DEFAULT_ARGS.items():
if not hasattr(args, arg):
setattr(args, arg, value)
missing.append(arg)
return missing


level: Optional[Literal["problem", "problemset"]] = None
Expand Down
129 changes: 67 additions & 62 deletions bin/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,6 @@
fatal("BAPCtools requires at least Python 3.10.")


# A path is a problem directory if it contains a `problem.yaml` file.
def is_problem_directory(path: Path) -> bool:
return (path / "problem.yaml").is_file()


# Changes the working directory to the root of the contest.
# Returns the "level" of the current command (either 'problem' or 'problemset')
# and, if `level == 'problem'`, the directory of the problem.
Expand Down Expand Up @@ -160,7 +155,8 @@ def fallback_problems() -> list[tuple[Path, str]]:
if path.name == problem_dir.name:
found_label = label
problems = [Problem(Path(problem_dir.name), tmpdir, found_label)]
else: # config.level == 'problemset'
else:
assert config.level == "problemset"
# If problems.yaml is available, use it.
problemsyaml = problems_yaml()
if problemsyaml:
Expand Down Expand Up @@ -1005,42 +1001,90 @@ def build_parser() -> SuppressingParser:
return parser


def run_parsed_arguments(args: argparse.Namespace) -> None:
def find_personal_config() -> Optional[Path]:
if is_windows():
app_data = os.getenv("AppData")
return Path(app_data) if app_data else None
else:
home = os.getenv("HOME")
xdg_config_home = os.getenv("XDG_CONFIG_HOME")
return (
Path(xdg_config_home) if xdg_config_home else Path(home) / ".config" if home else None
)


def read_personal_config() -> dict[str, Any]:
args = {}
home_config = find_personal_config()

for config_file in [
# Highest prio: contest directory
Path() / ".bapctools.yaml",
Path() / ".." / ".bapctools.yaml",
] + (
# Lowest prio: user config directory
[home_config / "bapctools" / "config.yaml"] if home_config else []
):
if not config_file.is_file():
continue
config_data = read_yaml(config_file) or {}
for arg, value in config_data.items():
if arg not in args:
args[arg] = value

return args


def run_parsed_arguments(args: argparse.Namespace, personal_config: bool = True) -> None:
# Don't zero newly allocated memory for this and any subprocess
# Will likely only have an effect on linux
os.environ["MALLOC_PERTURB_"] = str(0b01011001)

# Process arguments
config.args = args
config.set_default_args()
missing_args = config.set_default_args()

action = config.args.action
# cd to contest directory
problem_dir = change_directory()
level = config.level
contest_name = Path().cwd().name

# Split submissions and testcases when needed.
if action in ["run", "fuzz", "time_limit"]:
if config.args.submissions:
config.args.submissions, config.args.testcases = split_submissions_and_testcases(
config.args.submissions
)
else:
config.args.testcases = []
if personal_config:
personal_args = read_personal_config()
for arg in missing_args:
if arg in personal_args:
setattr(config.args, arg, personal_args[arg])

action = config.args.action

# upgrade commands.
if action == "upgrade":
upgrade.upgrade()
upgrade.upgrade(problem_dir)
return

# Skel commands.
if action == "new_contest":
os.chdir(config.current_working_directory)
skel.new_contest()
return

if action == "new_problem":
os.chdir(config.current_working_directory)
skel.new_problem()
return

# Get problems list and cd to contest directory
problem_dir = change_directory()
level = config.level
contest_name = Path().cwd().name
# get problems list
problems, tmpdir = get_problems(problem_dir)

# Split submissions and testcases when needed.
if action in ["run", "fuzz", "time_limit"]:
if config.args.submissions:
config.args.submissions, config.args.testcases = split_submissions_and_testcases(
config.args.submissions
)
else:
config.args.testcases = []

# Check non unique uuid
# TODO: check this even more globally?
uuids: dict[str, Problem] = {}
Expand Down Expand Up @@ -1373,54 +1417,15 @@ def run_parsed_arguments(args: argparse.Namespace) -> None:
sys.exit(1)


def find_personal_config() -> Optional[Path]:
if is_windows():
app_data = os.getenv("AppData")
return Path(app_data) if app_data else None
else:
home = os.getenv("HOME")
xdg_config_home = os.getenv("XDG_CONFIG_HOME")
return (
Path(xdg_config_home) if xdg_config_home else Path(home) / ".config" if home else None
)


def read_personal_config():
args = {}
home_config = find_personal_config()

for config_file in [
# Highest prio: contest directory
Path() / ".bapctools.yaml",
Path() / ".." / ".bapctools.yaml",
] + (
# Lowest prio: user config directory
[home_config / "bapctools" / "config.yaml"] if home_config else []
):
if not config_file.is_file():
continue
config_data = read_yaml(config_file) or {}
for arg, value in config_data.items():
if arg not in args:
args[arg] = value

return args


# Takes command line arguments
def main():
def interrupt_handler(sig: Any, frame: Any) -> None:
fatal("Running interrupted")

signal.signal(signal.SIGINT, interrupt_handler)

# Don't zero newly allocated memory for this and any subprocess
# Will likely only work on linux
os.environ["MALLOC_PERTURB_"] = str(0b01011001)

try:
parser = build_parser()
parser.set_defaults(**read_personal_config())
run_parsed_arguments(parser.parse_args())
except AbortException:
fatal("Running interrupted")
Expand All @@ -1442,7 +1447,7 @@ def test(args):
contest._problems_yaml = None
try:
parser = build_parser()
run_parsed_arguments(parser.parse_args(args))
run_parsed_arguments(parser.parse_args(), personal_config=False)
finally:
os.chdir(original_directory)
ProgressBar.current_bar = None
31 changes: 16 additions & 15 deletions bin/upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,26 +609,27 @@ def _upgrade(problem_path: Path, bar: ProgressBar) -> None:
bar.done()


def upgrade() -> None:
def upgrade(problem_dir: Optional[Path]) -> None:
if not has_ryaml:
error("upgrade needs the ruamel.yaml python3 library. Install python[3]-ruamel.yaml.")
return
cwd = Path().cwd()

def is_problem_directory(path: Path) -> bool:
return (path / "problem.yaml").is_file()

if is_problem_directory(cwd):
paths = [cwd]
if config.level == "problem":
assert problem_dir
if not is_problem_directory(problem_dir):
fatal(f"{problem_dir} does not contain a problem.yaml")
paths = [problem_dir]
bar = ProgressBar("upgrade", items=paths)
else:
paths = [p for p in cwd.iterdir() if is_problem_directory(p)]

bar = ProgressBar("upgrade", items=["contest.yaml", *paths])

bar.start("contest.yaml")
if (cwd / "contest.yaml").is_file():
upgrade_contest_yaml(cwd / "contest.yaml", bar)
bar.done()
assert config.level == "problemset"
contest_dir = Path().cwd()
paths = [p for p in contest_dir.iterdir() if is_problem_directory(p)]
bar = ProgressBar("upgrade", items=["contest.yaml", *paths])

bar.start("contest.yaml")
if (contest_dir / "contest.yaml").is_file():
upgrade_contest_yaml(contest_dir / "contest.yaml", bar)
bar.done()

for path in paths:
_upgrade(path, bar)
Expand Down
5 changes: 5 additions & 0 deletions bin/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -1461,6 +1461,11 @@ def inc_label(label: str) -> str:
return "A" + label


# A path is a problem directory if it contains a `problem.yaml` file.
def is_problem_directory(path: Path) -> bool:
return (path / "problem.yaml").is_file()


def combine_hashes(values: Sequence[str]) -> str:
hasher = hashlib.sha512(usedforsecurity=False)
for item in sorted(values):
Expand Down
Loading