From 9fa82869979b4f5357252525b31254a1e8bd63eb Mon Sep 17 00:00:00 2001 From: Piper Merriam Date: Tue, 19 Jul 2016 15:58:20 -0600 Subject: [PATCH 1/5] Initial implementation --- conftest.py | 6 + requirements-dev.txt | 2 + requirements.txt | 0 solc/__init__.py | 5 + solc/exceptions.py | 6 + solc/main.py | 128 ++++++++++++++ solc/utils/filesystem.py | 19 +++ solc/utils/formatting.py | 31 ++++ solc/utils/string.py | 90 ++++++++++ solc/utils/types.py | 41 +++++ solc/wrapper.py | 157 ++++++++++++++++++ .../test_compile_from_source_code.py | 22 +++ .../test_compiler_from_source_file.py | 30 ++++ tests/utility/test_is_executable_available.py | 11 ++ tests/utility/test_solc_version.py | 16 ++ tests/wrapper/test_solc_wrapper.py | 54 ++++++ 16 files changed, 618 insertions(+) create mode 100644 conftest.py create mode 100644 requirements-dev.txt create mode 100644 requirements.txt create mode 100644 solc/__init__.py create mode 100644 solc/exceptions.py create mode 100644 solc/main.py create mode 100644 solc/utils/filesystem.py create mode 100644 solc/utils/formatting.py create mode 100644 solc/utils/string.py create mode 100644 solc/utils/types.py create mode 100644 solc/wrapper.py create mode 100644 tests/compilation/test_compile_from_source_code.py create mode 100644 tests/compilation/test_compiler_from_source_file.py create mode 100644 tests/utility/test_is_executable_available.py create mode 100644 tests/utility/test_solc_version.py create mode 100644 tests/wrapper/test_solc_wrapper.py diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..1ed2f51 --- /dev/null +++ b/conftest.py @@ -0,0 +1,6 @@ +import pytest + + +@pytest.fixture() +def contracts_dir(tmpdir): + return str(tmpdir.mkdir("contracts")) diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..fd44baa --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +pytest>=2.9.2 +tox>=2.3.1 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/solc/__init__.py b/solc/__init__.py new file mode 100644 index 0000000..d7dcbce --- /dev/null +++ b/solc/__init__.py @@ -0,0 +1,5 @@ +from .main import ( + get_solc_version, + compile_files, + compile_source, +) diff --git a/solc/exceptions.py b/solc/exceptions.py new file mode 100644 index 0000000..08993f8 --- /dev/null +++ b/solc/exceptions.py @@ -0,0 +1,6 @@ +class SolcError(Exception): + pass + + +class CompileError(Exception): + pass diff --git a/solc/main.py b/solc/main.py new file mode 100644 index 0000000..a380b7e --- /dev/null +++ b/solc/main.py @@ -0,0 +1,128 @@ +import functools +import json +import re +from io import BytesIO + +from .exceptions import ( + SolcError, +) + +from .utils.formatting import ( + add_0x_prefix, +) +from .utils.types import ( + is_string, +) +from .utils.string import ( + coerce_return_to_text, +) +from .utils.filesystem import ( + is_executable_available, +) +from .wrapper import ( + SOLC_BINARY, + solc_wrapper, +) + + +version_regex = re.compile('Version: ([0-9]+\.[0-9]+\.[0-9]+(-[a-f0-9]+)?)') + + +is_solc_available = functools.partial(is_executable_available, SOLC_BINARY) + + +def get_solc_version(): + stdoutdata, stderrdata = solc_wrapper(version=True) + version_match = version_regex.search(stdoutdata) + if version_match is None: + raise SolcError( + "Unable to extract version string from command output: `{0}`".format( + stdoutdata, + ) + ) + return version_match.groups()[0] + + +def _parse_compiler_output(stdoutdata): + contracts = json.loads(stdoutdata)['contracts'] + + for _, data in contracts.items(): + data['abi'] = json.loads(data['abi']) + + sorted_contracts = sorted(contracts.items(), key=lambda c: c[0]) + + compiler_version = get_solc_version() + + return { + contract_name: { + 'abi': contract_data['abi'], + 'code': add_0x_prefix(contract_data['bin']), + 'code_runtime': add_0x_prefix(contract_data['bin-runtime']), + 'source': None, + 'meta': { + 'compilerVersion': compiler_version, + 'language': 'Solidity', + 'languageVersion': '0', + }, + } + for contract_name, contract_data + in sorted_contracts + } + + +ALL_OUTPUT_VALUES = [ + "abi", + "asm", + "ast", + "bin", + "bin-runtime", + "clone-bin", + "devdoc", + "interface", + "opcodes", + "userdoc", +] + + +def compile_source(source, output_values=ALL_OUTPUT_VALUES, **kwargs): + if 'stdin_bytes' in kwargs: + raise ValueError( + "The `stdin_bytes` keyword is not allowed in the `compile_source` function" + ) + if 'combined_json' in kwargs: + raise ValueError( + "The `combined_json` keyword is not allowed in the `compile_source` function" + ) + + combined_json = ','.join(output_values) + + stdoutdata, stderrdata = solc_wrapper( + stdin_bytes=source, + combined_json=combined_json, + **kwargs + ) + + contracts = _parse_compiler_output(stdoutdata) + return contracts + + +def compile_files(source_files, output_values=ALL_OUTPUT_VALUES, **kwargs): + if 'source_files' in kwargs: + raise ValueError( + "The `source_files` keyword is not allowed in the `compile_files` function" + ) + if 'combined_json' in kwargs: + raise ValueError( + "The `combined_json` keyword is not allowed in the `compile_files` function" + ) + + combined_json = ','.join(output_values) + + stdoutdata, stderrdata = solc_wrapper( + source_files=source_files, + combined_json=combined_json, + **kwargs + ) + + contracts = _parse_compiler_output(stdoutdata) + return contracts diff --git a/solc/utils/filesystem.py b/solc/utils/filesystem.py new file mode 100644 index 0000000..0f3bcf7 --- /dev/null +++ b/solc/utils/filesystem.py @@ -0,0 +1,19 @@ +import os + + +def is_executable_available(program): + def is_exe(fpath): + return os.path.isfile(fpath) and os.access(fpath, os.X_OK) + + fpath = os.path.dirname(program) + if fpath: + if is_exe(program): + return True + else: + for path in os.environ["PATH"].split(os.pathsep): + path = path.strip('"') + exe_file = os.path.join(path, program) + if is_exe(exe_file): + return True + + return False diff --git a/solc/utils/formatting.py b/solc/utils/formatting.py new file mode 100644 index 0000000..f78f03c --- /dev/null +++ b/solc/utils/formatting.py @@ -0,0 +1,31 @@ +from .string import ( + force_bytes, + force_text, +) +from .types import ( + is_bytes, +) + + +def is_prefixed(value, prefix): + return value.startswith( + force_bytes(prefix) if is_bytes(value) else force_text(prefix) + ) + + +def is_0x_prefixed(value): + return is_prefixed(value, '0x') + + +def remove_0x_prefix(value): + if is_0x_prefixed(value): + return value[2:] + return value + + +def add_0x_prefix(value): + if is_0x_prefixed(value): + return value + + prefix = b'0x' if is_bytes(value) else '0x' + return prefix + value diff --git a/solc/utils/string.py b/solc/utils/string.py new file mode 100644 index 0000000..fd21e1c --- /dev/null +++ b/solc/utils/string.py @@ -0,0 +1,90 @@ +import sys +import functools + +from .types import ( + is_bytes, + is_text, + is_string, +) + +if sys.version_info.major == 2: + def force_bytes(value): + if is_bytes(value): + return str(value) + elif is_text(value): + return value.encode('latin1') + else: + raise TypeError("Unsupported type: {0}".format(type(value))) + + def force_text(value): + if is_text(value): + return value + elif is_bytes(value): + return unicode(force_bytes(value), 'latin1') # NOQA + else: + raise TypeError("Unsupported type: {0}".format(type(value))) +else: + def force_bytes(value): + if is_bytes(value): + return bytes(value) + elif is_text(value): + return bytes(value, 'latin1') + else: + raise TypeError("Unsupported type: {0}".format(type(value))) + + def force_text(value): + if is_text(value): + return value + elif is_bytes(value): + return str(value, 'latin1') + else: + raise TypeError("Unsupported type: {0}".format(type(value))) + + +def force_obj_to_bytes(obj): + if is_string(obj): + return force_bytes(obj) + elif isinstance(obj, dict): + return { + k: force_obj_to_bytes(v) for k, v in obj.items() + } + elif isinstance(obj, (list, tuple)): + return type(obj)(force_obj_to_bytes(v) for v in obj) + else: + return obj + + +def force_obj_to_text(obj): + if is_string(obj): + return force_text(obj) + elif isinstance(obj, dict): + return { + k: force_obj_to_text(v) for k, v in obj.items() + } + elif isinstance(obj, (list, tuple)): + return type(obj)(force_obj_to_text(v) for v in obj) + else: + return obj + + +def coerce_args_to_bytes(fn): + @functools.wraps(fn) + def inner(*args, **kwargs): + bytes_args = force_obj_to_bytes(args) + bytes_kwargs = force_obj_to_bytes(kwargs) + return fn(*bytes_args, **bytes_kwargs) + return inner + + +def coerce_return_to_bytes(fn): + @functools.wraps(fn) + def inner(*args, **kwargs): + return force_obj_to_bytes(fn(*args, **kwargs)) + return inner + + +def coerce_return_to_text(fn): + @functools.wraps(fn) + def inner(*args, **kwargs): + return force_obj_to_text(fn(*args, **kwargs)) + return inner diff --git a/solc/utils/types.py b/solc/utils/types.py new file mode 100644 index 0000000..f9b3686 --- /dev/null +++ b/solc/utils/types.py @@ -0,0 +1,41 @@ +import sys + + +if sys.version_info.major == 2: + integer_types = (int, long) # NOQA + bytes_types = (bytes, bytearray) + text_types = (unicode,) # NOQA + string_types = (basestring, bytearray) # NOQA +else: + integer_types = (int,) + bytes_types = (bytes, bytearray) + text_types = (str,) + string_types = (bytes, str, bytearray) + + +def is_integer(value): + return isinstance(value, integer_types) and not isinstance(value, bool) + + +def is_bytes(value): + return isinstance(value, bytes_types) + + +def is_text(value): + return isinstance(value, text_types) + + +def is_string(value): + return isinstance(value, string_types) + + +def is_boolean(value): + return isinstance(value, bool) + + +def is_object(obj): + return isinstance(obj, dict) + + +def is_array(obj): + return isinstance(obj, list) diff --git a/solc/wrapper.py b/solc/wrapper.py new file mode 100644 index 0000000..f4baa0d --- /dev/null +++ b/solc/wrapper.py @@ -0,0 +1,157 @@ +import subprocess +import textwrap + +from .exceptions import ( + CompileError, +) +from .utils.string import ( + coerce_return_to_text, +) + + +SOLC_BINARY = 'solc' + + +@coerce_return_to_text +def solc_wrapper(solc_binary=SOLC_BINARY, + stdin_bytes=None, + help=None, + version=None, + add_std=None, + combined_json=None, + optimize=None, + optimize_runs=None, + libraries=None, + output_dir=None, + gas=None, + assemble=None, + link=None, + source_files=None, + ast=None, + ast_json=None, + asm=None, + asm_json=None, + opcodes=None, + bin=None, + bin_runtime=None, + clone_bin=None, + abi=None, + interface=None, + hashes=None, + userdoc=None, + devdoc=None, + formal=None, + success_return_code=0): + command = ['solc'] + + if help: + command.append('--help') + + if version: + command.append('--version') + + if add_std: + command.append('--add-std') + + if optimize: + command.append('--optimize') + + if optimize_runs is not None: + command.extend(('--optimize-runs', str(optimize_runs))) + + if libraries is not None: + command.extend(('--libraries', libraries)) + + if output_dir is not None: + command.extend(('--output-dir', output_dir)) + + if combined_json: + command.extend(('--combined-json', combined_json)) + + if gas: + command.append('--gas') + + if assemble: + command.append('--assemble') + + if source_files is not None: + command.extend(source_files) + + if link: + command.append('--link') + + # + # Output configuration + # + if ast: + command.append('--ast') + + if ast_json: + command.append('--ast-json') + + if asm: + command.append('--asm') + + if asm_json: + command.append('--asm-json') + + if opcodes: + command.append('--opcodes') + + if bin: + command.append('--bin') + + if bin_runtime: + command.append('--bin-runtime') + + if clone_bin: + command.append('--clone-bin') + + if abi: + command.append('--abi') + + if interface: + command.append('--interface') + + if hashes: + command.append('--hashes') + + if userdoc: + command.append('--userdoc') + + if devdoc: + command.append('--devdoc') + + if formal: + command.append('--formal') + + if stdin_bytes is not None: + stdin = subprocess.Popen(['echo', stdin_bytes], stdout=subprocess.PIPE).stdout + else: + stdin = subprocess.PIPE + + proc = subprocess.Popen(command, + stdin=stdin, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + stdoutdata, stderrdata = proc.communicate() + + if proc.returncode != success_return_code: + error_message = textwrap.dedent((""" + Compilation Failed + > command: `{command}` + > return code: `{return_code}` + > stderr: + {stderrdata} + > stdout: + {stdoutdata} + """).format( + command=' '.join(command), + return_code=proc.returncode, + stderrdata=stderrdata, + stdoutdata=stdoutdata, + )).strip() + raise CompileError(error_message) + + return stdoutdata, stderrdata diff --git a/tests/compilation/test_compile_from_source_code.py b/tests/compilation/test_compile_from_source_code.py new file mode 100644 index 0000000..a0d54be --- /dev/null +++ b/tests/compilation/test_compile_from_source_code.py @@ -0,0 +1,22 @@ +from solc import ( + get_solc_version, + compile_source, +) + + +def test_source_code_compilation(): + SOURCE = "contract Foo { function Foo() {} function return13() returns (uint) { return 13; } }" + output = compile_source(SOURCE) + assert output + assert 'Foo' in output + + foo_contract_data = output['Foo'] + assert 'code' in foo_contract_data + assert 'code_runtime' in foo_contract_data + assert 'source' in foo_contract_data + assert 'meta' in foo_contract_data + assert 'compilerVersion' in foo_contract_data['meta'] + + # TODO: figure out how to include source. + assert foo_contract_data['source'] is None + assert foo_contract_data['meta']['compilerVersion'] == get_solc_version() diff --git a/tests/compilation/test_compiler_from_source_file.py b/tests/compilation/test_compiler_from_source_file.py new file mode 100644 index 0000000..833ad59 --- /dev/null +++ b/tests/compilation/test_compiler_from_source_file.py @@ -0,0 +1,30 @@ +import os + +from solc import ( + get_solc_version, + compile_files, +) + + +def test_source_files_compilation(contracts_dir): + SOURCE = "contract Foo { function Foo() {} function return13() returns (uint) { return 13; } }" + + source_file_path = os.path.join(contracts_dir, 'Foo.sol') + with open(source_file_path, 'w') as source_file: + source_file.write(SOURCE) + + output = compile_files([source_file_path]) + + assert output + assert 'Foo' in output + + foo_contract_data = output['Foo'] + assert 'code' in foo_contract_data + assert 'code_runtime' in foo_contract_data + assert 'source' in foo_contract_data + assert 'meta' in foo_contract_data + assert 'compilerVersion' in foo_contract_data['meta'] + + # TODO: figure out how to include source. + assert foo_contract_data['source'] is None + assert foo_contract_data['meta']['compilerVersion'] == get_solc_version() diff --git a/tests/utility/test_is_executable_available.py b/tests/utility/test_is_executable_available.py new file mode 100644 index 0000000..f4076b4 --- /dev/null +++ b/tests/utility/test_is_executable_available.py @@ -0,0 +1,11 @@ +from solc.utils.filesystem import ( + is_executable_available, +) + + +def test_ls_is_available(): + assert is_executable_available('ls') is True + + +def test_for_unavailable_executable(): + assert is_executable_available('there_should_not_be_an_executable_by_this_name') is False diff --git a/tests/utility/test_solc_version.py b/tests/utility/test_solc_version.py new file mode 100644 index 0000000..4d0cbb8 --- /dev/null +++ b/tests/utility/test_solc_version.py @@ -0,0 +1,16 @@ +import re + +from solc import get_solc_version + + +def test_get_solc_version(): + raw_version_string = get_solc_version() + version, _, commit_sha = raw_version_string.partition('-') + assert version + assert commit_sha + + major, minor, patch = version.split('.') + + assert major.isdigit() + assert minor.isdigit() + assert patch.isdigit() diff --git a/tests/wrapper/test_solc_wrapper.py b/tests/wrapper/test_solc_wrapper.py new file mode 100644 index 0000000..c3d286b --- /dev/null +++ b/tests/wrapper/test_solc_wrapper.py @@ -0,0 +1,54 @@ +import os + +from solc.wrapper import ( + solc_wrapper, +) + + +def test_help(): + output, err = solc_wrapper(help=True, success_return_code=1) + assert output + assert 'Solidity' in output + assert not err + + +def test_version(): + output, err = solc_wrapper(version=True) + assert output + assert 'Version' in output + assert not err + + +def test_providing_stdin(): + stdin_bytes = b"contract Foo { function Foo() {} }" + output, err = solc_wrapper(stdin_bytes=stdin_bytes, bin=True) + assert output + assert 'Foo' in output + assert not err + + +def test_providing_single_source_file(contracts_dir): + source_file_path = os.path.join(contracts_dir, 'Foo.sol') + with open(source_file_path, 'w') as source_file: + source_file.write("contract Foo { function Foo() {} }") + + output, err = solc_wrapper(source_files=[source_file_path], bin=True) + assert output + assert 'Foo' in output + assert not err + + +def test_providing_multiple_source_files(contracts_dir): + source_file_a_path = os.path.join(contracts_dir, 'Foo.sol') + source_file_b_path = os.path.join(contracts_dir, 'Bar.sol') + + with open(source_file_a_path, 'w') as source_file: + source_file.write("contract Foo { function Foo() {} }") + with open(source_file_b_path, 'w') as source_file: + source_file.write("contract Bar { function Bar() {} }") + + output, err = solc_wrapper(source_files=[source_file_a_path, source_file_b_path], bin=True) + assert output + assert 'Foo' in output + assert 'Bar' in output + assert not err From 3fcd218580364f44123fd38e3b14eab2fe8e5864 Mon Sep 17 00:00:00 2001 From: Piper Merriam Date: Tue, 19 Jul 2016 15:59:54 -0600 Subject: [PATCH 2/5] fix travis --- .travis.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index ba198a1..a0914f1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,13 +6,9 @@ sudo: required before_install: - travis_retry sudo add-apt-repository -y ppa:ethereum/ethereum - travis_retry sudo apt-get update - - travis_retry sudo apt-get install -y ethereum - - mkdir -p ~/.ethash - - geth makedag 0 ~/.ethash + - travis_retry sudo apt-get install -y solc cache: pip: true - directories: - - ~/.ethash env: matrix: - TOX_ENV=py27 From 6b17b5053420d53289cf8f74ce5e7f12d097f628 Mon Sep 17 00:00:00 2001 From: Piper Merriam Date: Tue, 19 Jul 2016 16:12:14 -0600 Subject: [PATCH 3/5] fix readme example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 82fbe1b..61bd228 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ pip install py-solc }, }, } ->>> compile_source(["/path/to/Foo.sol", "/path/to/Bar.sol"]) +>>> compile_files(["/path/to/Foo.sol", "/path/to/Bar.sol"]) { 'Foo': { 'abi': [{'inputs': [], 'type': 'constructor'}], From 122cf3806970f112500fb1bdf2216d75820d88ac Mon Sep 17 00:00:00 2001 From: Piper Merriam Date: Tue, 19 Jul 2016 16:31:11 -0600 Subject: [PATCH 4/5] test fixes --- solc/__init__.py | 4 +++- solc/main.py | 9 ++------- solc/utils/formatting.py | 2 ++ solc/utils/string.py | 2 ++ solc/wrapper.py | 2 ++ 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/solc/__init__.py b/solc/__init__.py index d7dcbce..1a23611 100644 --- a/solc/__init__.py +++ b/solc/__init__.py @@ -1,4 +1,6 @@ -from .main import ( +from __future__ import absolute_import + +from .main import ( # NOQA get_solc_version, compile_files, compile_source, diff --git a/solc/main.py b/solc/main.py index a380b7e..499d06f 100644 --- a/solc/main.py +++ b/solc/main.py @@ -1,7 +1,8 @@ +from __future__ import absolute_import + import functools import json import re -from io import BytesIO from .exceptions import ( SolcError, @@ -10,12 +11,6 @@ from .utils.formatting import ( add_0x_prefix, ) -from .utils.types import ( - is_string, -) -from .utils.string import ( - coerce_return_to_text, -) from .utils.filesystem import ( is_executable_available, ) diff --git a/solc/utils/formatting.py b/solc/utils/formatting.py index f78f03c..31f1566 100644 --- a/solc/utils/formatting.py +++ b/solc/utils/formatting.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + from .string import ( force_bytes, force_text, diff --git a/solc/utils/string.py b/solc/utils/string.py index fd21e1c..1057c14 100644 --- a/solc/utils/string.py +++ b/solc/utils/string.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import sys import functools diff --git a/solc/wrapper.py b/solc/wrapper.py index f4baa0d..7d70fa5 100644 --- a/solc/wrapper.py +++ b/solc/wrapper.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import subprocess import textwrap From c340724907c41904a89b8c4cb323a41e0c06100d Mon Sep 17 00:00:00 2001 From: Piper Merriam Date: Tue, 19 Jul 2016 16:31:56 -0600 Subject: [PATCH 5/5] missing __init__.py file --- solc/utils/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 solc/utils/__init__.py diff --git a/solc/utils/__init__.py b/solc/utils/__init__.py new file mode 100644 index 0000000..e69de29