Skip to content

Commit 56660db

Browse files
authored
Merge pull request Kattis#359 from gkreitz/dont_write_to_parent_dir_when_rendering_statement
Don't write to parent dir when rendering problem statement
2 parents b6a161f + c21517c commit 56660db

File tree

4 files changed

+85
-73
lines changed

4 files changed

+85
-73
lines changed

problemtools/problem2pdf.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,15 @@ def latex2pdf(options: argparse.Namespace, statement_file: Path) -> bool:
101101

102102
origcwd = os.getcwd()
103103

104-
os.chdir(os.path.dirname(texfile))
104+
os.chdir(texfile.parent)
105105
params = ['pdflatex', '-interaction=nonstopmode']
106106
output = None
107107
if options.quiet:
108108
output = open(os.devnull, 'w')
109109
if options.nopdf:
110110
params.append('-draftmode')
111111

112-
params.append(texfile)
112+
params.append(str(texfile.name))
113113

114114
status = subprocess.call(params, stdout=output)
115115
if status == 0:
@@ -121,7 +121,7 @@ def latex2pdf(options: argparse.Namespace, statement_file: Path) -> bool:
121121
os.chdir(origcwd)
122122

123123
if status == 0 and not options.nopdf:
124-
shutil.move(os.path.splitext(texfile)[0] + '.pdf', destfile)
124+
shutil.move(texfile.with_suffix('.pdf'), destfile)
125125

126126
if status:
127127
return False

problemtools/template.py

