Skip to content
Merged
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
6 changes: 3 additions & 3 deletions problemtools/problem2pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,15 @@ def latex2pdf(options: argparse.Namespace, statement_file: Path) -> bool:

origcwd = os.getcwd()

os.chdir(os.path.dirname(texfile))
os.chdir(texfile.parent)
params = ['pdflatex', '-interaction=nonstopmode']
output = None
if options.quiet:
output = open(os.devnull, 'w')
if options.nopdf:
params.append('-draftmode')

params.append(texfile)
params.append(str(texfile.name))

status = subprocess.call(params, stdout=output)
if status == 0:
Expand All @@ -120,7 +120,7 @@ def latex2pdf(options: argparse.Namespace, statement_file: Path) -> bool:
os.chdir(origcwd)

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

if status:
return False
Expand Down
126 changes: 67 additions & 59 deletions problemtools/template.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import os.path
import glob
import tempfile
import shutil
from pathlib import Path
Expand All @@ -10,89 +9,98 @@ class Template:

Our problemset.cls latex class was originally written to make it easy to
render a problemset pdf from a bunch of problems for a contest. When we
want to render a pdf for a single problem, we need to dump a small,
temporary tex file in the parent directory (essentially a minified
problemset with just one problem). This class deals with creating and
cleaning up that template. The template has to be written in the parent
directory of problem_root.
want to render a pdf for a single problem, we essentially create a minified
problemset with a single problem.

This class creates a temporary directory where it writes a .tex file and a
problemset.cls file. Run latex on that tex file to render the problem statement.
The temporary directory and its contents are removed on exit.

We still support the user providing their own problemset.cls in the parent
directory of the problem. This will likely be removed at some point (I don't
think anyone uses this). It can be turned off by setting ignore_parent_cls=True

