Skip to content

Commit 0f9c83a

Browse files
introduce envgen config with exec_after_problem_created / exec_after_… (kyuridenamida#59)
* introduce envgen config with exec_on_each_problem_dir / exec_on_contest_dir (+ unit tests) * Reinforce unittest * codegen -> codestyle, envgen -> postprocess in toml * Remove unused exception definition * Refactor: rename source codeswith PostProcessConfig and CodeStyleConfig * Fix potential issue that users can specify wrong configuration name and it is ignored silently. * fix test resouces' names
1 parent 8a710a3 commit 0f9c83a

22 files changed

+210
-76
lines changed
Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
1-
from typing import TextIO
2-
3-
import toml
4-
51
INDENT_TYPE_SPACE = 'space'
62
INDENT_TYPE_TAB = 'tab'
73

84

9-
class ConfigInitError(Exception):
5+
class CodeStyleConfigInitError(Exception):
106
pass
117

128

13-
class CodeGenConfig:
9+
class CodeStyleConfig:
1410
def __init__(self,
1511
indent_type: str = INDENT_TYPE_SPACE,
1612
indent_width: int = 4,
1713
):
1814

1915
if indent_type not in [INDENT_TYPE_SPACE, INDENT_TYPE_TAB]:
20-
raise ConfigInitError("indent_type must be 'space' or 'tab'")
16+
raise CodeStyleConfigInitError(
17+
"indent_type must be 'space' or 'tab'")
2118

2219
if indent_width < 0:
23-
raise ConfigInitError("indent_width must be a positive integer")
20+
raise CodeStyleConfigInitError(
21+
"indent_width must be a positive integer")
2422

2523
self.indent_type = indent_type
2624
self.indent_width = indent_width
@@ -29,8 +27,3 @@ def indent(self, depth):
2927
if self.indent_type == INDENT_TYPE_SPACE:
3028
return " " * self.indent_width * depth
3129
return "\t" * self.indent_width * depth
32-
33-
@classmethod
34-
def load(cls, fp: TextIO):
35-
kwargs = toml.load(fp).get("codegen")
36-
return CodeGenConfig(**kwargs)

atcodertools/codegen/cpp_code_generator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from atcodertools.codegen.code_gen_config import CodeGenConfig
1+
from atcodertools.codegen.code_style_config import CodeStyleConfig
22
from atcodertools.models.analyzer.analyzed_variable import AnalyzedVariable
33
from atcodertools.models.analyzer.simple_format import Pattern, SingularPattern, ParallelPattern, TwoDimensionalPattern
44
from atcodertools.models.constpred.problem_constant_set import ProblemConstantSet
@@ -24,7 +24,7 @@ def _loop_header(var: Variable, for_second_index: bool):
2424

2525
class CppCodeGenerator(CodeGenerator):
2626

27-
def __init__(self, template: str, config: CodeGenConfig = CodeGenConfig()):
27+
def __init__(self, template: str, config: CodeStyleConfig = CodeStyleConfig()):
2828
self._template = template
2929
self._prediction_result = None
3030
self._config = config

atcodertools/config/__init__.py

Whitespace-only changes.

atcodertools/config/config.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from typing import TextIO
2+
3+
import toml
4+
5+
from atcodertools.codegen.code_style_config import CodeStyleConfig
6+
from atcodertools.tools.postprocess_config import PostprocessConfig
7+
8+
9+
class Config:
10+
def __init__(self,
11+
code_style_config: CodeStyleConfig = CodeStyleConfig(),
12+
postprocess_config: PostprocessConfig = PostprocessConfig(),
13+
):
14+
self.code_style_config = code_style_config
15+
self.postprocess_config = postprocess_config
16+
17+
@classmethod
18+
def load(cls, fp: TextIO):
19+
config_dic = toml.load(fp)
20+
code_style_config_dic = config_dic.get('codestyle', {})
21+
postprocess_config_dic = config_dic.get('postprocess', {})
22+
return Config(
23+
code_style_config=CodeStyleConfig(**code_style_config_dic),
24+
postprocess_config=PostprocessConfig(**postprocess_config_dic),
25+
)
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1-
[codegen]
1+
[codestyle]
22
indent_type = 'space' # 'tab' or 'space'
33
indent_width = 4
4+
5+
[postprocess]
6+
## exec_on_each_problem_dir is executed after every problem directory's creation
7+
## with its problem directory as cwd like arc001/A
8+
#exec_on_each_problem_dir='clang-format -i ./*.cpp'
9+
10+
## exec_on_contest_dir is executed once after `atcoder-tools gen`
11+
## with the contest directory as cwd like arc001/
12+
#exec_on_contest_dir=

atcodertools/tools/envgen.py

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
from time import sleep
99
from typing import Tuple, Optional
1010

11-
from atcodertools.codegen.code_gen_config import CodeGenConfig
1211
from atcodertools.codegen.cpp_code_generator import CppCodeGenerator
1312
from atcodertools.codegen.java_code_generator import JavaCodeGenerator
13+
from atcodertools.config.config import Config
14+
from atcodertools.fileutils.create_contest_file import create_examples, create_code_from
1415
from atcodertools.constprediction.constants_prediction import predict_constants
1516
from atcodertools.fileutils.create_contest_file import create_examples, \
1617
create_code_from
@@ -41,16 +42,25 @@ def extension(lang: str):
4142
OUT_EXAMPLE_FORMAT = "out_{}.txt"
4243

4344

45+
def output_splitter():
46+
# for readability
47+
print("=================================================", file=sys.stderr)
48+
49+
50+
def _message_on_execution(cwd: str, cmd: str):
51+
return "Executing the following command in `{}`: {}".format(cwd, cmd)
52+
53+
4454
def prepare_procedure(atcoder_client: AtCoderClient,
4555
problem: Problem,
4656
workspace_root_path: str,
4757
template_code_path: str,
4858
replacement_code_path: str,
4959
lang: str,
50-
config: CodeGenConfig,
60+
config: Config,
5161
):
5262
pid = problem.get_alphabet()
53-
workspace_dir_path = os.path.join(
63+
problem_dir_path = os.path.join(
5464
workspace_root_path,
5565
problem.get_contest().get_id(),
5666
pid)
@@ -78,13 +88,13 @@ def emit_info(text):
7888
if len(content.get_samples()) == 0:
7989
emit_info("No samples.")
8090
else:
81-
os.makedirs(workspace_dir_path, exist_ok=True)
82-
create_examples(content.get_samples(), workspace_dir_path,
91+
os.makedirs(problem_dir_path, exist_ok=True)
92+
create_examples(content.get_samples(), problem_dir_path,
8393
IN_EXAMPLE_FORMAT, OUT_EXAMPLE_FORMAT)
8494
emit_info("Created examples.")
8595

8696
code_file_path = os.path.join(
87-
workspace_dir_path,
97+
problem_dir_path,
8898
"main.{}".format(extension(lang)))
8999

90100
# If there is an existing code, just create backup
@@ -119,7 +129,7 @@ def emit_info(text):
119129
create_code_from(
120130
result,
121131
constants,
122-
gen_class(template, config),
132+
gen_class(template, config.code_style_config),
123133
code_file_path)
124134
emit_info(
125135
"Prediction succeeded -- Saved auto-generated code to '{}'".format(code_file_path))
@@ -138,7 +148,7 @@ def emit_info(text):
138148
code_file_path))
139149

140150
# Save metadata
141-
metadata_path = os.path.join(workspace_dir_path, "metadata.json")
151+
metadata_path = os.path.join(problem_dir_path, "metadata.json")
142152
Metadata(problem,
143153
os.path.basename(code_file_path),
144154
IN_EXAMPLE_FORMAT.replace("{}", "*"),
@@ -147,23 +157,31 @@ def emit_info(text):
147157
).save_to(metadata_path)
148158
emit_info("Saved metadata to {}".format(metadata_path))
149159

160+
if config.postprocess_config.exec_cmd_on_problem_dir is not None:
161+
emit_info(_message_on_execution(problem_dir_path,
162+
config.postprocess_config.exec_cmd_on_problem_dir))
163+
config.postprocess_config.execute_on_problem_dir(
164+
problem_dir_path)
150165

151-
def func(argv: Tuple[AtCoderClient, Problem, str, str, str, str, CodeGenConfig]):
166+
output_splitter()
167+
168+
169+
def func(argv: Tuple[AtCoderClient, Problem, str, str, str, str, Config]):
152170
atcoder_client, problem, workspace_root_path, template_code_path, replacement_code_path, lang, config = argv
153171
prepare_procedure(
154172
atcoder_client, problem, workspace_root_path, template_code_path,
155173
replacement_code_path, lang, config)
156174

157175

158-
def prepare_workspace(atcoder_client: AtCoderClient,
159-
contest_id: str,
160-
workspace_root_path: str,
161-
template_code_path: str,
162-
replacement_code_path: str,
163-
lang: str,
164-
parallel: bool,
165-
config: CodeGenConfig,
166-
):
176+
def prepare_contest(atcoder_client: AtCoderClient,
177+
contest_id: str,
178+
workspace_root_path: str,
179+
template_code_path: str,
180+
replacement_code_path: str,
181+
lang: str,
182+
parallel: bool,
183+
config: Config,
184+
):
167185
retry_duration = 1.5
168186
while True:
169187
problem_list = atcoder_client.download_problem_list(
@@ -176,13 +194,23 @@ def prepare_workspace(atcoder_client: AtCoderClient,
176194

177195
tasks = [(atcoder_client, problem, workspace_root_path, template_code_path, replacement_code_path, lang, config) for
178196
problem in problem_list]
197+
198+
output_splitter()
199+
179200
if parallel:
180201
thread_pool = Pool(processes=cpu_count())
181202
thread_pool.map(func, tasks)
182203
else:
183204
for argv in tasks:
184205
func(argv)
185206

207+
if config.postprocess_config.exec_cmd_on_contest_dir is not None:
208+
contest_dir_path = os.path.join(workspace_root_path, contest_id)
209+
logging.info(_message_on_execution(contest_dir_path,
210+
config.postprocess_config.exec_cmd_on_contest_dir))
211+
config.postprocess_config.execute_on_contest_dir(
212+
contest_dir_path)
213+
186214

187215
DEFAULT_WORKSPACE_DIR_PATH = os.path.join(
188216
expanduser("~"), "atcoder-workspace")
@@ -217,11 +245,11 @@ def check_lang(lang: str):
217245
os.path.join(script_dir_path, "./atcodertools-default.toml"))
218246

219247

220-
def get_code_gen_config(config_path: Optional[str] = None):
221-
def _load(path: str):
248+
def get_config(config_path: Optional[str] = None) -> Config:
249+
def _load(path: str) -> Config:
222250
logging.info("Going to load {} as config".format(path))
223251
with open(path, 'r') as f:
224-
return CodeGenConfig.load(f)
252+
return Config.load(f)
225253

226254
if config_path:
227255
return _load(config_path)
@@ -285,7 +313,7 @@ def main(prog, args):
285313
help="File path to your config file\n{0}{1}".format("[Default (Primary)] {}\n".format(
286314
USER_CONFIG_PATH),
287315
"[Default (Secondary)] {}\n".format(
288-
DEFAULT_CONFIG_PATH))
316+
DEFAULT_CONFIG_PATH))
289317
)
290318