Lines changed: 67 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import os.path
2-
import glob
32
import tempfile
43
import shutil
54
from pathlib import Path
@@ -10,89 +9,98 @@ class Template:
109
1110
Our problemset.cls latex class was originally written to make it easy to
1211
render a problemset pdf from a bunch of problems for a contest. When we
13-
want to render a pdf for a single problem, we need to dump a small,
14-
temporary tex file in the parent directory (essentially a minified
15-
problemset with just one problem). This class deals with creating and
16-
cleaning up that template. The template has to be written in the parent
17-
directory of problem_root.
12+
want to render a pdf for a single problem, we essentially create a minified
13+
problemset with a single problem.
14+
15+
This class creates a temporary directory where it writes a .tex file and a
16+
problemset.cls file. Run latex on that tex file to render the problem statement.
17+
The temporary directory and its contents are removed on exit.
18+
19+
We still support the user providing their own problemset.cls in the parent
20+
directory of the problem. This will likely be removed at some point (I don't
21+
think anyone uses this). It can be turned off by setting ignore_parent_cls=True
1822
1923
Usage:
2024
with Template(problem_root, texfile) as templ:
21-
texfile = templ.get_file_name()
22-
os.chdir(os.path.dirname(texfile))
23-
subprocess.call(['pdflatex', texfile])
25+
texfile_path = templ.get_file_name()
26+
os.chdir(os.path.dirname(texfile_path))
27+
subprocess.call(['pdflatex', texfile_path])
28+
# Copy the resulting pdf elsewhere before closing the context
2429
"""
2530

26-
def __init__(self, problem_root: Path, texfile: Path, language: str, force_copy_cls=False):
31+
TEMPLATE_FILENAME = 'template.tex'
32+
CLS_FILENAME = 'problemset.cls'
33+
34+
def __init__(self, problem_root: Path, texfile: Path, language: str, ignore_parent_cls=False):
2735
assert texfile.suffix == '.tex', f'Template asked to render {texfile}, which does not end in .tex'
2836
assert texfile.is_relative_to(problem_root), f'Template called with tex {texfile} outside of problem {problem_root}'
2937

3038
self.problem_root = problem_root
3139
self.statement_directory = texfile.relative_to(problem_root).parent
3240
self.statement_filename = texfile.name
33-
self.templatefile = 'template.tex'
34-
self.clsfile = 'problemset.cls'
3541
self.language = language
3642

37-
templatepaths = [
38-
os.path.join(os.path.dirname(__file__), 'templates/latex'),
39-
os.path.join(os.path.dirname(__file__), '../templates/latex'),
40-
'/usr/lib/problemtools/templates/latex',
41-
]
43+
self._tempdir: tempfile.TemporaryDirectory | None = None
44+
self.texfile: Path | None = None
45+
46+
templatepaths = map(
47+
Path,
48+
[
49+
os.path.join(os.path.dirname(__file__), 'templates/latex'),
50+
os.path.join(os.path.dirname(__file__), '../templates/latex'),
51+
'/usr/lib/problemtools/templates/latex',
52+
],
53+
)
4254
try:
43-
self.templatepath = next(
44-
(p for p in templatepaths if os.path.isdir(p) and os.path.isfile(os.path.join(p, self.templatefile)))
45-
)
55+
templatepath = next(p for p in templatepaths if p.is_dir() and (p / self.TEMPLATE_FILENAME).is_file())
4656
except StopIteration:
47-
raise Exception('Could not find directory with latex template "%s"' % self.templatefile)
57+
raise Exception('Could not find directory with latex template "%s"' % self.TEMPLATE_FILENAME)
58+
self.templatefile = templatepath / self.TEMPLATE_FILENAME
4859

4960
sample_dir = problem_root / 'data' / 'sample'
5061
if sample_dir.is_dir():
5162
self.samples = sorted({file.stem for file in sample_dir.iterdir() if file.suffix in ['.in', '.interaction']})
5263
else:
5364
self.samples = []
5465

55-
self.problemset_cls = problem_root.parent / 'problemset.cls'
56-
self.copy_cls = True
57-
if self.problemset_cls.is_file() and not force_copy_cls:
58-
print(f'{self.problemset_cls} exists, will not copy it -- in case of weirdness this is likely culprit')
59-
self.copy_cls = False
66+
problemset_cls_parent = problem_root.parent / 'problemset.cls'
67+
if not ignore_parent_cls and problemset_cls_parent.is_file():
68+
print(f'{problemset_cls_parent} exists, using it -- in case of weirdness this is likely culprit')
69+
self.clsfile = problemset_cls_parent
70+
else:
71+
self.clsfile = templatepath / self.CLS_FILENAME
6072

6173
def __enter__(self):
62-
if self.copy_cls:
63-
shutil.copyfile(os.path.join(self.templatepath, self.clsfile), self.problemset_cls)
64-
65-
(templfd, self.filename) = tempfile.mkstemp(suffix='.tex', dir=self.problem_root.parent)
66-
templout = os.fdopen(templfd, 'w')
67-
templin = open(os.path.join(self.templatepath, self.templatefile))
68-
data = {
69-
'directory': self.problem_root.name,
70-
'statement_directory': self.statement_directory,
71-
'statement_filename': self.statement_filename,
72-
'language': self.language,
73-
}
74-
for line in templin:
75-
try:
76-
templout.write(line % data)
77-
except KeyError:
78-
# This is a bit ugly I guess
79-
for sample in self.samples:
80-
data['sample'] = sample
74+
self._tempdir = tempfile.TemporaryDirectory(prefix='problemtools-')
75+
temp_dir_path = Path(self._tempdir.name)
76+
77+
shutil.copyfile(self.clsfile, temp_dir_path / self.CLS_FILENAME)
78+
79+
self.texfile = temp_dir_path / 'main.tex'
80+
with open(self.texfile, 'w') as templout, open(self.templatefile) as templin:
81+
data = {
82+
'problemparent': str(self.problem_root.parent.resolve()),
83+
'directory': self.problem_root.name,
84+
'statement_directory': self.statement_directory.as_posix(),
85+
'statement_filename': self.statement_filename,
86+
'language': self.language,
87+
}
88+
for line in templin:
89+
try:
8190
templout.write(line % data)
82-
if self.samples:
83-
del data['sample']
84-
templout.close()
85-
templin.close()
91+
except KeyError:
92+
# This is a bit ugly I guess
93+
for sample in self.samples:
94+
data['sample'] = sample
95+
templout.write(line % data)
96+
if self.samples:
97+
del data['sample']
8698
return self
8799

88100
def __exit__(self, exc_type, exc_value, exc_traceback):
89-
if self.problemset_cls is not None and self.copy_cls and os.path.isfile(self.problemset_cls):
90-
os.remove(self.problemset_cls)
91-
if self.filename is not None:
92-
for f in glob.glob(os.path.splitext(self.filename)[0] + '.*'):
93-
if os.path.isfile(f):
94-
os.remove(f)
95-
96-
def get_file_name(self) -> str: # We should later change this to a Path
97-
assert os.path.isfile(self.filename)
98-
return self.filename
101+
if self._tempdir:
102+
self._tempdir.cleanup()
103+
104+
def get_file_name(self) -> Path:
105+
assert self.texfile and self.texfile.is_file()
106+
return self.texfile

problemtools/templates/latex/problemset.cls

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
\newcommand*{\licenseblurb}[1]{\def\@licenseblurb{#1}}
6666
\newcommand*{\statementdirectory}[1]{\def\@statementdirectory{#1}}
6767
\newcommand*{\statementfilename}[1]{\def\@statementfilename{#1}}
68+
\newcommand*{\problemparentpath}[1]{\def\@problemparentpath{#1}}
6869
% \problemlanguge is solely for backwards compatibility on the off chance someone external uses problemset.cls. Probably not needed
6970
\newcommand*{\problemlanguage}[1]{\def\@problemlanguage{#1}\statementfilename{problem#1.tex}}
7071
\contestname{}
@@ -74,8 +75,10 @@
7475
\licenseblurb{}
7576
\statementdirectory{problem_statement} % Default to the old standard directory on the off chance someone external uses problemset.cls
7677
\statementfilename{}
78+
\problemparentpath{}
7779
\problemlanguage{}
7880

81+
\newcommand{\@problempath}[1]{\ifx\@problemparentpath\@empty#1\else\@problemparentpath/#1\fi}
7982

8083
% Command to set a header logo
8184
\newsavebox{\PS@headerbox}
@@ -173,17 +176,17 @@
173176
%% Problem inclusion
174177
\newcommand{\includeproblem}[1]{
175178
\startproblem{#1}
176-
\import{#1/\@statementdirectory/}{\@statementfilename}
179+
\import{\@problempath{#1}/\@statementdirectory/}{\@statementfilename}
177180

178181

179182
%% Automatically include samples 1..9, if enabled
180183
\ifplastex\else
181184
\if@autoincludesamples
182185
\foreach \SampleNum in {1,...,9} {
183-
\IfFileExists{\@problemid/data/sample/\SampleNum.interaction}{
184-
\displaysampleinteraction{\@problemid/data/sample/\SampleNum}
185-
}{\IfFileExists{\@problemid/data/sample/\SampleNum.in}{
186-
\displaysample{\@problemid/data/sample/\SampleNum}
186+
\IfFileExists{\@problempath{\@problemid}/data/sample/\SampleNum.interaction}{
187+
\displaysampleinteraction{\@problempath{\@problemid}/data/sample/\SampleNum}
188+
}{\IfFileExists{\@problempath{\@problemid}/data/sample/\SampleNum.in}{
189+
\displaysample{\@problempath{\@problemid}/data/sample/\SampleNum}
187190
}{}
188191
}
189192
}
@@ -215,8 +218,8 @@
215218
\if@problemnumbers {\huge Problem \problemnumber\\[3mm]} \fi
216219
{\LARGE #1}
217220
\if@problemids {\\[2mm]{\Large Problem ID: #2}} \fi
218-
\IfFileExists{#2/.timelimit}{
219-
\openin\ps@timelimitfile=#2/.timelimit
221+
\IfFileExists{\@problempath{#2}/.timelimit}{
222+
\openin\ps@timelimitfile=\@problempath{#2}/.timelimit
220223
\read\ps@timelimitfile to\ps@timelimit
221224
\\[2mm]{\Large Time limit:\ps@formattime{\ps@timelimit}}
222225
\closein\ps@timelimitfile
@@ -246,11 +249,11 @@
246249
%% Define the command used to give sample data
247250
%% Takes filename as parameter
248251
\newcommand{\includesample}[1]{
249-
\IfFileExists{\@problemid/data/sample/#1.interaction}{
250-
\displaysampleinteraction{\@problemid/data/sample/#1}
252+
\IfFileExists{\@problempath{\@problemid}/data/sample/#1.interaction}{
253+
\displaysampleinteraction{\@problempath{\@problemid}/data/sample/#1}
251254
}{
252-
\IfFileExists{\@problemid/data/sample/#1.in}{
253-
\displaysample{\@problemid/data/sample/#1}
255+
\IfFileExists{\@problempath{\@problemid}/data/sample/#1.in}{
256+
\displaysample{\@problempath{\@problemid}/data/sample/#1}
254257
}{
255258
\ClassError{problemset}{Can't find any sample named #1}{}
256259
}

problemtools/templates/latex/template.tex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
%% If you want to add comments in this file, you need to use %%, as it must be compatible with python's templates
44
\problemlanguage{%(language)s} %% We inject problemlanguage to be backwards compatible with custom problemset.cls
5+
\problemparentpath{%(problemparent)s}
56
\statementdirectory{%(statement_directory)s}
67
\statementfilename{%(statement_filename)s}
78

0 commit comments

Comments
 (0)