Usage:
with Template(problem_root, texfile) as templ:
texfile = templ.get_file_name()
os.chdir(os.path.dirname(texfile))
subprocess.call(['pdflatex', texfile])
texfile_path = templ.get_file_name()
os.chdir(os.path.dirname(texfile_path))
subprocess.call(['pdflatex', texfile_path])
# Copy the resulting pdf elsewhere before closing the context
"""

def __init__(self, problem_root: Path, texfile: Path, language: str, force_copy_cls=False):
TEMPLATE_FILENAME = 'template.tex'
CLS_FILENAME = 'problemset.cls'

def __init__(self, problem_root: Path, texfile: Path, language: str, ignore_parent_cls=False):
assert texfile.suffix == '.tex', f'Template asked to render {texfile}, which does not end in .tex'
assert texfile.is_relative_to(problem_root), f'Template called with tex {texfile} outside of problem {problem_root}'

self.problem_root = problem_root
self.statement_directory = texfile.relative_to(problem_root).parent
self.statement_filename = texfile.name
self.templatefile = 'template.tex'
self.clsfile = 'problemset.cls'
self.language = language

templatepaths = [
os.path.join(os.path.dirname(__file__), 'templates/latex'),
os.path.join(os.path.dirname(__file__), '../templates/latex'),
'/usr/lib/problemtools/templates/latex',
]
self._tempdir: tempfile.TemporaryDirectory | None = None
self.texfile: Path | None = None

templatepaths = map(
Path,
[
os.path.join(os.path.dirname(__file__), 'templates/latex'),
os.path.join(os.path.dirname(__file__), '../templates/latex'),
'/usr/lib/problemtools/templates/latex',
],
)
try:
self.templatepath = next(
(p for p in templatepaths if os.path.isdir(p) and os.path.isfile(os.path.join(p, self.templatefile)))
)
templatepath = next(p for p in templatepaths if p.is_dir() and (p / self.TEMPLATE_FILENAME).is_file())
except StopIteration:
raise Exception('Could not find directory with latex template "%s"' % self.templatefile)
raise Exception('Could not find directory with latex template "%s"' % self.TEMPLATE_FILENAME)
self.templatefile = templatepath / self.TEMPLATE_FILENAME

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

self.problemset_cls = problem_root.parent / 'problemset.cls'
self.copy_cls = True
if self.problemset_cls.is_file() and not force_copy_cls:
print(f'{self.problemset_cls} exists, will not copy it -- in case of weirdness this is likely culprit')
self.copy_cls = False
problemset_cls_parent = problem_root.parent / 'problemset.cls'
if not ignore_parent_cls and problemset_cls_parent.is_file():
print(f'{problemset_cls_parent} exists, using it -- in case of weirdness this is likely culprit')
self.clsfile = problemset_cls_parent
else:
self.clsfile = templatepath / self.CLS_FILENAME

def __enter__(self):
if self.copy_cls:
shutil.copyfile(os.path.join(self.templatepath, self.clsfile), self.problemset_cls)

(templfd, self.filename) = tempfile.mkstemp(suffix='.tex', dir=self.problem_root.parent)
templout = os.fdopen(templfd, 'w')
templin = open(os.path.join(self.templatepath, self.templatefile))
data = {
'directory': self.problem_root.name,
'statement_directory': self.statement_directory,
'statement_filename': self.statement_filename,
'language': self.language,
}
for line in templin:
try:
templout.write(line % data)
except KeyError:
# This is a bit ugly I guess
for sample in self.samples:
data['sample'] = sample
self._tempdir = tempfile.TemporaryDirectory(prefix='problemtools-')
temp_dir_path = Path(self._tempdir.name)

shutil.copyfile(self.clsfile, temp_dir_path / self.CLS_FILENAME)

self.texfile = temp_dir_path / 'main.tex'
with open(self.texfile, 'w') as templout, open(self.templatefile) as templin:
data = {
'problemparent': str(self.problem_root.parent.resolve()),
'directory': self.problem_root.name,
'statement_directory': self.statement_directory.as_posix(),
'statement_filename': self.statement_filename,
'language': self.language,
}
for line in templin:
try:
templout.write(line % data)
if self.samples:
del data['sample']
templout.close()
templin.close()
except KeyError:
# This is a bit ugly I guess
for sample in self.samples:
data['sample'] = sample
templout.write(line % data)
if self.samples:
del data['sample']
return self

def __exit__(self, exc_type, exc_value, exc_traceback):
if self.problemset_cls is not None and self.copy_cls and os.path.isfile(self.problemset_cls):
os.remove(self.problemset_cls)
if self.filename is not None:
for f in glob.glob(os.path.splitext(self.filename)[0] + '.*'):
if os.path.isfile(f):
os.remove(f)

def get_file_name(self) -> str: # We should later change this to a Path
assert os.path.isfile(self.filename)
return self.filename
if self._tempdir:
self._tempdir.cleanup()

def get_file_name(self) -> Path:
assert self.texfile and self.texfile.is_file()
return self.texfile
25 changes: 14 additions & 11 deletions problemtools/templates/latex/problemset.cls
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
\newcommand*{\licenseblurb}[1]{\def\@licenseblurb{#1}}
\newcommand*{\statementdirectory}[1]{\def\@statementdirectory{#1}}
\newcommand*{\statementfilename}[1]{\def\@statementfilename{#1}}
\newcommand*{\problemparentpath}[1]{\def\@problemparentpath{#1}}
% \problemlanguge is solely for backwards compatibility on the off chance someone external uses problemset.cls. Probably not needed
\newcommand*{\problemlanguage}[1]{\def\@problemlanguage{#1}\statementfilename{problem#1.tex}}
\contestname{}
Expand All @@ -74,8 +75,10 @@
\licenseblurb{}
\statementdirectory{problem_statement} % Default to the old standard directory on the off chance someone external uses problemset.cls
\statementfilename{}
\problemparentpath{}
\problemlanguage{}

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

% Command to set a header logo
\newsavebox{\PS@headerbox}
Expand Down Expand Up @@ -173,17 +176,17 @@
%% Problem inclusion
\newcommand{\includeproblem}[1]{
\startproblem{#1}
\import{#1/\@statementdirectory/}{\@statementfilename}
\import{\@problempath{#1}/\@statementdirectory/}{\@statementfilename}


%% Automatically include samples 1..9, if enabled
\ifplastex\else
\if@autoincludesamples
\foreach \SampleNum in {1,...,9} {
\IfFileExists{\@problemid/data/sample/\SampleNum.interaction}{
\displaysampleinteraction{\@problemid/data/sample/\SampleNum}
}{\IfFileExists{\@problemid/data/sample/\SampleNum.in}{
\displaysample{\@problemid/data/sample/\SampleNum}
\IfFileExists{\@problempath{\@problemid}/data/sample/\SampleNum.interaction}{
\displaysampleinteraction{\@problempath{\@problemid}/data/sample/\SampleNum}
}{\IfFileExists{\@problempath{\@problemid}/data/sample/\SampleNum.in}{
\displaysample{\@problempath{\@problemid}/data/sample/\SampleNum}
}{}
}
}
Expand Down Expand Up @@ -215,8 +218,8 @@
\if@problemnumbers {\huge Problem \problemnumber\\[3mm]} \fi
{\LARGE #1}
\if@problemids {\\[2mm]{\Large Problem ID: #2}} \fi
\IfFileExists{#2/.timelimit}{
\openin\ps@timelimitfile=#2/.timelimit
\IfFileExists{\@problempath{#2}/.timelimit}{
\openin\ps@timelimitfile=\@problempath{#2}/.timelimit
\read\ps@timelimitfile to\ps@timelimit
\\[2mm]{\Large Time limit:\ps@formattime{\ps@timelimit}}
\closein\ps@timelimitfile
Expand Down Expand Up @@ -246,11 +249,11 @@
%% Define the command used to give sample data
%% Takes filename as parameter
\newcommand{\includesample}[1]{
\IfFileExists{\@problemid/data/sample/#1.interaction}{
\displaysampleinteraction{\@problemid/data/sample/#1}
\IfFileExists{\@problempath{\@problemid}/data/sample/#1.interaction}{
\displaysampleinteraction{\@problempath{\@problemid}/data/sample/#1}
}{
\IfFileExists{\@problemid/data/sample/#1.in}{
\displaysample{\@problemid/data/sample/#1}
\IfFileExists{\@problempath{\@problemid}/data/sample/#1.in}{
\displaysample{\@problempath{\@problemid}/data/sample/#1}
}{
\ClassError{problemset}{Can't find any sample named #1}{}
}
Expand Down
1 change: 1 addition & 0 deletions problemtools/templates/latex/template.tex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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

Expand Down