├── .gitattributes ├── .gitignore ├── .python-version ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── dev_requirements.txt ├── pyepm ├── __init__.py ├── _version.py ├── api.py ├── colors.py ├── config.py ├── deploy.py ├── pyepm.py └── utils.py ├── requirements.txt ├── setup.py ├── test ├── fixtures │ ├── example.yaml │ ├── namecoin.se │ ├── short_namecoin.se │ ├── subcurrency.se │ └── wallet.sol ├── helpers.py ├── test_api.py ├── test_deploy.py └── test_utils.py ├── tox.ini └── versioneer.py /.gitattributes: -------------------------------------------------------------------------------- 1 | _version.py export-subst 2 | pyepm/_version.py export-subst 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | __pycache__/ 3 | *~ 4 | [#]*[#] 5 | .*.swp 6 | .*.swo 7 | .*.swn 8 | .~ 9 | .DS_Store 10 | /tmp/ 11 | /.venv/ 12 | /dist/ 13 | /*.egg-info/ 14 | /.tox/ 15 | /bin/ 16 | /develop-eggs/ 17 | /eggs/ 18 | .installed.cfg 19 | logging.conf 20 | *.log 21 | .coverage 22 | *.se 23 | *.sol 24 | *.binary 25 | *.yaml 26 | MANIFEST 27 | upload -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 2.7.6 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | install: 5 | - pip install -r requirements.txt 6 | - pip install -r dev_requirements.txt 7 | - pip install -e . 8 | script: 9 | - py.test -vvrs 10 | - flake8 11 | notifications: 12 | irc: 13 | channels: 14 | - "chat.freenode.net#etherex-dev" 15 | use_notice: true 16 | skip_join: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include versioneer.py 2 | include pyepm/_version.py 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PyEPM 2 | ===== 3 | [![Build Status](https://travis-ci.org/etherex/pyepm.svg?branch=master)](https://travis-ci.org/etherex/pyepm) [![Join the chat at https://gitter.im/etherex/pyepm](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/etherex/pyepm?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | Python-based EPM (Ethereum Package Manager) for Serpent 2 and Solidity contract deployment using YAML for package definitions. 6 | 7 | [![asciicast](https://asciinema.org/a/3rhk5uy1055d8neunwx26jw8r.png)](https://asciinema.org/a/3rhk5uy1055d8neunwx26jw8r) 8 | 9 | ## Installation 10 | `pip install pyepm` 11 | 12 | #### Development 13 | ``` 14 | git clone https://github.com/etherex/pyepm.git 15 | cd pyepm 16 | pip install -e . 17 | ``` 18 | 19 | ## Requirements 20 | Ethereum node ([go-ethereum](https://github.com/ethereum/go-ethereum), [cpp-ethereum](https://github.com/ethereum/cpp-ethereum)) with JSON RPC enabled. 21 | 22 | ## Configuration 23 | 24 | First run `pyepm -h` to create a config file in `~/.pyepm/config` on Linux and OSX and `~/AppData/Roaming/PyEPM` on Windows. 25 | 26 | Then edit the configuration file, make sure you set the `address` from which to deploy contracts. 27 | 28 | You will need a package definition file in YAML format to get started (see example below). You can use your deployed contracts' names as variables (prefixed with `$`) in later `transact` or `call` steps, making contract initialization a lot easier and less dependent on fixed contract addresses. 29 | 30 | ``` 31 | - 32 | # Set some variables. 33 | set: 34 | NameReg: "0x72ba7d8e73fe8eb666ea66babc8116a41bfb10e2" 35 | - 36 | # Deploy contracts 37 | deploy: 38 | NameCoin: 39 | contract: namecoin.se 40 | retry: 15 41 | wait: True 42 | - 43 | deploy: 44 | Subcurrency: 45 | contract: subcurrency.se 46 | gas: 100000 47 | endowment: 1000000000000000000 48 | retry: 30 49 | wait: True 50 | - 51 | # Make transactions, here we register the previously deployed 52 | # 'Subcurrency' contract with the deployed NameCoin 53 | transact: 54 | RegisterSubToNameCoin: 55 | to: $NameCoin 56 | sig: register:[int256,int256]:int256 57 | data: 58 | - $Subcurrency 59 | - SubcurrencyName 60 | gas: 100000 61 | gas_price: 10000000000000 62 | value: 0 63 | retry: 30 64 | wait: True 65 | - 66 | transact: 67 | TestEncoding: 68 | to: $NameReg 69 | sig: some_method:[int256,int256,int256]:int256 70 | data: 71 | - $Subcurrency 72 | - 42 73 | - "\x01\x00" 74 | gas: 100000 75 | gas_price: 10000000000000 76 | value: 0 77 | wait: False 78 | - 79 | # Contract calls with return values 80 | call: 81 | GetNameFromNameCoin: 82 | to: $NameCoin 83 | sig: get_name:[int256]:int256 84 | data: 85 | - $Subcurrency 86 | - 87 | # Another deploy 88 | deploy: 89 | extra: 90 | contract: short_namecoin.se 91 | retry: 10 92 | wait: True 93 | - 94 | # Deploy Solidity contract 95 | deploy: 96 | Wallet: 97 | contract: wallet.sol 98 | solidity: 99 | - multiowned 100 | - daylimit 101 | - multisig 102 | - Wallet 103 | gas: 2500000 104 | retry: 30 105 | wait: True 106 | - 107 | # Transact to deployed Solidity contract name 108 | transact: 109 | ToWallet: 110 | to: $Wallet 111 | sig: kill:[$Subcurrency]:int256 112 | retry: 15 113 | wait: True 114 | ``` 115 | 116 | ## Usage 117 | 118 | `pyepm YourPackageDefinitions.yaml` 119 | 120 | ``` 121 | usage: pyepm [-h] [-v] [-r HOST] [-p PORT] [-a ADDRESS] [-g GAS] [-c CONFIG] 122 | [-V VERBOSITY] [-L LOGGING] 123 | filename [filename ...] 124 | 125 | positional arguments: 126 | filename Package definition filenames in YAML format 127 | 128 | optional arguments: 129 | -h, --help show this help message and exit 130 | -v, --version show program's version number and exit 131 | -r HOST, --host HOST JSONRPC host (default: 127.0.0.1). 132 | -p PORT, --port PORT JSONRPC port (default: 8545). 133 | -a ADDRESS, --address ADDRESS 134 | Set the address from which to deploy contracts. 135 | -g GAS, --gas GAS Set the default amount of gas for deployment. 136 | -c CONFIG, --config CONFIG 137 | Use another configuration file. 138 | -V VERBOSITY, --verbose VERBOSITY 139 | <0 - 3> Set the log verbosity from 0 to 3 (default: 1) 140 | -L LOGGING, --logging LOGGING 141 | set the console log 142 | level for logger1, logger2, etc. Empty loggername 143 | means root-logger, e.g. ':DEBUG,:INFO'. Overrides '-V' 144 | ``` 145 | 146 | ## TODO 147 | - Support using variables across multiple definition files 148 | - Export addresses of deployed contracts in a json file 149 | - Post-deployment hooks 150 | - Support named values (1 ether) 151 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | pytest 3 | pytest-mock 4 | pytest-xdist 5 | -------------------------------------------------------------------------------- /pyepm/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author: caktux 4 | # @Date: 2014-12-21 08:28:41 5 | # @Last Modified by: caktux 6 | # @Last Modified time: 2014-12-21 11:42:56 7 | 8 | from ._version import get_versions 9 | __version__ = get_versions()['version'] 10 | del get_versions 11 | -------------------------------------------------------------------------------- /pyepm/_version.py: -------------------------------------------------------------------------------- 1 | 2 | # This file helps to compute a version number in source trees obtained from 3 | # git-archive tarball (such as those provided by githubs download-from-tag 4 | # feature). Distribution tarballs (built by setup.py sdist) and build 5 | # directories (produced by setup.py build) will contain a much shorter file 6 | # that just contains the computed version number. 7 | 8 | # This file is released into the public domain. Generated by 9 | # versioneer-0.13 (https://github.com/warner/python-versioneer) 10 | 11 | # these strings will be replaced by git during git-archive 12 | git_refnames = " (HEAD -> master, tag: 1.0.2)" 13 | git_full = "b85159bc3861c6e67201479c13fd5efd50932683" 14 | 15 | # these strings are filled in when 'setup.py versioneer' creates _version.py 16 | tag_prefix = "" 17 | parentdir_prefix = "pyepm-" 18 | versionfile_source = "pyepm/_version.py" 19 | 20 | import errno 21 | import os 22 | import re 23 | import subprocess 24 | import sys 25 | 26 | 27 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): 28 | assert isinstance(commands, list) 29 | p = None 30 | for c in commands: 31 | try: 32 | # remember shell=False, so use git.cmd on windows, not just git 33 | p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, 34 | stderr=(subprocess.PIPE if hide_stderr 35 | else None)) 36 | break 37 | except EnvironmentError: 38 | e = sys.exc_info()[1] 39 | if e.errno == errno.ENOENT: 40 | continue 41 | if verbose: 42 | print("unable to run %s" % args[0]) 43 | print(e) 44 | return None 45 | else: 46 | if verbose: 47 | print("unable to find command, tried %s" % (commands,)) 48 | return None 49 | stdout = p.communicate()[0].strip() 50 | if sys.version >= '3': 51 | stdout = stdout.decode() 52 | if p.returncode != 0: 53 | if verbose: 54 | print("unable to run %s (error)" % args[0]) 55 | return None 56 | return stdout 57 | 58 | 59 | def versions_from_parentdir(parentdir_prefix, root, verbose=False): 60 | # Source tarballs conventionally unpack into a directory that includes 61 | # both the project name and a version string. 62 | dirname = os.path.basename(root) 63 | if not dirname.startswith(parentdir_prefix): 64 | if verbose: 65 | print("guessing rootdir is '%s', but '%s' doesn't start with " 66 | "prefix '%s'" % (root, dirname, parentdir_prefix)) 67 | return None 68 | return {"version": dirname[len(parentdir_prefix):], "full": ""} 69 | 70 | 71 | def git_get_keywords(versionfile_abs): 72 | # the code embedded in _version.py can just fetch the value of these 73 | # keywords. When used from setup.py, we don't want to import _version.py, 74 | # so we do it with a regexp instead. This function is not used from 75 | # _version.py. 76 | keywords = {} 77 | try: 78 | f = open(versionfile_abs, "r") 79 | for line in f.readlines(): 80 | if line.strip().startswith("git_refnames ="): 81 | mo = re.search(r'=\s*"(.*)"', line) 82 | if mo: 83 | keywords["refnames"] = mo.group(1) 84 | if line.strip().startswith("git_full ="): 85 | mo = re.search(r'=\s*"(.*)"', line) 86 | if mo: 87 | keywords["full"] = mo.group(1) 88 | f.close() 89 | except EnvironmentError: 90 | pass 91 | return keywords 92 | 93 | 94 | def git_versions_from_keywords(keywords, tag_prefix, verbose=False): 95 | if not keywords: 96 | return {} # keyword-finding function failed to find keywords 97 | refnames = keywords["refnames"].strip() 98 | if refnames.startswith("$Format"): 99 | if verbose: 100 | print("keywords are unexpanded, not using") 101 | return {} # unexpanded, so not in an unpacked git-archive tarball 102 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 103 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 104 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 105 | TAG = "tag: " 106 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 107 | if not tags: 108 | # Either we're using git < 1.8.3, or there really are no tags. We use 109 | # a heuristic: assume all version tags have a digit. The old git %d 110 | # expansion behaves like git log --decorate=short and strips out the 111 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 112 | # between branches and tags. By ignoring refnames without digits, we 113 | # filter out many common branch names like "release" and 114 | # "stabilization", as well as "HEAD" and "master". 115 | tags = set([r for r in refs if re.search(r'\d', r)]) 116 | if verbose: 117 | print("discarding '%s', no digits" % ",".join(refs - tags)) 118 | if verbose: 119 | print("likely tags: %s" % ",".join(sorted(tags))) 120 | for ref in sorted(tags): 121 | # sorting will prefer e.g. "2.0" over "2.0rc1" 122 | if ref.startswith(tag_prefix): 123 | r = ref[len(tag_prefix):] 124 | if verbose: 125 | print("picking %s" % r) 126 | return {"version": r, 127 | "full": keywords["full"].strip()} 128 | # no suitable tags, so we use the full revision id 129 | if verbose: 130 | print("no suitable tags, using full revision id") 131 | return {"version": keywords["full"].strip(), 132 | "full": keywords["full"].strip()} 133 | 134 | 135 | def git_versions_from_vcs(tag_prefix, root, verbose=False): 136 | # this runs 'git' from the root of the source tree. This only gets called 137 | # if the git-archive 'subst' keywords were *not* expanded, and 138 | # _version.py hasn't already been rewritten with a short version string, 139 | # meaning we're inside a checked out source tree. 140 | 141 | if not os.path.exists(os.path.join(root, ".git")): 142 | if verbose: 143 | print("no .git in %s" % root) 144 | return {} 145 | 146 | GITS = ["git"] 147 | if sys.platform == "win32": 148 | GITS = ["git.cmd", "git.exe"] 149 | stdout = run_command(GITS, ["describe", "--tags", "--dirty", "--always"], 150 | cwd=root) 151 | if stdout is None: 152 | return {} 153 | if not stdout.startswith(tag_prefix): 154 | if verbose: 155 | fmt = "tag '%s' doesn't start with prefix '%s'" 156 | print(fmt % (stdout, tag_prefix)) 157 | return {} 158 | tag = stdout[len(tag_prefix):] 159 | stdout = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 160 | if stdout is None: 161 | return {} 162 | full = stdout.strip() 163 | if tag.endswith("-dirty"): 164 | full += "-dirty" 165 | return {"version": tag, "full": full} 166 | 167 | 168 | def get_versions(default={"version": "unknown", "full": ""}, verbose=False): 169 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 170 | # __file__, we can work backwards from there to the root. Some 171 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 172 | # case we can only use expanded keywords. 173 | 174 | keywords = {"refnames": git_refnames, "full": git_full} 175 | ver = git_versions_from_keywords(keywords, tag_prefix, verbose) 176 | if ver: 177 | return ver 178 | 179 | try: 180 | root = os.path.realpath(__file__) 181 | # versionfile_source is the relative path from the top of the source 182 | # tree (where the .git directory might live) to this file. Invert 183 | # this to find the root from __file__. 184 | for i in range(len(versionfile_source.split('/'))): 185 | root = os.path.dirname(root) 186 | except NameError: 187 | return default 188 | 189 | return (git_versions_from_vcs(tag_prefix, root, verbose) 190 | or versions_from_parentdir(parentdir_prefix, root, verbose) 191 | or default) 192 | -------------------------------------------------------------------------------- /pyepm/api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author: jorisbontje 4 | # @Date: 2014-08-03 13:53:04 5 | # @Last Modified by: caktux 6 | # @Last Modified time: 2015-04-05 01:37:30 7 | 8 | import json 9 | import logging 10 | import requests 11 | import sys 12 | import time 13 | from colors import colors 14 | from uuid import uuid4 15 | 16 | from ethereum import abi 17 | from serpent import get_prefix, decode_datalist 18 | from utils import unhex 19 | 20 | logger = logging.getLogger(__name__) 21 | logging.getLogger("requests").setLevel(logging.WARNING) 22 | 23 | def abi_data(sig, data): 24 | prefix = get_prefix(sig) 25 | data_abi = hex(prefix).rstrip('L') 26 | logger.debug("ABI prefix: %s" % data_abi) 27 | 28 | types = sig.split(':')[1][1:-1].split(',') 29 | logger.debug("ABI types: %s" % types) 30 | 31 | for i, s in enumerate(data): 32 | if isinstance(data[i], (str, unicode)) and data[i][:2] == "0x": 33 | data[i] = unhex(data[i]) 34 | logger.debug("ABI data: %s" % data) 35 | 36 | data_abi += abi.encode_abi(types, data).encode('hex') 37 | logger.debug("ABI encoded: %s" % data_abi) 38 | 39 | return data_abi 40 | 41 | class ApiException(Exception): 42 | def __init__(self, code, message): 43 | self.code = code 44 | self.message = message 45 | 46 | def __str__(self): 47 | return "code=%d, message=\"%s\"" % (self.code, self.message) 48 | 49 | 50 | class Api(object): 51 | 52 | def __init__(self, config): 53 | self.host = config.get('api', 'host') 54 | self.port = config.getint('api', 'port') 55 | self.jsonrpc_url = "http://%s:%s" % (self.host, self.port) 56 | logger.debug("Deploying to %s" % self.jsonrpc_url) 57 | 58 | address = config.get("api", "address") 59 | if not address.startswith('0x'): 60 | address = '0x' + address 61 | self.address = address 62 | 63 | self.gas = config.getint("deploy", "gas") 64 | self.gas_price = config.getint("deploy", "gas_price") 65 | self.fixed_price = config.getboolean("deploy", "fixed_price") 66 | self.gas_price_modifier = config.getfloat("deploy", "gas_price_modifier") 67 | 68 | self.retry = config.getint("deploy", "retry") 69 | self.skip = config.getint("deploy", "skip") 70 | 71 | def _rpc_post(self, method, params): 72 | if params is None: 73 | params = [] 74 | 75 | payload = { 76 | "jsonrpc": "2.0", 77 | "id": str(uuid4()), 78 | "method": method, 79 | "params": params} 80 | headers = {'content-type': 'application/json'} 81 | 82 | logger.debug(json.dumps(payload)) 83 | 84 | r = requests.post(self.jsonrpc_url, data=json.dumps(payload), headers=headers) 85 | if r.status_code >= 400: 86 | raise ApiException(r.status_code, r.reason) 87 | 88 | response = r.json() 89 | 90 | logger.debug(response) 91 | 92 | if 'error' in response: 93 | raise ApiException(response['error']['code'], response['error']['message']) 94 | 95 | return response.get('result') 96 | 97 | def accounts(self): 98 | return self._rpc_post('eth_accounts', None) 99 | 100 | def balance_at(self, address, defaultBlock='latest'): 101 | params = [address, defaultBlock] 102 | balance = self._rpc_post('eth_getBalance', params) 103 | if balance is not None: 104 | return unhex(balance) 105 | return 0 106 | 107 | def block(self, nr, includeTransactions=False): 108 | params = [hex(nr).rstrip('L'), includeTransactions] 109 | return self._rpc_post('eth_getBlockByNumber', params) 110 | 111 | def defaultBlock(self): 112 | raise DeprecationWarning('the function `defaultBlock` is deprecated, use `defaultBlock` as function argument in your request') 113 | 114 | def setDefaultBlock(self): 115 | raise DeprecationWarning('the method `setDefaultBlock` is deprecated, use `defaultBlock` as function argument in your request') 116 | 117 | def transaction_count(self, address=None, defaultBlock='latest'): 118 | if address is None: 119 | address = self.address 120 | params = [str(address), defaultBlock] 121 | try: 122 | hexcount = self._rpc_post('eth_getTransactionCount', params) 123 | if hexcount is not None: 124 | count = unhex(hexcount) 125 | else: 126 | return None 127 | logger.debug("Tx count: %s" % count) 128 | except Exception as e: 129 | logger.info("Failed Tx count, returning None: %s" % e) 130 | count = None 131 | return count 132 | 133 | def transaction(self, transactionHash): 134 | params = [transactionHash] 135 | return self._rpc_post('eth_getTransactionByHash', params) 136 | 137 | def check(self): 138 | raise DeprecationWarning('the method `check` is no longer available') 139 | 140 | def coinbase(self): 141 | return self._rpc_post('eth_coinbase', None) 142 | 143 | def gasprice(self): 144 | result = self._rpc_post('eth_gasPrice', None) 145 | logger.debug("Got gas price: %s" % result) 146 | if result is not None: 147 | return unhex(result) 148 | return None 149 | 150 | def is_contract_at(self, address, defaultBlock='latest'): 151 | params = [address, defaultBlock] 152 | result = self._rpc_post('eth_getCode', params) 153 | if result is not None: 154 | return unhex(result) != 0 155 | return False 156 | 157 | def is_listening(self): 158 | return self._rpc_post('net_listening', None) 159 | 160 | def is_mining(self): 161 | return self._rpc_post('eth_mining', None) 162 | 163 | def key(self): 164 | raise DeprecationWarning('the function `key` is no longer available in the JSON RPC API') 165 | 166 | def keys(self): 167 | raise DeprecationWarning('the function `keys` is no longer available in the JSON RPC API') 168 | 169 | def last_block(self): 170 | return self.block(self.number()) 171 | 172 | def lll(self, contract): 173 | params = { 174 | 's': contract 175 | } 176 | return self._rpc_post('eth_compileLLL', params) 177 | 178 | def logs(self, filter): 179 | params = [filter] 180 | return self._rpc_post('eth_getLogs', params) 181 | 182 | def number(self): 183 | return unhex(self._rpc_post('eth_blockNumber', None)) 184 | 185 | def peer_count(self): 186 | return unhex(self._rpc_post('net_peerCount', None)) 187 | 188 | def state_at(self, address, index, defaultBlock='latest'): 189 | raise DeprecationWarning('the method `stateAt` is no longer available in the JSON RPC API, use `eth_getStorageAt` (`storage_at` in PyEPM) instead.') 190 | 191 | def storage_at(self, address, index, defaultBlock='latest'): 192 | params = [address, hex(index), defaultBlock] 193 | return self._rpc_post('eth_getStorageAt', params) 194 | 195 | def create(self, code, from_=None, gas=None, gas_price=None, endowment=0): 196 | if not code.startswith('0x'): 197 | code = '0x' + code 198 | # params = [{'code': code}] 199 | 200 | if gas is None: 201 | gas = self.gas 202 | if gas_price is None: 203 | gas_price = self.gas_price 204 | if from_ is None: 205 | from_ = self.address 206 | if not self.fixed_price: 207 | net_price = self.gasprice() 208 | if net_price is None: 209 | gas_price = self.gas_price 210 | else: 211 | logger.info(" Gas price: {:.4f} szabo * {:.4f}".format(float(net_price) / 1000000000000, self.gas_price_modifier)) 212 | gas_price = int(net_price * self.gas_price_modifier) 213 | logger.info(" Our price: %s" % "{:,}".format(gas_price)) 214 | 215 | params = [{ 216 | 'data': code, 217 | 'from': from_, 218 | 'gas': hex(gas).rstrip('L'), 219 | 'gasPrice': hex(gas_price).rstrip('L'), 220 | 'value': hex(endowment).rstrip('L') 221 | }] 222 | return self._rpc_post('eth_sendTransaction', params) 223 | 224 | def get_contract_address(self, tx_hash): 225 | receipt = self._rpc_post('eth_getTransactionReceipt', [tx_hash]) 226 | if receipt and 'contractAddress' in receipt: 227 | return receipt['contractAddress'] 228 | return "0x0" 229 | 230 | def transact(self, dest, sig=None, data=None, gas=None, gas_price=None, value=0, from_=None, fun_name=None): 231 | if not dest.startswith('0x'): 232 | dest = '0x' + dest 233 | 234 | if fun_name is not None: 235 | raise DeprecationWarning("The `fun_name` definition is deprecated, use `serpent mk_signature `" 236 | " output for your method in `sig` instead.") 237 | if sig is not None: 238 | data = abi_data(sig, data) 239 | 240 | if from_ is None: 241 | from_ = self.address 242 | 243 | if gas is None: 244 | gas = self.gas 245 | if gas_price is None: 246 | gas_price = self.gas_price 247 | if not self.fixed_price: 248 | net_price = self.gasprice() 249 | if net_price is None: 250 | gas_price = self.gas_price 251 | else: 252 | logger.info(" Gas price: {:.4f} szabo * {:.4f}".format(float(net_price) / 1000000000000, self.gas_price_modifier)) 253 | gas_price = int(net_price * self.gas_price_modifier) 254 | logger.info(" Our price: %s" % "{:,}".format(gas_price)) 255 | 256 | params = [{ 257 | 'from': from_, 258 | 'to': dest, 259 | 'data': data, 260 | 'gas': hex(gas).rstrip('L'), 261 | 'gasPrice': hex(gas_price).rstrip('L'), 262 | 'value': hex(value).rstrip('L')}] 263 | return self._rpc_post('eth_sendTransaction', params) 264 | 265 | def call(self, dest, sig=None, data=None, gas=None, gas_price=None, value=0, from_=None, defaultBlock='latest', fun_name=None): 266 | if not dest.startswith('0x'): 267 | dest = '0x' + dest 268 | 269 | if fun_name is not None: 270 | raise DeprecationWarning("The `fun_name` definition is deprecated, use `serpent mk_signature `" 271 | " output for your method in `sig` instead.") 272 | if sig is not None: 273 | data = abi_data(sig, data) 274 | 275 | if from_ is None: 276 | from_ = self.address 277 | 278 | if gas is None: 279 | gas = self.gas 280 | if gas_price is None: 281 | gas_price = self.gas_price 282 | if not self.fixed_price: 283 | net_price = self.gasprice() 284 | if net_price is None: 285 | gas_price = self.gas_price 286 | else: 287 | logger.info(" Gas price: {:.4f} szabo * {:.4f}".format(float(net_price) / 1000000000000, self.gas_price_modifier)) 288 | gas_price = int(net_price * self.gas_price_modifier) 289 | logger.info(" Our price: %s" % "{:,}".format(gas_price)) 290 | 291 | params = [{ 292 | 'from': from_, 293 | 'to': dest, 294 | 'data': data, 295 | 'gas': hex(gas).rstrip('L'), 296 | 'gasPrice': hex(gas_price).rstrip('L'), 297 | 'value': hex(value).rstrip('L')}, defaultBlock] 298 | r = self._rpc_post('eth_call', params) 299 | if r is not None: 300 | return decode_datalist(r[2:].decode('hex')) 301 | return [] 302 | 303 | def wait_for_contract(self, address, defaultBlock='latest', retry=None, skip=None, verbose=False): 304 | if retry == 1: 305 | retry = self.retry 306 | if skip == 1: 307 | skip = self.skip 308 | 309 | msg = ' (%s%s%s)' % (('retrying in %ss' % retry) if retry else '', 310 | ', ' if retry and skip else '', 311 | ('skipping in %ss' % skip) if skip else '') 312 | 313 | if verbose: 314 | if defaultBlock == 'pending': 315 | sys.stdout.write(' Waiting for contract at %s%s' % (address, msg)) 316 | else: 317 | sys.stdout.write(' Waiting for contract to be mined%s' % msg) 318 | start_time = time.time() 319 | 320 | delta = 0 321 | while True: 322 | if verbose: 323 | sys.stdout.write('.') 324 | sys.stdout.flush() 325 | time.sleep(1) 326 | codeat = self.is_contract_at(address, defaultBlock) 327 | if codeat: 328 | break 329 | 330 | delta = time.time() - start_time 331 | 332 | if skip and delta > skip: 333 | logger.info(" Took too long, " + colors.WARNING + "skipping" + colors.ENDC + "...") 334 | break 335 | if retry and delta > retry: 336 | logger.info(" Took too long, " + colors.WARNING + "retrying" + colors.ENDC + "...") 337 | return False 338 | 339 | if verbose: 340 | if defaultBlock == 'pending': 341 | logger.info(" Took %ds" % delta) 342 | elif not ((skip and delta > skip) or (retry and delta > retry)): 343 | logger.info(" " + colors.OKGREEN + "Ready!" + colors.ENDC + " Mining took %ds" % delta) 344 | return True 345 | 346 | def wait_for_transaction(self, transactionHash, defaultBlock='latest', retry=None, skip=None, verbose=False): 347 | if retry == 1: 348 | retry = self.retry 349 | if skip == 1: 350 | skip = self.skip 351 | 352 | msg = ' (%s%s%s)' % (('retrying in %ss' % retry) if retry else '', 353 | ', ' if retry and skip else '', 354 | ('skipping in %ss' % skip) if skip else '') 355 | 356 | if verbose: 357 | if defaultBlock == 'pending': 358 | sys.stdout.write(' Waiting for transaction%s' % msg) 359 | else: 360 | sys.stdout.write(' Waiting for transaction to be mined%s' % msg) 361 | start_time = time.time() 362 | 363 | delta = 0 364 | while True: 365 | if verbose: 366 | sys.stdout.write('.') 367 | sys.stdout.flush() 368 | time.sleep(1) 369 | result = self.transaction(transactionHash) # no defaultBlock, check result instead 370 | logger.debug("Transaction result: %s" % result) 371 | if isinstance(result, dict): 372 | if result['blockNumber'] is not None: 373 | break 374 | if defaultBlock == 'pending' and result['blockNumber'] is None: 375 | break 376 | elif result == "0x01": # For test_deploy's mocked RPC.. TODO make sure there's no side effects 377 | return result 378 | 379 | delta = time.time() - start_time 380 | 381 | if skip and delta > skip: 382 | logger.info(" Took too long, " + colors.FAIL + "skipping" + colors.ENDC + "...") 383 | break 384 | if retry and delta > retry: 385 | logger.info(" Took too long, " + colors.WARNING + "retrying" + colors.ENDC + "...") 386 | return False 387 | 388 | if verbose: 389 | if defaultBlock == 'pending': 390 | logger.info(" Took %ds" % delta) 391 | elif not ((skip and delta > skip) or (retry and delta > retry)): 392 | logger.info(" " + colors.OKGREEN + "Ready!" + colors.ENDC + " Mining took %ds" % delta) 393 | return True 394 | 395 | def wait_for_next_block(self, from_block=None, retry=None, skip=None, verbose=False): 396 | if from_block is None: 397 | last_block = self.last_block() 398 | else: 399 | last_block = from_block 400 | 401 | if retry == 1: 402 | retry = self.retry 403 | if skip == 1: 404 | skip = self.skip 405 | 406 | msg = ' (%s%s%s)' % (('retrying in %ss' % retry) if retry else '', 407 | ', ' if retry and skip else '', 408 | ('skipping in %ss' % skip) if skip else '') 409 | 410 | if verbose: 411 | sys.stdout.write('Waiting for next block to be mined%s' % msg) 412 | start_time = time.time() 413 | 414 | delta = 0 415 | while True: 416 | if verbose: 417 | sys.stdout.write('.') 418 | sys.stdout.flush() 419 | time.sleep(1) 420 | block = self.last_block() 421 | if block != last_block: 422 | break 423 | 424 | delta = time.time() - start_time 425 | 426 | if skip and delta > skip: 427 | logger.info(" Took too long, " + colors.FAIL + "skipping" + colors.ENDC + "...") 428 | break 429 | if retry and delta > retry: 430 | logger.info(" Took too long, " + colors.WARNING + "retrying" + colors.ENDC + "...") 431 | return False 432 | 433 | if verbose and not ((skip and delta > skip) or (retry and delta > retry)): 434 | logger.info(" " + colors.OKGREEN + "Ready!" + colors.ENDC + " Mining took %ds" % delta) 435 | return True 436 | -------------------------------------------------------------------------------- /pyepm/colors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | class colors: 5 | HEADER = '\033[95m' 6 | OKBLUE = '\033[94m' 7 | OKGREEN = '\033[92m' 8 | WARNING = '\033[93m' 9 | FAIL = '\033[91m' 10 | ENDC = '\033[0m' 11 | BOLD = '\033[1m' 12 | UNDERLINE = '\033[4m' 13 | -------------------------------------------------------------------------------- /pyepm/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author: caktux 4 | # @Date: 2014-12-21 12:44:20 5 | # @Last Modified by: caktux 6 | # @Last Modified time: 2015-04-04 23:55:17 7 | 8 | import os 9 | import StringIO 10 | import ConfigParser 11 | from utils import config_dir 12 | 13 | def default_config_dir(): 14 | config_dir._set_default() 15 | return config_dir.path 16 | 17 | def default_config_path(): 18 | return os.path.join(default_config_dir(), 'config') 19 | 20 | config_template = """ 21 | [api] 22 | # JSONRPC host and port 23 | host = 127.0.0.1 24 | port = 8545 25 | address = 0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826 26 | 27 | [deploy] 28 | gas = 100000 29 | gas_price = 50000000000 30 | fixed_price = False 31 | gas_price_modifier = 1.00 32 | retry = 60 33 | skip = 90 34 | 35 | [misc] 36 | config_dir = {0} 37 | verbosity = 1 38 | 39 | # :INFO, :WARN, :DEBUG, pyepm.deploy:DEBUG ... 40 | logging = :INFO 41 | """.format(default_config_dir()) 42 | 43 | 44 | def get_default_config(): 45 | f = StringIO.StringIO() 46 | f.write(config_template) 47 | f.seek(0) 48 | config = ConfigParser.ConfigParser() 49 | config.readfp(f) 50 | return config 51 | 52 | def read_config(cfg_path=default_config_path()): 53 | # create default if not existent 54 | if not os.path.exists(cfg_path): 55 | open(cfg_path, 'w').write(config_template) 56 | # extend on the default config 57 | config = get_default_config() 58 | config.read(cfg_path) 59 | return config 60 | 61 | def dump_config(config): 62 | r = [''] 63 | for section in config.sections(): 64 | for a, v in config.items(section): 65 | r.append('[%s] %s = %r' % (section, a, v)) 66 | return '\n'.join(r) 67 | -------------------------------------------------------------------------------- /pyepm/deploy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author: caktux 4 | # @Date: 2014-12-21 12:44:20 5 | # @Last Modified by: caktux 6 | # @Last Modified time: 2015-04-05 01:25:39 7 | 8 | import logging 9 | 10 | import os 11 | import api 12 | import json 13 | import yaml 14 | import subprocess 15 | from colors import colors 16 | from distutils import spawn 17 | from serpent import compile 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | class Deploy(object): 22 | def __init__(self, filename, config): 23 | self.filename = filename 24 | self.config = config 25 | 26 | def deploy(self, wait=False): 27 | default_from = self.config.get('api', 'address') 28 | default_gas = int(self.config.getint('deploy', 'gas')) 29 | default_gas_price = int(self.config.getint('deploy', 'gas_price')) 30 | 31 | # Load YAML definitions 32 | definitions = self.load_yaml() 33 | 34 | logger.debug("\nParsing %s..." % self.filename) 35 | path = os.path.dirname(self.filename) 36 | 37 | for definition in definitions: 38 | for key in definition: 39 | logger.info(colors.HEADER + "\n%s: " % key + colors.ENDC) 40 | 41 | if key == 'set': 42 | for variable in definition[key]: 43 | replacement = definition[key][variable] 44 | definitions = self.replace(variable, definitions, replacement) 45 | logger.debug(definitions) 46 | 47 | if key == 'deploy': 48 | for name in definition[key]: 49 | # Reset default values at each definition 50 | contract_names = [] 51 | from_ = default_from 52 | gas = default_gas 53 | gas_price = default_gas_price 54 | value = 0 55 | retry = False 56 | skip = False 57 | wait = False 58 | for option in definition[key][name]: 59 | if option == 'contract': 60 | contract = definition[key][name][option] 61 | if option == 'solidity': 62 | contract_names = definition[key][name][option] 63 | if option == 'from': 64 | from_ = definition[key][name][option] 65 | if option == 'gas': 66 | gas = int(definition[key][name][option]) 67 | if option == 'gas_price': 68 | gas_price = int(definition[key][name][option]) 69 | if option == 'value': 70 | value = int(definition[key][name][option]) 71 | if option == 'endowment': 72 | value = int(definition[key][name][option]) 73 | if option == 'wait': 74 | wait = definition[key][name][option] 75 | if option == 'skip': 76 | skip = int(definition[key][name][option]) 77 | if option == 'retry': 78 | retry = int(definition[key][name][option]) 79 | logger.info(" Deploying " + colors.BOLD + "%s" % os.path.join(path, contract) + colors.ENDC + "...") 80 | addresses = self.create("%s" % os.path.join(path, contract), 81 | from_, gas, gas_price, value, 82 | retry, skip, wait, 83 | contract_names=contract_names if contract_names else name) 84 | if isinstance(addresses, list): 85 | for address in addresses: 86 | definitions = self.replace(name, definitions, address, True) 87 | else: 88 | definitions = self.replace(name, definitions, addresses, True) 89 | logger.debug(definitions) 90 | 91 | if key in ['transact', 'call']: 92 | for name in definition[key]: 93 | # Reset default values at each definition 94 | from_ = default_from 95 | to = None 96 | sig = None 97 | data = '' 98 | gas = default_gas 99 | gas_price = default_gas_price 100 | value = 0 101 | retry = False 102 | skip = False 103 | wait = False 104 | for option in definition[key][name]: 105 | if option == 'from': 106 | from_ = definition[key][name][option] 107 | if option == 'to': 108 | to = definition[key][name][option] 109 | if option == 'fun_name': 110 | raise DeprecationWarning("The `fun_name` definition is deprecated, use `serpent mk_signature `" 111 | " output for your method in `sig` instead.") 112 | if option == 'sig': 113 | sig = definition[key][name][option] 114 | if option == 'data': 115 | dat = definition[key][name][option] 116 | if isinstance(dat, list): 117 | for i, d in enumerate(dat): 118 | if isinstance(d, (basestring)) and not d.startswith("0x") and not d.startswith("$"): 119 | if d != d.decode('string_escape'): 120 | definition[key][name][option][i] = d.decode('string_escape') 121 | else: 122 | padded = "0x" + d.encode('hex') 123 | definition[key][name][option][i] = u"%s" % padded 124 | logger.info(" Converting " + colors.BOLD + "'%s'" % d.encode('unicode-escape') + colors.ENDC + 125 | " string to " + colors.BOLD + "%s" % padded + colors.ENDC) 126 | data = definition[key][name][option] 127 | if option == 'gas': 128 | gas = int(definition[key][name][option]) 129 | if option == 'gas_price': 130 | gas_price = int(definition[key][name][option]) 131 | if option == 'value': 132 | value = int(definition[key][name][option]) 133 | if option == 'retry': 134 | retry = int(definition[key][name][option]) 135 | if option == 'skip': 136 | skip = int(definition[key][name][option]) 137 | if option == 'wait': 138 | wait = definition[key][name][option] 139 | logger.info(" %s " % ("Transaction" if key == 'transact' else "Call") + 140 | colors.BOLD + "%s" % name + colors.ENDC + " to " + 141 | colors.BOLD + "%s " % to + colors.ENDC + "...") 142 | if data: 143 | bluedata = [] 144 | for dat in data: 145 | bluedata.append(colors.OKBLUE + "%s" % dat + colors.ENDC) 146 | logger.info(" with data: [" + ", ".join(bluedata) + "]") 147 | if key == 'transact': 148 | self.transact(to, from_, sig, data, gas, gas_price, value, retry, skip, wait) 149 | elif key == 'call': 150 | self.call(to, from_, sig, data, gas, gas_price, value) 151 | 152 | logger.info("\n" + colors.OKGREEN + "Done!" + colors.ENDC + "\n") 153 | 154 | def compile_solidity(self, contract, contract_names=[]): 155 | if not spawn.find_executable("solc"): 156 | raise Exception("solc compiler not found") 157 | 158 | subprocess.call(["solc", "--input-file", contract, "--binary", "file"]) 159 | contracts = [] 160 | 161 | if not isinstance(contract_names, list): 162 | raise Exception("Contract names must be list") 163 | if not contract_names: 164 | contract_names = [contract[:-4]] 165 | for contract_name in contract_names: 166 | filename = "%s.binary" % contract_name 167 | evm = "0x" + open(filename).read() 168 | contracts.append((contract_name, evm)) 169 | 170 | return contracts 171 | 172 | def create(self, contract, from_, gas, gas_price, value, retry, skip, wait, contract_names=None): 173 | instance = api.Api(self.config) 174 | verbose = (True if self.config.get('misc', 'verbosity') > 1 else False) 175 | 176 | tx_hashes = self.try_create_deploy(contract, from_, gas, gas_price, value, retry, skip, wait, verbose, contract_names) 177 | 178 | if isinstance(tx_hashes, list): 179 | return tx_hashes # actually addresses from Solidity 180 | 181 | # Wait for Serpent contract in pending state 182 | # TODO this should be here, but they screwed up eth_getTransactionReceipt... 183 | # address = instance.get_contract_address(tx_hashes) 184 | # self.log_contract(address, contract_names) 185 | if not retry: 186 | instance.wait_for_transaction(tx_hashes, defaultBlock='pending', retry=retry, skip=skip, verbose=verbose) 187 | else: 188 | successful = False 189 | while not successful: 190 | successful = instance.wait_for_transaction(tx_hashes, defaultBlock='pending', retry=retry, skip=skip, verbose=verbose) 191 | if not successful: 192 | tx_hashes = self.try_create_deploy(contract, from_, gas, gas_price, value, retry, skip, wait, verbose, contract_names) 193 | # TODO ... once it's fixed 194 | # address = instance.get_contract_address(tx_hashes) 195 | # self.log_contract(address, contract_names) 196 | 197 | # Wait for contract being mined 198 | if wait: 199 | if not retry: 200 | instance.wait_for_transaction(tx_hashes, retry=retry, skip=skip, verbose=verbose) 201 | else: 202 | successful = False 203 | while not successful: 204 | successful = instance.wait_for_transaction(tx_hashes, retry=retry, skip=skip, verbose=verbose) 205 | if not successful: 206 | tx_hashes = self.try_create_deploy(contract, from_, gas, gas_price, value, retry, skip, wait, verbose, contract_names) 207 | 208 | # TODO ... once it's fixed, take it out 209 | address = instance.get_contract_address(tx_hashes) 210 | self.log_contract(address, contract_names) 211 | return address 212 | 213 | def try_create_deploy(self, contract, from_, gas, gas_price, value, retry, skip, wait, verbose, contract_names): 214 | instance = api.Api(self.config) 215 | tx_hashes = [] 216 | 217 | if contract[-3:] == 'sol' or isinstance(contract_names, list): 218 | contracts = self.compile_solidity(contract, contract_names) 219 | 220 | for contract_name, contract in contracts: 221 | logger.debug("%s: %s" % (contract_name, contract)) 222 | 223 | tx_hash = self.try_create(contract, contract_name=contract_name, from_=from_, gas=gas, gas_price=gas_price, value=value) 224 | 225 | if not retry: 226 | instance.wait_for_transaction(tx_hash, defaultBlock='pending', retry=retry, skip=skip, verbose=verbose) 227 | # TODO ... once it's fixed 228 | # address = instance.get_contract_address(tx_hash) 229 | # self.log_contract(address, contract_name) 230 | else: 231 | successful = False 232 | while not successful: 233 | successful = instance.wait_for_transaction(tx_hash, defaultBlock='pending', retry=retry, skip=skip, verbose=verbose) 234 | if not successful: 235 | tx_hash = self.try_create(contract, contract_name=contract_name, from_=from_, gas=gas, gas_price=gas_price, value=value) 236 | # TODO ... once it's fixed 237 | # address = instance.get_contract_address(tx_hash) 238 | # self.log_contract(address, contract_name) 239 | 240 | if wait: 241 | if not retry: 242 | instance.wait_for_transaction(tx_hash, retry=retry, skip=skip, verbose=verbose) 243 | else: 244 | successful = False 245 | while not successful: 246 | successful = instance.wait_for_transaction(tx_hash, retry=retry, skip=skip, verbose=verbose) 247 | if not successful: 248 | tx_hash = self.try_create(contract, contract_name=contract_name, from_=from_, gas=gas, gas_price=gas_price, value=value) 249 | 250 | # TODO ... once it's fixed, move on up 251 | address = instance.get_contract_address(tx_hash) 252 | self.log_contract(address, contract_name) 253 | tx_hashes.append(tx_hash) 254 | else: 255 | contract = compile(open(contract).read()).encode('hex') 256 | tx_hash = self.try_create(contract, contract_name=contract_names, from_=from_, gas=gas, gas_price=gas_price, value=value) 257 | 258 | if tx_hashes: 259 | return tx_hashes 260 | return tx_hash 261 | 262 | def try_create(self, contract, from_, gas, gas_price, value, contract_name=None): 263 | instance = api.Api(self.config) 264 | tx_hash = instance.create(contract, from_=from_, gas=gas, gas_price=gas_price, endowment=value) 265 | return tx_hash 266 | 267 | def log_contract(self, address, contract_name=None): 268 | if contract_name: 269 | logger.info(" Contract " + colors.BOLD + "'%s'" % contract_name + colors.ENDC + 270 | " will be available at " + colors.WARNING + "%s" % address + colors.ENDC) 271 | else: 272 | logger.info(" Contract will be available at " + colors.WARNING + "%s" % address + colors.ENDC) 273 | 274 | def transact(self, to, from_, sig, data, gas, gas_price, value, retry, skip, wait): 275 | instance = api.Api(self.config) 276 | # from_count = instance.transaction_count(defaultBlock='pending') 277 | verbose = (True if self.config.get('misc', 'verbosity') > 1 else False) 278 | 279 | result = self.try_transact(to, from_, sig, data, gas, gas_price, value) 280 | 281 | # Wait for transaction in Tx pool 282 | if not retry: 283 | instance.wait_for_transaction(transactionHash=result, defaultBlock='pending', retry=retry, skip=skip, verbose=verbose) 284 | else: 285 | successful = False 286 | while not successful: 287 | successful = instance.wait_for_transaction(transactionHash=result, defaultBlock='pending', retry=retry, skip=skip, verbose=verbose) 288 | if not successful: 289 | result = self.try_transact(to, from_, sig, data, gas, gas_price, value) 290 | 291 | # Wait for transaction being mined 292 | if wait: 293 | if result.startswith("0x"): 294 | if not retry: 295 | instance.wait_for_transaction(transactionHash=result, retry=retry, skip=skip, verbose=verbose) 296 | else: 297 | successful = False 298 | while not successful: 299 | successful = instance.wait_for_transaction(transactionHash=result, retry=retry, skip=skip, verbose=verbose) 300 | if not successful: 301 | result = self.try_transact(to, from_, sig, data, gas, gas_price, value) 302 | else: 303 | from_block = instance.last_block() 304 | if not retry: 305 | instance.wait_for_next_block(from_block=from_block, retry=retry, skip=skip, verbose=verbose) 306 | else: 307 | successful = False 308 | while not successful: 309 | successful = instance.wait_for_next_block(from_block=from_block, retry=retry, skip=skip, verbose=verbose) 310 | if not successful: 311 | result = self.try_transact(to, from_, sig, data, gas, gas_price, value) 312 | 313 | def try_transact(self, to, from_, sig, data, gas, gas_price, value): 314 | instance = api.Api(self.config) 315 | result = instance.transact(to, from_=from_, sig=sig, data=data, gas=gas, gas_price=gas_price, value=value) 316 | logger.info(" Result: " + colors.BOLD + "%s" % (result if result else "OK") + colors.ENDC) 317 | return result 318 | 319 | def call(self, to, from_, sig, data, gas, gas_price, value): 320 | instance = api.Api(self.config) 321 | 322 | result = instance.call(to, sig=sig, data=data, gas=gas, gas_price=gas_price, value=value) 323 | logger.info(" Result: " + colors.BOLD + "%s" % result + colors.ENDC) 324 | 325 | return result 326 | 327 | def replace(self, variable, definitions, replacement, isContract=False): 328 | # Replace variables 329 | count = 0 330 | for repdef in definitions: 331 | for repkey in repdef: 332 | for repname in repdef[repkey]: 333 | if repkey != 'set': 334 | for repoption in repdef[repkey][repname]: 335 | to_replace = repdef[repkey][repname][repoption] 336 | if to_replace == "$%s" % variable: 337 | logger.debug("- Replacing $%s with %s" % (variable, replacement)) 338 | repdef[repkey][repname][repoption] = replacement 339 | count = count + 1 340 | if repoption == 'data': 341 | for i, repdata in enumerate(to_replace): 342 | if repdata == "$%s" % variable: 343 | logger.debug("- Replacing $%s with %s" % (variable, replacement)) 344 | repdef[repkey][repname][repoption][i] = replacement 345 | count = count + 1 346 | if count: 347 | logger.info(" %sReplacing $%s with " % ((" " if isContract else ""), variable) + 348 | colors.BOLD + "%s" % replacement + colors.ENDC + " (%s)" % count) 349 | 350 | return definitions 351 | 352 | def load_yaml(self): 353 | logger.debug("\nLoading %s..." % self.filename) 354 | f = open(self.filename) 355 | data = yaml.load(f) 356 | f.close() 357 | logger.debug(json.dumps(data, indent=4)) 358 | 359 | return data 360 | -------------------------------------------------------------------------------- /pyepm/pyepm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author: caktux 4 | # @Date: 2014-12-21 12:44:20 5 | # @Last Modified by: caktux 6 | # @Last Modified time: 2015-02-18 02:02:31 7 | 8 | import os 9 | import config as c 10 | import logging 11 | import logging.config 12 | import deploy 13 | from colors import colors 14 | from utils import config_dir, configure_logging 15 | from argparse import ArgumentParser 16 | from . import __version__ 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | def parse_arguments(parser): 21 | parser.add_argument( 22 | "-r", "--host", 23 | dest="host", 24 | help=" JSONRPC host (default: 127.0.0.1).") 25 | parser.add_argument( 26 | "-p", "--port", 27 | dest="port", 28 | help=" JSONRPC port (default: 8545).") 29 | parser.add_argument( 30 | "-a", "--address", 31 | dest="address", 32 | help="Set the address from which to deploy contracts.") 33 | parser.add_argument( 34 | "-g", "--gas", 35 | dest="gas", 36 | help="Set the default amount of gas for deployment.") 37 | parser.add_argument( 38 | "-c", "--config", 39 | dest="config", 40 | help="Use another configuration file.") 41 | parser.add_argument( 42 | "-V", "--verbose", 43 | dest="verbosity", 44 | help="<0 - 3> Set the log verbosity from 0 to 3 (default: 1)") 45 | parser.add_argument( 46 | "-L", "--logging", 47 | dest="logging", 48 | help=" set the console log level for" 49 | " logger1, logger2, etc. Empty loggername means root-logger," 50 | " e.g. ':DEBUG,:INFO'. Overrides '-V'") 51 | parser.add_argument( 52 | "filename", 53 | nargs='+', 54 | help="Package definition filenames in YAML format") 55 | 56 | return parser.parse_args() 57 | 58 | def create_config(parser): 59 | options = parse_arguments(parser) 60 | 61 | # 1) read the default config at "~/.pyepm" 62 | config = c.read_config() 63 | 64 | # 2) read config from file 65 | cfg_fn = getattr(options, 'config') 66 | if cfg_fn: 67 | if not os.path.exists(cfg_fn): 68 | c.read_config(cfg_fn) # creates default 69 | config.read(cfg_fn) 70 | 71 | # 3) apply cmd line options to config 72 | for section in config.sections(): 73 | for a, v in config.items(section): 74 | if getattr(options, a, None) is not None: 75 | config.set(section, a, getattr(options, a)) 76 | 77 | # set config_dir 78 | config_dir.set(config.get('misc', 'config_dir')) 79 | 80 | return config 81 | 82 | 83 | def main(): 84 | config = c.get_default_config() 85 | parser = ArgumentParser(version=__version__) 86 | 87 | config = create_config(parser) 88 | 89 | # Logging 90 | configure_logging(config.get('misc', 'logging') or '', verbosity=config.getint('misc', 'verbosity')) 91 | logger.info(colors.HEADER + '=====' + colors.ENDC) 92 | logger.info(colors.OKGREEN + 'PyEPM ' + colors.ENDC + '%s', __version__) 93 | logger.info(colors.HEADER + '=====' + colors.ENDC) 94 | 95 | logger.debug(c.dump_config(config)) 96 | 97 | args = parser.parse_args() 98 | 99 | for filename in args.filename: 100 | if not os.path.exists(filename): 101 | logger.warn("\nFile does not exist: %s" % filename) 102 | else: 103 | logger.info("\nDeploying " + colors.BOLD + "%s" % filename + colors.ENDC + "...") 104 | deployment = deploy.Deploy(filename, config) 105 | deployment.deploy() 106 | 107 | 108 | if __name__ == '__main__': 109 | main() 110 | -------------------------------------------------------------------------------- /pyepm/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author: caktux 4 | # @Date: 2014-12-21 12:44:20 5 | # @Last Modified by: caktux 6 | # @Last Modified time: 2015-02-18 01:05:24 7 | 8 | import os 9 | import sys 10 | import pprint 11 | import logging 12 | import logging.config 13 | 14 | class ConfigDir(object): 15 | 16 | ethdirs = { 17 | "linux2": "~/.pyepm", 18 | "darwin": "~/.pyepm", 19 | "win32": "~/AppData/Roaming/PyEPM", 20 | "win64": "~/AppData/Roaming/PyEPM", 21 | } 22 | 23 | def __init__(self): 24 | self._path = None 25 | 26 | def set(self, path): 27 | path = os.path.abspath(path) 28 | if not os.path.exists(path): 29 | os.makedirs(path) 30 | assert os.path.isdir(path) 31 | self._path = path 32 | 33 | def _set_default(self): 34 | p = self.ethdirs.get(sys.platform, self.ethdirs['linux2']) 35 | self.set(os.path.expanduser(os.path.normpath(p))) 36 | 37 | @property 38 | def path(self): 39 | if not self._path: 40 | self._set_default() 41 | return self._path 42 | 43 | def configure_logging(loggerlevels=':DEBUG', verbosity=1): 44 | logconfig = dict( 45 | version=1, 46 | disable_existing_loggers=False, 47 | formatters=dict( 48 | debug=dict( 49 | format='%(message)s' # '%(threadName)s:%(module)s: %(message)s' 50 | ), 51 | minimal=dict( 52 | format='%(message)s' 53 | ), 54 | ), 55 | handlers=dict( 56 | default={ 57 | 'level': 'INFO', 58 | 'class': 'logging.StreamHandler', 59 | 'formatter': 'minimal' 60 | }, 61 | verbose={ 62 | 'level': 'DEBUG', 63 | 'class': 'logging.StreamHandler', 64 | 'formatter': 'debug' 65 | }, 66 | ), 67 | loggers=dict() 68 | ) 69 | 70 | for loggerlevel in filter(lambda _: ':' in _, loggerlevels.split(',')): 71 | name, level = loggerlevel.split(':') 72 | logconfig['loggers'][name] = dict( 73 | handlers=['verbose'], level=level, propagate=False) 74 | 75 | if len(logconfig['loggers']) == 0: 76 | logconfig['loggers'][''] = dict( 77 | handlers=['default'], 78 | level={0: 'ERROR', 1: 'WARNING', 2: 'INFO', 3: 'DEBUG'}.get( 79 | verbosity), 80 | propagate=True) 81 | 82 | logging.config.dictConfig(logconfig) 83 | logging.debug("Logging config: \n%s\n=====" % pprint.pformat(logconfig, width=4)) 84 | 85 | config_dir = ConfigDir() 86 | 87 | def unhex(str): 88 | """Converts an 'ethereum' style hex value to decimal""" 89 | if str == "0x": 90 | return 0 91 | return int(str, 16) 92 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyyaml 2 | requests 3 | https://github.com/ethereum/serpent/tarball/develop 4 | https://github.com/ethereum/pyethereum/tarball/develop 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | import versioneer 5 | versioneer.VCS = 'git' 6 | versioneer.versionfile_source = 'pyepm/_version.py' 7 | versioneer.versionfile_build = 'pyepm/_version.py' 8 | versioneer.tag_prefix = '' # tags are like 1.2.0 9 | versioneer.parentdir_prefix = 'pyepm-' # dirname like 'myproject-1.2.0' 10 | 11 | CONSOLE_SCRIPTS = ['pyepm=pyepm.pyepm:main'] 12 | LONG = """ 13 | Python-based EPM (Ethereum Package Manager) for Serpent 2 contract deployment using YAML for package definitions. 14 | """ 15 | 16 | setup(name="pyepm", 17 | packages=find_packages("."), 18 | description='Python Ethereum Package Manager', 19 | long_description=LONG, 20 | author="caktux", 21 | author_email="caktux@gmail.com", 22 | url='https://github.com/etherex/pyepm/', 23 | install_requires=[ 24 | 'pyyaml', 25 | 'ethereum', 26 | 'ethereum-serpent', 27 | 'requests' 28 | ], 29 | entry_points=dict(console_scripts=CONSOLE_SCRIPTS), 30 | version=versioneer.get_version(), 31 | cmdclass=versioneer.get_cmdclass(), 32 | classifiers=[ 33 | "Development Status :: 2 - Pre-Alpha", 34 | "Environment :: Console", 35 | "License :: OSI Approved :: MIT License", 36 | "Operating System :: MacOS :: MacOS X", 37 | "Operating System :: POSIX :: Linux", 38 | "Programming Language :: Python :: 2.7", 39 | ]) 40 | -------------------------------------------------------------------------------- /test/fixtures/example.yaml: -------------------------------------------------------------------------------- 1 | - 2 | # Set some variables. 3 | set: 4 | NameReg: "0x72ba7d8e73fe8eb666ea66babc8116a41bfb10e2" 5 | - 6 | # Deploy contracts 7 | deploy: 8 | NameCoin: 9 | contract: namecoin.se 10 | retry: 15 11 | wait: True 12 | - 13 | deploy: 14 | Subcurrency: 15 | contract: subcurrency.se 16 | gas: 100000 17 | endowment: 1000000000000000000 18 | retry: 30 19 | wait: True 20 | - 21 | # Make transactions, here we register the previously deployed 22 | # 'Subcurrency' contract with the deployed NameCoin 23 | transact: 24 | RegisterSubToNameCoin: 25 | to: $NameCoin 26 | sig: register:[int256,int256]:int256 27 | data: 28 | - $Subcurrency 29 | - SubcurrencyName 30 | gas: 100000 31 | gas_price: 10000000000000 32 | value: 0 33 | retry: 30 34 | wait: True 35 | - 36 | transact: 37 | TestEncoding: 38 | to: $NameReg 39 | sig: some_method:[int256,int256,int256]:int256 40 | data: 41 | - $Subcurrency 42 | - 42 43 | - "\x01\x00" 44 | gas: 100000 45 | gas_price: 10000000000000 46 | value: 0 47 | wait: False 48 | - 49 | # Contract calls with return values 50 | call: 51 | GetNameFromNameCoin: 52 | to: $NameCoin 53 | sig: get_name:[int256]:int256 54 | data: 55 | - $Subcurrency 56 | - 57 | # Another deploy 58 | deploy: 59 | extra: 60 | contract: short_namecoin.se 61 | retry: 10 62 | wait: True 63 | - 64 | # Deploy Solidity contract 65 | deploy: 66 | Wallet: 67 | contract: wallet.sol 68 | solidity: 69 | - multiowned 70 | - daylimit 71 | - multisig 72 | - Wallet 73 | gas: 2500000 74 | retry: 30 75 | wait: True 76 | - 77 | # Transact to deployed Solidity contract name 78 | transact: 79 | ToWallet: 80 | to: $Wallet 81 | sig: kill:[$Subcurrency]:int256 82 | retry: 15 83 | wait: True 84 | -------------------------------------------------------------------------------- /test/fixtures/namecoin.se: -------------------------------------------------------------------------------- 1 | def register(k, v): 2 | if !self.storage[k]: # Is the key not yet taken? 3 | # Then take it! 4 | self.storage[k] = v 5 | return(1) 6 | else: 7 | return(0) // Otherwise do nothing 8 | 9 | def get_name(k): 10 | return(self.storage[k]) 11 | -------------------------------------------------------------------------------- /test/fixtures/short_namecoin.se: -------------------------------------------------------------------------------- 1 | def register(k, v): 2 | if !self.storage[k]: 3 | self.storage[k] = v 4 | -------------------------------------------------------------------------------- /test/fixtures/subcurrency.se: -------------------------------------------------------------------------------- 1 | def init(): 2 | self.storage[msg.sender] = 1000000 3 | 4 | def balance_query(k): 5 | return(self.storage[addr]) 6 | 7 | def send(to, value): 8 | fromvalue = self.storage[msg.sender] 9 | if fromvalue >= value: 10 | self.storage[from] = fromvalue - value 11 | self.storage[to] += value 12 | -------------------------------------------------------------------------------- /test/fixtures/wallet.sol: -------------------------------------------------------------------------------- 1 | //sol Wallet 2 | // Multi-sig, daily-limited account proxy/wallet. 3 | // @authors: 4 | // Gav Wood 5 | // inheritable "property" contract that enables methods to be protected by requiring the acquiescence of either a 6 | // single, or, crucially, each of a number of, designated owners. 7 | // usage: 8 | // use modifiers onlyowner (just own owned) or onlymanyowners(hash), whereby the same hash must be provided by 9 | // some number (specified in constructor) of the set of owners (specified in the constructor, modifiable) before the 10 | // interior is executed. 11 | contract multiowned { 12 | 13 | // TYPES 14 | 15 | // struct for the status of a pending operation. 16 | struct PendingState { 17 | uint yetNeeded; 18 | uint ownersDone; 19 | uint index; 20 | } 21 | 22 | // EVENTS 23 | 24 | // this contract only has five types of events: it can accept a confirmation, in which case 25 | // we record owner and operation (hash) alongside it. 26 | event Confirmation(address owner, bytes32 operation); 27 | event Revoke(address owner, bytes32 operation); 28 | // some others are in the case of an owner changing. 29 | event OwnerChanged(address oldOwner, address newOwner); 30 | event OwnerAdded(address newOwner); 31 | event OwnerRemoved(address oldOwner); 32 | // the last one is emitted if the required signatures change 33 | event RequirementChanged(uint newRequirement); 34 | 35 | // MODIFIERS 36 | 37 | // simple single-sig function modifier. 38 | modifier onlyowner { 39 | if (isOwner(msg.sender)) 40 | _ 41 | } 42 | // multi-sig function modifier: the operation must have an intrinsic hash in order 43 | // that later attempts can be realised as the same underlying operation and 44 | // thus count as confirmations. 45 | modifier onlymanyowners(bytes32 _operation) { 46 | if (confirmAndCheck(_operation)) 47 | _ 48 | } 49 | 50 | // METHODS 51 | 52 | // constructor is given number of sigs required to do protected "onlymanyowners" transactions 53 | // as well as the selection of addresses capable of confirming them. 54 | function multiowned() { 55 | m_required = 1; 56 | m_numOwners = 1; 57 | m_owners[m_numOwners] = uint(msg.sender); 58 | m_ownerIndex[uint(msg.sender)] = m_numOwners; 59 | } 60 | 61 | // Revokes a prior confirmation of the given operation 62 | function revoke(bytes32 _operation) external { 63 | uint ownerIndex = m_ownerIndex[uint(msg.sender)]; 64 | // make sure they're an owner 65 | if (ownerIndex == 0) return; 66 | uint ownerIndexBit = 2**ownerIndex; 67 | var pending = m_pending[_operation]; 68 | if (pending.ownersDone & ownerIndexBit > 0) { 69 | pending.yetNeeded++; 70 | pending.ownersDone -= ownerIndexBit; 71 | Revoke(msg.sender, _operation); 72 | } 73 | } 74 | 75 | // Replaces an owner `_from` with another `_to`. 76 | function changeOwner(address _from, address _to) onlymanyowners(sha3(msg.data, block.number)) external { 77 | if (isOwner(_to)) return; 78 | uint ownerIndex = m_ownerIndex[uint(_from)]; 79 | if (ownerIndex == 0) return; 80 | 81 | clearPending(); 82 | m_owners[ownerIndex] = uint(_to); 83 | m_ownerIndex[uint(_from)] = 0; 84 | m_ownerIndex[uint(_to)] = ownerIndex; 85 | OwnerChanged(_from, _to); 86 | } 87 | function addOwner(address _owner) onlymanyowners(sha3(msg.data, block.number)) external { 88 | if (isOwner(_owner)) return; 89 | 90 | clearPending(); 91 | if (m_numOwners >= c_maxOwners) 92 | reorganizeOwners(); 93 | if (m_numOwners >= c_maxOwners) 94 | return; 95 | m_numOwners++; 96 | m_owners[m_numOwners] = uint(_owner); 97 | m_ownerIndex[uint(_owner)] = m_numOwners; 98 | OwnerAdded(_owner); 99 | } 100 | 101 | function removeOwner(address _owner) onlymanyowners(sha3(msg.data, block.number)) external { 102 | uint ownerIndex = m_ownerIndex[uint(_owner)]; 103 | if (ownerIndex == 0) return; 104 | if (m_required > m_numOwners - 1) return; 105 | 106 | m_owners[ownerIndex] = 0; 107 | m_ownerIndex[uint(_owner)] = 0; 108 | clearPending(); 109 | reorganizeOwners(); //make sure m_numOwner is equal to the number of owners and always points to the optimal free slot 110 | OwnerRemoved(_owner); 111 | } 112 | 113 | function changeRequirement(uint _newRequired) onlymanyowners(sha3(msg.data, block.number)) external { 114 | if (_newRequired > m_numOwners) return; 115 | m_required = _newRequired; 116 | clearPending(); 117 | RequirementChanged(_newRequired); 118 | } 119 | 120 | function isOwner(address _addr) returns (bool) { 121 | return m_ownerIndex[uint(_addr)] > 0; 122 | } 123 | 124 | function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool) { 125 | var pending = m_pending[_operation]; 126 | uint ownerIndex = m_ownerIndex[uint(_owner)]; 127 | 128 | // make sure they're an owner 129 | if (ownerIndex == 0) return false; 130 | 131 | // determine the bit to set for this owner. 132 | uint ownerIndexBit = 2**ownerIndex; 133 | if (pending.ownersDone & ownerIndexBit == 0) { 134 | return false; 135 | } else { 136 | return true; 137 | } 138 | } 139 | 140 | // INTERNAL METHODS 141 | 142 | function confirmAndCheck(bytes32 _operation) internal returns (bool) { 143 | // determine what index the present sender is: 144 | uint ownerIndex = m_ownerIndex[uint(msg.sender)]; 145 | // make sure they're an owner 146 | if (ownerIndex == 0) return; 147 | 148 | var pending = m_pending[_operation]; 149 | // if we're not yet working on this operation, switch over and reset the confirmation status. 150 | if (pending.yetNeeded == 0) { 151 | // reset count of confirmations needed. 152 | pending.yetNeeded = m_required; 153 | // reset which owners have confirmed (none) - set our bitmap to 0. 154 | pending.ownersDone = 0; 155 | pending.index = m_pendingIndex.length++; 156 | m_pendingIndex[pending.index] = _operation; 157 | } 158 | // determine the bit to set for this owner. 159 | uint ownerIndexBit = 2**ownerIndex; 160 | // make sure we (the message sender) haven't confirmed this operation previously. 161 | if (pending.ownersDone & ownerIndexBit == 0) { 162 | Confirmation(msg.sender, _operation); 163 | // ok - check if count is enough to go ahead. 164 | if (pending.yetNeeded <= 1) { 165 | // enough confirmations: reset and run interior. 166 | delete m_pendingIndex[m_pending[_operation].index]; 167 | delete m_pending[_operation]; 168 | return true; 169 | } 170 | else 171 | { 172 | // not enough: record that this owner in particular confirmed. 173 | pending.yetNeeded--; 174 | pending.ownersDone |= ownerIndexBit; 175 | } 176 | } 177 | } 178 | 179 | function reorganizeOwners() private returns (bool) { 180 | uint free = 1; 181 | while (free < m_numOwners) 182 | { 183 | while (free < m_numOwners && m_owners[free] != 0) free++; 184 | while (m_numOwners > 1 && m_owners[m_numOwners] == 0) m_numOwners--; 185 | if (free < m_numOwners && m_owners[m_numOwners] != 0 && m_owners[free] == 0) 186 | { 187 | m_owners[free] = m_owners[m_numOwners]; 188 | m_ownerIndex[m_owners[free]] = free; 189 | m_owners[m_numOwners] = 0; 190 | } 191 | } 192 | } 193 | 194 | function clearPending() internal { 195 | uint length = m_pendingIndex.length; 196 | for (uint i = 0; i < length; ++i) 197 | if (m_pendingIndex[i] != 0) 198 | delete m_pending[m_pendingIndex[i]]; 199 | delete m_pendingIndex; 200 | } 201 | 202 | // FIELDS 203 | 204 | // the number of owners that must confirm the same operation before it is run. 205 | uint public m_required; 206 | // pointer used to find a free slot in m_owners 207 | uint public m_numOwners; 208 | 209 | // list of owners 210 | uint[256] m_owners; 211 | uint constant c_maxOwners = 250; 212 | // index on the list of owners to allow reverse lookup 213 | mapping(uint => uint) m_ownerIndex; 214 | // the ongoing operations. 215 | mapping(bytes32 => PendingState) m_pending; 216 | bytes32[] m_pendingIndex; 217 | } 218 | 219 | // inheritable "property" contract that enables methods to be protected by placing a linear limit (specifiable) 220 | // on a particular resource per calendar day. is multiowned to allow the limit to be altered. resource that method 221 | // uses is specified in the modifier. 222 | contract daylimit is multiowned { 223 | 224 | // MODIFIERS 225 | 226 | // simple modifier for daily limit. 227 | modifier limitedDaily(uint _value) { 228 | if (underLimit(_value)) 229 | _ 230 | } 231 | 232 | // METHODS 233 | 234 | // constructor - just records the present day's index. 235 | function daylimit() { 236 | m_lastDay = today(); 237 | } 238 | // (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today. 239 | function setDailyLimit(uint _newLimit) onlymanyowners(sha3(msg.data, block.number)) external { 240 | m_dailyLimit = _newLimit; 241 | } 242 | // (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today. 243 | function resetSpentToday() onlymanyowners(sha3(msg.data, block.number)) external { 244 | m_spentToday = 0; 245 | } 246 | 247 | // INTERNAL METHODS 248 | 249 | // checks to see if there is at least `_value` left from the daily limit today. if there is, subtracts it and 250 | // returns true. otherwise just returns false. 251 | function underLimit(uint _value) internal onlyowner returns (bool) { 252 | // reset the spend limit if we're on a different day to last time. 253 | if (today() > m_lastDay) { 254 | m_spentToday = 0; 255 | m_lastDay = today(); 256 | } 257 | // check to see if there's enough left - if so, subtract and return true. 258 | if (m_spentToday + _value >= m_spentToday && m_spentToday + _value <= m_dailyLimit) { 259 | m_spentToday += _value; 260 | return true; 261 | } 262 | return false; 263 | } 264 | // determines today's index. 265 | function today() private constant returns (uint) { return now / 1 days; } 266 | 267 | // FIELDS 268 | 269 | uint public m_dailyLimit; 270 | uint m_spentToday; 271 | uint m_lastDay; 272 | } 273 | 274 | // interface contract for multisig proxy contracts; see below for docs. 275 | contract multisig { 276 | 277 | // EVENTS 278 | 279 | // logged events: 280 | // Funds has arrived into the wallet (record how much). 281 | event Deposit(address from, uint value); 282 | // Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going). 283 | event SingleTransact(address owner, uint value, address to, bytes data); 284 | // Multi-sig transaction going out of the wallet (record who signed for it last, the operation hash, how much, and to whom it's going). 285 | event MultiTransact(address owner, bytes32 operation, uint value, address to, bytes data); 286 | // Confirmation still needed for a transaction. 287 | event ConfirmationNeeded(bytes32 operation, address initiator, uint value, address to, bytes data); 288 | 289 | // FUNCTIONS 290 | 291 | // TODO: document 292 | /*function changeOwner(address _from, address _to) external; 293 | function execute(address _to, uint _value, bytes _data) external returns (bytes32); 294 | function confirm(bytes32 _h) returns (bool);*/ 295 | } 296 | 297 | // usage: 298 | // bytes32 h = Wallet(w).from(oneOwner).transact(to, value, data); 299 | // Wallet(w).from(anotherOwner).confirm(h); 300 | contract Wallet is multisig, multiowned, daylimit { 301 | 302 | // TYPES 303 | 304 | // Transaction structure to remember details of transaction lest it need be saved for a later call. 305 | struct Transaction { 306 | address to; 307 | uint value; 308 | bytes data; 309 | } 310 | 311 | // EVENTS 312 | 313 | event Created(bytes32 indexed identifier); 314 | 315 | // METHODS 316 | 317 | // constructor - just pass on the owner arra to the multiowned. 318 | function Wallet(bytes32 identifier) { 319 | Created(identifier); 320 | } 321 | 322 | // kills the contract sending everything to `_to`. 323 | function kill(address _to) onlymanyowners(sha3(msg.data, block.number)) external { 324 | suicide(_to); 325 | } 326 | 327 | // gets called when no other function matches 328 | function() { 329 | // just being sent some cash? 330 | if (msg.value > 0) 331 | Deposit(msg.sender, msg.value); 332 | } 333 | 334 | // Outside-visible transact entry point. Executes transacion immediately if below daily spend limit. 335 | // If not, goes into multisig process. We provide a hash on return to allow the sender to provide 336 | // shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value 337 | // and _data arguments). They still get the option of using them if they want, anyways. 338 | function execute(address _to, uint _value, bytes _data) external onlyowner returns (bytes32 _r) { 339 | // first, take the opportunity to check that we're under the daily limit. 340 | if (underLimit(_value)) { 341 | SingleTransact(msg.sender, _value, _to, _data); 342 | // yes - just execute the call. 343 | _to.call.value(_value)(_data); 344 | return 0; 345 | } 346 | // determine our operation hash. 347 | _r = sha3(msg.data, block.number); 348 | if (!confirm(_r) && m_txs[_r].to == 0) { 349 | m_txs[_r].to = _to; 350 | m_txs[_r].value = _value; 351 | m_txs[_r].data = _data; 352 | ConfirmationNeeded(_r, msg.sender, _value, _to, _data); 353 | } 354 | } 355 | 356 | // confirm a transaction through just the hash. we use the previous transactions map, m_txs, in order 357 | // to determine the body of the transaction from the hash provided. 358 | function confirm(bytes32 _h) onlymanyowners(_h) returns (bool) { 359 | if (m_txs[_h].to != 0) { 360 | m_txs[_h].to.call.value(m_txs[_h].value)(m_txs[_h].data); 361 | MultiTransact(msg.sender, _h, m_txs[_h].value, m_txs[_h].to, m_txs[_h].data); 362 | delete m_txs[_h]; 363 | return true; 364 | } 365 | } 366 | 367 | // INTERNAL METHODS 368 | 369 | function clearPending() internal { 370 | uint length = m_pendingIndex.length; 371 | for (uint i = 0; i < length; ++i) 372 | delete m_txs[m_pendingIndex[i]]; 373 | super.clearPending(); 374 | } 375 | 376 | // FIELDS 377 | 378 | // pending transactions we have at present. 379 | mapping (bytes32 => Transaction) m_txs; 380 | } 381 | -------------------------------------------------------------------------------- /test/helpers.py: -------------------------------------------------------------------------------- 1 | from distutils import spawn 2 | import mock 3 | import pytest 4 | import requests 5 | 6 | from pyepm import config as c 7 | 8 | config = c.get_default_config() 9 | 10 | has_solc = spawn.find_executable("solc") 11 | 12 | solc = pytest.mark.skipif(not has_solc, reason="solc compiler not found") 13 | 14 | COW_ADDRESS = '0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826' 15 | 16 | def is_hex(s): 17 | try: 18 | int(s, 16) 19 | return True 20 | except ValueError: 21 | return False 22 | 23 | def mock_json_response(status_code=200, error=None, result=None): 24 | m = mock.MagicMock(spec=requests.Response) 25 | m.status_code = status_code 26 | base_json_response = {u'jsonrpc': u'2.0', u'id': u'c7c427a5-b6e9-4dbf-b218-a6f9d4f09246'} 27 | json_response = dict(base_json_response) 28 | if result: 29 | json_response[u'result'] = result 30 | elif error: 31 | json_response[u'error'] = error 32 | if status_code >= 400: 33 | m.reason = 'Error Reason' 34 | m.json.return_value = json_response 35 | return m 36 | -------------------------------------------------------------------------------- /test/test_api.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyepm import api 4 | 5 | from helpers import COW_ADDRESS, config, mock_json_response 6 | 7 | def test_api_exception_error_response(mocker): 8 | instance = api.Api(config) 9 | mocker.patch('requests.post', return_value=mock_json_response(error={'code': 31337, 'message': 'Too Elite'})) 10 | with pytest.raises(api.ApiException) as excinfo: 11 | instance.coinbase() 12 | assert excinfo.value.code == 31337 13 | assert excinfo.value.message == 'Too Elite' 14 | 15 | def test_api_exception_status_code(mocker): 16 | instance = api.Api(config) 17 | mocker.patch('requests.post', return_value=mock_json_response(status_code=404)) 18 | with pytest.raises(api.ApiException) as excinfo: 19 | instance.coinbase() 20 | assert excinfo.value.code == 404 21 | assert excinfo.value.message == 'Error Reason' 22 | 23 | def mock_rpc(mocker, rpc_fun, rpc_args, json_result, rpc_method, rpc_params): 24 | instance = api.Api(config) 25 | instance.fixed_price = True 26 | 27 | mocker.patch('requests.post', return_value=mock_json_response(result=json_result)) 28 | mock_rpc_post = mocker.patch.object(instance, '_rpc_post', side_effect=instance._rpc_post) 29 | 30 | result = getattr(instance, rpc_fun)(*rpc_args) 31 | mock_rpc_post.assert_called_with(rpc_method, rpc_params) 32 | return result 33 | 34 | def test_accounts(mocker): 35 | accounts = ['0x7adf3b3bce3a5c8c17e8b243f4c331dd97c60579'] 36 | assert mock_rpc(mocker, 'accounts', [], json_result=accounts, 37 | rpc_method='eth_accounts', rpc_params=None) == accounts 38 | 39 | def test_balance_at_zero(mocker): 40 | address = '0x7adf3b3bce3a5c8c17e8b243f4c331dd97c60579' 41 | balance = '0x' 42 | assert mock_rpc(mocker, 'balance_at', [address], json_result=balance, 43 | rpc_method='eth_getBalance', rpc_params=[address, 'latest']) == 0 44 | 45 | def test_balance_at_non_zero(mocker): 46 | address = '0x7adf3b3bce3a5c8c17e8b243f4c331dd97c60579' 47 | balance = '0x01495010e21ff5d000' 48 | assert mock_rpc(mocker, 'balance_at', [address], json_result=balance, 49 | rpc_method='eth_getBalance', rpc_params=[address, 'latest']) == 23729485000000000000 50 | 51 | def test_block(mocker): 52 | nr = 1711 53 | block = { 54 | 'nonce': '0x0bacf24ff9c36870be684a1e8b8f875864c6ea69248373db87266199e6a2ca16', 55 | 'transactionsRoot': '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', 56 | 'hash': '806eee83f9aaa349031bd0dccd50241cc898c65cd36b8fa53aaaee3638d27488', 57 | 'sha3Uncles': '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', 58 | 'miner': '0xba5a55dec63ca9b4f8108dec82b9d734320c7057', 59 | 'parentHash': '0xc4781fe71e48632aba0dc95b24a23eef638e961ecb54e8a236c4c561b75298b6', 60 | 'extraData': '0x0000000000000000000000000000000000000000000000000000000000000000', 61 | 'gasLimit': 187806, 62 | 'number': 1711, 63 | 'stateRoot': '0xfdc5440b69a0361051d319d5c463ce45bcd03bc8622fbe2a5739b6a469f5da50', 64 | 'difficulty': '0x022eec', 65 | 'timestamp': 1423664646} 66 | assert mock_rpc(mocker, 'block', [nr], json_result=block, 67 | rpc_method='eth_getBlockByNumber', rpc_params=[hex(nr), False]) == block 68 | 69 | def test_coinbase(mocker): 70 | coinbase = '0x7adf3b3bce3a5c8c17e8b243f4c331dd97c60579' 71 | assert mock_rpc(mocker, 'coinbase', [], json_result=coinbase, 72 | rpc_method='eth_coinbase', rpc_params=None) == coinbase 73 | 74 | def test_gasprice(mocker): 75 | gas_price = 10000000000000 76 | assert mock_rpc(mocker, 'gasprice', [], json_result=hex(gas_price), 77 | rpc_method='eth_gasPrice', rpc_params=None) == gas_price 78 | 79 | def test_create(mocker): 80 | code = '0xdeadbeef' 81 | address = '0x6489ecbe173ac43dadb9f4f098c3e663e8438dd7' 82 | rpc_params = [{'gas': hex(100000), 83 | 'data': '0xdeadbeef', 84 | 'from': COW_ADDRESS, 85 | 'value': hex(0), 86 | 'gasPrice': hex(50000000000)}] 87 | assert mock_rpc(mocker, 'create', [code], json_result=address, 88 | rpc_method='eth_sendTransaction', rpc_params=rpc_params) == address 89 | 90 | def test_logs(mocker): 91 | filter = {'address': '0x77045e71a7a2c50903d88e564cd72fab11e82051'} 92 | logs = [ 93 | {'address': '0x77045e71a7a2c50903d88e564cd72fab11e82051', 94 | 'data': '0x00000000000000000000000077045e71a7a2c50903d88e564cd72fab11e82051', 95 | 'hash': '0x13f31e4ca94e117c4cd80d3577f87145ea626a262ecb784f7000b090de92043f', 96 | 'number': 1, 97 | 'topic': ['0x000000000000000000000000cd2a3d9f938e13cd947ec05abc7fe734df8dd826']}] 98 | assert mock_rpc(mocker, 'logs', [filter], json_result=logs, 99 | rpc_method='eth_getLogs', rpc_params=[filter]) == logs 100 | 101 | def test_is_contract_at_contract_exists(mocker): 102 | address = '0x6489ecbe173ac43dadb9f4f098c3e663e8438dd7' 103 | code = '0xdeadbeef' 104 | assert mock_rpc(mocker, 'is_contract_at', [address], json_result=code, 105 | rpc_method='eth_getCode', rpc_params=[address, 'latest']) 106 | 107 | def test_is_contract_at_contract_doesnt_exists_cpp_client(mocker): 108 | address = '0x6489ecbe173ac43dadb9f4f098c3e663e8438dd7' 109 | code = '0x0000000000000000000000000000000000000000000000000000000000000000' 110 | assert not mock_rpc(mocker, 'is_contract_at', [address], json_result=code, 111 | rpc_method='eth_getCode', rpc_params=[address, 'latest']) 112 | 113 | def test_is_contract_at_contract_doesnt_exists_go_client(mocker): 114 | address = '0x6489ecbe173ac43dadb9f4f098c3e663e8438dd7' 115 | code = '0x' 116 | assert not mock_rpc(mocker, 'is_contract_at', [address], json_result=code, 117 | rpc_method='eth_getCode', rpc_params=[address, 'latest']) 118 | 119 | def test_is_listening(mocker): 120 | assert mock_rpc(mocker, 'is_listening', [], json_result=True, 121 | rpc_method='net_listening', rpc_params=None) 122 | 123 | def test_is_mining(mocker): 124 | assert mock_rpc(mocker, 'is_mining', [], json_result=True, 125 | rpc_method='eth_mining', rpc_params=None) 126 | 127 | def test_number(mocker): 128 | assert mock_rpc(mocker, 'number', [], json_result=hex(42), 129 | rpc_method='eth_blockNumber', rpc_params=None) == 42 130 | 131 | def test_peer_count(mocker): 132 | assert mock_rpc(mocker, 'peer_count', [], json_result=hex(8), 133 | rpc_method='net_peerCount', rpc_params=None) == 8 134 | 135 | def test_storage_at(mocker): 136 | address = "0x407d73d8a49eeb85d32cf465507dd71d507100c1" 137 | idx = 1 138 | assert mock_rpc(mocker, 'storage_at', [address, idx], json_result={'0x03'}, 139 | rpc_method='eth_getStorageAt', rpc_params=[address, hex(idx), 'latest']) == {'0x03'} 140 | 141 | def test_transact(mocker): 142 | address = '0x6489ecbe173ac43dadb9f4f098c3e663e8438dd7' 143 | rpc_params = [{'gas': hex(100000), 144 | 'from': COW_ADDRESS, 145 | 'to': address, 146 | 'data': None, 147 | 'value': hex(0), 148 | 'gasPrice': hex(50000000000)}] 149 | assert mock_rpc(mocker, 'transact', [address], json_result=None, 150 | rpc_method='eth_sendTransaction', rpc_params=rpc_params) is None 151 | 152 | def test_call_multiply(mocker): 153 | address = '0x6489ecbe173ac43dadb9f4f098c3e663e8438dd7' 154 | sig = 'multiply:[int256]:int256' 155 | data = [3] 156 | value = 0 157 | gas = 100000 158 | gas_price = 50000000000 159 | data_abi = '0x1df4f1440000000000000000000000000000000000000000000000000000000000000003' 160 | json_result = '0x0000000000000000000000000000000000000000000000000000000000000015' 161 | rpc_params = [{'gas': hex(gas), 162 | 'from': COW_ADDRESS, 163 | 'to': address, 164 | 'data': data_abi, 165 | 'value': hex(value), 166 | 'gasPrice': hex(gas_price)}, 'latest'] 167 | assert mock_rpc(mocker, 'call', [address, sig, data, gas, gas_price, value], json_result=json_result, 168 | rpc_method='eth_call', rpc_params=rpc_params) == [21] 169 | 170 | def test_call_returning_array(mocker): 171 | address = '0x7b089cfe50c1a5fe5b0da352348a43bba81addd4' 172 | sig = 'get_stats:[]:int256[]' 173 | data = [] 174 | data_abi = '0x61837e41' 175 | json_result = '0x0000000000000000000000000000000000000000000000000000000000000003' +\ 176 | '0000000000000000000000000000000000000000000000000000000000000002' +\ 177 | '0000000000000000000000000000000000000000000000000000000000000001' +\ 178 | '0000000000000000000000000000000000000000000000000000000000000000' 179 | rpc_params = [{'gas': hex(100000), 180 | 'from': COW_ADDRESS, 181 | 'to': address, 182 | 'data': data_abi, 183 | 'value': hex(0), 184 | 'gasPrice': hex(50000000000)}, 'latest'] 185 | assert mock_rpc(mocker, 'call', [address, sig, data], json_result=json_result, 186 | rpc_method='eth_call', rpc_params=rpc_params) == [3, 2, 1, 0] # with length prefix of 3 187 | -------------------------------------------------------------------------------- /test/test_deploy.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyepm import deploy 4 | 5 | from helpers import config, has_solc, is_hex, mock_json_response, solc 6 | 7 | def test_load_yaml(): 8 | deployment = deploy.Deploy('test/fixtures/example.yaml', config) 9 | result = deployment.load_yaml() 10 | assert result == [ 11 | { 12 | 'set': { 13 | 'NameReg': '0x72ba7d8e73fe8eb666ea66babc8116a41bfb10e2' 14 | } 15 | }, 16 | { 17 | 'deploy': { 18 | 'NameCoin': { 19 | 'contract': 'namecoin.se', 20 | 'retry': 15, 21 | 'wait': True 22 | } 23 | }, 24 | }, 25 | { 26 | 'deploy': { 27 | 'Subcurrency': { 28 | 'contract': 'subcurrency.se', 29 | 'endowment': 1000000000000000000, 30 | 'gas': 100000, 31 | 'retry': 30, 32 | 'wait': True 33 | } 34 | } 35 | }, 36 | { 37 | 'transact': { 38 | 'RegisterSubToNameCoin': { 39 | 'to': '$NameCoin', 40 | 'sig': 'register:[int256,int256]:int256', 41 | 'gas': 100000, 42 | 'gas_price': 10000000000000, 43 | 'value': 0, 44 | 'data': [ 45 | '$Subcurrency', 46 | 'SubcurrencyName' 47 | ], 48 | 'retry': 30, 49 | 'wait': True 50 | } 51 | } 52 | }, 53 | { 54 | 'transact': { 55 | 'TestEncoding': { 56 | 'sig': 'some_method:[int256,int256,int256]:int256', 57 | 'gas': 100000, 58 | 'gas_price': 10000000000000, 59 | 'to': '$NameReg', 60 | 'value': 0, 61 | 'data': [ 62 | '$Subcurrency', 63 | 42, 64 | '\x01\x00'], 65 | 'wait': False 66 | } 67 | } 68 | }, 69 | { 70 | 'call': { 71 | 'GetNameFromNameCoin': { 72 | 'sig': 'get_name:[int256]:int256', 73 | 'to': '$NameCoin', 74 | 'data': [ 75 | '$Subcurrency' 76 | ] 77 | } 78 | } 79 | }, 80 | { 81 | 'deploy': { 82 | 'extra': { 83 | 'contract': 'short_namecoin.se', 84 | 'retry': 10, 85 | 'wait': True 86 | } 87 | } 88 | }, 89 | { 90 | 'deploy': { 91 | 'Wallet': { 92 | 'contract': 'wallet.sol', 93 | 'solidity': [ 94 | 'multiowned', 95 | 'daylimit', 96 | 'multisig', 97 | 'Wallet' 98 | ], 99 | 'gas': 2500000, 100 | 'retry': 30, 101 | 'wait': True 102 | } 103 | } 104 | }, 105 | { 106 | 'transact': { 107 | 'ToWallet': { 108 | 'to': '$Wallet', 109 | 'sig': 'kill:[$Subcurrency]:int256', 110 | 'retry': 15, 111 | 'wait': True 112 | } 113 | } 114 | } 115 | ] 116 | 117 | def test_deploy(mocker): 118 | deployment = deploy.Deploy('test/fixtures/example.yaml', config) 119 | mocker.patch('requests.post', return_value=mock_json_response(status_code=200, result='0x01')) 120 | mocker.patch('time.sleep') 121 | if not has_solc: 122 | with pytest.raises(Exception) as excinfo: 123 | deployment.deploy() 124 | assert excinfo.value.message == 'solc compiler not found' 125 | else: 126 | deployment.deploy() 127 | 128 | @solc 129 | def test_compile_solidity(mocker): 130 | contract = 'test/fixtures/wallet.sol' 131 | deployment = deploy.Deploy('test/fixtures/example.yaml', config) 132 | contract_names = ['multiowned', 'daylimit', 'multisig', 'Wallet'] 133 | contracts = deployment.compile_solidity(contract, contract_names) 134 | 135 | assert len(contracts) == 4 136 | for idx, (contract_name, code) in enumerate(contracts): 137 | assert contract_name == contract_names[idx] 138 | assert is_hex(code) 139 | -------------------------------------------------------------------------------- /test/test_utils.py: -------------------------------------------------------------------------------- 1 | from pyepm.utils import unhex 2 | 3 | def test_unhex(): 4 | assert unhex("0x") == 0 5 | assert unhex("0x0") == 0 6 | assert unhex("0xdeadbeef") == 3735928559 7 | assert unhex("deadbeef") == 3735928559 8 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E302 3 | max-line-length = 160 4 | exclude = versioneer.py, pyepm/_version.py 5 | 6 | [pytest] 7 | norecursedirs = .git 8 | -------------------------------------------------------------------------------- /versioneer.py: -------------------------------------------------------------------------------- 1 | 2 | # Version: 0.13 3 | 4 | """ 5 | The Versioneer 6 | ============== 7 | 8 | * like a rocketeer, but for versions! 9 | * https://github.com/warner/python-versioneer 10 | * Brian Warner 11 | * License: Public Domain 12 | * Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, and pypy 13 | * [![Latest Version](https://pypip.in/version/versioneer/badge.svg?style=flat)](https://pypi.python.org/pypi/versioneer/) 14 | * [![Build Status](https://travis-ci.org/warner/python-versioneer.png?branch=master)](https://travis-ci.org/warner/python-versioneer) 15 | 16 | This is a tool for managing a recorded version number in distutils-based 17 | python projects. The goal is to remove the tedious and error-prone "update 18 | the embedded version string" step from your release process. Making a new 19 | release should be as easy as recording a new tag in your version-control 20 | system, and maybe making new tarballs. 21 | 22 | 23 | ## Quick Install 24 | 25 | * `pip install versioneer` to somewhere to your $PATH 26 | * run `versioneer-installer` in your source tree: this installs `versioneer.py` 27 | * follow the instructions below (also in the `versioneer.py` docstring) 28 | 29 | ## Version Identifiers 30 | 31 | Source trees come from a variety of places: 32 | 33 | * a version-control system checkout (mostly used by developers) 34 | * a nightly tarball, produced by build automation 35 | * a snapshot tarball, produced by a web-based VCS browser, like github's 36 | "tarball from tag" feature 37 | * a release tarball, produced by "setup.py sdist", distributed through PyPI 38 | 39 | Within each source tree, the version identifier (either a string or a number, 40 | this tool is format-agnostic) can come from a variety of places: 41 | 42 | * ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows 43 | about recent "tags" and an absolute revision-id 44 | * the name of the directory into which the tarball was unpacked 45 | * an expanded VCS keyword ($Id$, etc) 46 | * a `_version.py` created by some earlier build step 47 | 48 | For released software, the version identifier is closely related to a VCS 49 | tag. Some projects use tag names that include more than just the version 50 | string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool 51 | needs to strip the tag prefix to extract the version identifier. For 52 | unreleased software (between tags), the version identifier should provide 53 | enough information to help developers recreate the same tree, while also 54 | giving them an idea of roughly how old the tree is (after version 1.2, before 55 | version 1.3). Many VCS systems can report a description that captures this, 56 | for example 'git describe --tags --dirty --always' reports things like 57 | "0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the 58 | 0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has 59 | uncommitted changes. 60 | 61 | The version identifier is used for multiple purposes: 62 | 63 | * to allow the module to self-identify its version: `myproject.__version__` 64 | * to choose a name and prefix for a 'setup.py sdist' tarball 65 | 66 | ## Theory of Operation 67 | 68 | Versioneer works by adding a special `_version.py` file into your source 69 | tree, where your `__init__.py` can import it. This `_version.py` knows how to 70 | dynamically ask the VCS tool for version information at import time. However, 71 | when you use "setup.py build" or "setup.py sdist", `_version.py` in the new 72 | copy is replaced by a small static file that contains just the generated 73 | version data. 74 | 75 | `_version.py` also contains `$Revision$` markers, and the installation 76 | process marks `_version.py` to have this marker rewritten with a tag name 77 | during the "git archive" command. As a result, generated tarballs will 78 | contain enough information to get the proper version. 79 | 80 | 81 | ## Installation 82 | 83 | First, decide on values for the following configuration variables: 84 | 85 | * `VCS`: the version control system you use. Currently accepts "git". 86 | 87 | * `versionfile_source`: 88 | 89 | A project-relative pathname into which the generated version strings should 90 | be written. This is usually a `_version.py` next to your project's main 91 | `__init__.py` file, so it can be imported at runtime. If your project uses 92 | `src/myproject/__init__.py`, this should be `src/myproject/_version.py`. 93 | This file should be checked in to your VCS as usual: the copy created below 94 | by `setup.py versioneer` will include code that parses expanded VCS 95 | keywords in generated tarballs. The 'build' and 'sdist' commands will 96 | replace it with a copy that has just the calculated version string. 97 | 98 | This must be set even if your project does not have any modules (and will 99 | therefore never import `_version.py`), since "setup.py sdist" -based trees 100 | still need somewhere to record the pre-calculated version strings. Anywhere 101 | in the source tree should do. If there is a `__init__.py` next to your 102 | `_version.py`, the `setup.py versioneer` command (described below) will 103 | append some `__version__`-setting assignments, if they aren't already 104 | present. 105 | 106 | * `versionfile_build`: 107 | 108 | Like `versionfile_source`, but relative to the build directory instead of 109 | the source directory. These will differ when your setup.py uses 110 | 'package_dir='. If you have `package_dir={'myproject': 'src/myproject'}`, 111 | then you will probably have `versionfile_build='myproject/_version.py'` and 112 | `versionfile_source='src/myproject/_version.py'`. 113 | 114 | If this is set to None, then `setup.py build` will not attempt to rewrite 115 | any `_version.py` in the built tree. If your project does not have any 116 | libraries (e.g. if it only builds a script), then you should use 117 | `versionfile_build = None` and override `distutils.command.build_scripts` 118 | to explicitly insert a copy of `versioneer.get_version()` into your 119 | generated script. 120 | 121 | * `tag_prefix`: 122 | 123 | a string, like 'PROJECTNAME-', which appears at the start of all VCS tags. 124 | If your tags look like 'myproject-1.2.0', then you should use 125 | tag_prefix='myproject-'. If you use unprefixed tags like '1.2.0', this 126 | should be an empty string. 127 | 128 | * `parentdir_prefix`: 129 | 130 | a string, frequently the same as tag_prefix, which appears at the start of 131 | all unpacked tarball filenames. If your tarball unpacks into 132 | 'myproject-1.2.0', this should be 'myproject-'. 133 | 134 | This tool provides one script, named `versioneer-installer`. That script does 135 | one thing: write a copy of `versioneer.py` into the current directory. 136 | 137 | To versioneer-enable your project: 138 | 139 | * 1: Run `versioneer-installer` to copy `versioneer.py` into the top of your 140 | source tree. 141 | 142 | * 2: add the following lines to the top of your `setup.py`, with the 143 | configuration values you decided earlier: 144 | 145 | import versioneer 146 | versioneer.VCS = 'git' 147 | versioneer.versionfile_source = 'src/myproject/_version.py' 148 | versioneer.versionfile_build = 'myproject/_version.py' 149 | versioneer.tag_prefix = '' # tags are like 1.2.0 150 | versioneer.parentdir_prefix = 'myproject-' # dirname like 'myproject-1.2.0' 151 | 152 | * 3: add the following arguments to the setup() call in your setup.py: 153 | 154 | version=versioneer.get_version(), 155 | cmdclass=versioneer.get_cmdclass(), 156 | 157 | * 4: now run `setup.py versioneer`, which will create `_version.py`, and will 158 | modify your `__init__.py` (if one exists next to `_version.py`) to define 159 | `__version__` (by calling a function from `_version.py`). It will also 160 | modify your `MANIFEST.in` to include both `versioneer.py` and the generated 161 | `_version.py` in sdist tarballs. 162 | 163 | * 5: commit these changes to your VCS. To make sure you won't forget, 164 | `setup.py versioneer` will mark everything it touched for addition. 165 | 166 | ## Post-Installation Usage 167 | 168 | Once established, all uses of your tree from a VCS checkout should get the 169 | current version string. All generated tarballs should include an embedded 170 | version string (so users who unpack them will not need a VCS tool installed). 171 | 172 | If you distribute your project through PyPI, then the release process should 173 | boil down to two steps: 174 | 175 | * 1: git tag 1.0 176 | * 2: python setup.py register sdist upload 177 | 178 | If you distribute it through github (i.e. users use github to generate 179 | tarballs with `git archive`), the process is: 180 | 181 | * 1: git tag 1.0 182 | * 2: git push; git push --tags 183 | 184 | Currently, all version strings must be based upon a tag. Versioneer will 185 | report "unknown" until your tree has at least one tag in its history. This 186 | restriction will be fixed eventually (see issue #12). 187 | 188 | ## Version-String Flavors 189 | 190 | Code which uses Versioneer can learn about its version string at runtime by 191 | importing `_version` from your main `__init__.py` file and running the 192 | `get_versions()` function. From the "outside" (e.g. in `setup.py`), you can 193 | import the top-level `versioneer.py` and run `get_versions()`. 194 | 195 | Both functions return a dictionary with different keys for different flavors 196 | of the version string: 197 | 198 | * `['version']`: condensed tag+distance+shortid+dirty identifier. For git, 199 | this uses the output of `git describe --tags --dirty --always` but strips 200 | the tag_prefix. For example "0.11-2-g1076c97-dirty" indicates that the tree 201 | is like the "1076c97" commit but has uncommitted changes ("-dirty"), and 202 | that this commit is two revisions ("-2-") beyond the "0.11" tag. For 203 | released software (exactly equal to a known tag), the identifier will only 204 | contain the stripped tag, e.g. "0.11". 205 | 206 | * `['full']`: detailed revision identifier. For Git, this is the full SHA1 207 | commit id, followed by "-dirty" if the tree contains uncommitted changes, 208 | e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac-dirty". 209 | 210 | Some variants are more useful than others. Including `full` in a bug report 211 | should allow developers to reconstruct the exact code being tested (or 212 | indicate the presence of local changes that should be shared with the 213 | developers). `version` is suitable for display in an "about" box or a CLI 214 | `--version` output: it can be easily compared against release notes and lists 215 | of bugs fixed in various releases. 216 | 217 | In the future, this will also include a 218 | [PEP-0440](http://legacy.python.org/dev/peps/pep-0440/) -compatible flavor 219 | (e.g. `1.2.post0.dev123`). This loses a lot of information (and has no room 220 | for a hash-based revision id), but is safe to use in a `setup.py` 221 | "`version=`" argument. It also enables tools like *pip* to compare version 222 | strings and evaluate compatibility constraint declarations. 223 | 224 | The `setup.py versioneer` command adds the following text to your 225 | `__init__.py` to place a basic version in `YOURPROJECT.__version__`: 226 | 227 | from ._version import get_versions 228 | __version__ = get_versions()['version'] 229 | del get_versions 230 | 231 | ## Updating Versioneer 232 | 233 | To upgrade your project to a new release of Versioneer, do the following: 234 | 235 | * install the new Versioneer (`pip install -U versioneer` or equivalent) 236 | * re-run `versioneer-installer` in your source tree to replace your copy of 237 | `versioneer.py` 238 | * edit `setup.py`, if necessary, to include any new configuration settings 239 | indicated by the release notes 240 | * re-run `setup.py versioneer` to replace `SRC/_version.py` 241 | * commit any changed files 242 | 243 | ### Upgrading from 0.10 to 0.11 244 | 245 | You must add a `versioneer.VCS = "git"` to your `setup.py` before re-running 246 | `setup.py versioneer`. This will enable the use of additional version-control 247 | systems (SVN, etc) in the future. 248 | 249 | ### Upgrading from 0.11 to 0.12 250 | 251 | Nothing special. 252 | 253 | ## Future Directions 254 | 255 | This tool is designed to make it easily extended to other version-control 256 | systems: all VCS-specific components are in separate directories like 257 | src/git/ . The top-level `versioneer.py` script is assembled from these 258 | components by running make-versioneer.py . In the future, make-versioneer.py 259 | will take a VCS name as an argument, and will construct a version of 260 | `versioneer.py` that is specific to the given VCS. It might also take the 261 | configuration arguments that are currently provided manually during 262 | installation by editing setup.py . Alternatively, it might go the other 263 | direction and include code from all supported VCS systems, reducing the 264 | number of intermediate scripts. 265 | 266 | 267 | ## License 268 | 269 | To make Versioneer easier to embed, all its code is hereby released into the 270 | public domain. The `_version.py` that it creates is also in the public 271 | domain. 272 | 273 | """ 274 | 275 | import os, sys, re, subprocess, errno 276 | from distutils.core import Command 277 | from distutils.command.sdist import sdist as _sdist 278 | from distutils.command.build import build as _build 279 | 280 | # these configuration settings will be overridden by setup.py after it 281 | # imports us 282 | versionfile_source = None 283 | versionfile_build = None 284 | tag_prefix = None 285 | parentdir_prefix = None 286 | VCS = None 287 | 288 | # these dictionaries contain VCS-specific tools 289 | LONG_VERSION_PY = {} 290 | 291 | 292 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): 293 | assert isinstance(commands, list) 294 | p = None 295 | for c in commands: 296 | try: 297 | # remember shell=False, so use git.cmd on windows, not just git 298 | p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, 299 | stderr=(subprocess.PIPE if hide_stderr 300 | else None)) 301 | break 302 | except EnvironmentError: 303 | e = sys.exc_info()[1] 304 | if e.errno == errno.ENOENT: 305 | continue 306 | if verbose: 307 | print("unable to run %s" % args[0]) 308 | print(e) 309 | return None 310 | else: 311 | if verbose: 312 | print("unable to find command, tried %s" % (commands,)) 313 | return None 314 | stdout = p.communicate()[0].strip() 315 | if sys.version >= '3': 316 | stdout = stdout.decode() 317 | if p.returncode != 0: 318 | if verbose: 319 | print("unable to run %s (error)" % args[0]) 320 | return None 321 | return stdout 322 | 323 | LONG_VERSION_PY['git'] = ''' 324 | # This file helps to compute a version number in source trees obtained from 325 | # git-archive tarball (such as those provided by githubs download-from-tag 326 | # feature). Distribution tarballs (built by setup.py sdist) and build 327 | # directories (produced by setup.py build) will contain a much shorter file 328 | # that just contains the computed version number. 329 | 330 | # This file is released into the public domain. Generated by 331 | # versioneer-0.13 (https://github.com/warner/python-versioneer) 332 | 333 | # these strings will be replaced by git during git-archive 334 | git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" 335 | git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" 336 | 337 | # these strings are filled in when 'setup.py versioneer' creates _version.py 338 | tag_prefix = "%(TAG_PREFIX)s" 339 | parentdir_prefix = "%(PARENTDIR_PREFIX)s" 340 | versionfile_source = "%(VERSIONFILE_SOURCE)s" 341 | 342 | import errno 343 | import os 344 | import re 345 | import subprocess 346 | import sys 347 | 348 | 349 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): 350 | assert isinstance(commands, list) 351 | p = None 352 | for c in commands: 353 | try: 354 | # remember shell=False, so use git.cmd on windows, not just git 355 | p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, 356 | stderr=(subprocess.PIPE if hide_stderr 357 | else None)) 358 | break 359 | except EnvironmentError: 360 | e = sys.exc_info()[1] 361 | if e.errno == errno.ENOENT: 362 | continue 363 | if verbose: 364 | print("unable to run %%s" %% args[0]) 365 | print(e) 366 | return None 367 | else: 368 | if verbose: 369 | print("unable to find command, tried %%s" %% (commands,)) 370 | return None 371 | stdout = p.communicate()[0].strip() 372 | if sys.version >= '3': 373 | stdout = stdout.decode() 374 | if p.returncode != 0: 375 | if verbose: 376 | print("unable to run %%s (error)" %% args[0]) 377 | return None 378 | return stdout 379 | 380 | 381 | def versions_from_parentdir(parentdir_prefix, root, verbose=False): 382 | # Source tarballs conventionally unpack into a directory that includes 383 | # both the project name and a version string. 384 | dirname = os.path.basename(root) 385 | if not dirname.startswith(parentdir_prefix): 386 | if verbose: 387 | print("guessing rootdir is '%%s', but '%%s' doesn't start with " 388 | "prefix '%%s'" %% (root, dirname, parentdir_prefix)) 389 | return None 390 | return {"version": dirname[len(parentdir_prefix):], "full": ""} 391 | 392 | 393 | def git_get_keywords(versionfile_abs): 394 | # the code embedded in _version.py can just fetch the value of these 395 | # keywords. When used from setup.py, we don't want to import _version.py, 396 | # so we do it with a regexp instead. This function is not used from 397 | # _version.py. 398 | keywords = {} 399 | try: 400 | f = open(versionfile_abs, "r") 401 | for line in f.readlines(): 402 | if line.strip().startswith("git_refnames ="): 403 | mo = re.search(r'=\s*"(.*)"', line) 404 | if mo: 405 | keywords["refnames"] = mo.group(1) 406 | if line.strip().startswith("git_full ="): 407 | mo = re.search(r'=\s*"(.*)"', line) 408 | if mo: 409 | keywords["full"] = mo.group(1) 410 | f.close() 411 | except EnvironmentError: 412 | pass 413 | return keywords 414 | 415 | 416 | def git_versions_from_keywords(keywords, tag_prefix, verbose=False): 417 | if not keywords: 418 | return {} # keyword-finding function failed to find keywords 419 | refnames = keywords["refnames"].strip() 420 | if refnames.startswith("$Format"): 421 | if verbose: 422 | print("keywords are unexpanded, not using") 423 | return {} # unexpanded, so not in an unpacked git-archive tarball 424 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 425 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 426 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 427 | TAG = "tag: " 428 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 429 | if not tags: 430 | # Either we're using git < 1.8.3, or there really are no tags. We use 431 | # a heuristic: assume all version tags have a digit. The old git %%d 432 | # expansion behaves like git log --decorate=short and strips out the 433 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 434 | # between branches and tags. By ignoring refnames without digits, we 435 | # filter out many common branch names like "release" and 436 | # "stabilization", as well as "HEAD" and "master". 437 | tags = set([r for r in refs if re.search(r'\d', r)]) 438 | if verbose: 439 | print("discarding '%%s', no digits" %% ",".join(refs-tags)) 440 | if verbose: 441 | print("likely tags: %%s" %% ",".join(sorted(tags))) 442 | for ref in sorted(tags): 443 | # sorting will prefer e.g. "2.0" over "2.0rc1" 444 | if ref.startswith(tag_prefix): 445 | r = ref[len(tag_prefix):] 446 | if verbose: 447 | print("picking %%s" %% r) 448 | return {"version": r, 449 | "full": keywords["full"].strip()} 450 | # no suitable tags, so we use the full revision id 451 | if verbose: 452 | print("no suitable tags, using full revision id") 453 | return {"version": keywords["full"].strip(), 454 | "full": keywords["full"].strip()} 455 | 456 | 457 | def git_versions_from_vcs(tag_prefix, root, verbose=False): 458 | # this runs 'git' from the root of the source tree. This only gets called 459 | # if the git-archive 'subst' keywords were *not* expanded, and 460 | # _version.py hasn't already been rewritten with a short version string, 461 | # meaning we're inside a checked out source tree. 462 | 463 | if not os.path.exists(os.path.join(root, ".git")): 464 | if verbose: 465 | print("no .git in %%s" %% root) 466 | return {} 467 | 468 | GITS = ["git"] 469 | if sys.platform == "win32": 470 | GITS = ["git.cmd", "git.exe"] 471 | stdout = run_command(GITS, ["describe", "--tags", "--dirty", "--always"], 472 | cwd=root) 473 | if stdout is None: 474 | return {} 475 | if not stdout.startswith(tag_prefix): 476 | if verbose: 477 | fmt = "tag '%%s' doesn't start with prefix '%%s'" 478 | print(fmt %% (stdout, tag_prefix)) 479 | return {} 480 | tag = stdout[len(tag_prefix):] 481 | stdout = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 482 | if stdout is None: 483 | return {} 484 | full = stdout.strip() 485 | if tag.endswith("-dirty"): 486 | full += "-dirty" 487 | return {"version": tag, "full": full} 488 | 489 | 490 | def get_versions(default={"version": "unknown", "full": ""}, verbose=False): 491 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 492 | # __file__, we can work backwards from there to the root. Some 493 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 494 | # case we can only use expanded keywords. 495 | 496 | keywords = {"refnames": git_refnames, "full": git_full} 497 | ver = git_versions_from_keywords(keywords, tag_prefix, verbose) 498 | if ver: 499 | return ver 500 | 501 | try: 502 | root = os.path.realpath(__file__) 503 | # versionfile_source is the relative path from the top of the source 504 | # tree (where the .git directory might live) to this file. Invert 505 | # this to find the root from __file__. 506 | for i in range(len(versionfile_source.split('/'))): 507 | root = os.path.dirname(root) 508 | except NameError: 509 | return default 510 | 511 | return (git_versions_from_vcs(tag_prefix, root, verbose) 512 | or versions_from_parentdir(parentdir_prefix, root, verbose) 513 | or default) 514 | ''' 515 | 516 | 517 | def git_get_keywords(versionfile_abs): 518 | # the code embedded in _version.py can just fetch the value of these 519 | # keywords. When used from setup.py, we don't want to import _version.py, 520 | # so we do it with a regexp instead. This function is not used from 521 | # _version.py. 522 | keywords = {} 523 | try: 524 | f = open(versionfile_abs, "r") 525 | for line in f.readlines(): 526 | if line.strip().startswith("git_refnames ="): 527 | mo = re.search(r'=\s*"(.*)"', line) 528 | if mo: 529 | keywords["refnames"] = mo.group(1) 530 | if line.strip().startswith("git_full ="): 531 | mo = re.search(r'=\s*"(.*)"', line) 532 | if mo: 533 | keywords["full"] = mo.group(1) 534 | f.close() 535 | except EnvironmentError: 536 | pass 537 | return keywords 538 | 539 | 540 | def git_versions_from_keywords(keywords, tag_prefix, verbose=False): 541 | if not keywords: 542 | return {} # keyword-finding function failed to find keywords 543 | refnames = keywords["refnames"].strip() 544 | if refnames.startswith("$Format"): 545 | if verbose: 546 | print("keywords are unexpanded, not using") 547 | return {} # unexpanded, so not in an unpacked git-archive tarball 548 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 549 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 550 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 551 | TAG = "tag: " 552 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 553 | if not tags: 554 | # Either we're using git < 1.8.3, or there really are no tags. We use 555 | # a heuristic: assume all version tags have a digit. The old git %d 556 | # expansion behaves like git log --decorate=short and strips out the 557 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 558 | # between branches and tags. By ignoring refnames without digits, we 559 | # filter out many common branch names like "release" and 560 | # "stabilization", as well as "HEAD" and "master". 561 | tags = set([r for r in refs if re.search(r'\d', r)]) 562 | if verbose: 563 | print("discarding '%s', no digits" % ",".join(refs-tags)) 564 | if verbose: 565 | print("likely tags: %s" % ",".join(sorted(tags))) 566 | for ref in sorted(tags): 567 | # sorting will prefer e.g. "2.0" over "2.0rc1" 568 | if ref.startswith(tag_prefix): 569 | r = ref[len(tag_prefix):] 570 | if verbose: 571 | print("picking %s" % r) 572 | return {"version": r, 573 | "full": keywords["full"].strip()} 574 | # no suitable tags, so we use the full revision id 575 | if verbose: 576 | print("no suitable tags, using full revision id") 577 | return {"version": keywords["full"].strip(), 578 | "full": keywords["full"].strip()} 579 | 580 | 581 | def git_versions_from_vcs(tag_prefix, root, verbose=False): 582 | # this runs 'git' from the root of the source tree. This only gets called 583 | # if the git-archive 'subst' keywords were *not* expanded, and 584 | # _version.py hasn't already been rewritten with a short version string, 585 | # meaning we're inside a checked out source tree. 586 | 587 | if not os.path.exists(os.path.join(root, ".git")): 588 | if verbose: 589 | print("no .git in %s" % root) 590 | return {} 591 | 592 | GITS = ["git"] 593 | if sys.platform == "win32": 594 | GITS = ["git.cmd", "git.exe"] 595 | stdout = run_command(GITS, ["describe", "--tags", "--dirty", "--always"], 596 | cwd=root) 597 | if stdout is None: 598 | return {} 599 | if not stdout.startswith(tag_prefix): 600 | if verbose: 601 | fmt = "tag '%s' doesn't start with prefix '%s'" 602 | print(fmt % (stdout, tag_prefix)) 603 | return {} 604 | tag = stdout[len(tag_prefix):] 605 | stdout = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 606 | if stdout is None: 607 | return {} 608 | full = stdout.strip() 609 | if tag.endswith("-dirty"): 610 | full += "-dirty" 611 | return {"version": tag, "full": full} 612 | 613 | 614 | def do_vcs_install(manifest_in, versionfile_source, ipy): 615 | GITS = ["git"] 616 | if sys.platform == "win32": 617 | GITS = ["git.cmd", "git.exe"] 618 | files = [manifest_in, versionfile_source] 619 | if ipy: 620 | files.append(ipy) 621 | try: 622 | me = __file__ 623 | if me.endswith(".pyc") or me.endswith(".pyo"): 624 | me = os.path.splitext(me)[0] + ".py" 625 | versioneer_file = os.path.relpath(me) 626 | except NameError: 627 | versioneer_file = "versioneer.py" 628 | files.append(versioneer_file) 629 | present = False 630 | try: 631 | f = open(".gitattributes", "r") 632 | for line in f.readlines(): 633 | if line.strip().startswith(versionfile_source): 634 | if "export-subst" in line.strip().split()[1:]: 635 | present = True 636 | f.close() 637 | except EnvironmentError: 638 | pass 639 | if not present: 640 | f = open(".gitattributes", "a+") 641 | f.write("%s export-subst\n" % versionfile_source) 642 | f.close() 643 | files.append(".gitattributes") 644 | run_command(GITS, ["add", "--"] + files) 645 | 646 | def versions_from_parentdir(parentdir_prefix, root, verbose=False): 647 | # Source tarballs conventionally unpack into a directory that includes 648 | # both the project name and a version string. 649 | dirname = os.path.basename(root) 650 | if not dirname.startswith(parentdir_prefix): 651 | if verbose: 652 | print("guessing rootdir is '%s', but '%s' doesn't start with " 653 | "prefix '%s'" % (root, dirname, parentdir_prefix)) 654 | return None 655 | return {"version": dirname[len(parentdir_prefix):], "full": ""} 656 | 657 | SHORT_VERSION_PY = """ 658 | # This file was generated by 'versioneer.py' (0.13) from 659 | # revision-control system data, or from the parent directory name of an 660 | # unpacked source archive. Distribution tarballs contain a pre-generated copy 661 | # of this file. 662 | 663 | version_version = '%(version)s' 664 | version_full = '%(full)s' 665 | def get_versions(default={}, verbose=False): 666 | return {'version': version_version, 'full': version_full} 667 | 668 | """ 669 | 670 | DEFAULT = {"version": "unknown", "full": "unknown"} 671 | 672 | def versions_from_file(filename): 673 | versions = {} 674 | try: 675 | with open(filename) as f: 676 | for line in f.readlines(): 677 | mo = re.match("version_version = '([^']+)'", line) 678 | if mo: 679 | versions["version"] = mo.group(1) 680 | mo = re.match("version_full = '([^']+)'", line) 681 | if mo: 682 | versions["full"] = mo.group(1) 683 | except EnvironmentError: 684 | return {} 685 | 686 | return versions 687 | 688 | def write_to_version_file(filename, versions): 689 | with open(filename, "w") as f: 690 | f.write(SHORT_VERSION_PY % versions) 691 | 692 | print("set %s to '%s'" % (filename, versions["version"])) 693 | 694 | 695 | def get_root(): 696 | try: 697 | return os.path.dirname(os.path.abspath(__file__)) 698 | except NameError: 699 | return os.path.dirname(os.path.abspath(sys.argv[0])) 700 | 701 | def vcs_function(vcs, suffix): 702 | return getattr(sys.modules[__name__], '%s_%s' % (vcs, suffix), None) 703 | 704 | def get_versions(default=DEFAULT, verbose=False): 705 | # returns dict with two keys: 'version' and 'full' 706 | assert versionfile_source is not None, "please set versioneer.versionfile_source" 707 | assert tag_prefix is not None, "please set versioneer.tag_prefix" 708 | assert parentdir_prefix is not None, "please set versioneer.parentdir_prefix" 709 | assert VCS is not None, "please set versioneer.VCS" 710 | 711 | # I am in versioneer.py, which must live at the top of the source tree, 712 | # which we use to compute the root directory. py2exe/bbfreeze/non-CPython 713 | # don't have __file__, in which case we fall back to sys.argv[0] (which 714 | # ought to be the setup.py script). We prefer __file__ since that's more 715 | # robust in cases where setup.py was invoked in some weird way (e.g. pip) 716 | root = get_root() 717 | versionfile_abs = os.path.join(root, versionfile_source) 718 | 719 | # extract version from first of _version.py, VCS command (e.g. 'git 720 | # describe'), parentdir. This is meant to work for developers using a 721 | # source checkout, for users of a tarball created by 'setup.py sdist', 722 | # and for users of a tarball/zipball created by 'git archive' or github's 723 | # download-from-tag feature or the equivalent in other VCSes. 724 | 725 | get_keywords_f = vcs_function(VCS, "get_keywords") 726 | versions_from_keywords_f = vcs_function(VCS, "versions_from_keywords") 727 | if get_keywords_f and versions_from_keywords_f: 728 | vcs_keywords = get_keywords_f(versionfile_abs) 729 | ver = versions_from_keywords_f(vcs_keywords, tag_prefix) 730 | if ver: 731 | if verbose: print("got version from expanded keyword %s" % ver) 732 | return ver 733 | 734 | ver = versions_from_file(versionfile_abs) 735 | if ver: 736 | if verbose: print("got version from file %s %s" % (versionfile_abs,ver)) 737 | return ver 738 | 739 | versions_from_vcs_f = vcs_function(VCS, "versions_from_vcs") 740 | if versions_from_vcs_f: 741 | ver = versions_from_vcs_f(tag_prefix, root, verbose) 742 | if ver: 743 | if verbose: print("got version from VCS %s" % ver) 744 | return ver 745 | 746 | ver = versions_from_parentdir(parentdir_prefix, root, verbose) 747 | if ver: 748 | if verbose: print("got version from parentdir %s" % ver) 749 | return ver 750 | 751 | if verbose: print("got version from default %s" % default) 752 | return default 753 | 754 | def get_version(verbose=False): 755 | return get_versions(verbose=verbose)["version"] 756 | 757 | class cmd_version(Command): 758 | description = "report generated version string" 759 | user_options = [] 760 | boolean_options = [] 761 | def initialize_options(self): 762 | pass 763 | def finalize_options(self): 764 | pass 765 | def run(self): 766 | ver = get_version(verbose=True) 767 | print("Version is currently: %s" % ver) 768 | 769 | 770 | class cmd_build(_build): 771 | def run(self): 772 | versions = get_versions(verbose=True) 773 | _build.run(self) 774 | # now locate _version.py in the new build/ directory and replace it 775 | # with an updated value 776 | if versionfile_build: 777 | target_versionfile = os.path.join(self.build_lib, versionfile_build) 778 | print("UPDATING %s" % target_versionfile) 779 | os.unlink(target_versionfile) 780 | with open(target_versionfile, "w") as f: 781 | f.write(SHORT_VERSION_PY % versions) 782 | 783 | if 'cx_Freeze' in sys.modules: # cx_freeze enabled? 784 | from cx_Freeze.dist import build_exe as _build_exe 785 | 786 | class cmd_build_exe(_build_exe): 787 | def run(self): 788 | versions = get_versions(verbose=True) 789 | target_versionfile = versionfile_source 790 | print("UPDATING %s" % target_versionfile) 791 | os.unlink(target_versionfile) 792 | with open(target_versionfile, "w") as f: 793 | f.write(SHORT_VERSION_PY % versions) 794 | 795 | _build_exe.run(self) 796 | os.unlink(target_versionfile) 797 | with open(versionfile_source, "w") as f: 798 | assert VCS is not None, "please set versioneer.VCS" 799 | LONG = LONG_VERSION_PY[VCS] 800 | f.write(LONG % {"DOLLAR": "$", 801 | "TAG_PREFIX": tag_prefix, 802 | "PARENTDIR_PREFIX": parentdir_prefix, 803 | "VERSIONFILE_SOURCE": versionfile_source, 804 | }) 805 | 806 | class cmd_sdist(_sdist): 807 | def run(self): 808 | versions = get_versions(verbose=True) 809 | self._versioneer_generated_versions = versions 810 | # unless we update this, the command will keep using the old version 811 | self.distribution.metadata.version = versions["version"] 812 | return _sdist.run(self) 813 | 814 | def make_release_tree(self, base_dir, files): 815 | _sdist.make_release_tree(self, base_dir, files) 816 | # now locate _version.py in the new base_dir directory (remembering 817 | # that it may be a hardlink) and replace it with an updated value 818 | target_versionfile = os.path.join(base_dir, versionfile_source) 819 | print("UPDATING %s" % target_versionfile) 820 | os.unlink(target_versionfile) 821 | with open(target_versionfile, "w") as f: 822 | f.write(SHORT_VERSION_PY % self._versioneer_generated_versions) 823 | 824 | INIT_PY_SNIPPET = """ 825 | from ._version import get_versions 826 | __version__ = get_versions()['version'] 827 | del get_versions 828 | """ 829 | 830 | class cmd_update_files(Command): 831 | description = "install/upgrade Versioneer files: __init__.py SRC/_version.py" 832 | user_options = [] 833 | boolean_options = [] 834 | def initialize_options(self): 835 | pass 836 | def finalize_options(self): 837 | pass 838 | def run(self): 839 | print(" creating %s" % versionfile_source) 840 | with open(versionfile_source, "w") as f: 841 | assert VCS is not None, "please set versioneer.VCS" 842 | LONG = LONG_VERSION_PY[VCS] 843 | f.write(LONG % {"DOLLAR": "$", 844 | "TAG_PREFIX": tag_prefix, 845 | "PARENTDIR_PREFIX": parentdir_prefix, 846 | "VERSIONFILE_SOURCE": versionfile_source, 847 | }) 848 | 849 | ipy = os.path.join(os.path.dirname(versionfile_source), "__init__.py") 850 | if os.path.exists(ipy): 851 | try: 852 | with open(ipy, "r") as f: 853 | old = f.read() 854 | except EnvironmentError: 855 | old = "" 856 | if INIT_PY_SNIPPET not in old: 857 | print(" appending to %s" % ipy) 858 | with open(ipy, "a") as f: 859 | f.write(INIT_PY_SNIPPET) 860 | else: 861 | print(" %s unmodified" % ipy) 862 | else: 863 | print(" %s doesn't exist, ok" % ipy) 864 | ipy = None 865 | 866 | # Make sure both the top-level "versioneer.py" and versionfile_source 867 | # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so 868 | # they'll be copied into source distributions. Pip won't be able to 869 | # install the package without this. 870 | manifest_in = os.path.join(get_root(), "MANIFEST.in") 871 | simple_includes = set() 872 | try: 873 | with open(manifest_in, "r") as f: 874 | for line in f: 875 | if line.startswith("include "): 876 | for include in line.split()[1:]: 877 | simple_includes.add(include) 878 | except EnvironmentError: 879 | pass 880 | # That doesn't cover everything MANIFEST.in can do 881 | # (http://docs.python.org/2/distutils/sourcedist.html#commands), so 882 | # it might give some false negatives. Appending redundant 'include' 883 | # lines is safe, though. 884 | if "versioneer.py" not in simple_includes: 885 | print(" appending 'versioneer.py' to MANIFEST.in") 886 | with open(manifest_in, "a") as f: 887 | f.write("include versioneer.py\n") 888 | else: 889 | print(" 'versioneer.py' already in MANIFEST.in") 890 | if versionfile_source not in simple_includes: 891 | print(" appending versionfile_source ('%s') to MANIFEST.in" % 892 | versionfile_source) 893 | with open(manifest_in, "a") as f: 894 | f.write("include %s\n" % versionfile_source) 895 | else: 896 | print(" versionfile_source already in MANIFEST.in") 897 | 898 | # Make VCS-specific changes. For git, this means creating/changing 899 | # .gitattributes to mark _version.py for export-time keyword 900 | # substitution. 901 | do_vcs_install(manifest_in, versionfile_source, ipy) 902 | 903 | def get_cmdclass(): 904 | cmds = {'version': cmd_version, 905 | 'versioneer': cmd_update_files, 906 | 'build': cmd_build, 907 | 'sdist': cmd_sdist, 908 | } 909 | if 'cx_Freeze' in sys.modules: # cx_freeze enabled? 910 | cmds['build_exe'] = cmd_build_exe 911 | del cmds['build'] 912 | 913 | return cmds 914 | --------------------------------------------------------------------------------