Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/constants #368

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions bin/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@

SEED_DEPENDENCY_RETRIES = 10

SUBSTITUTE_PREFIX = '{%'
SUBSTITUTE_SUFFIX = '%}'
SUBSTITUTE_NAME_REGEX = re.compile(r'[a-zA-Z0-9_.-]+')
SUBSTITUTE_REGEX = re.compile(
f'{re.escape(SUBSTITUTE_PREFIX)}({SUBSTITUTE_NAME_REGEX.pattern}){re.escape(SUBSTITUTE_SUFFIX)}'
)

# The root directory of the BAPCtools repository.
tools_root = Path(__file__).resolve().parent.parent

Expand Down
43 changes: 19 additions & 24 deletions bin/latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,28 @@ def prepare_problem(problem, language):
create_samples_file(problem, language)


def get_tl(problem):
# Get the problem constants (overwritten/extended for latex).
def problem_constants(problem, language):
problem_config = problem.settings
tl = problem_config.timelimit
tl = int(tl) if abs(tl - int(tl)) < 0.0001 else tl

if 'print_timelimit' in contest_yaml():
print_tl = contest_yaml()['print_timelimit']
else:
print_tl = not config.args.no_timelimit

return tl if print_tl else ''
if not contest_yaml()['print_timelimit']:
tl = ''
elif config.args.no_timelimit:
tl = ''

return {
**problem.settings.constants,
'problemdir': problem.path.absolute().as_posix(),
'problemdirname': problem.name,
'problemlabel': problem.label,
'problemauthor': problem.settings.author,
'timelimit': tl,
'problemyamlname': problem.settings.name[language].replace('_', ' '),
'builddir': latex_builddir(problem, language).as_posix(),
}


def make_environment():
Expand Down Expand Up @@ -247,15 +258,7 @@ def build_problem_pdf(problem, language, solution=False, web=False):
util.copy_and_substitute(
local_data if local_data.is_file() else config.tools_root / 'latex' / main_file,
builddir / main_file,
{
'problemlabel': problem.label,
'problemyamlname': problem.settings.name[language].replace('_', ' '),
'problemauthor': problem.settings.author,
'timelimit': get_tl(problem),
'problemdir': problem.path.absolute().as_posix(),
'problemdirname': problem.name,
'builddir': builddir.as_posix(),
},
problem_constants(problem, language),
)

return build_latex_pdf(builddir, builddir / main_file, language, bar, problem.path)
Expand Down Expand Up @@ -375,15 +378,7 @@ def build_contest_pdf(contest, problems, tmpdir, language, solutions=False, web=

problems_data += util.substitute(
per_problem_data,
{
'problemlabel': problem.label,
'problemyamlname': problem.settings.name[language].replace('_', ' '),
'problemauthor': problem.settings.author,
'timelimit': get_tl(problem),
'problemdir': problem.path.absolute().as_posix(),
'problemdirname': problem.name,
'builddir': latex_builddir(problem, language).as_posix(),
},
problem_constants(problem, language),
)

if solutions:
Expand Down
44 changes: 40 additions & 4 deletions bin/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ def __init__(self, path, tmpdir, label=None):
self.path = path
self.tmpdir = tmpdir / self.name
self.tmpdir.mkdir(parents=True, exist_ok=True)
# The label for the problem: A, B, A1, A2, X, ...
self.label = label
# Read problem.yaml and domjudge-problem.ini into self.settings Namespace object.
self._read_settings()

Expand All @@ -42,9 +44,6 @@ def __init__(self, path, tmpdir, label=None):
# Dictionary from path to parsed file contents.
self._testdata_yamls = dict()

# The label for the problem: A, B, A1, A2, X, ...
self.label = label

# TODO: transform this into nice warnings
assert path.is_dir()
if not Problem._SHORTNAME_REGEX.match(self.name):
Expand Down Expand Up @@ -108,6 +107,7 @@ def _read_settings(self):
'validator_flags': [],
'author': '',
'uuid': None,
'constants': dict(),
}

yaml_path = self.path / 'problem.yaml'
Expand Down Expand Up @@ -171,6 +171,41 @@ def _read_settings(self):
yaml_path.write_text(raw)
log(f'Generated UUID for {self.name}, added to problem.yaml')

