Repository: kmyk/online-judge-tools
Pull request #127 (2018-01-06 10:50:38)

Release/0.1.28
CI一発で通ってくれ :pray: :pray: :pray:


Patch:
diff --git a/onlinejudge/atcoder.py b/onlinejudge/atcoder.py
index 7fed2ba..e60d179 100644
--- a/onlinejudge/atcoder.py
+++ b/onlinejudge/atcoder.py
@@ -204,7 +204,7 @@ class AtCoderProblem(onlinejudge.problem.Problem):
         assert language in self.get_language_dict(session=session)
         session = session or utils.new_default_session()
         # get
-        url = 'http://{}.contest.atcoder.jp/submit'.format(self.contest_id)
+        url = 'http://{}.contest.atcoder.jp/submit'.format(self.contest_id)  # TODO: use beta.atcoder.jp
         resp = utils.request('GET', url, session=session)
         msgs = AtCoderService._get_messages_from_cookie(resp.cookies)
         if AtCoderService._report_messages(msgs, unexpected=True):
@@ -231,7 +231,9 @@ class AtCoderProblem(onlinejudge.problem.Problem):
             # example: https://practice.contest.atcoder.jp/submissions/me#32174
             # CAUTION: this URL is not a URL of the submission
             log.success('success: result: %s', resp.url)
-            return onlinejudge.submission.CompatibilitySubmission(resp.url)
+            # NOTE: ignore the returned legacy URL and use beta.atcoder.jp's one
+            url = 'https://beta.atcoder.jp/contests/{}/submissions/me'.format(self.contest_id)
+            return onlinejudge.submission.CompatibilitySubmission(url)
         else:
             log.failure('failure')
             return None
diff --git a/onlinejudge/implementation/command/code_statistics.py b/onlinejudge/implementation/command/code_statistics.py
new file mode 100644
index 0000000..3b84b25
--- /dev/null
+++ b/onlinejudge/implementation/command/code_statistics.py
@@ -0,0 +1,42 @@
+# Python Version: 3.x
+import onlinejudge
+import onlinejudge.implementation.utils as utils
+import onlinejudge.implementation.logging as log
+import string
+import os
+import os.path
+import subprocess
+import contextlib
+
+
+def get_char_class(c):
+    assert isinstance(c, int)
+    assert 0 <= c < 256
+    if chr(c) in string.ascii_letters + string.digits:
+        return 'alnum'
+    elif chr(c) in ' \t\n':
+        return 'whitespace'
+    elif 32 < c < 127:
+        return 'symbol'
+    else:
+        return 'binary'
+
+def get_statistics(s):
+    assert isinstance(s, bytes)
+    stat = {
+        'binary': 0,
+        'alnum': 0,
+        'symbol': 0,
+        'whitespace': 0,
+    }
+    for c in s:
+        stat[get_char_class(c)] += 1
+    return stat
+
+def code_statistics(args):
+    with open(args.file, 'rb') as fh:
+        code = fh.read()
+    stat = get_statistics(code)
+    stat['size'] = len(code)
+    for key in ( 'size', 'binary', 'alnum', 'symbol', 'whitespace' ):
+        log.info('%s = %d', key, stat[key])
diff --git a/onlinejudge/implementation/main.py b/onlinejudge/implementation/main.py
index 271c8d8..5f95e6c 100644
--- a/onlinejudge/implementation/main.py
+++ b/onlinejudge/implementation/main.py
@@ -10,6 +10,7 @@ from onlinejudge.implementation.command.generate_scanner import generate_scanner
 from onlinejudge.implementation.command.test import test, generate_output
 from onlinejudge.implementation.command.split_input import split_input, split_input_auto_footer
 from onlinejudge.implementation.command.test_reactive import test_reactive
+from onlinejudge.implementation.command.code_statistics import code_statistics
 import argparse
 import sys
 import os
@@ -134,6 +135,8 @@ tips:
     subparser.add_argument('-s', '--silent', action='store_true', help='don\'t report output and correct answer even if not AC  (for --mode all)')
     subparser.add_argument('-e', '--error', type=float, help='check as floating point number: correct if its absolute or relative error doesn\'t exceed it')
     subparser.add_argument('-t', '--tle', type=float)
