Skip to content

Commit 53bfc24

Browse files
authored
Merge pull request #111 from online-judge-tools/update-yukicoder
Update for yukicoder
2 parents 51d89c5 + 47a8fe0 commit 53bfc24

File tree

6 files changed

+87
-31
lines changed

6 files changed

+87
-31
lines changed

onlinejudge/_implementation/testcase_zipper.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def get(self) -> List[TestCase]:
4141
return self._testcases
4242

4343

44-
def extract_from_files(files: Iterator[Tuple[str, bytes]], format: str = '%s.%e', out: str = 'out') -> List[TestCase]:
44+
def extract_from_files(files: Iterator[Tuple[str, bytes]], format: str = '%s.%e', out: str = 'out', *, ignore_unmatched_samples: bool = False) -> List[TestCase]:
4545
"""
4646
:param out: is the extension for output files. This is used when the zip-file contains files like `sample-1.ans` instead of `sample-1.out`.
4747
"""
@@ -60,19 +60,20 @@ def extract_from_files(files: Iterator[Tuple[str, bytes]], format: str = '%s.%e'
6060
for name in sorted(names.keys()):
6161
data = names[name]
6262
if 'in' not in data or out not in data:
63-
logger.error('dangling sample found: %s', str(data))
64-
assert False
63+
logger.error('unmatched sample found: %s', str(data))
64+
if not ignore_unmatched_samples:
65+
raise RuntimeError('unmatched sample found: {}'.format(data))
6566
else:
6667
testcases += [TestCase(name, *data['in'], *data[out])]
6768
return testcases
6869

6970

70-
def extract_from_zip(zip_data: bytes, format: str, out: str = 'out') -> List[TestCase]:
71+
def extract_from_zip(zip_data: bytes, format: str, out: str = 'out', *, ignore_unmatched_samples: bool = False) -> List[TestCase]:
7172
def iterate():
7273
with zipfile.ZipFile(io.BytesIO(zip_data)) as fh:
7374
for filename in fh.namelist():
7475
if filename.endswith('/'): # TODO: use `fh.getinfo(filename).is_dir()` after we stop supporting Python 3.5
7576
continue
7677
yield filename, fh.read(filename)
7778

78-
return extract_from_files(iterate(), format=format, out=out)
79+
return extract_from_files(iterate(), format=format, out=out, ignore_unmatched_samples=ignore_unmatched_samples)

onlinejudge/_implementation/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def parse_content(parent: Union[bs4.NavigableString, bs4.Tag, bs4.Comment]) -> b
7272
return bs4.NavigableString(res)
7373

7474

75+
# TODO: send referer by default
7576
class FormSender:
7677
def __init__(self, form: bs4.Tag, url: str):
7778
assert isinstance(form, bs4.Tag)

onlinejudge/service/yukicoder.py

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,18 @@ def _get_contests(cls, *, session: Optional[requests.Session] = None) -> List[Di
7272
session = session or utils.get_default_session()
7373
if cls._contests is None:
7474
cls._contests = []
75-
for url in ('https://yukicoder.me/api/v1/contest/past', 'https://yukicoder.me/api/v1/contest/future'):
75+
for tense in ('past', 'current', 'future'):
76+
url = 'https://yukicoder.me/api/v1/contest/{}'.format(tense)
7677
resp = utils.request('GET', url, session=session)
7778
cls._contests.extend(json.loads(resp.content.decode()))
7879
return cls._contests
7980

81+
@classmethod
82+
def _get_csrf_token(cls, *, session: requests.Session) -> str:
83+
url = 'https://yukicoder.me/csrf_token'
84+
resp = utils.request('GET', url, session=session)
85+
return resp.content.decode()
86+
8087

8188
class YukicoderContest(onlinejudge.type.Contest):
8289
"""
@@ -93,11 +100,10 @@ def list_problems(self, *, session: Optional[requests.Session] = None) -> Sequen
93100
"""
94101

95102
session = session or utils.get_default_session()
96-
for contest in YukicoderService._get_contests(session=session):
97-
if contest['Id'] == self.contest_id:
98-
table = {problem['ProblemId']: problem['No'] for problem in YukicoderService._get_problems(session=session)}
99-
return [YukicoderProblem(problem_no=table[problem_id]) for problem_id in contest['ProblemIdList']]
100-
raise RuntimeError('Failed to get the contest information from API: {}'.format(self.get_url()))
103+
url = 'https://yukicoder.me/api/v1/contest/id/{}'.format(self.contest_id)
104+
resp = utils.request('GET', url, session=session)
105+
data = json.loads(resp.content.decode())
106+
return [YukicoderProblem(problem_id=problem_id) for problem_id in data['ProblemIdList']]
101107

102108
def get_url(self) -> str:
103109
return 'https://yukicoder.me/contests/{}'.format(self.contest_id)
@@ -156,7 +162,7 @@ def download_system_cases(self, *, session: Optional[requests.Session] = None) -
156162
url = '{}/testcase.zip'.format(self.get_url())
157163
resp = utils.request('GET', url, session=session)
158164
fmt = 'test_%e/%s'
159-
return onlinejudge._implementation.testcase_zipper.extract_from_zip(resp.content, fmt)
165+
return onlinejudge._implementation.testcase_zipper.extract_from_zip(resp.content, fmt, ignore_unmatched_samples=True) # NOTE: yukicoder's test sets sometimes contain garbages. The owner insists that this is an intended behavior, so we need to ignore them.
160166

161167
def _parse_sample_tag(self, tag: bs4.Tag) -> Optional[Tuple[str, str]]:
162168
assert isinstance(tag, bs4.Tag)
@@ -177,6 +183,8 @@ def submit_code(self, code: bytes, language_id: LanguageId, *, filename: Optiona
177183
:raises NotLoggedInError:
178184
"""
179185

186+
# NOTE: An implementation with the official API exists at 492d8d7. This is reverted at 2b7e6f5 because the API ignores cookies and says "提出するにはログインが必要です" at least at that time.
187+
180188
session = session or utils.get_default_session()
181189
# get
182190
url = self.get_url() + '/submit'
@@ -192,7 +200,7 @@ def submit_code(self, code: bytes, language_id: LanguageId, *, filename: Optiona
192200
form.set('lang', language_id)
193201
form.set_file('file', filename or 'code', code)
194202
form.unset('custom_test')
195-
resp = form.request(session=session)
203+
resp = form.request(headers={'referer': url}, session=session)
196204
resp.raise_for_status()
197205
# result
198206
if 'submissions' in resp.url:
@@ -208,16 +216,10 @@ def submit_code(self, code: bytes, language_id: LanguageId, *, filename: Optiona
208216

209217
def get_available_languages(self, *, session: Optional[requests.Session] = None) -> List[Language]:
210218
session = session or utils.get_default_session()
211-
# get
212-
# We use the problem page since it is available without logging in
213-
resp = utils.request('GET', self.get_url(), session=session)
214-
# parse
215-
soup = bs4.BeautifulSoup(resp.content.decode(resp.encoding), utils.html_parser)
216-
select = soup.find('select', id='lang')
217-
languages = [] # type: List[Language]
218-
for option in select.find_all('option'):
219-
languages += [Language(option.attrs['value'], ' '.join(option.string.split()))]
220-
return languages
219+
url = 'https://yukicoder.me/api/v1/languages'
220+
resp = utils.request('GET', url, session=session)
221+
data = json.loads(resp.content.decode())
222+
return [Language(language['Id'], language['Name'] + ' (' + language['Ver'] + ')') for language in data]
221223

222224
def get_url(self) -> str:
223225
if self.problem_no:

tests/get_problem_yukicoder.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import os
12
import unittest
23

3-
import tests.utils
44
from onlinejudge_api.main import main
55

6+
YUKICODER_TOKEN = os.environ.get('YUKICODER_TOKEN')
7+
68

79
class GetProblemYukicoderTest(unittest.TestCase):
810
def test_100(self):
@@ -31,7 +33,7 @@ def test_100(self):
3133
actual = main(['get-problem', url], debug=True)
3234
self.assertEqual(expected, actual)
3335

34-
@unittest.skipIf(not tests.utils.is_logged_in('https://yukicoder.me/'), 'login is required')
36+
@unittest.skipIf(YUKICODER_TOKEN is None, '$YUKICODER_TOKEN is required')
3537
def test_2_system(self):
3638
"""This tests about system cases.
3739
"""

tests/service_yukicoder.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -136,12 +136,12 @@ def test_from_url(self):
136136

137137
def test_list_problems(self):
138138
self.assertEqual(YukicoderContest.from_url('https://yukicoder.me/contests/276').list_problems(), [
139-
YukicoderProblem(problem_no=1168),
140-
YukicoderProblem(problem_no=1169),
141-
YukicoderProblem(problem_no=1170),
142-
YukicoderProblem(problem_no=1171),
143-
YukicoderProblem(problem_no=1172),
144-
YukicoderProblem(problem_no=1173),
139+
YukicoderProblem(problem_id=4401),
140+
YukicoderProblem(problem_id=4809),
141+
YukicoderProblem(problem_id=4387),
142+
YukicoderProblem(problem_id=4729),
143+
YukicoderProblem(problem_id=4271),
144+
YukicoderProblem(problem_id=4255),
145145
])
146146

147147

tests/submit_code_yukicoder.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import os
2+
import pathlib
3+
import tempfile
4+
import textwrap
5+
import unittest
6+
7+
from onlinejudge_api.main import main
8+
9+
YUKICODER_TOKEN = os.environ.get('YUKICODER_TOKEN')
10+
11+
12+
@unittest.skipIf(YUKICODER_TOKEN is None, '$YUKICODER_TOKEN is required')
13+
class SubmitYukicoderTest(unittest.TestCase):
14+
def test_9000(self):
15+
url = 'https://yukicoder.me/problems/no/9000'
16+
filename = 'main.py'
17+
code = textwrap.dedent(r"""
18+
#!/usr/bin/env python3
19+
print "Hello World!"
20+
""")
21+
22+
with tempfile.TemporaryDirectory() as tempdir:
23+
path = pathlib.Path(tempdir) / filename
24+
with open(path, 'w') as fh:
25+
fh.write(code)
26+
language_id = main(['guess-language-id', '--file', str(path), url], debug=True)['result']['id']
27+
data = main(['submit-code', '--file', str(path), '--language', language_id, url], debug=True)
28+
self.assertEqual(data['status'], 'ok')
29+
30+
def test_527(self):
31+
url = 'https://yukicoder.me/problems/527'
32+
filename = 'main.cpp'
33+
code = textwrap.dedent(r"""
34+
#include <bits/stdc++.h>
35+
using namespace std;
36+
int main() {
37+
int a, b; cin >> a >> b;
38+
string s; cin >> s;
39+
cout << a + b << ' ' << s << endl;
40+
return 0;
41+
}
42+
""")
43+
44+
with tempfile.TemporaryDirectory() as tempdir:
45+
path = pathlib.Path(tempdir) / filename
46+
with open(path, 'w') as fh:
47+
fh.write(code)
48+
language_id = main(['guess-language-id', '--file', str(path), url], debug=True)['result']['id']
49+
data = main(['submit-code', '--file', str(path), '--language', language_id, url], debug=True)
50+
self.assertEqual(data['status'], 'ok')

0 commit comments

Comments
 (0)