# read constants
if not isinstance(self.settings.constants, dict):
fatal(f'could not parse constants in {self.name}/problem.yaml')
raw_constants = self.settings.constants
self.settings.constants = {
k: v
for k, v in raw_constants.items()
if isinstance(v, (str, int, float))
and config.SUBSTITUTE_NAME_REGEX.fullmatch(k) is not None
}
for k in raw_constants:
if k not in self.settings.constants:
if config.SUBSTITUTE_NAME_REGEX.fullmatch(key) is None:
error(f'invalid name "{k}" for constant in {self.name}/problem.yaml (ignored)')
else:
error(f'invalid value for constant {k} in {self.name}/problem.yaml (ignored)')

# reserved constants (and backwards compatibility)
known_constants = {
'timelimit': self.settings.timelimit,
}
reserved_constants = list(known_constants.keys()) + [
'problemdir',
'problemdirname',
'problemlabel',
'problemauthor',
'problemyamlname', # localised for problem statements
'builddir', # used by problem statements
]
for k in reserved_constants:
if k in self.settings.constants:
warn(f'found reserved key "{k}" in constants of {self.name}/problem.yaml. Ignored.')
self.settings.constants.pop(k)
self.settings.constants.update(known_constants)

def get_testdata_yaml(p, path, key, bar, name=None) -> str | None:
"""
Find the testdata flags applying at the given path for the given key.
Expand Down Expand Up @@ -205,7 +240,8 @@ def get_testdata_yaml(p, path, key, bar, name=None) -> str | None:
if f in p._testdata_yamls:
flags = p._testdata_yamls[f]
else:
p._testdata_yamls[f] = flags = read_yaml(f, plain=True)
raw = substitute(f.read_text(), p.settings.constants)
p._testdata_yamls[f] = flags = parse_yaml(raw, path=f, plain=True)

# Validate and exctract the flags
for k in flags:
Expand Down
14 changes: 10 additions & 4 deletions bin/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def sanitizer():
# - path: source file/directory
# - short_path: the path relative to problem/subdir/, or None
# - tmpdir: the build directory in tmpfs. This is only created when build() is called.
# - input_files: list of source files linked into tmpdir
# - input_files: list of source files linked/copied into tmpdir
# - language: the detected language
# - env: the environment variables used for compile/run command substitution
# - hash: a hash of all of the program including all source files
Expand Down Expand Up @@ -139,6 +139,8 @@ def __init__(self, problem, path, deps=None, *, skip_double_build_warning=False)

self.ok = True
self.built = False
# True for all Prgrams, for now
self.substitute_constants = True

# Detect language, dependencies, and main file
if deps:
Expand Down Expand Up @@ -418,13 +420,17 @@ def build(self, bar):
self.input_files = []
hashes = []
for f in self.source_files:
ensure_symlink(self.tmpdir / f.name, f)
self.input_files.append(self.tmpdir / f.name)
if not f.is_file():
self.ok = False
self.bar.error(f'{str(f)} is not a file')
return False
hashes.append(hash_file(f))
tmpf = self.tmpdir / f.name
if not self.substitute_constants or not has_substitute(f):
ensure_symlink(tmpf, f)
else:
copy_and_substitute(f, tmpf, self.problem.settings.constants)
self.input_files.append(tmpf)
hashes.append(hash_file(tmpf))
self.hash = combine_hashes(hashes)

if not self._get_language(self.source_files):
Expand Down
29 changes: 23 additions & 6 deletions bin/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -901,15 +901,32 @@ def ensure_symlink(link, target, output=False, relative=False):
link.symlink_to(target.resolve(), target.is_dir())


def has_substitute(inpath):
try:
data = inpath.read_text()
except UnicodeDecodeError:
return False
return config.SUBSTITUTE_REGEX.search(data) is not None


def substitute(data, variables):
if variables is None:
return data
variables = {}

for key in variables:
r = ''
if variables[key] != None:
r = variables[key]
data = data.replace('{%' + key + '%}', str(r))
return data
if config.SUBSTITUTE_NAME_REGEX.fullmatch(key) is None:
warn(f'substitution key {key} does not match {config.SUBSTITUTE_NAME_REGEX.pattern}')

def substitute_function(match):
name = match.group(1)
if name in variables:
return str(variables[name]) if variables[name] is not None else ''
else:
variable = match.group()
warn(f"Found pattern '{variable}' but no substitution was provided. Skipped.")
return variable

return config.SUBSTITUTE_REGEX.sub(substitute_function, data)


def copy_and_substitute(inpath, outpath, variables):
Expand Down