291319
args = parser.parse_args(args)
@@ -309,17 +337,17 @@ def main(prog, args):
309337
else:
310338
logging.info("Downloading data without login.")
311339

312-
prepare_workspace(client,
313-
args.contest_id,
314-
args.workspace,
315-
args.template if args.template is not None else get_default_template_path(
316-
args.lang),
317-
args.replacement if args.replacement is not None else get_default_replacement_path(
318-
args.lang),
319-
args.lang,
320-
args.parallel,
321-
get_code_gen_config(args.config)
322-
)
340+
prepare_contest(client,
341+
args.contest_id,
342+
args.workspace,
343+
args.template if args.template is not None else get_default_template_path(
344+
args.lang),
345+
args.replacement if args.replacement is not None else get_default_replacement_path(
346+
args.lang),
347+
args.lang,
348+
args.parallel,
349+
get_config(args.config)
350+
)
323351

324352

325353
if __name__ == "__main__":
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import subprocess
2+
3+
4+
def _run_command(exec_cmd: str, current_working_dir: str) -> str:
5+
proc = subprocess.run(exec_cmd,
6+
shell=True,
7+
stdout=subprocess.PIPE,
8+
stderr=subprocess.STDOUT,
9+
cwd=current_working_dir)
10+
return proc.stdout.decode("utf8")
11+
12+
13+
class PostprocessConfig:
14+
def __init__(self,
15+
exec_on_each_problem_dir: str = None,
16+
exec_on_contest_dir: str = None,
17+
):
18+
self.exec_cmd_on_problem_dir = exec_on_each_problem_dir
19+
self.exec_cmd_on_contest_dir = exec_on_contest_dir
20+
21+
def execute_on_problem_dir(self, problem_dir: str) -> str:
22+
assert self.exec_cmd_on_problem_dir is not None
23+
return _run_command(self.exec_cmd_on_problem_dir, problem_dir)
24+
25+
def execute_on_contest_dir(self, contest_dir: str) -> str:
26+
assert self.exec_cmd_on_contest_dir is not None
27+
return _run_command(self.exec_cmd_on_contest_dir, contest_dir)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[postprocess]
2+
exec_on_each_problem_dir='touch exec_on_each_problem_dir_ok'
3+
exec_on_contest_dir="touch exec_on_contest_dir_ok"

tests/resources/test_atc_env/test_prepare_workspace/agc029/A/exec_on_each_problem_dir_ok

Whitespace-only changes.

tests/resources/test_atc_env/test_prepare_workspace/agc029/B/exec_on_each_problem_dir_ok

Whitespace-only changes.

0 commit comments

Comments
 (0)