From ff8a89b5c58e9fe80c645a5c9a3fd7b8b26f97b8 Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Fri, 18 Sep 2020 07:50:55 +0900 Subject: [PATCH] Add expander --- .github/workflows/python-package.yml | 3 +- README.md | 12 ++- README_ja.md | 12 ++- atcoder/__init__.py | 3 + atcoder/__main__.py | 144 +++++++++++++++++++++++++++ setup.cfg | 14 +++ setup.py | 3 + 7 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 atcoder/__init__.py create mode 100644 atcoder/__main__.py create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 533f7da..9e37371 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -26,8 +26,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest pep8-naming - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install .[lint,test] - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names diff --git a/README.md b/README.md index e34f28a..33caeaa 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,19 @@ ac-library-python is a Python port of [AtCoder Library (ACL)](https://atcoder.jp + scc + twosat +## Install + +``` +pip install git+https://github.com/not522/ac-library-python +``` + ## Usage -Copy and paste the library into your code. +The following command outputs a single combined code which can run in online judge systems. -We also plan to provide a feature to combine dependent libraries into a single file. +``` +python -m atcoder [your-source-code] -o [single-combined-code] +``` ## FAQ diff --git a/README_ja.md b/README_ja.md index e959969..ba472c1 100644 --- a/README_ja.md +++ b/README_ja.md @@ -39,11 +39,19 @@ ac-library-pythonは、[AtCoder Library (ACL)](https://atcoder.jp/posts/517)のP + scc + twosat +## インストール + +``` +pip install git+https://github.com/not522/ac-library-python +``` + ## 使い方 -提出用のコードに本ライブラリの必要な部分をコピー&ペーストしてご利用ください。 +以下のコマンドでオンラインジャッジで実行可能な結合されたソースコードが出力されます。 -また、依存関係にあるライブラリを一つのファイルにまとめる機能を提供する予定です。 +``` +python -m atcoder [your-source-code] -o [single-combined-code] +``` ## よくある質問 diff --git a/atcoder/__init__.py b/atcoder/__init__.py new file mode 100644 index 0000000..b30ba70 --- /dev/null +++ b/atcoder/__init__.py @@ -0,0 +1,3 @@ +# Python port of AtCoder Library. + +__version__ = '0.0.1' diff --git a/atcoder/__main__.py b/atcoder/__main__.py new file mode 100644 index 0000000..da301d2 --- /dev/null +++ b/atcoder/__main__.py @@ -0,0 +1,144 @@ +import argparse +import ast +import importlib +import inspect +import re +from typing import List, Optional + + +class ImportInfo: + def __init__(self, lineno: int, end_lineno: int, + import_from: Optional[str] = None, + name: Optional[str] = None, + asname: Optional[str] = None) -> None: + self.lineno = lineno + self.end_lineno = end_lineno + self.import_from = import_from + self.name = name + self.asname = asname + + +def iter_child_nodes( + node: ast.AST, + import_info: Optional[ImportInfo] = None) -> List[ImportInfo]: + result = [] + + if isinstance(node, ast.alias): + if import_info: + result.append(ImportInfo( + import_info.lineno, import_info.end_lineno, + import_info.import_from, node.name, node.asname)) + return result + + if isinstance(node, ast.Import): + for name in node.names: + if re.match(r'^atcoder\.?', name.name): + import_info = ImportInfo(node.lineno, node.end_lineno) + elif isinstance(node, ast.ImportFrom): + import_info = ImportInfo(node.lineno, node.end_lineno, node.module) + + for child in ast.iter_child_nodes(node): + result += iter_child_nodes(child, import_info) + return result + + +class ModuleImporter: + def __init__(self) -> None: + self.imported_modules = [] + + def import_module(self, import_from: Optional[str], name: str, + asname: Optional[str] = None) -> str: + result = '' + + if import_from is None: + module_name = name + else: + try: + module_name = import_from + '.' + name + importlib.import_module(module_name) + except ImportError: + module_name = import_from + + if module_name not in self.imported_modules: + self.imported_modules.append(module_name) + + module = importlib.import_module(module_name) + source = inspect.getsource(module) + lines = source.split('\n') + imports = iter_child_nodes(ast.parse(source)) + + import_lines = [] + for import_info in imports: + result += self.import_module( + import_info.import_from, import_info.name, + import_info.asname) + for line in range(import_info.lineno - 1, + import_info.end_lineno): + import_lines.append(line) + + for lineno, line in enumerate(lines): + if lineno not in import_lines: + continue + lines[lineno] = '# ' + line # TODO(not): indent + + modules = module_name.split('.') + for i in range(len(modules) - 1): + result += self.import_module(None, '.'.join(modules[:i + 1])) + + code = '_' + module_name.replace('.', '_') + '_code' + result += f'{code} = """\n' + result += '\n'.join(lines) + result += '"""\n\n' + result += f"{module_name} = types.ModuleType('{module_name}')\n" + result += f'exec({code}, {module_name}.__dict__)\n' + + if import_from is None: + if asname is None: + if name != module_name: + result += f'{name} = {module_name}\n' + else: + result += f'{asname} = {module_name}\n' + else: + if asname is None: + if name != import_from + '.' + name: + result += f'{name} = {import_from}.{name}\n' + else: + result += f'{asname} = {import_from}.{name}\n' + + return result + '\n' + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument('src', help='Source code') + parser.add_argument('-o', '--output', help='Single combined code') + args = parser.parse_args() + + with open(args.src) as f: + lines = f.readlines() + imports = iter_child_nodes(ast.parse(''.join(lines))) + + importer = ModuleImporter() + result = 'import types\n\n' + import_lines = [] + for import_info in imports: + result += importer.import_module( + import_info.import_from, import_info.name, import_info.asname) + for line in range(import_info.lineno - 1, import_info.end_lineno): + import_lines.append(line) + + for lineno, line in enumerate(lines): + if lineno not in import_lines: + continue + lines[lineno] = '# ' + line # TODO(not): indent + result += ''.join(lines) + + if args.output: + with open(args.output, 'w') as f: + f.write(result) + else: + print(result, end='') + + +if __name__ == '__main__': + main() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..681052f --- /dev/null +++ b/setup.cfg @@ -0,0 +1,14 @@ +[metadata] +name = ac-library-python +version = attr: atcoder.__version__ +description = Python port of AtCoder Library (ACL). +license = CC0 + +[options] +packages = find: + +[options.extras_require] +lint = + flake8 + pep8-naming +test = pytest diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b908cbe --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +import setuptools + +setuptools.setup()