Skip to content

Commit

Permalink
added compile-before, case-insensitive and fixed bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
JeanExtreme002 committed Apr 21, 2024
1 parent ac8ab04 commit 4f777a0
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 62 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ This project provides useful CLI tools for competitive programming, such as algo
$ pip install FastSnake
```

Use one of the commands below to check if the installation was successful.
```
$ fastsnake -v
```
or
```
$ python3 -m fastsnake -v
```

## Basic Usage:
Starting a contest from Codeforces...
```
Expand Down
2 changes: 1 addition & 1 deletion fastsnake/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
A Python Helper CLI for Competitive Programming
"""

__version__ = "1.4.9"
__version__ = "1.4.10"

__author__ = "Jean Loui Bernard Silva de Jesus"
__github__ = "https://github.com/JeanExtreme002"
Expand Down
4 changes: 4 additions & 0 deletions fastsnake/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from fastsnake.application.application import main

if __name__ == "__main__":
main()
47 changes: 18 additions & 29 deletions fastsnake/application/application.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,20 @@
from fastsnake.application.arg_parser import main_parser
from fastsnake.application.config import contest_config_filename
from fastsnake.application.external import add_external_module, delete_external_module
from fastsnake.application.contest import start_contest
from fastsnake.application.runner import run_test, run_test_generator
from fastsnake.application.runner import compile, run_test, run_test_generator
from fastsnake.util import atcoder
from fastsnake.util import codeforces
from fastsnake.util.compiler import compile_code

from typing import List

import fastsnake
import json
import os


project_path = os.path.join(os.path.dirname(__file__), "..")
args = main_parser.parse_args()


def compile(filename: str, problem: bool = False) -> None:
"""
Compile a solution.
"""
# If the provided filename does not contains PY extension, check if it is a contest problem.
if not filename.endswith(".py") and problem:
with open(contest_config_filename) as file:
config = json.load(file)

filename = os.path.join(config["solutions_namespace"], filename + ".py")

# Get the output filename and compile the solution.
base_name = os.path.basename(filename)
directory = os.path.dirname(filename)

output_filename = os.path.join(directory, "compiled_" + base_name)

compile_code(filename, output_filename)


def load_atcoder_problem(contest_id: str, problem: str, directory: str, namespace: str) -> None:
"""
Download test cases from AtCoder of a problem.
Expand Down Expand Up @@ -228,12 +205,24 @@ def main() -> None:
# Test the solution.
if args.command == "test":
if args.generator:
result = run_test_generator(args.problem, args.generator, args.step_counter, debug=args.debug)
run_test_generator(
args.problem,
args.generator,
args.step_counter,
compile_before=args.compile_before,
compile_after=args.test_and_compile,
case_insensitive=args.case_insensitive,
debug=args.debug
)
else:
result = run_test(args.problem, args.step_counter, debug=args.debug)

if result and args.test_and_compile:
compile(args.problem, problem=True)
run_test(
args.problem,
args.step_counter,
compile_before=args.compile_before,
compile_after=args.test_and_compile,
case_insensitive=args.case_insensitive,
debug=args.debug
)

# Compile a fastsnake solution.
elif args.command == "compile":
Expand Down
4 changes: 3 additions & 1 deletion fastsnake/application/arg_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
# Testing Solutions.
test_parser = command_parser.add_parser("test", help="Test a solution for a contest problem")
test_parser.add_argument("problem", type=str, help="Problem of the contest")
test_parser.add_argument("-c", "--compile", action="store_true", dest="test_and_compile", help="Test and compile the solution")
test_parser.add_argument("-c", "--compile", action="store_true", dest="test_and_compile", help="Test the solution and compile the solution")
test_parser.add_argument("-cb", "--compile-before", action="store_true", dest="compile_before", help="Compile the solution and test after")
test_parser.add_argument("-g", "--generator", type=int, metavar="n_tests", dest="generator", help="Use generator module to test the solution")
test_parser.add_argument("-s", "--step-counter", action="store_true", dest="step_counter", help="Returns the approximate number of steps executed")
test_parser.add_argument("-ci", "--case-insensitive", action="store_true", dest="case_insensitive", help="Indicates the output is case insensitive")
test_parser.add_argument("--return-temp-module-for-debug", action="store_true", dest="debug")