+    subparser.add_argument('--no-ignore-backup', action='store_false', dest='ignore_backup')
+    subparser.add_argument('--ignore-backup', action='store_true', help='ignore backup files and hidden files (i.e. files like "*~", "\\#*\\#" and ".*") (default)')
     subparser.add_argument('test', nargs='*', help='paths of test cases. (if empty: globbed from --format)')
 
     # generate scanner
@@ -144,9 +147,10 @@ tips:
             epilog='''\
 supported services:
   AtCoder
-  Yukicoder
   HackerRank
 
+  (Yukicoder has been removed)
+
 example:
   http://agc001.contest.atcoder.jp/tasks/agc001_a
   input format:
@@ -185,6 +189,8 @@ tips:
     subparser.add_argument('--shell', action='store_true', help='use the --command as a shellscript instead of a path')
     subparser.add_argument('-f', '--format', default='test/%s.%e', help='a format string to recognize the relationship of test cases. (default: "test/%%s.%%e")')
     subparser.add_argument('test', nargs='*', help='paths of input cases. (if empty: globbed from --format)')
+    subparser.add_argument('--no-ignore-backup', action='store_false', dest='ignore_backup')
+    subparser.add_argument('--ignore-backup', action='store_true', help='ignore backup files and hidden files (i.e. files like "*~", "\\#*\\#" and ".*") (default)')
 
     # split input
     subparser = subparsers.add_parser('split-input',
@@ -262,6 +268,15 @@ example:
     subparser.add_argument('--shell', action='store_true', help='use the judge  and --command as a shellscript instead of a path')
     subparser.add_argument('judge', help='judge program using standard I/O')
 
+    # code statistics
+    subparser = subparsers.add_parser('code-statistics',
+            aliases=[ 'c/s' ],
+            help='print the code statistics used in Anarchy Golf',
+            formatter_class=argparse.RawTextHelpFormatter,
+            epilog='''\
+''')
+    subparser.add_argument('file')
+
     args = parser.parse_args(args=args)
 
     # logging
@@ -291,6 +306,8 @@ example:
         generate_output(args)
     elif args.subcommand in [ 'split-input', 's/i' ]:
         split_input(args)
+    elif args.subcommand in [ 'code-statistics', 'c/s' ]:
+        code_statistics(args)
     else:
         parser.print_help(file=sys.stderr)
         sys.exit(1)
diff --git a/onlinejudge/implementation/utils.py b/onlinejudge/implementation/utils.py
index 7b6f40b..bf519d3 100644
--- a/onlinejudge/implementation/utils.py
+++ b/onlinejudge/implementation/utils.py
@@ -141,7 +141,8 @@ class SampleZipper(object):
             self.dangling = { 'data': s, 'name': name }
         else:
             if re.search('input', name, re.IGNORECASE) or re.search('入力', name):
-                log.warning('strange name for output string: %s', name)
+                if not (re.search('output', name, re.IGNORECASE) or re.search('出力', name)):  # to ignore titles like "Output for Sample Input 1"
+                    log.warning('strange name for output string: %s', name)
             self.data += [ {
                 'input': self.dangling,
                 'output': { 'data': s, 'name': name },
diff --git a/onlinejudge/implementation/version.py b/onlinejudge/implementation/version.py
index 1c5d65c..bc8d9ad 100644
--- a/onlinejudge/implementation/version.py
+++ b/onlinejudge/implementation/version.py
@@ -3,5 +3,5 @@ __author__  = 'Kimiyuki Onaka'
 __email__ = 'kimiyuki95@gmail.com'
 __license__ = 'MIT License'
 __url__ = 'https://github.com/kmyk/online-judge-tools'
-__version_info__ = ( 0, 1, 27, 'final', 0 )
+__version_info__ = ( 0, 1, 28, 'final', 0 )
 __version__ = '.'.join(map(str, __version_info__[: 3]))
diff --git a/onlinejudge/yukicoder.py b/onlinejudge/yukicoder.py
index 3cc9cc1..2892906 100644
--- a/onlinejudge/yukicoder.py
+++ b/onlinejudge/yukicoder.py
@@ -79,14 +79,14 @@ class YukicoderService(onlinejudge.service.Service):
                 and result.netloc == 'yukicoder.me':
             return cls()
 
-    # example: {"Id":10,"Name":"yuki2006","Solved":280,"Level":34,"Rank":59,"Score":52550,"Points":7105,"Notice":"匿名ユーザーの情報は取れません。ユーザー名が重複している場合は最初に作られたIDが優先されます（その場合は運営にご報告いただければマージします）。このAPIはベータ版です。予告なく変更される場合があります。404を返したら廃止です。"}
-    def get_user(self, id=None, name=None, session=None):
+    def _issue_official_api(self, api, id=None, name=None, session=None):
         assert (id is not None) != (name is not None)
         if id is not None:
             assert isinstance(id, int)
-            url = 'https://yukicoder.me/api/v1/user/%d' % id
+            sometihng = { 'user': '', 'solved': 'id/' }[api]
+            url = 'https://yukicoder.me/api/v1/{}/{}{}'.format(api, sometihng, id)
         else:
-            url = 'https://yukicoder.me/api/v1/user/name/%s' % urllib.parse.quote(name)
+            url = 'https://yukicoder.me/api/v1/{}/name/{}'.format(api, urllib.parse.quote(name))
         session = session or utils.new_default_session()
         try:
             resp = utils.request('GET', url, session=session)
@@ -95,6 +95,15 @@ class YukicoderService(onlinejudge.service.Service):
             return None
         return json.loads(resp.content.decode(resp.encoding))
 
+    # example: {"Id":10,"Name":"yuki2006","Solved":280,"Level":34,"Rank":59,"Score":52550,"Points":7105,"Notice":"匿名ユーザーの情報は取れません。ユーザー名が重複している場合は最初に作られたIDが優先されます（その場合は運営にご報告いただければマージします）。このAPIはベータ版です。予告なく変更される場合があります。404を返したら廃止です。"}
+    def get_user(self, *args, **kwargs):
+        return self._issue_official_api('user', *args, **kwargs)
+
+    # https://twitter.com/yukicoder/status/935943170210258944
+    # example: [{"No":46,"ProblemId":43,"Title":"はじめのn歩","AuthorId":25,"TesterId":0,"Level":1,"ProblemType":0,"Tags":"実装"}]
+    def get_solved(self, *args, **kwargs):
+        return self._issue_official_api('solved', *args, **kwargs)
+
     # example: https://yukicoder.me/users/237/favorite
     def get_user_favorite(self, id, session=None):
         url = 'https://yukicoder.me/users/%d/favorite' % id
@@ -307,74 +316,6 @@ class YukicoderProblem(onlinejudge.problem.Problem):
                     return cls(problem_id=int(n))
             return cls()
 
-    # Fri Jan  6 16:49:14 JST 2017
-    _language_dict = {}
-    _language_dict['cpp']        = { 'description': 'C++11 (gcc 4.8.5)' }
-    _language_dict['cpp14' ]     = { 'description': 'C++14 (gcc 6.2.0)' }
-    _language_dict['c']          = { 'description': 'C (gcc 4.8.5)' }
-    _language_dict['java8']      = { 'description': 'Java8 (openjdk 1.8.0_111)' }
-    _language_dict['csharp']     = { 'description': 'C# (mono 4.6.1)' }
-    _language_dict['perl']       = { 'description': 'Perl (5.16.3)' }
-    _language_dict['perl6']      = { 'description': 'Perl6 (rakudo 2016.10-114-g8e79509)' }
-    _language_dict['php']        = { 'description': 'PHP (5.4.16)' }
-    _language_dict['python']     = { 'description': 'Python2 (2.7.11)' }
-    _language_dict['python3']    = { 'description': 'Python3 (3.5.1)' }
-    _language_dict['pypy2']      = { 'description': 'PyPy2 (4.0.0)' }
-    _language_dict['pypy3']      = { 'description': 'PyPy3 (2.4.0)' }
-    _language_dict['ruby']       = { 'description': 'Ruby (2.3.1p112)' }
-    _language_dict['d']          = { 'description': 'D (dmd 2.071.1)' }
-    _language_dict['go']         = { 'description': 'Go (1.7.3)' }
-    _language_dict['haskell']    = { 'description': 'Haskell (7.8.3)' }
-    _language_dict['scala']      = { 'description': 'Scala (2.11.8)' }
-    _language_dict['nim']        = { 'description': 'Nim (0.15.2)' }
-    _language_dict['rust']       = { 'description': 'Rust (1.12.1)' }
-    _language_dict['kotlin']     = { 'description': 'Kotlin (1.0.2)' }
-    _language_dict['scheme']     = { 'description': 'Scheme (Gauche-0.9.4)' }
-    _language_dict['crystal']    = { 'description': 'Crystal (0.19.4)' }
-    _language_dict['ocaml']      = { 'description': 'OCaml (4.01.1)' }
-    _language_dict['fsharp']     = { 'description': 'F# (4.0)' }
-    _language_dict['elixir']     = { 'description': 'Elixir (0.12.5)' }
-    _language_dict['lua']        = { 'description': 'Lua (LuaJit 2.0.4)' }
-    _language_dict['fortran']    = { 'description': 'Fortran (gFortran 4.8.5)' }
-    _language_dict['node']       = { 'description': 'JavaScript (node v7.0.0)' }
-    _language_dict['vim']        = { 'description': 'Vim script (v8.0.0124)' }
-    _language_dict['sh']         = { 'description': 'Bash (Bash 4.2.46)' }
-    _language_dict['text']       = { 'description': 'Text (cat 8.22)' }
-    _language_dict['nasm']       = { 'description': 'Assembler (nasm 2.10.07)' }
-    _language_dict['bf']         = { 'description': 'Brainfuck (BFI 1.1)' }
-    _language_dict['Whitespace'] = { 'description': 'Whitespace (0.3)' }
-    def get_language_dict(self, session=None):  # TODO: get dynamically
-        return self.__class__._language_dict
-
-    def submit(self, code, language, session=None):
-        assert language in self.get_language_dict(session=session)
-        session = session or utils.new_default_session()
-        # get
-        resp = utils.request('GET', self.get_url() + '/submit', session=session)
-        # parse
-        soup = bs4.BeautifulSoup(resp.content.decode(resp.encoding), utils.html_parser)
-        form = soup.find('form', action=re.compile(r'/submit$'))
-        if not form:
-            log.error('form not found')
-            return None
-        log.debug('form: %s', str(form))
-        # post
-        form = utils.FormSender(form, url=resp.url)
-        if False:
-            form.set('source', code)
-        else:
-            form.set_file('file', ('source', code))
-        form.set('lang', language)
-        resp = form.request(session=session)
-        resp.raise_for_status()
-        # result
-        if '/submissions/' in resp.url:
-            log.success('success: result: %s', resp.url)
-            return onlinejudge.submission.CompatibilitySubmission(resp.url, problem=self)
-        else:
-            log.failure('failure')
-            return None
-
     def get_service(self):
         return YukicoderService()
 
diff --git a/readme.md b/readme.md
index 3dee098..4e07675 100644
--- a/readme.md
+++ b/readme.md
@@ -29,7 +29,7 @@ Tools for online judge services. Downloading sample cases, Testing/Submitting yo
     -   TopCoder
 -   Submit your solution
     -   AtCoder
-    -   Yukicoder
+    -   ~~Yukicoder~~ (removed)
     -   HackerRank
     -   TopCoder (Marathon Match)
 -   Generate scanner for input  (experimental)
@@ -39,6 +39,7 @@ Tools for online judge services. Downloading sample cases, Testing/Submitting yo
 -   Test your solution for reactive problem
 -   Generate output files from input and reference implementation
 -   Split an input file with many cases to files
+-   Print the code statistics used in Anarchy Golf
 
 ## How to install
 


Test patch:
diff --git a/onlinejudge/implementation/command/test.py b/onlinejudge/implementation/command/test.py
index e7ad3c0..248f6ad 100644
--- a/onlinejudge/implementation/command/test.py
+++ b/onlinejudge/implementation/command/test.py
@@ -35,6 +35,19 @@ def path_from_format(format, name, ext):
     table['e'] = ext
     return utils.parcentformat(format, table)
 
+def is_backup_or_hidden_file(path):
+    basename = os.path.basename(path)
+    return basename.endswith('~') or (basename.startswith('#') and basename.endswith('#')) or basename.startswith('.')
+
+def drop_backup_or_hidden_files(paths):
+    result = []
+    for path in paths:
+        if is_backup_or_hidden_file(path):
+            log.warning('ignore a backup file: %s', path)
+        else:
+            result += [ path ]
+    return result
+
 def construct_relationship_of_files(paths, format):
     tests = collections.defaultdict(dict)
     for path in paths:
@@ -82,6 +95,8 @@ def compare_as_floats(xs, ys, error):
 def test(args):
     if not args.test:
         args.test = glob_with_format(args.format) # by default
+    if args.ignore_backup:
+        args.test = drop_backup_or_hidden_files(args.test)
     tests = construct_relationship_of_files(args.test, args.format)
     # run tests
     if args.error: # float mode
@@ -163,6 +178,8 @@ def test(args):
 def generate_output(args):
     if not args.test:
         args.test = glob_with_format(args.format) # by default
+    if args.ignore_backup:
+        args.test = drop_backup_or_hidden_files(args.test)
     tests = construct_relationship_of_files(args.test, args.format)
     for name, it in sorted(tests.items()):
         log.emit('')
diff --git a/tests/code_statistics.py b/tests/code_statistics.py
new file mode 100644
index 0000000..96d631a
--- /dev/null
+++ b/tests/code_statistics.py
@@ -0,0 +1,24 @@
+import unittest
+
+from onlinejudge.implementation.command.code_statistics import get_statistics
+
+class CodeStatisticsTest(unittest.TestCase):
+
+    def test_get_statistics_1(self):
+        # http://golf.shinh.org/reveal.rb?Turn+a+1d+array+into+a+5d+array/mitchs+%28cheat%29_1465127634&bf
+        code = b'''<-],<[->>[.<]>>>.,+]\x00 ;\x01e\x0b'''
+        stat = get_statistics(code)
+        self.assertEqual(stat['binary'], 3)
+        self.assertEqual(stat['alnum'], 1)
+        self.assertEqual(stat['symbol'], 21)
+
+    def test_get_statistics_2(self):
+        # http://golf.shinh.org/reveal.rb?DozzFizzBuzz/%2520_1434433980
+        code = b'''\
+\x01oiuea1.>g1+00p25*,>:00g\\2g%#v_:6%1g,:5%1v
+DFBMPQ@_^#`"c"g  $_^#`"\x18": +1<,,"zz" ,g0+<
+\x02\x03\x05\x07\x0b\x0d\x11\x13\x17\x1d\x1f%)+/5;=CGIOSYa'''
+        stat = get_statistics(code)
+        self.assertEqual(stat['binary'], 13)
+        self.assertEqual(stat['alnum'], 46)
+        self.assertEqual(stat['symbol'], 46)
diff --git a/tests/yukicoder.py b/tests/yukicoder.py
index e2e1787..af8a35d 100644
--- a/tests/yukicoder.py
+++ b/tests/yukicoder.py
@@ -7,16 +7,40 @@ class YukicoderTest(unittest.TestCase):
 
     def test_get_user_10(self):
         data = YukicoderService().get_user(id=10)
+        self.assertIn('Id', data)
+        self.assertIn('Name', data)
+        self.assertIn('Solved', data)
+        self.assertIn('Level', data)
+        self.assertIn('Rank', data)
+        self.assertIn('Score', data)
+        self.assertIn('Points', data)
         self.assertEqual(data['Id'], 10)
         self.assertEqual(data['Name'], 'yuki2006')
     def test_get_user_yuki2006(self):
         data = YukicoderService().get_user(name='yuki2006')
         self.assertEqual(data['Id'], 10)
         self.assertEqual(data['Name'], 'yuki2006')
-    # a workaround. see https://yukicoder.slack.com/archives/C15A2JVKJ/p1512970067000103
-    # def test_get_user_0(self):
-    #     data = YukicoderService().get_user(id=0)
-    #     self.assertIs(data, None)
+    def test_get_user_0(self):
+        data = YukicoderService().get_user(id=0)
+        self.assertIs(data, None)
+
+    def test_get_solved_10(self):
+        data = YukicoderService().get_solved(id=10)
+        self.assertGreater(len(data), 200)
+        self.assertIn('No', data[0])
+        self.assertIn('ProblemId', data[0])
+        self.assertIn('Title', data[0])
+        self.assertIn('AuthorId', data[0])
+        self.assertIn('TesterId', data[0])
+        self.assertIn('Level', data[0])
+        self.assertIn('ProblemType', data[0])
+        self.assertIn('Tags', data[0])
+    def test_get_solved_yuki2006(self):
+        data = YukicoderService().get_solved(name='yuki2006')
+        self.assertGreater(len(data), 200)
+    def test_get_solved_0(self):
+        data = YukicoderService().get_solved(id=0)
+        self.assertIs(data, None)
 
     def test_get_user_favorite_10(self):
         data = YukicoderService().get_user_favorite(id=10)