# Compiling Solutions.
Expand Down
13 changes: 8 additions & 5 deletions fastsnake/application/contest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,20 @@ def start_contest(
file.write("# Solution for problem " + problem + "\n\n")
file.write("# from fastsnake.algorithms.something import *\n")
file.write("# from fastsnake.structures.something import *\n")
file.write("# from fastsnake.external.your_external_module import *\n")
file.write("\n")
file.write("# Just remove the fastsnake code below if you will not use it.\n")
file.write("from fastsnake.entries import *\n")
file.write("inputi = input_int\n")
file.write("inputa = input_int_array\n")
file.write("inputm = input_int_matrix\n")
file.write("puti = input_int\n")
file.write("puta = input_int_array\n")
file.write("putm = input_int_matrix\n")
file.write("\n")
file.write("for test_case in range(int(input())):\n")
file.write(" n = int(input())\n")
file.write(" pass\n")
file.write("\n\n")
file.write("# [WARNING]: THIS IS AN **UNCOMPILED** SOLUTION!! Remember to REMOVE fastsnake code if you will NOT COMPILE this code.\n")
file.write("\n\n")

# Create folder with Python modules for writting test case generators.
if not os.path.exists(config["test_generators_namespace"]):
Expand All @@ -70,14 +73,14 @@ def start_contest(
file.write("def gen_string_array(size: int, start: int, end: int, letters: str = string.ascii_lowercase):\n")
file.write(" return ' '.join(gen_string(gen_int(start, end), letters) for _ in range(size))\n")
file.write("\n\n")
file.write("def generate(test_id: int) -> \"Generator\": # Yield any data type (it will be converted to str later)\n")
file.write("def generate(test_id: int, case_insensitive: bool) -> \"Generator\": # Yield any data type (it will be converted to str later)\n")
file.write(" # Sample code ...\n")
file.write(" yield gen_int(0, 100)\n")
file.write(" yield gen_int_array(10, 0, 100)\n")
file.write(" yield gen_string(10, string.ascii_uppercase)\n")
file.write(" yield gen_string_array(10, 1, 20, string.ascii_uppercase + string.ascii_lowercase)\n")
file.write("\n\n")
file.write("def test_output(input_: list[str], output: str) -> bool:\n")
file.write("def test_output(input_: list[str], output: str, case_insensitive: bool) -> bool:\n")
file.write(" raise NotImplementedError()")
file.write("\n")

Expand Down
107 changes: 81 additions & 26 deletions fastsnake/application/runner.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from fastsnake.application.config import contest_config_filename
from fastsnake.util.compiler import compile_code
from fastsnake.util.step_counter import inject_step_counter

from tempfile import NamedTemporaryFile

import importlib
import json
import os
Expand All @@ -11,7 +13,29 @@
import string


def run_test(problem: str, step_counter: bool = False, debug: bool = False) -> bool:
def compile(filename: str) -> None:
"""
Compile a solution.
"""
# Get the output filename and compile the solution.
base_name = os.path.basename(filename)
directory = os.path.dirname(filename)

output_filename = os.path.join(directory, "compiled_" + base_name)

compile_code(filename, output_filename)

return output_filename


def run_test(
problem: str,
step_counter: bool = False,
compile_before: bool = False,
compile_after: bool = False,
case_insensitive: bool = False,
debug: bool = False
) -> bool:
"""
Run the solution for a problem of the contest.
"""
Expand All @@ -21,6 +45,16 @@ def run_test(problem: str, step_counter: bool = False, debug: bool = False) -> b
if not problem in config["problems"]:
raise ValueError(f"Invalid problem ID: {problem}")

# Get the source code.
module = os.path.join(config["solutions_namespace"], problem.upper() + ".py")

if compile_before:
module = compile(module)

with open(module) as file:
source_code = file.read() + "\n"

# Check the solution for the test cases.
test_case = 0

for filename in os.listdir(config["test_cases_namespace"]):
Expand All @@ -38,35 +72,30 @@ def run_test(problem: str, step_counter: bool = False, debug: bool = False) -> b
# Copy the module, injecting a code for loading input data.
inject = f"import sys\nsys.stdin = open(r'{input_filename}', 'r')\n\n"

module = os.path.join(config["solutions_namespace"], problem.upper() + ".py")

with open(module) as module:
code = module.read()

with NamedTemporaryFile("w", delete=False) as module:
module.write(inject + code)
with NamedTemporaryFile("w", delete=False) as temp_module:
temp_module.write(inject + source_code)

# Inject the step counter to the code, if required.
step_counter_variable = None

if step_counter:
step_counter_variable = inject_step_counter(module.name, module.name)
step_counter_variable = inject_step_counter(temp_module.name, temp_module.name)

# Inject code to get a specific success code.
ascii_range = string.ascii_lowercase + string.ascii_uppercase
success_code = ":success_code" + "".join(random.choice(ascii_range) for _ in range(100)) + ":"

with open(module.name, mode="a") as file:
with open(temp_module.name, mode="a") as file:
file.write(f"print('{success_code}')\n")

# Print the name of the module that will be executed, if debug is True.
if debug: print(f"[DEBUG] Temp Module Path of Test #{test_case}:", module.name)
if debug: print(f"[DEBUG] Temp Module Path of Test #{test_case}:", temp_module.name)

# Run the solution.
command = "python" if "win32" in sys.platform else "python3"

process = subprocess.Popen(
[command, module.name],
[command, temp_module.name],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=False,
Expand Down Expand Up @@ -99,6 +128,10 @@ def run_test(problem: str, step_counter: bool = False, debug: bool = False) -> b
output = file.read().strip().replace("\r", "").rstrip("\n")

# Compare the outputs.
if case_insensitive:
result = result.lower()
output = output.lower()

if output != result or not success:
with open(input_filename) as file:
input_data = file.read()
Expand All @@ -119,10 +152,21 @@ def run_test(problem: str, step_counter: bool = False, debug: bool = False) -> b
print(f"SUCCESS!! Your solution was accepted at all {test_case} test cases.")
if step_counter: print(f"Approximate number of steps executed: {step_counter_result}")

if compile_after and not compile_before:
compile(module)

return True


def run_test_generator(problem: str, tests: int = 1, step_counter: bool = False, debug: bool = False) -> bool:
def run_test_generator(
problem: str,
tests: int = 1,
step_counter: bool = False,
compile_before: bool = False,
compile_after: bool = False,
case_insensitive: bool = False,
debug: bool = False
) -> bool:
"""
Run the solution for a problem of the contest.
"""
Expand All @@ -132,6 +176,15 @@ def run_test_generator(problem: str, tests: int = 1, step_counter: bool = False,
if not problem in config["problems"]:
raise ValueError(f"Invalid problem ID: {problem}")

# Get the source code.
module = os.path.join(config["solutions_namespace"], problem.upper() + ".py")

if compile_before:
module = compile(module)

with open(module) as file:
source_code = file.read() + "\n"

# Import the generator module.
path = config["test_generators_namespace"]

Expand All @@ -142,7 +195,7 @@ def run_test_generator(problem: str, tests: int = 1, step_counter: bool = False,

# Run the tests.
for test_id in range(tests):
input_data = [str(line) for line in generator.generate(test_id)]
input_data = [str(line) for line in generator.generate(test_id, case_insensitive=case_insensitive)]

# Create an input file.
with NamedTemporaryFile("w", delete=False) as input_file:
Expand All @@ -154,35 +207,30 @@ def run_test_generator(problem: str, tests: int = 1, step_counter: bool = False,
# Copy the module, injecting a code for loading input data.
inject = f"import sys\nsys.stdin = open(r'{input_filename}', 'r')\n\n"

module = os.path.join(config["solutions_namespace"], problem.upper() + ".py")

with open(module) as module:
code = module.read()

with NamedTemporaryFile("w", delete=False) as module:
module.write(inject + code)
with NamedTemporaryFile("w", delete=False) as temp_module:
temp_module.write(inject + source_code)

# Inject the step counter to the code, if required.
step_counter_variable = None

if step_counter:
step_counter_variable = inject_step_counter(module.name, module.name)
step_counter_variable = inject_step_counter(temp_module.name, temp_module.name)

# Inject code to get a specific success code.
ascii_range = string.ascii_lowercase + string.ascii_uppercase
success_code = ":success_code" + "".join(random.choice(ascii_range) for _ in range(100)) + ":"

with open(module.name, mode="a") as file:
with open(temp_module.name, mode="a") as file:
file.write(f"print('{success_code}')\n")

# Print the name of the module that will be executed, if debug is True.
if debug: print(f"[DEBUG] Temp Module Path of Test #{test_id}:", module.name)
if debug: print(f"[DEBUG] Temp Module Path of Test #{test_id}:", temp_module.name)

# Run the solution.
command = "python" if "win32" in sys.platform else "python3"

process = subprocess.Popen(
[command, module.name],
[command, temp_module.name],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=False,
Expand All @@ -209,8 +257,11 @@ def run_test_generator(problem: str, tests: int = 1, step_counter: bool = False,
result = result.rstrip("\n")

# Check the output.
if case_insensitive:
result = result.lower()

try:
check = generator.test_output(input_data, result)
check = generator.test_output(input_data, result, case_insensitive=case_insensitive)
except NotImplementedError:
print("ERROR: You must implement the generator() and test_ouput() at the generator module.")
return False
Expand All @@ -227,4 +278,8 @@ def run_test_generator(problem: str, tests: int = 1, step_counter: bool = False,
if step_counter: print(f"Approximate number of steps executed for test #{test_id}: {step_counter_result}")

print(f"SUCCESS!! Your solution was accepted at all {tests} generated tests.")

if compile_after and not compile_before:
compile(module)

return True
3 changes: 3 additions & 0 deletions fastsnake/util/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,6 @@ def compile_code(input_filename: str, output_filename: str) -> None:
# Write the compiled code.
with open(output_filename, "w") as file:
file.write(string)
file.write("\n\n# " + "=" * 50 + "\n")
file.write("# COMPILED SOLUTION \n")
file.write("# " + "=" * 50 + "\n\n")

0 comments on commit 4f777a0

Please sign in to comment.