├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── requirements.txt ├── rqalpha_mod_incremental ├── __init__.py ├── _version.py ├── mod.py ├── persist_providers.py └── recorders.py ├── setup.cfg ├── setup.py └── versioneer.py /.gitignore: -------------------------------------------------------------------------------- 1 | /*.egg-info 2 | .cache/ 3 | dist/ 4 | .idea 5 | __pycache__ 6 | algoengine.iml 7 | rqalpha.system.log 8 | *.bcolz 9 | *.pk 10 | *.h5 11 | .vscode 12 | *.c 13 | *.so 14 | *.pyc 15 | build/ 16 | bundle/ 17 | bundle 18 | persist/ 19 | .coverage 20 | .tox 21 | rqalpha_test_coverage_report/ 22 | .tmp.pkl 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include versioneer.py 3 | include rqalpha_mod_incremental/_version.py 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | incremental Mod 3 | =============================== 4 | 5 | RQAlpha 增量运行 Mod 6 | 7 | 启用该 Mod 后,可以增量运行回测,方便长期跟踪策略而不必反复运行跑过的日期。 8 | 9 | 可以每天根据当日数据只运行当天的回测,节约计算资源,方便跟踪大量的策略。 10 | 11 | 目前自带两种持久化模式,可以通过 CsvRecorder 将状态持久化到磁盘上,或者通过 MongodbRecorder 将状态保存在数据库。 12 | 13 | 开启或关闭增量运行 Mod 14 | =============================== 15 | 16 | .. code-block:: bash 17 | 18 | # 关闭增量运行 Mod 19 | $ rqalpha mod disable incremental 20 | 21 | # 启用增量运行 Mod 22 | $ rqalpha mod enable incremental 23 | 24 | 模块配置项 25 | =============================== 26 | 27 | .. code-block:: python 28 | 29 | { 30 | "strategy_id": "1", 31 | # 是否启用 csv 保存 feeds 功能,可以设置为 MongodbRecorder 32 | "recorder": "CsvRecorder", 33 | # 持久化数据输出文件夹 34 | "persist_folder": None, 35 | # mongodb 36 | "mongo_url": "mongodb://localhost", 37 | "mongo_dbname": "rqalpha_records", 38 | } 39 | 40 | 运行 41 | =============================== 42 | 43 | 使用默认的文件持久化 44 | 45 | .. code-block:: bash 46 | 47 | rqalpha run -f ~/strategy.py -s 2017-09-01 -e 2017-10-01 --account stock 100000 -l verbose --persist-folder ~/strategy-persist/strategy-1/ 48 | # 接着上次运行继续增量运行回测 49 | # 此时传入的 account 信息会被持久化的数据覆盖 50 | rqalpha run -f ~/strategy.py -s 2017-10-02 -e 2017-11-01 --account stock 100000 -l verbose --persist-folder ~/strategy-persist/strategy-1/ 51 | 52 | 使用数据库持久化 53 | 54 | .. code-block:: bash 55 | 56 | rqalpha run -f ~/strategy.py -s 2017-10-16 -e 2017-10-20 --account stock 100000 --recorder MongodbRecorder --mongo-url mongodb://localhost --strategy-id 1 57 | # 接着上次运行继续增量运行回测 58 | # 此时传入的 account 信息会被持久化的数据覆盖 59 | rqalpha run -f ~/strategy.py -s 2017-10-21 -e 2017-10-30 --account stock 100000 --recorder MongodbRecorder --mongo-url mongodb://localhost --strategy-id 1 60 | 61 | 数据分析 62 | =============================== 63 | 64 | 读取 csv 65 | 66 | .. code-block:: python 67 | 68 | import pandas as pd 69 | 70 | portfolio_df = pd.read_csv("~/strategy-persist/strategy-1/portfolio.csv") 71 | bm_portfolio_df = pd.read_csv("~/strategy-persist/strategy-1/bm_portfolio.csv") 72 | trade_df = pd.read_csv("~/strategy-persist/strategy-1/trade.csv") 73 | 74 | 读取 mongodb 75 | 76 | .. code-block:: python 77 | 78 | import pandas as pd 79 | import pymongo 80 | 81 | db = pymongo.MongoClient(mongo_url)["rqalpha_records"] 82 | pd.DataFrame(db["portfolio"].find_one({"strategy_id": '1'}, {"data": 1, "_id": 0})["data"]) 83 | pd.DataFrame(list(db["trade"].find({"strategy_id": '1'}, {"_id": 0}))) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | rqalpha>=5.3.5 -------------------------------------------------------------------------------- /rqalpha_mod_incremental/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Ricequant, Inc 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | """ 17 | 18 | 在策略中 设置config['mod']["incremental"]控制债券mod配置 19 | 20 | .. code-block:: python 21 | 22 | __config__ = { 23 | # 持久化文件夹路径,不设置则不启用mod 24 | "persist_folder": None, 25 | # 策略id,持久化的回测将保存在以strategy_id命名的文件夹下 26 | "strategy_id": 1, 27 | } 28 | """ 29 | 30 | import click 31 | from rqalpha import cli 32 | 33 | from ._version import get_versions 34 | __version__ = get_versions()['version'] 35 | del get_versions 36 | 37 | 38 | __config__ = { 39 | # 是否启用 csv 保存 feeds 功能,可以设置为 MongodbRecorder 40 | "recorder": "CsvRecorder", 41 | # 当设置为 CsvRecorder 的时候使用,持久化数据输出文件夹 42 | "persist_folder": None, 43 | # 当设置为 MongodbRecorder 的时候使用 44 | "strategy_id": 1, 45 | "mongo_url": None, 46 | "mongo_dbname": "rqalpha_records", 47 | "priority": 111, 48 | } 49 | 50 | 51 | def load_mod(): 52 | from .mod import IncrementalMod 53 | return IncrementalMod() 54 | 55 | 56 | cli_prefix = "mod__incremental__" 57 | 58 | cli.commands['run'].params.append( 59 | click.Option( 60 | ("--persist-folder", cli_prefix + "persist_folder"), 61 | help="[incremental] persist folder" 62 | ) 63 | ) 64 | 65 | cli.commands['run'].params.append( 66 | click.Option( 67 | ("--strategy-id", cli_prefix + "strategy_id"), 68 | help="[incremental] strategy id " 69 | ) 70 | ) 71 | 72 | cli.commands['run'].params.append( 73 | click.Option( 74 | ("--recorder", cli_prefix + "recorder"), 75 | type=click.Choice(["CsvRecorder", "MongodbRecorder"]), 76 | help="[incremental] recorder name" 77 | ) 78 | ) 79 | 80 | cli.commands['run'].params.append( 81 | click.Option( 82 | ("--mongo-url", cli_prefix + "mongo_url"), 83 | help="[incremental] recorder mongo url" 84 | ) 85 | ) 86 | 87 | -------------------------------------------------------------------------------- /rqalpha_mod_incremental/_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.18 (https://github.com/warner/python-versioneer) 10 | 11 | """Git implementation of _version.py.""" 12 | 13 | import errno 14 | import os 15 | import re 16 | import subprocess 17 | import sys 18 | 19 | 20 | def get_keywords(): 21 | """Get the keywords needed to look up the version information.""" 22 | # these strings will be replaced by git during git-archive. 23 | # setup.py/versioneer.py will grep for the variable names, so they must 24 | # each be defined on a line of their own. _version.py will just call 25 | # get_keywords(). 26 | git_refnames = "$Format:%d$" 27 | git_full = "$Format:%H$" 28 | git_date = "$Format:%ci$" 29 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 30 | return keywords 31 | 32 | 33 | class VersioneerConfig: 34 | """Container for Versioneer configuration parameters.""" 35 | 36 | 37 | def get_config(): 38 | """Create, populate and return the VersioneerConfig() object.""" 39 | # these strings are filled in when 'setup.py versioneer' creates 40 | # _version.py 41 | cfg = VersioneerConfig() 42 | cfg.VCS = "git" 43 | cfg.style = "pep440-ricequant" 44 | cfg.tag_prefix = "" 45 | cfg.parentdir_prefix = "rqsdk_auth" 46 | cfg.versionfile_source = "rqsdk_auth/_version.py" 47 | cfg.verbose = False 48 | return cfg 49 | 50 | 51 | class NotThisMethod(Exception): 52 | """Exception raised if a method is not valid for the current scenario.""" 53 | 54 | 55 | LONG_VERSION_PY = {} 56 | HANDLERS = {} 57 | 58 | 59 | def register_vcs_handler(vcs, method): # decorator 60 | """Decorator to mark a method as the handler for a particular VCS.""" 61 | def decorate(f): 62 | """Store f in HANDLERS[vcs][method].""" 63 | if vcs not in HANDLERS: 64 | HANDLERS[vcs] = {} 65 | HANDLERS[vcs][method] = f 66 | return f 67 | return decorate 68 | 69 | 70 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 71 | env=None): 72 | """Call the given command(s).""" 73 | assert isinstance(commands, list) 74 | p = None 75 | for c in commands: 76 | try: 77 | dispcmd = str([c] + args) 78 | # remember shell=False, so use git.cmd on windows, not just git 79 | p = subprocess.Popen([c] + args, cwd=cwd, env=env, 80 | stdout=subprocess.PIPE, 81 | stderr=(subprocess.PIPE if hide_stderr 82 | else None)) 83 | break 84 | except EnvironmentError: 85 | e = sys.exc_info()[1] 86 | if e.errno == errno.ENOENT: 87 | continue 88 | if verbose: 89 | print("unable to run %s" % dispcmd) 90 | print(e) 91 | return None, None 92 | else: 93 | if verbose: 94 | print("unable to find command, tried %s" % (commands,)) 95 | return None, None 96 | stdout = p.communicate()[0].strip() 97 | if sys.version_info[0] >= 3: 98 | stdout = stdout.decode() 99 | if p.returncode != 0: 100 | if verbose: 101 | print("unable to run %s (error)" % dispcmd) 102 | print("stdout was %s" % stdout) 103 | return None, p.returncode 104 | return stdout, p.returncode 105 | 106 | 107 | def versions_from_parentdir(parentdir_prefix, root, verbose): 108 | """Try to determine the version from the parent directory name. 109 | 110 | Source tarballs conventionally unpack into a directory that includes both 111 | the project name and a version string. We will also support searching up 112 | two directory levels for an appropriately named parent directory 113 | """ 114 | rootdirs = [] 115 | 116 | for i in range(3): 117 | dirname = os.path.basename(root) 118 | if dirname.startswith(parentdir_prefix): 119 | return {"version": dirname[len(parentdir_prefix):], 120 | "full-revisionid": None, 121 | "dirty": False, "error": None, "date": None} 122 | else: 123 | rootdirs.append(root) 124 | root = os.path.dirname(root) # up a level 125 | 126 | if verbose: 127 | print("Tried directories %s but none started with prefix %s" % 128 | (str(rootdirs), parentdir_prefix)) 129 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 130 | 131 | 132 | @register_vcs_handler("git", "get_keywords") 133 | def git_get_keywords(versionfile_abs): 134 | """Extract version information from the given file.""" 135 | # the code embedded in _version.py can just fetch the value of these 136 | # keywords. When used from setup.py, we don't want to import _version.py, 137 | # so we do it with a regexp instead. This function is not used from 138 | # _version.py. 139 | keywords = {} 140 | try: 141 | f = open(versionfile_abs, "r") 142 | for line in f.readlines(): 143 | if line.strip().startswith("git_refnames ="): 144 | mo = re.search(r'=\s*"(.*)"', line) 145 | if mo: 146 | keywords["refnames"] = mo.group(1) 147 | if line.strip().startswith("git_full ="): 148 | mo = re.search(r'=\s*"(.*)"', line) 149 | if mo: 150 | keywords["full"] = mo.group(1) 151 | if line.strip().startswith("git_date ="): 152 | mo = re.search(r'=\s*"(.*)"', line) 153 | if mo: 154 | keywords["date"] = mo.group(1) 155 | f.close() 156 | except EnvironmentError: 157 | pass 158 | return keywords 159 | 160 | 161 | @register_vcs_handler("git", "keywords") 162 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 163 | """Get version information from git keywords.""" 164 | if not keywords: 165 | raise NotThisMethod("no keywords at all, weird") 166 | date = keywords.get("date") 167 | if date is not None: 168 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 169 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 170 | # -like" string, which we must then edit to make compliant), because 171 | # it's been around since git-1.5.3, and it's too difficult to 172 | # discover which version we're using, or to work around using an 173 | # older one. 174 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 175 | refnames = keywords["refnames"].strip() 176 | if refnames.startswith("$Format"): 177 | if verbose: 178 | print("keywords are unexpanded, not using") 179 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 180 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 181 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 182 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 183 | TAG = "tag: " 184 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 185 | if not tags: 186 | # Either we're using git < 1.8.3, or there really are no tags. We use 187 | # a heuristic: assume all version tags have a digit. The old git %d 188 | # expansion behaves like git log --decorate=short and strips out the 189 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 190 | # between branches and tags. By ignoring refnames without digits, we 191 | # filter out many common branch names like "release" and 192 | # "stabilization", as well as "HEAD" and "master". 193 | tags = set([r for r in refs if re.search(r'\d', r)]) 194 | if verbose: 195 | print("discarding '%s', no digits" % ",".join(refs - tags)) 196 | if verbose: 197 | print("likely tags: %s" % ",".join(sorted(tags))) 198 | for ref in sorted(tags): 199 | # sorting will prefer e.g. "2.0" over "2.0rc1" 200 | if ref.startswith(tag_prefix): 201 | r = ref[len(tag_prefix):] 202 | if verbose: 203 | print("picking %s" % r) 204 | return {"version": r, 205 | "full-revisionid": keywords["full"].strip(), 206 | "dirty": False, "error": None, 207 | "date": date} 208 | # no suitable tags, so version is "0+unknown", but full hex is still there 209 | if verbose: 210 | print("no suitable tags, using unknown + full revision id") 211 | return {"version": "0+unknown", 212 | "full-revisionid": keywords["full"].strip(), 213 | "dirty": False, "error": "no suitable tags", "date": None} 214 | 215 | 216 | @register_vcs_handler("git", "pieces_from_vcs") 217 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 218 | """Get version from 'git describe' in the root of the source tree. 219 | 220 | This only gets called if the git-archive 'subst' keywords were *not* 221 | expanded, and _version.py hasn't already been rewritten with a short 222 | version string, meaning we're inside a checked out source tree. 223 | """ 224 | GITS = ["git"] 225 | if sys.platform == "win32": 226 | GITS = ["git.cmd", "git.exe"] 227 | 228 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, 229 | hide_stderr=True) 230 | if rc != 0: 231 | if verbose: 232 | print("Directory %s not under git control" % root) 233 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 234 | 235 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 236 | # if there isn't one, this yields HEX[-dirty] (no NUM) 237 | describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", 238 | "--always", "--long", 239 | "--match", "%s*" % tag_prefix], 240 | cwd=root) 241 | # --long was added in git-1.5.5 242 | if describe_out is None: 243 | raise NotThisMethod("'git describe' failed") 244 | describe_out = describe_out.strip() 245 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 246 | if full_out is None: 247 | raise NotThisMethod("'git rev-parse' failed") 248 | full_out = full_out.strip() 249 | 250 | pieces = {} 251 | pieces["long"] = full_out 252 | pieces["short"] = full_out[:7] # maybe improved later 253 | pieces["error"] = None 254 | 255 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 256 | # TAG might have hyphens. 257 | git_describe = describe_out 258 | 259 | # look for -dirty suffix 260 | dirty = git_describe.endswith("-dirty") 261 | pieces["dirty"] = dirty 262 | if dirty: 263 | git_describe = git_describe[:git_describe.rindex("-dirty")] 264 | 265 | # now we have TAG-NUM-gHEX or HEX 266 | 267 | if "-" in git_describe: 268 | # TAG-NUM-gHEX 269 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 270 | if not mo: 271 | # unparseable. Maybe git-describe is misbehaving? 272 | pieces["error"] = ("unable to parse git-describe output: '%s'" 273 | % describe_out) 274 | return pieces 275 | 276 | # tag 277 | full_tag = mo.group(1) 278 | if not full_tag.startswith(tag_prefix): 279 | if verbose: 280 | fmt = "tag '%s' doesn't start with prefix '%s'" 281 | print(fmt % (full_tag, tag_prefix)) 282 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 283 | % (full_tag, tag_prefix)) 284 | return pieces 285 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 286 | 287 | # distance: number of commits since tag 288 | pieces["distance"] = int(mo.group(2)) 289 | 290 | # commit: short hex revision ID 291 | pieces["short"] = mo.group(3) 292 | 293 | else: 294 | # HEX: no tags 295 | pieces["closest-tag"] = None 296 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], 297 | cwd=root) 298 | pieces["distance"] = int(count_out) # total number of commits 299 | 300 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 301 | date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], 302 | cwd=root)[0].strip() 303 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 304 | 305 | return pieces 306 | 307 | 308 | def plus_or_dot(pieces): 309 | """Return a + if we don't already have one, else return a .""" 310 | if "+" in pieces.get("closest-tag", ""): 311 | return "." 312 | return "+" 313 | 314 | 315 | def render_pep440(pieces): 316 | """Build up version string, with post-release "local version identifier". 317 | 318 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 319 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 320 | 321 | Exceptions: 322 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 323 | """ 324 | if pieces["closest-tag"]: 325 | rendered = pieces["closest-tag"] 326 | if pieces["distance"] or pieces["dirty"]: 327 | rendered += plus_or_dot(pieces) 328 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 329 | if pieces["dirty"]: 330 | rendered += ".dirty" 331 | else: 332 | # exception #1 333 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 334 | pieces["short"]) 335 | if pieces["dirty"]: 336 | rendered += ".dirty" 337 | return rendered 338 | 339 | 340 | def render_pep440_pre(pieces): 341 | """TAG[.post.devDISTANCE] -- No -dirty. 342 | 343 | Exceptions: 344 | 1: no tags. 0.post.devDISTANCE 345 | """ 346 | if pieces["closest-tag"]: 347 | rendered = pieces["closest-tag"] 348 | if pieces["distance"]: 349 | rendered += ".post.dev%d" % pieces["distance"] 350 | else: 351 | # exception #1 352 | rendered = "0.post.dev%d" % pieces["distance"] 353 | return rendered 354 | 355 | 356 | def render_pep440_post(pieces): 357 | """TAG[.postDISTANCE[.dev0]+gHEX] . 358 | 359 | The ".dev0" means dirty. Note that .dev0 sorts backwards 360 | (a dirty tree will appear "older" than the corresponding clean one), 361 | but you shouldn't be releasing software with -dirty anyways. 362 | 363 | Exceptions: 364 | 1: no tags. 0.postDISTANCE[.dev0] 365 | """ 366 | if pieces["closest-tag"]: 367 | rendered = pieces["closest-tag"] 368 | if pieces["distance"] or pieces["dirty"]: 369 | rendered += ".post%d" % pieces["distance"] 370 | if pieces["dirty"]: 371 | rendered += ".dev0" 372 | rendered += plus_or_dot(pieces) 373 | rendered += "g%s" % pieces["short"] 374 | else: 375 | # exception #1 376 | rendered = "0.post%d" % pieces["distance"] 377 | if pieces["dirty"]: 378 | rendered += ".dev0" 379 | rendered += "+g%s" % pieces["short"] 380 | return rendered 381 | 382 | 383 | def render_pep440_ricequant(pieces): 384 | """TAG[.devDISTANCE.gHEX[.dirty]] . 385 | 386 | Exceptions: 387 | 1: no tags. 0.devDISTANCE.gHEX[.dirty] 388 | """ 389 | if pieces["closest-tag"]: 390 | rendered = pieces["closest-tag"] 391 | if pieces["distance"] or pieces["dirty"]: 392 | rendered += ".dev%d" % pieces["distance"] 393 | rendered += ".g%s" % pieces["short"] 394 | else: 395 | # exception #1 396 | rendered = "0.dev%d" % pieces["distance"] 397 | rendered += ".g%s" % pieces["short"] 398 | 399 | if pieces["dirty"]: 400 | rendered += ".dirty" 401 | 402 | return rendered 403 | 404 | 405 | def render_pep440_old(pieces): 406 | """TAG[.postDISTANCE[.dev0]] . 407 | 408 | The ".dev0" means dirty. 409 | 410 | Eexceptions: 411 | 1: no tags. 0.postDISTANCE[.dev0] 412 | """ 413 | if pieces["closest-tag"]: 414 | rendered = pieces["closest-tag"] 415 | if pieces["distance"] or pieces["dirty"]: 416 | rendered += ".post%d" % pieces["distance"] 417 | if pieces["dirty"]: 418 | rendered += ".dev0" 419 | else: 420 | # exception #1 421 | rendered = "0.post%d" % pieces["distance"] 422 | if pieces["dirty"]: 423 | rendered += ".dev0" 424 | return rendered 425 | 426 | 427 | def render_git_describe(pieces): 428 | """TAG[-DISTANCE-gHEX][-dirty]. 429 | 430 | Like 'git describe --tags --dirty --always'. 431 | 432 | Exceptions: 433 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 434 | """ 435 | if pieces["closest-tag"]: 436 | rendered = pieces["closest-tag"] 437 | if pieces["distance"]: 438 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 439 | else: 440 | # exception #1 441 | rendered = pieces["short"] 442 | if pieces["dirty"]: 443 | rendered += "-dirty" 444 | return rendered 445 | 446 | 447 | def render_git_describe_long(pieces): 448 | """TAG-DISTANCE-gHEX[-dirty]. 449 | 450 | Like 'git describe --tags --dirty --always -long'. 451 | The distance/hash is unconditional. 452 | 453 | Exceptions: 454 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 455 | """ 456 | if pieces["closest-tag"]: 457 | rendered = pieces["closest-tag"] 458 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 459 | else: 460 | # exception #1 461 | rendered = pieces["short"] 462 | if pieces["dirty"]: 463 | rendered += "-dirty" 464 | return rendered 465 | 466 | 467 | def render(pieces, style): 468 | """Render the given version pieces into the requested style.""" 469 | if pieces["error"]: 470 | return {"version": "unknown", 471 | "full-revisionid": pieces.get("long"), 472 | "dirty": None, 473 | "error": pieces["error"], 474 | "date": None} 475 | 476 | if not style or style == "default": 477 | style = "pep440" # the default 478 | 479 | if style == "pep440": 480 | rendered = render_pep440(pieces) 481 | elif style == "pep440-pre": 482 | rendered = render_pep440_pre(pieces) 483 | elif style == "pep440-post": 484 | rendered = render_pep440_post(pieces) 485 | elif style == "pep440-old": 486 | rendered = render_pep440_old(pieces) 487 | elif style == 'pep440-ricequant': 488 | rendered = render_pep440_ricequant(pieces) 489 | elif style == "git-describe": 490 | rendered = render_git_describe(pieces) 491 | elif style == "git-describe-long": 492 | rendered = render_git_describe_long(pieces) 493 | else: 494 | raise ValueError("unknown style '%s'" % style) 495 | 496 | return {"version": rendered, "full-revisionid": pieces["long"], 497 | "dirty": pieces["dirty"], "error": None, 498 | "date": pieces.get("date")} 499 | 500 | 501 | def get_versions(): 502 | """Get version information or return default if unable to do so.""" 503 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 504 | # __file__, we can work backwards from there to the root. Some 505 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 506 | # case we can only use expanded keywords. 507 | 508 | cfg = get_config() 509 | verbose = cfg.verbose 510 | 511 | try: 512 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 513 | verbose) 514 | except NotThisMethod: 515 | pass 516 | 517 | try: 518 | root = os.path.realpath(__file__) 519 | # versionfile_source is the relative path from the top of the source 520 | # tree (where the .git directory might live) to this file. Invert 521 | # this to find the root from __file__. 522 | for i in cfg.versionfile_source.split('/'): 523 | root = os.path.dirname(root) 524 | except NameError: 525 | return {"version": "0+unknown", "full-revisionid": None, 526 | "dirty": None, 527 | "error": "unable to find root of source tree", 528 | "date": None} 529 | 530 | try: 531 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 532 | return render(pieces, cfg.style) 533 | except NotThisMethod: 534 | pass 535 | 536 | try: 537 | if cfg.parentdir_prefix: 538 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 539 | except NotThisMethod: 540 | pass 541 | 542 | return {"version": "0+unknown", "full-revisionid": None, 543 | "dirty": None, 544 | "error": "unable to compute version", "date": None} 545 | -------------------------------------------------------------------------------- /rqalpha_mod_incremental/mod.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Ricequant, Inc 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | import os 17 | import datetime 18 | 19 | from rqalpha.utils.logger import system_log 20 | from rqalpha.interface import AbstractMod 21 | from rqalpha.const import PERSIST_MODE, DEFAULT_ACCOUNT_TYPE 22 | from rqalpha_mod_incremental.persist_providers import DiskPersistProvider 23 | from rqalpha.utils.i18n import gettext as _ 24 | from rqalpha.data.base_data_source import BaseDataSource 25 | 26 | from rqalpha_mod_incremental import persist_providers, recorders 27 | from rqalpha.core.events import EVENT 28 | 29 | 30 | class IncrementalMod(AbstractMod): 31 | def __init__(self): 32 | self._env = None 33 | self._start_date = None 34 | self._end_date = None 35 | self._event_start_time = None 36 | # 上一次回测时的最后交易日期 37 | self._last_end_date = None 38 | 39 | def start_up(self, env, mod_config): 40 | self._env = env 41 | self._recorder = None 42 | self._mod_config = mod_config 43 | 44 | if not self._mod_config.persist_folder: 45 | return 46 | 47 | config = self._env.config 48 | if not env.data_source: 49 | env.set_data_source(BaseDataSource( 50 | config.base.data_bundle_path, 51 | getattr(config.base, "future_info", {}), 52 | DEFAULT_ACCOUNT_TYPE.FUTURE in env.config.base.accounts and env.config.base.futures_time_series_trading_parameters, 53 | env.config.base.end_date 54 | )) 55 | 56 | self._set_env_and_data_source() 57 | 58 | env.config.base.persist = True 59 | env.config.base.persist_mode = PERSIST_MODE.ON_NORMAL_EXIT 60 | 61 | env.event_bus.add_listener(EVENT.POST_SYSTEM_INIT, self._init) 62 | 63 | def _set_env_and_data_source(self): 64 | env = self._env 65 | mod_config = self._mod_config 66 | system_log.info("use recorder {}", mod_config.recorder) 67 | if mod_config.recorder == "CsvRecorder": 68 | if not mod_config.persist_folder: 69 | raise RuntimeError(_(u"You need to set persist_folder to use CsvRecorder")) 70 | persist_folder = os.path.join(mod_config.persist_folder, "persist", str(mod_config.strategy_id)) 71 | persist_provider = DiskPersistProvider(persist_folder) 72 | self._recorder = recorders.CsvRecorder(persist_folder) 73 | elif mod_config.recorder == "MongodbRecorder": 74 | if mod_config.strategy_id is None: 75 | raise RuntimeError(_(u"You need to set strategy_id")) 76 | persist_provider = persist_providers.MongodbPersistProvider(mod_config.strategy_id, mod_config.mongo_url, 77 | mod_config.mongo_dbname) 78 | self._recorder = recorders.MongodbRecorder(mod_config.strategy_id, 79 | mod_config.mongo_url, mod_config.mongo_dbname) 80 | else: 81 | raise RuntimeError(_(u"unknown recorder {}").format(mod_config.recorder)) 82 | 83 | if env.persist_provider is None: 84 | env.set_persist_provider(persist_provider) 85 | 86 | self._meta = { 87 | "strategy_id": mod_config.strategy_id, 88 | "origin_start_date": self._env.config.base.start_date.strftime("%Y-%m-%d"), 89 | "start_date": self._env.config.base.start_date.strftime("%Y-%m-%d"), 90 | "end_date": self._env.config.base.end_date.strftime("%Y-%m-%d"), 91 | "last_end_time": self._env.config.base.end_date.strftime("%Y-%m-%d"), 92 | "last_run_time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 93 | } 94 | 95 | event_start_time = self._env.config.base.start_date 96 | persist_meta = self._recorder.load_meta() 97 | if persist_meta: 98 | # 不修改回测开始时间 99 | self._env.config.base.start_date = datetime.datetime.strptime(persist_meta['start_date'], '%Y-%m-%d').date() 100 | event_start_time = datetime.datetime.strptime(persist_meta['last_end_time'], '%Y-%m-%d').date() + datetime.timedelta(days=1) 101 | # 代表历史有运行过,根据历史上次运行的end_date下一天设为事件发送的start_time 102 | self._meta["origin_start_date"] = persist_meta["origin_start_date"] 103 | self._meta["start_date"] = persist_meta["start_date"] 104 | if self._meta["last_end_time"] <= persist_meta["last_end_time"]: 105 | raise ValueError('The end_date should after end_date({}) last time'.format(persist_meta["last_end_time"])) 106 | self._last_end_date = datetime.datetime.strptime(persist_meta["last_end_time"], "%Y-%m-%d").date() 107 | self._event_start_time = event_start_time 108 | self._overwrite_event_data_source_func() 109 | 110 | def _overwrite_event_data_source_func(self): 111 | self._env.data_source.available_data_range = self._available_data_range 112 | self._env.event_source.events = self._events_decorator(self._env.event_source.events) 113 | 114 | def _available_data_range(self, frequency): 115 | return self._env.config.base.start_date, datetime.date.max 116 | 117 | def _events_decorator(self, original_events): 118 | def events(_, __, frequency): 119 | s, e = self._env.data_source.available_data_range(frequency) 120 | config_end_date = self._env.config.base.end_date 121 | event_end_date = e if e < config_end_date else config_end_date 122 | start_date, end_date = self._event_start_time, event_end_date 123 | yield from original_events(start_date, end_date, frequency) 124 | 125 | return events 126 | 127 | def _init(self, event): 128 | env = self._env 129 | env.event_bus.add_listener(EVENT.TRADE, self.on_trade) 130 | env.event_bus.prepend_listener(EVENT.POST_SETTLEMENT, self.on_settlement) 131 | env.event_bus.add_listener(EVENT.BEFORE_SYSTEM_RESTORED, self.on_before_system_restored) 132 | 133 | def on_before_system_restored(self, event): 134 | # 此时end_date已经经过调整,重新保存 135 | self._meta["last_end_time"] = self._env.config.base.end_date.strftime("%Y-%m-%d") 136 | if self._last_end_date: 137 | # 将日期调整为上一个区间的结束日期的收盘时间段 138 | _datetime = datetime.datetime.combine(self._last_end_date, datetime.time(hour=15, minute=30, second=0)) 139 | self._env.update_time(_datetime, _datetime) 140 | 141 | def on_trade(self, event): 142 | pass 143 | 144 | def on_settlement(self, event): 145 | """ 146 | 当本轮回测当前交易日期大于上一轮回测的最后交易日期时,才允许执行结算事件。 147 | 回测结算是放在before_trading中,在增量回测的过程中会导致出现临界点结算两次。 148 | """ 149 | if not self._last_end_date or self._env.trading_dt.date() > self._last_end_date: 150 | return False 151 | return True 152 | 153 | def tear_down(self, success, exception=None): 154 | if not self._mod_config.persist_folder: 155 | return 156 | if exception is None: 157 | self._recorder.store_meta(self._meta) 158 | self._recorder.flush() 159 | self._recorder.close() 160 | -------------------------------------------------------------------------------- /rqalpha_mod_incremental/persist_providers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import datetime 3 | 4 | from rqalpha.interface import AbstractPersistProvider 5 | 6 | 7 | class MongodbPersistProvider(AbstractPersistProvider): 8 | def __init__(self, strategy_id, mongo_url, mongo_db): 9 | import pymongo 10 | import gridfs 11 | 12 | persist_db = pymongo.MongoClient(mongo_url)[mongo_db] 13 | self._strategy_id = strategy_id 14 | self._fs = gridfs.GridFS(persist_db) 15 | 16 | def store(self, key, value): 17 | update_time = datetime.datetime.now() 18 | self._fs.put(value, strategy_id=self._strategy_id, key=key, update_time=update_time) 19 | for grid_out in self._fs.find({"strategy_id": self._strategy_id, "key": key, "update_time": {"$lt": update_time}}): 20 | self._fs.delete(grid_out._id) 21 | 22 | def load(self, key, large_file=False): 23 | import gridfs 24 | try: 25 | b = self._fs.get_last_version(strategy_id=self._strategy_id, key=key) 26 | return b.read() 27 | except gridfs.errors.NoFile: 28 | return None 29 | 30 | 31 | class DiskPersistProvider(AbstractPersistProvider): 32 | def __init__(self, path="./persist"): 33 | self._path = path 34 | try: 35 | os.makedirs(self._path) 36 | except: 37 | pass 38 | 39 | def store(self, key, value): 40 | assert isinstance(value, bytes), "value must be bytes" 41 | with open(os.path.join(self._path, key), "wb") as f: 42 | f.write(value) 43 | 44 | def load(self, key, large_file=False): 45 | try: 46 | with open(os.path.join(self._path, key), "rb") as f: 47 | return f.read() 48 | except IOError as e: 49 | return None 50 | 51 | def should_resume(self): 52 | return False 53 | 54 | def should_run_init(self): 55 | return False 56 | -------------------------------------------------------------------------------- /rqalpha_mod_incremental/recorders.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2017 Ricequant, Inc 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import abc 18 | import csv 19 | import os 20 | import json 21 | from collections import defaultdict 22 | 23 | from six import with_metaclass 24 | 25 | 26 | class Recorder(with_metaclass(abc.ABCMeta)): 27 | ''' 28 | 存储 trade 和 投资组合收益,根据 trade 可以恢复出 positions ,所以不需要存储 positions 29 | ''' 30 | @abc.abstractmethod 31 | def load_meta(self): 32 | raise NotImplementedError 33 | 34 | @abc.abstractmethod 35 | def store_meta(self, meta_dict): 36 | raise NotImplementedError 37 | 38 | @abc.abstractmethod 39 | def append_trade(self, trade): 40 | raise NotImplementedError 41 | 42 | @abc.abstractmethod 43 | def append_portfolio(self, dt, portfolio, benchmark_portfolio): 44 | raise NotImplementedError 45 | 46 | def flush(self): 47 | pass 48 | 49 | def close(self): 50 | pass 51 | 52 | 53 | class CsvRecorder(Recorder): 54 | 55 | TRADE_CSV_HEADER = [ 56 | "exec_id", 57 | "order_id", 58 | "order_book_id", 59 | "datetime", 60 | "last_price", 61 | "last_quantity", 62 | "transaction_cost", 63 | "side", 64 | "position_effect", 65 | ] 66 | 67 | PORTFOLIO_CSV_HEADER = [ 68 | "datetime", 69 | "portfolio_value", 70 | "market_value", 71 | "cash", 72 | "daily_pnl", 73 | "daily_returns", 74 | "total_returns", 75 | ] 76 | 77 | def __init__(self, folder): 78 | self._meta_json_path = os.path.join(folder, "meta.json") 79 | self._file_list = [] 80 | self._pending_tasks = [] 81 | 82 | def load_meta(self): 83 | if os.path.exists(self._meta_json_path): 84 | with open(self._meta_json_path, "r") as json_file: 85 | persist_meta = json.load(json_file) 86 | return persist_meta 87 | return None 88 | 89 | def store_meta(self, meta_dict): 90 | with open(self._meta_json_path, "w") as json_file: 91 | json_file.write(json.dumps(meta_dict)) 92 | 93 | def _create_writer(self, path, header): 94 | is_file_exist = os.path.exists(path) 95 | csv_file = open(path, "a") 96 | self._file_list.append(csv_file) 97 | writer = csv.DictWriter(csv_file, fieldnames=header) 98 | if not is_file_exist: 99 | writer.writeheader() 100 | 101 | return writer 102 | 103 | def append_trade(self, trade): 104 | self._pending_tasks.append((self._trade_writer, {key: getattr(trade, key) for key in self.TRADE_CSV_HEADER})) 105 | 106 | def _portfolio2dict(self, dt, portfolio): 107 | dic = {key: getattr(portfolio, key) for key in self.PORTFOLIO_CSV_HEADER if key != "datetime"} 108 | dic["datetime"] = dt 109 | return dic 110 | 111 | def append_portfolio(self, dt, portfolio, benchmark_portfolio): 112 | self._pending_tasks.append((self._portfolio_writer, self._portfolio2dict(dt, portfolio))) 113 | if benchmark_portfolio is not None: 114 | self._pending_tasks.append((self._bm_portfolio_writer, self._portfolio2dict(dt, benchmark_portfolio))) 115 | 116 | def flush(self): 117 | # TODO: use writerows 118 | for writer, data in self._pending_tasks: 119 | writer.writerow(data) 120 | 121 | def close(self): 122 | for csv_file in self._file_list: 123 | csv_file.close() 124 | 125 | 126 | class MongodbRecorder(Recorder): 127 | TRADE_CSV_HEADER = [ 128 | "exec_id", 129 | "order_id", 130 | "order_book_id", 131 | "datetime", 132 | "last_price", 133 | "last_quantity", 134 | "transaction_cost", 135 | "side", 136 | "position_effect", 137 | ] 138 | 139 | PORTFOLIO_CSV_HEADER = [ 140 | "datetime", 141 | "portfolio_value", 142 | "market_value", 143 | "cash", 144 | "daily_pnl", 145 | "daily_returns", 146 | "total_returns", 147 | ] 148 | 149 | def __init__(self, strategy_id, mongo_url, mongo_dbname): 150 | try: 151 | import pymongo 152 | except ImportError: 153 | raise RuntimeError(u"Missing pymongo, you need to install it by `pip install pymongo`") 154 | 155 | self._client = pymongo.MongoClient(mongo_url) 156 | self._db = self._client[mongo_dbname] 157 | self._strategy_id = strategy_id 158 | 159 | self._trade_list = [] 160 | self._portfolios_dict = defaultdict(list) 161 | 162 | def load_meta(self): 163 | return self._db["meta"].find_one({"strategy_id": self._strategy_id}) 164 | 165 | def store_meta(self, meta_dict): 166 | self._db["meta"].update({"strategy_id": self._strategy_id}, meta_dict, upsert=True) 167 | 168 | def _portfolio2dict(self, dt, portfolio): 169 | dic = {key: getattr(portfolio, key) for key in self.PORTFOLIO_CSV_HEADER if key != "datetime"} 170 | dic["datetime"] = dt 171 | return dic 172 | 173 | def append_trade(self, trade): 174 | trade_dict = {key: getattr(trade, key) for key in self.TRADE_CSV_HEADER} 175 | trade_dict["strategy_id"] = self._strategy_id 176 | trade_dict["side"] = str(trade_dict["side"]) 177 | trade_dict["position_effect"] = str(trade_dict["position_effect"]) 178 | self._trade_list.append(trade_dict) 179 | 180 | def append_portfolio(self, dt, portfolio, benchmark_portfolio): 181 | self._portfolios_dict["portfolio"].append(self._portfolio2dict(dt, portfolio)) 182 | if benchmark_portfolio is not None: 183 | self._portfolios_dict["bm_portfolio"].append(self._portfolio2dict(dt, benchmark_portfolio)) 184 | 185 | def flush(self): 186 | if self._trade_list: 187 | self._db["trade"].insert_many(self._trade_list) 188 | for name, p_list in self._portfolios_dict.items(): 189 | for portfolio_dict in p_list: 190 | self._db[name].update({"strategy_id": self._strategy_id}, {"$push": {"data": portfolio_dict}}, upsert=True) -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | 2 | # See the docstring in versioneer.py for instructions. Note that you must 3 | # re-run 'versioneer.py setup' after changing this section, and commit the 4 | # resulting files. 5 | 6 | [metadata] 7 | name = rqalpha-mod-incremental 8 | version = 0.0.9 9 | 10 | [versioneer] 11 | VCS = git 12 | style = pep440-ricequant 13 | versionfile_source = rqalpha_mod_incremental/_version.py 14 | versionfile_build = rqalpha_mod_incremental/_version.py 15 | tag_prefix = 16 | parentdir_prefix = rqalpha-mod-incremental -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2017 Ricequant, Inc 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | import versioneer 19 | from setuptools import find_packages, setup 20 | 21 | 22 | setup( 23 | name='rqalpha-mod-incremental', 24 | version=versioneer.get_version(), 25 | cmdclass=versioneer.get_cmdclass(), 26 | description='rqalpha-mod-incremental', 27 | packages=find_packages(exclude=[]), 28 | author='ricequant', 29 | author_email='public@ricequant.com', 30 | license='Apache License v2', 31 | package_data={'': ['*.*']}, 32 | install_requires=[ 33 | 'pymongo', 34 | 'rqalpha>=5.3.5' 35 | ], 36 | zip_safe=False, 37 | ) 38 | -------------------------------------------------------------------------------- /versioneer.py: -------------------------------------------------------------------------------- 1 | # Version: 0.28 2 | 3 | """The Versioneer - like a rocketeer, but for versions. 4 | 5 | The Versioneer 6 | ============== 7 | 8 | * like a rocketeer, but for versions! 9 | * https://github.com/python-versioneer/python-versioneer 10 | * Brian Warner 11 | * License: Public Domain (Unlicense) 12 | * Compatible with: Python 3.7, 3.8, 3.9, 3.10 and pypy3 13 | * [![Latest Version][pypi-image]][pypi-url] 14 | * [![Build Status][travis-image]][travis-url] 15 | 16 | This is a tool for managing a recorded version number in setuptools-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 | Versioneer provides two installation modes. The "classic" vendored mode installs 26 | a copy of versioneer into your repository. The experimental build-time dependency mode 27 | is intended to allow you to skip this step and simplify the process of upgrading. 28 | 29 | ### Vendored mode 30 | 31 | * `pip install versioneer` to somewhere in your $PATH 32 | * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is 33 | available, so you can also use `conda install -c conda-forge versioneer` 34 | * add a `[tool.versioneer]` section to your `pyproject.toml` or a 35 | `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) 36 | * Note that you will need to add `tomli; python_version < "3.11"` to your 37 | build-time dependencies if you use `pyproject.toml` 38 | * run `versioneer install --vendor` in your source tree, commit the results 39 | * verify version information with `python setup.py version` 40 | 41 | ### Build-time dependency mode 42 | 43 | * `pip install versioneer` to somewhere in your $PATH 44 | * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is 45 | available, so you can also use `conda install -c conda-forge versioneer` 46 | * add a `[tool.versioneer]` section to your `pyproject.toml` or a 47 | `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) 48 | * add `versioneer` (with `[toml]` extra, if configuring in `pyproject.toml`) 49 | to the `requires` key of the `build-system` table in `pyproject.toml`: 50 | ```toml 51 | [build-system] 52 | requires = ["setuptools", "versioneer[toml]"] 53 | build-backend = "setuptools.build_meta" 54 | ``` 55 | * run `versioneer install --no-vendor` in your source tree, commit the results 56 | * verify version information with `python setup.py version` 57 | 58 | ## Version Identifiers 59 | 60 | Source trees come from a variety of places: 61 | 62 | * a version-control system checkout (mostly used by developers) 63 | * a nightly tarball, produced by build automation 64 | * a snapshot tarball, produced by a web-based VCS browser, like github's 65 | "tarball from tag" feature 66 | * a release tarball, produced by "setup.py sdist", distributed through PyPI 67 | 68 | Within each source tree, the version identifier (either a string or a number, 69 | this tool is format-agnostic) can come from a variety of places: 70 | 71 | * ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows 72 | about recent "tags" and an absolute revision-id 73 | * the name of the directory into which the tarball was unpacked 74 | * an expanded VCS keyword ($Id$, etc) 75 | * a `_version.py` created by some earlier build step 76 | 77 | For released software, the version identifier is closely related to a VCS 78 | tag. Some projects use tag names that include more than just the version 79 | string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool 80 | needs to strip the tag prefix to extract the version identifier. For 81 | unreleased software (between tags), the version identifier should provide 82 | enough information to help developers recreate the same tree, while also 83 | giving them an idea of roughly how old the tree is (after version 1.2, before 84 | version 1.3). Many VCS systems can report a description that captures this, 85 | for example `git describe --tags --dirty --always` reports things like 86 | "0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the 87 | 0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has 88 | uncommitted changes). 89 | 90 | The version identifier is used for multiple purposes: 91 | 92 | * to allow the module to self-identify its version: `myproject.__version__` 93 | * to choose a name and prefix for a 'setup.py sdist' tarball 94 | 95 | ## Theory of Operation 96 | 97 | Versioneer works by adding a special `_version.py` file into your source 98 | tree, where your `__init__.py` can import it. This `_version.py` knows how to 99 | dynamically ask the VCS tool for version information at import time. 100 | 101 | `_version.py` also contains `$Revision$` markers, and the installation 102 | process marks `_version.py` to have this marker rewritten with a tag name 103 | during the `git archive` command. As a result, generated tarballs will 104 | contain enough information to get the proper version. 105 | 106 | To allow `setup.py` to compute a version too, a `versioneer.py` is added to 107 | the top level of your source tree, next to `setup.py` and the `setup.cfg` 108 | that configures it. This overrides several distutils/setuptools commands to 109 | compute the version when invoked, and changes `setup.py build` and `setup.py 110 | sdist` to replace `_version.py` with a small static file that contains just 111 | the generated version data. 112 | 113 | ## Installation 114 | 115 | See [INSTALL.md](./INSTALL.md) for detailed installation instructions. 116 | 117 | ## Version-String Flavors 118 | 119 | Code which uses Versioneer can learn about its version string at runtime by 120 | importing `_version` from your main `__init__.py` file and running the 121 | `get_versions()` function. From the "outside" (e.g. in `setup.py`), you can 122 | import the top-level `versioneer.py` and run `get_versions()`. 123 | 124 | Both functions return a dictionary with different flavors of version 125 | information: 126 | 127 | * `['version']`: A condensed version string, rendered using the selected 128 | style. This is the most commonly used value for the project's version 129 | string. The default "pep440" style yields strings like `0.11`, 130 | `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section 131 | below for alternative styles. 132 | 133 | * `['full-revisionid']`: detailed revision identifier. For Git, this is the 134 | full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". 135 | 136 | * `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the 137 | commit date in ISO 8601 format. This will be None if the date is not 138 | available. 139 | 140 | * `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that 141 | this is only accurate if run in a VCS checkout, otherwise it is likely to 142 | be False or None 143 | 144 | * `['error']`: if the version string could not be computed, this will be set 145 | to a string describing the problem, otherwise it will be None. It may be 146 | useful to throw an exception in setup.py if this is set, to avoid e.g. 147 | creating tarballs with a version string of "unknown". 148 | 149 | Some variants are more useful than others. Including `full-revisionid` in a 150 | bug report should allow developers to reconstruct the exact code being tested 151 | (or indicate the presence of local changes that should be shared with the 152 | developers). `version` is suitable for display in an "about" box or a CLI 153 | `--version` output: it can be easily compared against release notes and lists 154 | of bugs fixed in various releases. 155 | 156 | The installer adds the following text to your `__init__.py` to place a basic 157 | version in `YOURPROJECT.__version__`: 158 | 159 | from ._version import get_versions 160 | __version__ = get_versions()['version'] 161 | del get_versions 162 | 163 | ## Styles 164 | 165 | The setup.cfg `style=` configuration controls how the VCS information is 166 | rendered into a version string. 167 | 168 | The default style, "pep440", produces a PEP440-compliant string, equal to the 169 | un-prefixed tag name for actual releases, and containing an additional "local 170 | version" section with more detail for in-between builds. For Git, this is 171 | TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags 172 | --dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the 173 | tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and 174 | that this commit is two revisions ("+2") beyond the "0.11" tag. For released 175 | software (exactly equal to a known tag), the identifier will only contain the 176 | stripped tag, e.g. "0.11". 177 | 178 | Other styles are available. See [details.md](details.md) in the Versioneer 179 | source tree for descriptions. 180 | 181 | ## Debugging 182 | 183 | Versioneer tries to avoid fatal errors: if something goes wrong, it will tend 184 | to return a version of "0+unknown". To investigate the problem, run `setup.py 185 | version`, which will run the version-lookup code in a verbose mode, and will 186 | display the full contents of `get_versions()` (including the `error` string, 187 | which may help identify what went wrong). 188 | 189 | ## Known Limitations 190 | 191 | Some situations are known to cause problems for Versioneer. This details the 192 | most significant ones. More can be found on Github 193 | [issues page](https://github.com/python-versioneer/python-versioneer/issues). 194 | 195 | ### Subprojects 196 | 197 | Versioneer has limited support for source trees in which `setup.py` is not in 198 | the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are 199 | two common reasons why `setup.py` might not be in the root: 200 | 201 | * Source trees which contain multiple subprojects, such as 202 | [Buildbot](https://github.com/buildbot/buildbot), which contains both 203 | "master" and "slave" subprojects, each with their own `setup.py`, 204 | `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI 205 | distributions (and upload multiple independently-installable tarballs). 206 | * Source trees whose main purpose is to contain a C library, but which also 207 | provide bindings to Python (and perhaps other languages) in subdirectories. 208 | 209 | Versioneer will look for `.git` in parent directories, and most operations 210 | should get the right version string. However `pip` and `setuptools` have bugs 211 | and implementation details which frequently cause `pip install .` from a 212 | subproject directory to fail to find a correct version string (so it usually 213 | defaults to `0+unknown`). 214 | 215 | `pip install --editable .` should work correctly. `setup.py install` might 216 | work too. 217 | 218 | Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in 219 | some later version. 220 | 221 | [Bug #38](https://github.com/python-versioneer/python-versioneer/issues/38) is tracking 222 | this issue. The discussion in 223 | [PR #61](https://github.com/python-versioneer/python-versioneer/pull/61) describes the 224 | issue from the Versioneer side in more detail. 225 | [pip PR#3176](https://github.com/pypa/pip/pull/3176) and 226 | [pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve 227 | pip to let Versioneer work correctly. 228 | 229 | Versioneer-0.16 and earlier only looked for a `.git` directory next to the 230 | `setup.cfg`, so subprojects were completely unsupported with those releases. 231 | 232 | ### Editable installs with setuptools <= 18.5 233 | 234 | `setup.py develop` and `pip install --editable .` allow you to install a 235 | project into a virtualenv once, then continue editing the source code (and 236 | test) without re-installing after every change. 237 | 238 | "Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a 239 | convenient way to specify executable scripts that should be installed along 240 | with the python package. 241 | 242 | These both work as expected when using modern setuptools. When using 243 | setuptools-18.5 or earlier, however, certain operations will cause 244 | `pkg_resources.DistributionNotFound` errors when running the entrypoint 245 | script, which must be resolved by re-installing the package. This happens 246 | when the install happens with one version, then the egg_info data is 247 | regenerated while a different version is checked out. Many setup.py commands 248 | cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into 249 | a different virtualenv), so this can be surprising. 250 | 251 | [Bug #83](https://github.com/python-versioneer/python-versioneer/issues/83) describes 252 | this one, but upgrading to a newer version of setuptools should probably 253 | resolve it. 254 | 255 | 256 | ## Updating Versioneer 257 | 258 | To upgrade your project to a new release of Versioneer, do the following: 259 | 260 | * install the new Versioneer (`pip install -U versioneer` or equivalent) 261 | * edit `setup.cfg` and `pyproject.toml`, if necessary, 262 | to include any new configuration settings indicated by the release notes. 263 | See [UPGRADING](./UPGRADING.md) for details. 264 | * re-run `versioneer install --[no-]vendor` in your source tree, to replace 265 | `SRC/_version.py` 266 | * commit any changed files 267 | 268 | ## Future Directions 269 | 270 | This tool is designed to make it easily extended to other version-control 271 | systems: all VCS-specific components are in separate directories like 272 | src/git/ . The top-level `versioneer.py` script is assembled from these 273 | components by running make-versioneer.py . In the future, make-versioneer.py 274 | will take a VCS name as an argument, and will construct a version of 275 | `versioneer.py` that is specific to the given VCS. It might also take the 276 | configuration arguments that are currently provided manually during 277 | installation by editing setup.py . Alternatively, it might go the other 278 | direction and include code from all supported VCS systems, reducing the 279 | number of intermediate scripts. 280 | 281 | ## Similar projects 282 | 283 | * [setuptools_scm](https://github.com/pypa/setuptools_scm/) - a non-vendored build-time 284 | dependency 285 | * [minver](https://github.com/jbweston/miniver) - a lightweight reimplementation of 286 | versioneer 287 | * [versioningit](https://github.com/jwodder/versioningit) - a PEP 518-based setuptools 288 | plugin 289 | 290 | ## License 291 | 292 | To make Versioneer easier to embed, all its code is dedicated to the public 293 | domain. The `_version.py` that it creates is also in the public domain. 294 | Specifically, both are released under the "Unlicense", as described in 295 | https://unlicense.org/. 296 | 297 | [pypi-image]: https://img.shields.io/pypi/v/versioneer.svg 298 | [pypi-url]: https://pypi.python.org/pypi/versioneer/ 299 | [travis-image]: 300 | https://img.shields.io/travis/com/python-versioneer/python-versioneer.svg 301 | [travis-url]: https://travis-ci.com/github/python-versioneer/python-versioneer 302 | 303 | """ 304 | # pylint:disable=invalid-name,import-outside-toplevel,missing-function-docstring 305 | # pylint:disable=missing-class-docstring,too-many-branches,too-many-statements 306 | # pylint:disable=raise-missing-from,too-many-lines,too-many-locals,import-error 307 | # pylint:disable=too-few-public-methods,redefined-outer-name,consider-using-with 308 | # pylint:disable=attribute-defined-outside-init,too-many-arguments 309 | 310 | import configparser 311 | import errno 312 | import json 313 | import os 314 | import re 315 | import subprocess 316 | import sys 317 | import functools 318 | 319 | 320 | from pkg_resources import parse_version 321 | 322 | have_tomllib = True 323 | if sys.version_info >= (3, 11): 324 | import tomllib 325 | else: 326 | try: 327 | import tomli as tomllib 328 | except ImportError: 329 | have_tomllib = False 330 | 331 | 332 | class VersioneerConfig: 333 | """Container for Versioneer configuration parameters.""" 334 | 335 | 336 | def get_root(): 337 | """Get the project root directory. 338 | 339 | We require that all commands are run from the project root, i.e. the 340 | directory that contains setup.py, setup.cfg, and versioneer.py . 341 | """ 342 | root = os.path.realpath(os.path.abspath(os.getcwd())) 343 | setup_py = os.path.join(root, "setup.py") 344 | versioneer_py = os.path.join(root, "versioneer.py") 345 | if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): 346 | # allow 'python path/to/setup.py COMMAND' 347 | root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) 348 | setup_py = os.path.join(root, "setup.py") 349 | versioneer_py = os.path.join(root, "versioneer.py") 350 | if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): 351 | err = ("Versioneer was unable to run the project root directory. " 352 | "Versioneer requires setup.py to be executed from " 353 | "its immediate directory (like 'python setup.py COMMAND'), " 354 | "or in a way that lets it use sys.argv[0] to find the root " 355 | "(like 'python path/to/setup.py COMMAND').") 356 | raise VersioneerBadRootError(err) 357 | try: 358 | # Certain runtime workflows (setup.py install/develop in a setuptools 359 | # tree) execute all dependencies in a single python process, so 360 | # "versioneer" may be imported multiple times, and python's shared 361 | # module-import table will cache the first one. So we can't use 362 | # os.path.dirname(__file__), as that will find whichever 363 | # versioneer.py was first imported, even in later projects. 364 | my_path = os.path.realpath(os.path.abspath(__file__)) 365 | me_dir = os.path.normcase(os.path.splitext(my_path)[0]) 366 | vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) 367 | if me_dir != vsr_dir and "VERSIONEER_PEP518" not in globals(): 368 | print("Warning: build in %s is using versioneer.py from %s" 369 | % (os.path.dirname(my_path), versioneer_py)) 370 | except NameError: 371 | pass 372 | return root 373 | 374 | 375 | def get_config_from_root(root): 376 | """Read the project setup.cfg file to determine Versioneer config.""" 377 | # This might raise OSError (if setup.cfg is missing), or 378 | # configparser.NoSectionError (if it lacks a [versioneer] section), or 379 | # configparser.NoOptionError (if it lacks "VCS="). See the docstring at 380 | # the top of versioneer.py for instructions on writing your setup.cfg . 381 | pyproject_toml = os.path.join(root, "pyproject.toml") 382 | setup_cfg = os.path.join(root, "setup.cfg") 383 | section = None 384 | if os.path.exists(pyproject_toml) and have_tomllib: 385 | try: 386 | with open(pyproject_toml, 'rb') as fobj: 387 | pp = tomllib.load(fobj) 388 | section = pp['tool']['versioneer'] 389 | except (tomllib.TOMLDecodeError, KeyError): 390 | pass 391 | if not section: 392 | parser = configparser.ConfigParser() 393 | with open(setup_cfg) as cfg_file: 394 | parser.read_file(cfg_file) 395 | parser.get("versioneer", "VCS") # raise error if missing 396 | 397 | section = parser["versioneer"] 398 | 399 | cfg = VersioneerConfig() 400 | cfg.VCS = section['VCS'] 401 | cfg.style = section.get("style", "") 402 | cfg.versionfile_source = section.get("versionfile_source") 403 | cfg.versionfile_build = section.get("versionfile_build") 404 | cfg.tag_prefix = section.get("tag_prefix") 405 | if cfg.tag_prefix in ("''", '""', None): 406 | cfg.tag_prefix = "" 407 | cfg.parentdir_prefix = section.get("parentdir_prefix") 408 | cfg.verbose = section.get("verbose") 409 | return cfg 410 | 411 | 412 | class NotThisMethod(Exception): 413 | """Exception raised if a method is not valid for the current scenario.""" 414 | 415 | 416 | # these dictionaries contain VCS-specific tools 417 | LONG_VERSION_PY = {} 418 | HANDLERS = {} 419 | 420 | 421 | def register_vcs_handler(vcs, method): # decorator 422 | """Create decorator to mark a method as the handler of a VCS.""" 423 | def decorate(f): 424 | """Store f in HANDLERS[vcs][method].""" 425 | HANDLERS.setdefault(vcs, {})[method] = f 426 | return f 427 | return decorate 428 | 429 | 430 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 431 | env=None): 432 | """Call the given command(s).""" 433 | assert isinstance(commands, list) 434 | process = None 435 | 436 | popen_kwargs = {} 437 | if sys.platform == "win32": 438 | # This hides the console window if pythonw.exe is used 439 | startupinfo = subprocess.STARTUPINFO() 440 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 441 | popen_kwargs["startupinfo"] = startupinfo 442 | 443 | for command in commands: 444 | try: 445 | dispcmd = str([command] + args) 446 | # remember shell=False, so use git.cmd on windows, not just git 447 | process = subprocess.Popen([command] + args, cwd=cwd, env=env, 448 | stdout=subprocess.PIPE, 449 | stderr=(subprocess.PIPE if hide_stderr 450 | else None), **popen_kwargs) 451 | break 452 | except OSError: 453 | e = sys.exc_info()[1] 454 | if e.errno == errno.ENOENT: 455 | continue 456 | if verbose: 457 | print("unable to run %s" % dispcmd) 458 | print(e) 459 | return None, None 460 | else: 461 | if verbose: 462 | print("unable to find command, tried %s" % (commands,)) 463 | return None, None 464 | stdout = process.communicate()[0].strip().decode('utf-8') 465 | if process.returncode != 0: 466 | if verbose: 467 | print("unable to run %s (error)" % dispcmd) 468 | print("stdout was %s" % stdout) 469 | return None, process.returncode 470 | return stdout, process.returncode 471 | 472 | 473 | LONG_VERSION_PY['git'] = r''' 474 | # This file helps to compute a version number in source trees obtained from 475 | # git-archive tarball (such as those provided by githubs download-from-tag 476 | # feature). Distribution tarballs (built by setup.py sdist) and build 477 | # directories (produced by setup.py build) will contain a much shorter file 478 | # that just contains the computed version number. 479 | 480 | # This file is released into the public domain. 481 | # Generated by versioneer-0.28 482 | # https://github.com/python-versioneer/python-versioneer 483 | 484 | """Git implementation of _version.py.""" 485 | 486 | import errno 487 | import os 488 | import re 489 | import subprocess 490 | import sys 491 | import functools 492 | 493 | 494 | def get_keywords(): 495 | """Get the keywords needed to look up the version information.""" 496 | # these strings will be replaced by git during git-archive. 497 | # setup.py/versioneer.py will grep for the variable names, so they must 498 | # each be defined on a line of their own. _version.py will just call 499 | # get_keywords(). 500 | git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" 501 | git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" 502 | git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s" 503 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 504 | return keywords 505 | 506 | 507 | class VersioneerConfig: 508 | """Container for Versioneer configuration parameters.""" 509 | 510 | 511 | def get_config(): 512 | """Create, populate and return the VersioneerConfig() object.""" 513 | # these strings are filled in when 'setup.py versioneer' creates 514 | # _version.py 515 | cfg = VersioneerConfig() 516 | cfg.VCS = "git" 517 | cfg.style = "%(STYLE)s" 518 | cfg.tag_prefix = "%(TAG_PREFIX)s" 519 | cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" 520 | cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" 521 | cfg.verbose = False 522 | return cfg 523 | 524 | 525 | class NotThisMethod(Exception): 526 | """Exception raised if a method is not valid for the current scenario.""" 527 | 528 | 529 | LONG_VERSION_PY = {} 530 | HANDLERS = {} 531 | 532 | 533 | def register_vcs_handler(vcs, method): # decorator 534 | """Create decorator to mark a method as the handler of a VCS.""" 535 | def decorate(f): 536 | """Store f in HANDLERS[vcs][method].""" 537 | if vcs not in HANDLERS: 538 | HANDLERS[vcs] = {} 539 | HANDLERS[vcs][method] = f 540 | return f 541 | return decorate 542 | 543 | 544 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 545 | env=None): 546 | """Call the given command(s).""" 547 | assert isinstance(commands, list) 548 | process = None 549 | 550 | popen_kwargs = {} 551 | if sys.platform == "win32": 552 | # This hides the console window if pythonw.exe is used 553 | startupinfo = subprocess.STARTUPINFO() 554 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 555 | popen_kwargs["startupinfo"] = startupinfo 556 | 557 | for command in commands: 558 | try: 559 | dispcmd = str([command] + args) 560 | # remember shell=False, so use git.cmd on windows, not just git 561 | process = subprocess.Popen([command] + args, cwd=cwd, env=env, 562 | stdout=subprocess.PIPE, 563 | stderr=(subprocess.PIPE if hide_stderr 564 | else None), **popen_kwargs) 565 | break 566 | except OSError: 567 | e = sys.exc_info()[1] 568 | if e.errno == errno.ENOENT: 569 | continue 570 | if verbose: 571 | print("unable to run %%s" %% dispcmd) 572 | print(e) 573 | return None, None 574 | else: 575 | if verbose: 576 | print("unable to find command, tried %%s" %% (commands,)) 577 | return None, None 578 | stdout = process.communicate()[0].strip().decode() 579 | if process.returncode != 0: 580 | if verbose: 581 | print("unable to run %%s (error)" %% dispcmd) 582 | print("stdout was %%s" %% stdout) 583 | return None, process.returncode 584 | return stdout, process.returncode 585 | 586 | 587 | def versions_from_parentdir(parentdir_prefix, root, verbose): 588 | """Try to determine the version from the parent directory name. 589 | 590 | Source tarballs conventionally unpack into a directory that includes both 591 | the project name and a version string. We will also support searching up 592 | two directory levels for an appropriately named parent directory 593 | """ 594 | rootdirs = [] 595 | 596 | for _ in range(3): 597 | dirname = os.path.basename(root) 598 | if dirname.startswith(parentdir_prefix): 599 | return {"version": dirname[len(parentdir_prefix):], 600 | "full-revisionid": None, 601 | "dirty": False, "error": None, "date": None} 602 | rootdirs.append(root) 603 | root = os.path.dirname(root) # up a level 604 | 605 | if verbose: 606 | print("Tried directories %%s but none started with prefix %%s" %% 607 | (str(rootdirs), parentdir_prefix)) 608 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 609 | 610 | 611 | @register_vcs_handler("git", "get_keywords") 612 | def git_get_keywords(versionfile_abs): 613 | """Extract version information from the given file.""" 614 | # the code embedded in _version.py can just fetch the value of these 615 | # keywords. When used from setup.py, we don't want to import _version.py, 616 | # so we do it with a regexp instead. This function is not used from 617 | # _version.py. 618 | keywords = {} 619 | try: 620 | with open(versionfile_abs, "r") as fobj: 621 | for line in fobj: 622 | if line.strip().startswith("git_refnames ="): 623 | mo = re.search(r'=\s*"(.*)"', line) 624 | if mo: 625 | keywords["refnames"] = mo.group(1) 626 | if line.strip().startswith("git_full ="): 627 | mo = re.search(r'=\s*"(.*)"', line) 628 | if mo: 629 | keywords["full"] = mo.group(1) 630 | if line.strip().startswith("git_date ="): 631 | mo = re.search(r'=\s*"(.*)"', line) 632 | if mo: 633 | keywords["date"] = mo.group(1) 634 | except OSError: 635 | pass 636 | return keywords 637 | 638 | 639 | @register_vcs_handler("git", "keywords") 640 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 641 | """Get version information from git keywords.""" 642 | if "refnames" not in keywords: 643 | raise NotThisMethod("Short version file found") 644 | date = keywords.get("date") 645 | if date is not None: 646 | # Use only the last line. Previous lines may contain GPG signature 647 | # information. 648 | date = date.splitlines()[-1] 649 | 650 | # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant 651 | # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 652 | # -like" string, which we must then edit to make compliant), because 653 | # it's been around since git-1.5.3, and it's too difficult to 654 | # discover which version we're using, or to work around using an 655 | # older one. 656 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 657 | refnames = keywords["refnames"].strip() 658 | if refnames.startswith("$Format"): 659 | if verbose: 660 | print("keywords are unexpanded, not using") 661 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 662 | refs = {r.strip() for r in refnames.strip("()").split(",")} 663 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 664 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 665 | TAG = "tag: " 666 | tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} 667 | if not tags: 668 | # Either we're using git < 1.8.3, or there really are no tags. We use 669 | # a heuristic: assume all version tags have a digit. The old git %%d 670 | # expansion behaves like git log --decorate=short and strips out the 671 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 672 | # between branches and tags. By ignoring refnames without digits, we 673 | # filter out many common branch names like "release" and 674 | # "stabilization", as well as "HEAD" and "master". 675 | tags = {r for r in refs if re.search(r'\d', r)} 676 | if verbose: 677 | print("discarding '%%s', no digits" %% ",".join(refs - tags)) 678 | if verbose: 679 | print("likely tags: %%s" %% ",".join(sorted(tags))) 680 | for ref in sorted(tags): 681 | # sorting will prefer e.g. "2.0" over "2.0rc1" 682 | if ref.startswith(tag_prefix): 683 | r = ref[len(tag_prefix):] 684 | # Filter out refs that exactly match prefix or that don't start 685 | # with a number once the prefix is stripped (mostly a concern 686 | # when prefix is '') 687 | if not re.match(r'\d', r): 688 | continue 689 | if verbose: 690 | print("picking %%s" %% r) 691 | return {"version": r, 692 | "full-revisionid": keywords["full"].strip(), 693 | "dirty": False, "error": None, 694 | "date": date} 695 | # no suitable tags, so version is "0+unknown", but full hex is still there 696 | if verbose: 697 | print("no suitable tags, using unknown + full revision id") 698 | return {"version": "0+unknown", 699 | "full-revisionid": keywords["full"].strip(), 700 | "dirty": False, "error": "no suitable tags", "date": None} 701 | 702 | 703 | @register_vcs_handler("git", "pieces_from_vcs") 704 | def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): 705 | """Get version from 'git describe' in the root of the source tree. 706 | 707 | This only gets called if the git-archive 'subst' keywords were *not* 708 | expanded, and _version.py hasn't already been rewritten with a short 709 | version string, meaning we're inside a checked out source tree. 710 | """ 711 | GITS = ["git"] 712 | if sys.platform == "win32": 713 | GITS = ["git.cmd", "git.exe"] 714 | 715 | # GIT_DIR can interfere with correct operation of Versioneer. 716 | # It may be intended to be passed to the Versioneer-versioned project, 717 | # but that should not change where we get our version from. 718 | env = os.environ.copy() 719 | env.pop("GIT_DIR", None) 720 | runner = functools.partial(runner, env=env) 721 | 722 | _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, 723 | hide_stderr=not verbose) 724 | if rc != 0: 725 | if verbose: 726 | print("Directory %%s not under git control" %% root) 727 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 728 | 729 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 730 | # if there isn't one, this yields HEX[-dirty] (no NUM) 731 | describe_out, rc = runner(GITS, [ 732 | "describe", "--tags", "--dirty", "--always", "--long", 733 | "--match", "{}[[:digit:]]*".format(tag_prefix) 734 | ], cwd=root) 735 | # --long was added in git-1.5.5 736 | if describe_out is None: 737 | raise NotThisMethod("'git describe' failed") 738 | describe_out = describe_out.strip() 739 | full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) 740 | if full_out is None: 741 | raise NotThisMethod("'git rev-parse' failed") 742 | full_out = full_out.strip() 743 | 744 | pieces = {} 745 | pieces["long"] = full_out 746 | pieces["short"] = full_out[:7] # maybe improved later 747 | pieces["error"] = None 748 | 749 | branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], 750 | cwd=root) 751 | # --abbrev-ref was added in git-1.6.3 752 | if rc != 0 or branch_name is None: 753 | raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") 754 | branch_name = branch_name.strip() 755 | 756 | if branch_name == "HEAD": 757 | # If we aren't exactly on a branch, pick a branch which represents 758 | # the current commit. If all else fails, we are on a branchless 759 | # commit. 760 | branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) 761 | # --contains was added in git-1.5.4 762 | if rc != 0 or branches is None: 763 | raise NotThisMethod("'git branch --contains' returned error") 764 | branches = branches.split("\n") 765 | 766 | # Remove the first line if we're running detached 767 | if "(" in branches[0]: 768 | branches.pop(0) 769 | 770 | # Strip off the leading "* " from the list of branches. 771 | branches = [branch[2:] for branch in branches] 772 | if "master" in branches: 773 | branch_name = "master" 774 | elif not branches: 775 | branch_name = None 776 | else: 777 | # Pick the first branch that is returned. Good or bad. 778 | branch_name = branches[0] 779 | 780 | pieces["branch"] = branch_name 781 | 782 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 783 | # TAG might have hyphens. 784 | git_describe = describe_out 785 | 786 | # look for -dirty suffix 787 | dirty = git_describe.endswith("-dirty") 788 | pieces["dirty"] = dirty 789 | if dirty: 790 | git_describe = git_describe[:git_describe.rindex("-dirty")] 791 | 792 | # now we have TAG-NUM-gHEX or HEX 793 | 794 | if "-" in git_describe: 795 | # TAG-NUM-gHEX 796 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 797 | if not mo: 798 | # unparsable. Maybe git-describe is misbehaving? 799 | pieces["error"] = ("unable to parse git-describe output: '%%s'" 800 | %% describe_out) 801 | return pieces 802 | 803 | # tag 804 | full_tag = mo.group(1) 805 | if not full_tag.startswith(tag_prefix): 806 | if verbose: 807 | fmt = "tag '%%s' doesn't start with prefix '%%s'" 808 | print(fmt %% (full_tag, tag_prefix)) 809 | pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" 810 | %% (full_tag, tag_prefix)) 811 | return pieces 812 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 813 | 814 | # distance: number of commits since tag 815 | pieces["distance"] = int(mo.group(2)) 816 | 817 | # commit: short hex revision ID 818 | pieces["short"] = mo.group(3) 819 | 820 | else: 821 | # HEX: no tags 822 | pieces["closest-tag"] = None 823 | out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) 824 | pieces["distance"] = len(out.split()) # total number of commits 825 | 826 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 827 | date = runner(GITS, ["show", "-s", "--format=%%ci", "HEAD"], cwd=root)[0].strip() 828 | # Use only the last line. Previous lines may contain GPG signature 829 | # information. 830 | date = date.splitlines()[-1] 831 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 832 | 833 | return pieces 834 | 835 | 836 | def plus_or_dot(pieces): 837 | """Return a + if we don't already have one, else return a .""" 838 | if "+" in pieces.get("closest-tag", ""): 839 | return "." 840 | return "+" 841 | 842 | 843 | def render_pep440(pieces): 844 | """Build up version string, with post-release "local version identifier". 845 | 846 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 847 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 848 | 849 | Exceptions: 850 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 851 | """ 852 | if pieces["closest-tag"]: 853 | rendered = pieces["closest-tag"] 854 | if pieces["distance"] or pieces["dirty"]: 855 | rendered += plus_or_dot(pieces) 856 | rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) 857 | if pieces["dirty"]: 858 | rendered += ".dirty" 859 | else: 860 | # exception #1 861 | rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], 862 | pieces["short"]) 863 | if pieces["dirty"]: 864 | rendered += ".dirty" 865 | return rendered 866 | 867 | 868 | def render_pep440_branch(pieces): 869 | """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . 870 | 871 | The ".dev0" means not master branch. Note that .dev0 sorts backwards 872 | (a feature branch will appear "older" than the master branch). 873 | 874 | Exceptions: 875 | 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] 876 | """ 877 | if pieces["closest-tag"]: 878 | rendered = pieces["closest-tag"] 879 | if pieces["distance"] or pieces["dirty"]: 880 | if pieces["branch"] != "master": 881 | rendered += ".dev0" 882 | rendered += plus_or_dot(pieces) 883 | rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) 884 | if pieces["dirty"]: 885 | rendered += ".dirty" 886 | else: 887 | # exception #1 888 | rendered = "0" 889 | if pieces["branch"] != "master": 890 | rendered += ".dev0" 891 | rendered += "+untagged.%%d.g%%s" %% (pieces["distance"], 892 | pieces["short"]) 893 | if pieces["dirty"]: 894 | rendered += ".dirty" 895 | return rendered 896 | 897 | 898 | def pep440_split_post(ver): 899 | """Split pep440 version string at the post-release segment. 900 | 901 | Returns the release segments before the post-release and the 902 | post-release version number (or -1 if no post-release segment is present). 903 | """ 904 | vc = str.split(ver, ".post") 905 | return vc[0], int(vc[1] or 0) if len(vc) == 2 else None 906 | 907 | 908 | def render_pep440_pre(pieces): 909 | """TAG[.postN.devDISTANCE] -- No -dirty. 910 | 911 | Exceptions: 912 | 1: no tags. 0.post0.devDISTANCE 913 | """ 914 | if pieces["closest-tag"]: 915 | if pieces["distance"]: 916 | # update the post release segment 917 | tag_version, post_version = pep440_split_post(pieces["closest-tag"]) 918 | rendered = tag_version 919 | if post_version is not None: 920 | rendered += ".post%%d.dev%%d" %% (post_version + 1, pieces["distance"]) 921 | else: 922 | rendered += ".post0.dev%%d" %% (pieces["distance"]) 923 | else: 924 | # no commits, use the tag as the version 925 | rendered = pieces["closest-tag"] 926 | else: 927 | # exception #1 928 | rendered = "0.post0.dev%%d" %% pieces["distance"] 929 | return rendered 930 | 931 | 932 | def render_pep440_post(pieces): 933 | """TAG[.postDISTANCE[.dev0]+gHEX] . 934 | 935 | The ".dev0" means dirty. Note that .dev0 sorts backwards 936 | (a dirty tree will appear "older" than the corresponding clean one), 937 | but you shouldn't be releasing software with -dirty anyways. 938 | 939 | Exceptions: 940 | 1: no tags. 0.postDISTANCE[.dev0] 941 | """ 942 | if pieces["closest-tag"]: 943 | rendered = pieces["closest-tag"] 944 | if pieces["distance"] or pieces["dirty"]: 945 | rendered += ".post%%d" %% pieces["distance"] 946 | if pieces["dirty"]: 947 | rendered += ".dev0" 948 | rendered += plus_or_dot(pieces) 949 | rendered += "g%%s" %% pieces["short"] 950 | else: 951 | # exception #1 952 | rendered = "0.post%%d" %% pieces["distance"] 953 | if pieces["dirty"]: 954 | rendered += ".dev0" 955 | rendered += "+g%%s" %% pieces["short"] 956 | return rendered 957 | 958 | 959 | def render_pep440_post_branch(pieces): 960 | """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . 961 | 962 | The ".dev0" means not master branch. 963 | 964 | Exceptions: 965 | 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] 966 | """ 967 | if pieces["closest-tag"]: 968 | rendered = pieces["closest-tag"] 969 | if pieces["distance"] or pieces["dirty"]: 970 | rendered += ".post%%d" %% pieces["distance"] 971 | if pieces["branch"] != "master": 972 | rendered += ".dev0" 973 | rendered += plus_or_dot(pieces) 974 | rendered += "g%%s" %% pieces["short"] 975 | if pieces["dirty"]: 976 | rendered += ".dirty" 977 | else: 978 | # exception #1 979 | rendered = "0.post%%d" %% pieces["distance"] 980 | if pieces["branch"] != "master": 981 | rendered += ".dev0" 982 | rendered += "+g%%s" %% pieces["short"] 983 | if pieces["dirty"]: 984 | rendered += ".dirty" 985 | return rendered 986 | 987 | 988 | def render_pep440_old(pieces): 989 | """TAG[.postDISTANCE[.dev0]] . 990 | 991 | The ".dev0" means dirty. 992 | 993 | Exceptions: 994 | 1: no tags. 0.postDISTANCE[.dev0] 995 | """ 996 | if pieces["closest-tag"]: 997 | rendered = pieces["closest-tag"] 998 | if pieces["distance"] or pieces["dirty"]: 999 | rendered += ".post%%d" %% pieces["distance"] 1000 | if pieces["dirty"]: 1001 | rendered += ".dev0" 1002 | else: 1003 | # exception #1 1004 | rendered = "0.post%%d" %% pieces["distance"] 1005 | if pieces["dirty"]: 1006 | rendered += ".dev0" 1007 | return rendered 1008 | 1009 | 1010 | def render_git_describe(pieces): 1011 | """TAG[-DISTANCE-gHEX][-dirty]. 1012 | 1013 | Like 'git describe --tags --dirty --always'. 1014 | 1015 | Exceptions: 1016 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 1017 | """ 1018 | if pieces["closest-tag"]: 1019 | rendered = pieces["closest-tag"] 1020 | if pieces["distance"]: 1021 | rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) 1022 | else: 1023 | # exception #1 1024 | rendered = pieces["short"] 1025 | if pieces["dirty"]: 1026 | rendered += "-dirty" 1027 | return rendered 1028 | 1029 | 1030 | def render_git_describe_long(pieces): 1031 | """TAG-DISTANCE-gHEX[-dirty]. 1032 | 1033 | Like 'git describe --tags --dirty --always -long'. 1034 | The distance/hash is unconditional. 1035 | 1036 | Exceptions: 1037 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 1038 | """ 1039 | if pieces["closest-tag"]: 1040 | rendered = pieces["closest-tag"] 1041 | rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) 1042 | else: 1043 | # exception #1 1044 | rendered = pieces["short"] 1045 | if pieces["dirty"]: 1046 | rendered += "-dirty" 1047 | return rendered 1048 | 1049 | 1050 | def render(pieces, style): 1051 | """Render the given version pieces into the requested style.""" 1052 | if pieces["error"]: 1053 | return {"version": "unknown", 1054 | "full-revisionid": pieces.get("long"), 1055 | "dirty": None, 1056 | "error": pieces["error"], 1057 | "date": None} 1058 | 1059 | if not style or style == "default": 1060 | style = "pep440" # the default 1061 | 1062 | if style == "pep440": 1063 | rendered = render_pep440(pieces) 1064 | elif style == "pep440-branch": 1065 | rendered = render_pep440_branch(pieces) 1066 | elif style == "pep440-pre": 1067 | rendered = render_pep440_pre(pieces) 1068 | elif style == "pep440-post": 1069 | rendered = render_pep440_post(pieces) 1070 | elif style == "pep440-post-branch": 1071 | rendered = render_pep440_post_branch(pieces) 1072 | elif style == "pep440-old": 1073 | rendered = render_pep440_old(pieces) 1074 | elif style == "git-describe": 1075 | rendered = render_git_describe(pieces) 1076 | elif style == "git-describe-long": 1077 | rendered = render_git_describe_long(pieces) 1078 | else: 1079 | raise ValueError("unknown style '%%s'" %% style) 1080 | 1081 | return {"version": rendered, "full-revisionid": pieces["long"], 1082 | "dirty": pieces["dirty"], "error": None, 1083 | "date": pieces.get("date")} 1084 | 1085 | 1086 | def get_versions(): 1087 | """Get version information or return default if unable to do so.""" 1088 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 1089 | # __file__, we can work backwards from there to the root. Some 1090 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 1091 | # case we can only use expanded keywords. 1092 | 1093 | cfg = get_config() 1094 | verbose = cfg.verbose 1095 | 1096 | try: 1097 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 1098 | verbose) 1099 | except NotThisMethod: 1100 | pass 1101 | 1102 | try: 1103 | root = os.path.realpath(__file__) 1104 | # versionfile_source is the relative path from the top of the source 1105 | # tree (where the .git directory might live) to this file. Invert 1106 | # this to find the root from __file__. 1107 | for _ in cfg.versionfile_source.split('/'): 1108 | root = os.path.dirname(root) 1109 | except NameError: 1110 | return {"version": "0+unknown", "full-revisionid": None, 1111 | "dirty": None, 1112 | "error": "unable to find root of source tree", 1113 | "date": None} 1114 | 1115 | try: 1116 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 1117 | return render(pieces, cfg.style) 1118 | except NotThisMethod: 1119 | pass 1120 | 1121 | try: 1122 | if cfg.parentdir_prefix: 1123 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 1124 | except NotThisMethod: 1125 | pass 1126 | 1127 | return {"version": "0+unknown", "full-revisionid": None, 1128 | "dirty": None, 1129 | "error": "unable to compute version", "date": None} 1130 | ''' 1131 | 1132 | 1133 | @register_vcs_handler("git", "get_keywords") 1134 | def git_get_keywords(versionfile_abs): 1135 | """Extract version information from the given file.""" 1136 | # the code embedded in _version.py can just fetch the value of these 1137 | # keywords. When used from setup.py, we don't want to import _version.py, 1138 | # so we do it with a regexp instead. This function is not used from 1139 | # _version.py. 1140 | keywords = {} 1141 | try: 1142 | with open(versionfile_abs, "r") as fobj: 1143 | for line in fobj: 1144 | if line.strip().startswith("git_refnames ="): 1145 | mo = re.search(r'=\s*"(.*)"', line) 1146 | if mo: 1147 | keywords["refnames"] = mo.group(1) 1148 | if line.strip().startswith("git_full ="): 1149 | mo = re.search(r'=\s*"(.*)"', line) 1150 | if mo: 1151 | keywords["full"] = mo.group(1) 1152 | if line.strip().startswith("git_date ="): 1153 | mo = re.search(r'=\s*"(.*)"', line) 1154 | if mo: 1155 | keywords["date"] = mo.group(1) 1156 | except OSError: 1157 | pass 1158 | return keywords 1159 | 1160 | 1161 | @register_vcs_handler("git", "keywords") 1162 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 1163 | """Get version information from git keywords.""" 1164 | if "refnames" not in keywords: 1165 | raise NotThisMethod("Short version file found") 1166 | date = keywords.get("date") 1167 | if date is not None: 1168 | # Use only the last line. Previous lines may contain GPG signature 1169 | # information. 1170 | date = date.splitlines()[-1] 1171 | 1172 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 1173 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 1174 | # -like" string, which we must then edit to make compliant), because 1175 | # it's been around since git-1.5.3, and it's too difficult to 1176 | # discover which version we're using, or to work around using an 1177 | # older one. 1178 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 1179 | refnames = keywords["refnames"].strip() 1180 | if refnames.startswith("$Format"): 1181 | if verbose: 1182 | print("keywords are unexpanded, not using") 1183 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 1184 | refs = {r.strip() for r in refnames.strip("()").split(",")} 1185 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 1186 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 1187 | TAG = "tag: " 1188 | tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} 1189 | if not tags: 1190 | # Either we're using git < 1.8.3, or there really are no tags. We use 1191 | # a heuristic: assume all version tags have a digit. The old git %d 1192 | # expansion behaves like git log --decorate=short and strips out the 1193 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 1194 | # between branches and tags. By ignoring refnames without digits, we 1195 | # filter out many common branch names like "release" and 1196 | # "stabilization", as well as "HEAD" and "master". 1197 | tags = {r for r in refs if re.search(r'\d', r)} 1198 | if verbose: 1199 | print("discarding '%s', no digits" % ",".join(refs - tags)) 1200 | if verbose: 1201 | print("likely tags: %s" % ",".join(sorted(tags))) 1202 | for ref in sorted(tags): 1203 | # sorting will prefer e.g. "2.0" over "2.0rc1" 1204 | if ref.startswith(tag_prefix): 1205 | r = ref[len(tag_prefix):] 1206 | # Filter out refs that exactly match prefix or that don't start 1207 | # with a number once the prefix is stripped (mostly a concern 1208 | # when prefix is '') 1209 | if not re.match(r'\d', r): 1210 | continue 1211 | if verbose: 1212 | print("picking %s" % r) 1213 | return {"version": r, 1214 | "full-revisionid": keywords["full"].strip(), 1215 | "dirty": False, "error": None, 1216 | "date": date} 1217 | # no suitable tags, so version is "0+unknown", but full hex is still there 1218 | if verbose: 1219 | print("no suitable tags, using unknown + full revision id") 1220 | return {"version": "0+unknown", 1221 | "full-revisionid": keywords["full"].strip(), 1222 | "dirty": False, "error": "no suitable tags", "date": None} 1223 | 1224 | 1225 | def git_tracking_branch(): 1226 | GITS = ["git"] 1227 | branch_out, rc = run_command(GITS, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], hide_stderr=True) 1228 | if rc != 0: 1229 | return "notrack" 1230 | return branch_out 1231 | 1232 | 1233 | @register_vcs_handler("git", "pieces_from_vcs") 1234 | def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): 1235 | """Get version from 'git describe' in the root of the source tree. 1236 | 1237 | This only gets called if the git-archive 'subst' keywords were *not* 1238 | expanded, and _version.py hasn't already been rewritten with a short 1239 | version string, meaning we're inside a checked out source tree. 1240 | """ 1241 | GITS = ["git"] 1242 | if sys.platform == "win32": 1243 | GITS = ["git.cmd", "git.exe"] 1244 | 1245 | # GIT_DIR can interfere with correct operation of Versioneer. 1246 | # It may be intended to be passed to the Versioneer-versioned project, 1247 | # but that should not change where we get our version from. 1248 | env = os.environ.copy() 1249 | env.pop("GIT_DIR", None) 1250 | runner = functools.partial(runner, env=env) 1251 | 1252 | _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, 1253 | hide_stderr=not verbose) 1254 | if rc != 0: 1255 | if verbose: 1256 | print("Directory %s not under git control" % root) 1257 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 1258 | 1259 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 1260 | # if there isn't one, this yields HEX[-dirty] (no NUM) 1261 | describe_out, rc = runner(GITS, [ 1262 | "describe", "--tags", "--dirty", "--always", "--long", 1263 | "--match", "{}[[:digit:]]*".format(tag_prefix) 1264 | ], cwd=root) 1265 | # --long was added in git-1.5.5 1266 | if describe_out is None: 1267 | raise NotThisMethod("'git describe' failed") 1268 | describe_out = describe_out.strip() 1269 | full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) 1270 | if full_out is None: 1271 | raise NotThisMethod("'git rev-parse' failed") 1272 | full_out = full_out.strip() 1273 | 1274 | pieces = {} 1275 | pieces["long"] = full_out 1276 | pieces["short"] = full_out[:7] # maybe improved later 1277 | pieces["error"] = None 1278 | 1279 | branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], 1280 | cwd=root) 1281 | # --abbrev-ref was added in git-1.6.3 1282 | if rc != 0 or branch_name is None: 1283 | raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") 1284 | branch_name = branch_name.strip() 1285 | 1286 | if branch_name == "HEAD": 1287 | # If we aren't exactly on a branch, pick a branch which represents 1288 | # the current commit. If all else fails, we are on a branchless 1289 | # commit. 1290 | branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) 1291 | # --contains was added in git-1.5.4 1292 | if rc != 0 or branches is None: 1293 | raise NotThisMethod("'git branch --contains' returned error") 1294 | branches = branches.split("\n") 1295 | 1296 | # Remove the first line if we're running detached 1297 | if "(" in branches[0]: 1298 | branches.pop(0) 1299 | 1300 | # Strip off the leading "* " from the list of branches. 1301 | branches = [branch[2:] for branch in branches] 1302 | if "master" in branches: 1303 | branch_name = "master" 1304 | elif not branches: 1305 | branch_name = None 1306 | else: 1307 | # Pick the first branch that is returned. Good or bad. 1308 | branch_name = branches[0] 1309 | 1310 | pieces["branch"] = branch_name 1311 | 1312 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 1313 | # TAG might have hyphens. 1314 | git_describe = describe_out 1315 | 1316 | # look for -dirty suffix 1317 | dirty = git_describe.endswith("-dirty") 1318 | pieces["dirty"] = dirty 1319 | if dirty: 1320 | git_describe = git_describe[:git_describe.rindex("-dirty")] 1321 | 1322 | # now we have TAG-NUM-gHEX or HEX 1323 | 1324 | if "-" in git_describe: 1325 | # TAG-NUM-gHEX 1326 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 1327 | if not mo: 1328 | # unparsable. Maybe git-describe is misbehaving? 1329 | pieces["error"] = ("unable to parse git-describe output: '%s'" 1330 | % describe_out) 1331 | return pieces 1332 | 1333 | # tag 1334 | full_tag = mo.group(1) 1335 | if not full_tag.startswith(tag_prefix): 1336 | if verbose: 1337 | fmt = "tag '%s' doesn't start with prefix '%s'" 1338 | print(fmt % (full_tag, tag_prefix)) 1339 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 1340 | % (full_tag, tag_prefix)) 1341 | return pieces 1342 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 1343 | 1344 | # distance: number of commits since tag 1345 | pieces["distance"] = int(mo.group(2)) 1346 | 1347 | # commit: short hex revision ID 1348 | pieces["short"] = mo.group(3) 1349 | 1350 | else: 1351 | # HEX: no tags 1352 | pieces["closest-tag"] = None 1353 | out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) 1354 | pieces["distance"] = len(out.split()) # total number of commits 1355 | 1356 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 1357 | date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() 1358 | # Use only the last line. Previous lines may contain GPG signature 1359 | # information. 1360 | date = date.splitlines()[-1] 1361 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 1362 | 1363 | return pieces 1364 | 1365 | 1366 | def do_vcs_install(versionfile_source, ipy): 1367 | """Git-specific installation logic for Versioneer. 1368 | 1369 | For Git, this means creating/changing .gitattributes to mark _version.py 1370 | for export-subst keyword substitution. 1371 | """ 1372 | GITS = ["git"] 1373 | if sys.platform == "win32": 1374 | GITS = ["git.cmd", "git.exe"] 1375 | files = [versionfile_source] 1376 | if ipy: 1377 | files.append(ipy) 1378 | if "VERSIONEER_PEP518" not in globals(): 1379 | try: 1380 | my_path = __file__ 1381 | if my_path.endswith((".pyc", ".pyo")): 1382 | my_path = os.path.splitext(my_path)[0] + ".py" 1383 | versioneer_file = os.path.relpath(my_path) 1384 | except NameError: 1385 | versioneer_file = "versioneer.py" 1386 | files.append(versioneer_file) 1387 | present = False 1388 | try: 1389 | with open(".gitattributes", "r") as fobj: 1390 | for line in fobj: 1391 | if line.strip().startswith(versionfile_source): 1392 | if "export-subst" in line.strip().split()[1:]: 1393 | present = True 1394 | break 1395 | except OSError: 1396 | pass 1397 | if not present: 1398 | with open(".gitattributes", "a+") as fobj: 1399 | fobj.write("{} export-subst\n".format(versionfile_source)) 1400 | files.append(".gitattributes") 1401 | run_command(GITS, ["add", "--"] + files) 1402 | 1403 | 1404 | def versions_from_parentdir(parentdir_prefix, root, verbose): 1405 | """Try to determine the version from the parent directory name. 1406 | 1407 | Source tarballs conventionally unpack into a directory that includes both 1408 | the project name and a version string. We will also support searching up 1409 | two directory levels for an appropriately named parent directory 1410 | """ 1411 | rootdirs = [] 1412 | 1413 | for _ in range(3): 1414 | dirname = os.path.basename(root) 1415 | if dirname.startswith(parentdir_prefix): 1416 | return {"version": dirname[len(parentdir_prefix):], 1417 | "full-revisionid": None, 1418 | "dirty": False, "error": None, "date": None} 1419 | rootdirs.append(root) 1420 | root = os.path.dirname(root) # up a level 1421 | 1422 | if verbose: 1423 | print("Tried directories %s but none started with prefix %s" % 1424 | (str(rootdirs), parentdir_prefix)) 1425 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 1426 | 1427 | 1428 | SHORT_VERSION_PY = """ 1429 | # This file was generated by 'versioneer.py' (0.28) from 1430 | # revision-control system data, or from the parent directory name of an 1431 | # unpacked source archive. Distribution tarballs contain a pre-generated copy 1432 | # of this file. 1433 | 1434 | import json 1435 | 1436 | version_json = ''' 1437 | %s 1438 | ''' # END VERSION_JSON 1439 | 1440 | 1441 | def get_versions(): 1442 | return json.loads(version_json) 1443 | """ 1444 | 1445 | 1446 | def versions_from_file(filename): 1447 | """Try to determine the version from _version.py if present.""" 1448 | try: 1449 | with open(filename) as f: 1450 | contents = f.read() 1451 | except OSError: 1452 | raise NotThisMethod("unable to read _version.py") 1453 | mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", 1454 | contents, re.M | re.S) 1455 | if not mo: 1456 | mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", 1457 | contents, re.M | re.S) 1458 | if not mo: 1459 | raise NotThisMethod("no version_json in _version.py") 1460 | return json.loads(mo.group(1)) 1461 | 1462 | 1463 | def write_to_version_file(filename, versions): 1464 | """Write the given version number to the given _version.py file.""" 1465 | os.unlink(filename) 1466 | contents = json.dumps(versions, sort_keys=True, 1467 | indent=1, separators=(",", ": ")) 1468 | with open(filename, "w") as f: 1469 | f.write(SHORT_VERSION_PY % contents) 1470 | 1471 | print("set %s to '%s'" % (filename, versions["version"])) 1472 | 1473 | 1474 | def plus_or_dot(pieces): 1475 | """Return a + if we don't already have one, else return a .""" 1476 | if "+" in pieces.get("closest-tag", ""): 1477 | return "." 1478 | return "+" 1479 | 1480 | 1481 | def render_pep440(pieces): 1482 | """Build up version string, with post-release "local version identifier". 1483 | 1484 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 1485 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 1486 | 1487 | Exceptions: 1488 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 1489 | """ 1490 | if pieces["closest-tag"]: 1491 | rendered = pieces["closest-tag"] 1492 | if pieces["distance"] or pieces["dirty"]: 1493 | rendered += plus_or_dot(pieces) 1494 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 1495 | if pieces["dirty"]: 1496 | rendered += ".dirty" 1497 | else: 1498 | # exception #1 1499 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 1500 | pieces["short"]) 1501 | if pieces["dirty"]: 1502 | rendered += ".dirty" 1503 | return rendered 1504 | 1505 | 1506 | def render_pep440_branch(pieces): 1507 | """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . 1508 | 1509 | The ".dev0" means not master branch. Note that .dev0 sorts backwards 1510 | (a feature branch will appear "older" than the master branch). 1511 | 1512 | Exceptions: 1513 | 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] 1514 | """ 1515 | if pieces["closest-tag"]: 1516 | rendered = pieces["closest-tag"] 1517 | if pieces["distance"] or pieces["dirty"]: 1518 | if pieces["branch"] != "master": 1519 | rendered += ".dev0" 1520 | rendered += plus_or_dot(pieces) 1521 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 1522 | if pieces["dirty"]: 1523 | rendered += ".dirty" 1524 | else: 1525 | # exception #1 1526 | rendered = "0" 1527 | if pieces["branch"] != "master": 1528 | rendered += ".dev0" 1529 | rendered += "+untagged.%d.g%s" % (pieces["distance"], 1530 | pieces["short"]) 1531 | if pieces["dirty"]: 1532 | rendered += ".dirty" 1533 | return rendered 1534 | 1535 | 1536 | def pep440_split_post(ver): 1537 | """Split pep440 version string at the post-release segment. 1538 | 1539 | Returns the release segments before the post-release and the 1540 | post-release version number (or -1 if no post-release segment is present). 1541 | """ 1542 | vc = str.split(ver, ".post") 1543 | return vc[0], int(vc[1] or 0) if len(vc) == 2 else None 1544 | 1545 | 1546 | def render_pep440_pre(pieces): 1547 | """TAG[.postN.devDISTANCE] -- No -dirty. 1548 | 1549 | Exceptions: 1550 | 1: no tags. 0.post0.devDISTANCE 1551 | """ 1552 | if pieces["closest-tag"]: 1553 | if pieces["distance"]: 1554 | # update the post release segment 1555 | tag_version, post_version = pep440_split_post(pieces["closest-tag"]) 1556 | rendered = tag_version 1557 | if post_version is not None: 1558 | rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) 1559 | else: 1560 | rendered += ".post0.dev%d" % (pieces["distance"]) 1561 | else: 1562 | # no commits, use the tag as the version 1563 | rendered = pieces["closest-tag"] 1564 | else: 1565 | # exception #1 1566 | rendered = "0.post0.dev%d" % pieces["distance"] 1567 | return rendered 1568 | 1569 | 1570 | def render_pep440_post(pieces): 1571 | """TAG[.postDISTANCE[.dev0]+gHEX] . 1572 | 1573 | The ".dev0" means dirty. Note that .dev0 sorts backwards 1574 | (a dirty tree will appear "older" than the corresponding clean one), 1575 | but you shouldn't be releasing software with -dirty anyways. 1576 | 1577 | Exceptions: 1578 | 1: no tags. 0.postDISTANCE[.dev0] 1579 | """ 1580 | if pieces["closest-tag"]: 1581 | rendered = pieces["closest-tag"] 1582 | if pieces["distance"] or pieces["dirty"]: 1583 | rendered += ".post%d" % pieces["distance"] 1584 | if pieces["dirty"]: 1585 | rendered += ".dev0" 1586 | rendered += plus_or_dot(pieces) 1587 | rendered += "g%s" % pieces["short"] 1588 | else: 1589 | # exception #1 1590 | rendered = "0.post%d" % pieces["distance"] 1591 | if pieces["dirty"]: 1592 | rendered += ".dev0" 1593 | rendered += "+g%s" % pieces["short"] 1594 | return rendered 1595 | 1596 | 1597 | def render_pep440_post_branch(pieces): 1598 | """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . 1599 | 1600 | The ".dev0" means not master branch. 1601 | 1602 | Exceptions: 1603 | 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] 1604 | """ 1605 | if pieces["closest-tag"]: 1606 | rendered = pieces["closest-tag"] 1607 | if pieces["distance"] or pieces["dirty"]: 1608 | rendered += ".post%d" % pieces["distance"] 1609 | if pieces["branch"] != "master": 1610 | rendered += ".dev0" 1611 | rendered += plus_or_dot(pieces) 1612 | rendered += "g%s" % pieces["short"] 1613 | if pieces["dirty"]: 1614 | rendered += ".dirty" 1615 | else: 1616 | # exception #1 1617 | rendered = "0.post%d" % pieces["distance"] 1618 | if pieces["branch"] != "master": 1619 | rendered += ".dev0" 1620 | rendered += "+g%s" % pieces["short"] 1621 | if pieces["dirty"]: 1622 | rendered += ".dirty" 1623 | return rendered 1624 | 1625 | 1626 | def render_pep440_ricequant(pieces): 1627 | tag = pieces["closest-tag"] 1628 | parsed_tag = parse_version(tag) if tag else None 1629 | 1630 | working = working_version() 1631 | parsed_working = parse_version(working) 1632 | 1633 | rendered = working 1634 | 1635 | if tag: 1636 | # 当前开发系列正式版如果小于最近的tag,那么该tag肯定就是该系列的post版本了 1637 | # 当前开发版本应该总是大于最近的tag,那么当前到commit必须大于等于最近的commit 1638 | if parsed_working < parsed_tag: 1639 | if parsed_working.base_version == parsed_tag.base_version: 1640 | if not parsed_tag.is_postrelease: 1641 | raise Exception("Only post release tag allowed, tag %s" % tag) 1642 | if parsed_tag._version.post[0] != "post": 1643 | raise Exception("Only post release tag allowed, tag %s" % tag) 1644 | if pieces["distance"] > 0: 1645 | rendered += ".post%d" % (parsed_tag._version.post[1] + 1) 1646 | rendered += ".dev%d" % (pieces["distance"]) 1647 | elif pieces["distance"] == 0: 1648 | rendered += ".post%d" % parsed_tag._version.post[1] 1649 | else: 1650 | raise Exception("Developing version can not go back in time: %s < %s" % (working, tag)) 1651 | # 如果最近的tag是正式版tag,那么就是在开发该系列的.post1 1652 | elif parsed_working == parsed_tag: 1653 | if pieces["distance"] > 0: 1654 | rendered += ".post1.dev%d" % (pieces["distance"]) 1655 | # 如果正在开发到是一个新的系列,那么就从该系列的.dev0开始 1656 | else: 1657 | if pieces["distance"] >= 0: 1658 | rendered += ".dev%d" % (pieces["distance"]) 1659 | # 没有最近的tag等价于正在开发到是一个新的系列,那么就从该系列的.dev0开始 1660 | else: 1661 | if pieces["distance"] >= 0: 1662 | rendered += ".dev%d" % pieces["distance"] 1663 | 1664 | tracking_branch = git_tracking_branch() 1665 | # # 如果是dev和master分支或者hotfix分支来的,或者是一个tag,那就用pep440的版本号,否则带上git commit id 1666 | if tracking_branch in ["origin/develop", "origin/master"] or tracking_branch.startswith("origin/hotfix/") or pieces[ 1667 | "distance"] == 0: 1668 | if pieces["dirty"]: 1669 | rendered += ".dirty" 1670 | return rendered 1671 | 1672 | rendered += "+%s" % format(pieces["short"]) 1673 | if pieces["dirty"]: 1674 | rendered += ".dirty" 1675 | return rendered 1676 | 1677 | 1678 | def render_pep440_old(pieces): 1679 | """TAG[.postDISTANCE[.dev0]] . 1680 | 1681 | The ".dev0" means dirty. 1682 | 1683 | Exceptions: 1684 | 1: no tags. 0.postDISTANCE[.dev0] 1685 | """ 1686 | if pieces["closest-tag"]: 1687 | rendered = pieces["closest-tag"] 1688 | if pieces["distance"] or pieces["dirty"]: 1689 | rendered += ".post%d" % pieces["distance"] 1690 | if pieces["dirty"]: 1691 | rendered += ".dev0" 1692 | else: 1693 | # exception #1 1694 | rendered = "0.post%d" % pieces["distance"] 1695 | if pieces["dirty"]: 1696 | rendered += ".dev0" 1697 | return rendered 1698 | 1699 | 1700 | def render_git_describe(pieces): 1701 | """TAG[-DISTANCE-gHEX][-dirty]. 1702 | 1703 | Like 'git describe --tags --dirty --always'. 1704 | 1705 | Exceptions: 1706 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 1707 | """ 1708 | if pieces["closest-tag"]: 1709 | rendered = pieces["closest-tag"] 1710 | if pieces["distance"]: 1711 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 1712 | else: 1713 | # exception #1 1714 | rendered = pieces["short"] 1715 | if pieces["dirty"]: 1716 | rendered += "-dirty" 1717 | return rendered 1718 | 1719 | 1720 | def render_git_describe_long(pieces): 1721 | """TAG-DISTANCE-gHEX[-dirty]. 1722 | 1723 | Like 'git describe --tags --dirty --always -long'. 1724 | The distance/hash is unconditional. 1725 | 1726 | Exceptions: 1727 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 1728 | """ 1729 | if pieces["closest-tag"]: 1730 | rendered = pieces["closest-tag"] 1731 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 1732 | else: 1733 | # exception #1 1734 | rendered = pieces["short"] 1735 | if pieces["dirty"]: 1736 | rendered += "-dirty" 1737 | return rendered 1738 | 1739 | 1740 | def render(pieces, style): 1741 | """Render the given version pieces into the requested style.""" 1742 | if pieces["error"]: 1743 | return {"version": "unknown", 1744 | "full-revisionid": pieces.get("long"), 1745 | "dirty": None, 1746 | "error": pieces["error"], 1747 | "date": None} 1748 | 1749 | if not style or style == "default": 1750 | style = "pep440" # the default 1751 | 1752 | if style == "pep440": 1753 | rendered = render_pep440(pieces) 1754 | elif style == "pep440-ricequant": 1755 | rendered = render_pep440_ricequant(pieces) 1756 | elif style == "pep440-branch": 1757 | rendered = render_pep440_branch(pieces) 1758 | elif style == "pep440-pre": 1759 | rendered = render_pep440_pre(pieces) 1760 | elif style == "pep440-post": 1761 | rendered = render_pep440_post(pieces) 1762 | elif style == "pep440-post-branch": 1763 | rendered = render_pep440_post_branch(pieces) 1764 | elif style == "pep440-old": 1765 | rendered = render_pep440_old(pieces) 1766 | elif style == "git-describe": 1767 | rendered = render_git_describe(pieces) 1768 | elif style == "git-describe-long": 1769 | rendered = render_git_describe_long(pieces) 1770 | else: 1771 | raise ValueError("unknown style '%s'" % style) 1772 | 1773 | return {"version": rendered, "full-revisionid": pieces["long"], 1774 | "dirty": pieces["dirty"], "error": None, 1775 | "date": pieces.get("date")} 1776 | 1777 | 1778 | class VersioneerBadRootError(Exception): 1779 | """The project root directory is unknown or missing key files.""" 1780 | 1781 | 1782 | def get_versions(verbose=False): 1783 | """Get the project version from whatever source is available. 1784 | 1785 | Returns dict with two keys: 'version' and 'full'. 1786 | """ 1787 | if "versioneer" in sys.modules: 1788 | # see the discussion in cmdclass.py:get_cmdclass() 1789 | del sys.modules["versioneer"] 1790 | 1791 | root = get_root() 1792 | cfg = get_config_from_root(root) 1793 | 1794 | assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" 1795 | handlers = HANDLERS.get(cfg.VCS) 1796 | assert handlers, "unrecognized VCS '%s'" % cfg.VCS 1797 | verbose = verbose or cfg.verbose 1798 | assert cfg.versionfile_source is not None, \ 1799 | "please set versioneer.versionfile_source" 1800 | assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" 1801 | 1802 | versionfile_abs = os.path.join(root, cfg.versionfile_source) 1803 | 1804 | # extract version from first of: _version.py, VCS command (e.g. 'git 1805 | # describe'), parentdir. This is meant to work for developers using a 1806 | # source checkout, for users of a tarball created by 'setup.py sdist', 1807 | # and for users of a tarball/zipball created by 'git archive' or github's 1808 | # download-from-tag feature or the equivalent in other VCSes. 1809 | 1810 | get_keywords_f = handlers.get("get_keywords") 1811 | from_keywords_f = handlers.get("keywords") 1812 | if get_keywords_f and from_keywords_f: 1813 | try: 1814 | keywords = get_keywords_f(versionfile_abs) 1815 | ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) 1816 | if verbose: 1817 | print("got version from expanded keyword %s" % ver) 1818 | return ver 1819 | except NotThisMethod: 1820 | pass 1821 | 1822 | try: 1823 | ver = versions_from_file(versionfile_abs) 1824 | if verbose: 1825 | print("got version from file %s %s" % (versionfile_abs, ver)) 1826 | return ver 1827 | except NotThisMethod: 1828 | pass 1829 | 1830 | from_vcs_f = handlers.get("pieces_from_vcs") 1831 | if from_vcs_f: 1832 | try: 1833 | pieces = from_vcs_f(cfg.tag_prefix, root, verbose) 1834 | ver = render(pieces, cfg.style) 1835 | if verbose: 1836 | print("got version from VCS %s" % ver) 1837 | return ver 1838 | except NotThisMethod: 1839 | pass 1840 | 1841 | try: 1842 | if cfg.parentdir_prefix: 1843 | ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 1844 | if verbose: 1845 | print("got version from parentdir %s" % ver) 1846 | return ver 1847 | except NotThisMethod: 1848 | pass 1849 | 1850 | if verbose: 1851 | print("unable to compute version") 1852 | 1853 | return {"version": "0+unknown", "full-revisionid": None, 1854 | "dirty": None, "error": "unable to compute version", 1855 | "date": None} 1856 | 1857 | 1858 | def get_version(): 1859 | """Get the short version string for this project.""" 1860 | return get_versions()["version"] 1861 | 1862 | 1863 | def get_cmdclass(cmdclass=None): 1864 | """Get the custom setuptools subclasses used by Versioneer. 1865 | 1866 | If the package uses a different cmdclass (e.g. one from numpy), it 1867 | should be provide as an argument. 1868 | """ 1869 | if "versioneer" in sys.modules: 1870 | del sys.modules["versioneer"] 1871 | # this fixes the "python setup.py develop" case (also 'install' and 1872 | # 'easy_install .'), in which subdependencies of the main project are 1873 | # built (using setup.py bdist_egg) in the same python process. Assume 1874 | # a main project A and a dependency B, which use different versions 1875 | # of Versioneer. A's setup.py imports A's Versioneer, leaving it in 1876 | # sys.modules by the time B's setup.py is executed, causing B to run 1877 | # with the wrong versioneer. Setuptools wraps the sub-dep builds in a 1878 | # sandbox that restores sys.modules to it's pre-build state, so the 1879 | # parent is protected against the child's "import versioneer". By 1880 | # removing ourselves from sys.modules here, before the child build 1881 | # happens, we protect the child from the parent's versioneer too. 1882 | # Also see https://github.com/python-versioneer/python-versioneer/issues/52 1883 | 1884 | cmds = {} if cmdclass is None else cmdclass.copy() 1885 | 1886 | # we add "version" to setuptools 1887 | from setuptools import Command 1888 | 1889 | class cmd_version(Command): 1890 | description = "report generated version string" 1891 | user_options = [] 1892 | boolean_options = [] 1893 | 1894 | def initialize_options(self): 1895 | pass 1896 | 1897 | def finalize_options(self): 1898 | pass 1899 | 1900 | def run(self): 1901 | vers = get_versions(verbose=True) 1902 | print("Version: %s" % vers["version"]) 1903 | print(" full-revisionid: %s" % vers.get("full-revisionid")) 1904 | print(" dirty: %s" % vers.get("dirty")) 1905 | print(" date: %s" % vers.get("date")) 1906 | if vers["error"]: 1907 | print(" error: %s" % vers["error"]) 1908 | cmds["version"] = cmd_version 1909 | 1910 | # we override "build_py" in setuptools 1911 | # 1912 | # most invocation pathways end up running build_py: 1913 | # distutils/build -> build_py 1914 | # distutils/install -> distutils/build ->.. 1915 | # setuptools/bdist_wheel -> distutils/install ->.. 1916 | # setuptools/bdist_egg -> distutils/install_lib -> build_py 1917 | # setuptools/install -> bdist_egg ->.. 1918 | # setuptools/develop -> ? 1919 | # pip install: 1920 | # copies source tree to a tempdir before running egg_info/etc 1921 | # if .git isn't copied too, 'git describe' will fail 1922 | # then does setup.py bdist_wheel, or sometimes setup.py install 1923 | # setup.py egg_info -> ? 1924 | 1925 | # pip install -e . and setuptool/editable_wheel will invoke build_py 1926 | # but the build_py command is not expected to copy any files. 1927 | 1928 | # we override different "build_py" commands for both environments 1929 | if 'build_py' in cmds: 1930 | _build_py = cmds['build_py'] 1931 | else: 1932 | from setuptools.command.build_py import build_py as _build_py 1933 | 1934 | class cmd_build_py(_build_py): 1935 | def run(self): 1936 | root = get_root() 1937 | cfg = get_config_from_root(root) 1938 | versions = get_versions() 1939 | _build_py.run(self) 1940 | if getattr(self, "editable_mode", False): 1941 | # During editable installs `.py` and data files are 1942 | # not copied to build_lib 1943 | return 1944 | # now locate _version.py in the new build/ directory and replace 1945 | # it with an updated value 1946 | if cfg.versionfile_build: 1947 | target_versionfile = os.path.join(self.build_lib, 1948 | cfg.versionfile_build) 1949 | print("UPDATING %s" % target_versionfile) 1950 | write_to_version_file(target_versionfile, versions) 1951 | cmds["build_py"] = cmd_build_py 1952 | 1953 | if 'build_ext' in cmds: 1954 | _build_ext = cmds['build_ext'] 1955 | else: 1956 | from setuptools.command.build_ext import build_ext as _build_ext 1957 | 1958 | class cmd_build_ext(_build_ext): 1959 | def run(self): 1960 | root = get_root() 1961 | cfg = get_config_from_root(root) 1962 | versions = get_versions() 1963 | _build_ext.run(self) 1964 | if self.inplace: 1965 | # build_ext --inplace will only build extensions in 1966 | # build/lib<..> dir with no _version.py to write to. 1967 | # As in place builds will already have a _version.py 1968 | # in the module dir, we do not need to write one. 1969 | return 1970 | # now locate _version.py in the new build/ directory and replace 1971 | # it with an updated value 1972 | if not cfg.versionfile_build: 1973 | return 1974 | target_versionfile = os.path.join(self.build_lib, 1975 | cfg.versionfile_build) 1976 | if not os.path.exists(target_versionfile): 1977 | print("Warning: {} does not exist, skipping " 1978 | "version update. This can happen if you are running build_ext " 1979 | "without first running build_py.".format(target_versionfile)) 1980 | return 1981 | print("UPDATING %s" % target_versionfile) 1982 | write_to_version_file(target_versionfile, versions) 1983 | cmds["build_ext"] = cmd_build_ext 1984 | 1985 | if "cx_Freeze" in sys.modules: # cx_freeze enabled? 1986 | from cx_Freeze.dist import build_exe as _build_exe 1987 | # nczeczulin reports that py2exe won't like the pep440-style string 1988 | # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. 1989 | # setup(console=[{ 1990 | # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION 1991 | # "product_version": versioneer.get_version(), 1992 | # ... 1993 | 1994 | class cmd_build_exe(_build_exe): 1995 | def run(self): 1996 | root = get_root() 1997 | cfg = get_config_from_root(root) 1998 | versions = get_versions() 1999 | target_versionfile = cfg.versionfile_source 2000 | print("UPDATING %s" % target_versionfile) 2001 | write_to_version_file(target_versionfile, versions) 2002 | 2003 | _build_exe.run(self) 2004 | os.unlink(target_versionfile) 2005 | with open(cfg.versionfile_source, "w") as f: 2006 | LONG = LONG_VERSION_PY[cfg.VCS] 2007 | f.write(LONG % 2008 | {"DOLLAR": "$", 2009 | "STYLE": cfg.style, 2010 | "TAG_PREFIX": cfg.tag_prefix, 2011 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 2012 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 2013 | }) 2014 | cmds["build_exe"] = cmd_build_exe 2015 | del cmds["build_py"] 2016 | 2017 | if 'py2exe' in sys.modules: # py2exe enabled? 2018 | try: 2019 | from py2exe.setuptools_buildexe import py2exe as _py2exe 2020 | except ImportError: 2021 | from py2exe.distutils_buildexe import py2exe as _py2exe 2022 | 2023 | class cmd_py2exe(_py2exe): 2024 | def run(self): 2025 | root = get_root() 2026 | cfg = get_config_from_root(root) 2027 | versions = get_versions() 2028 | target_versionfile = cfg.versionfile_source 2029 | print("UPDATING %s" % target_versionfile) 2030 | write_to_version_file(target_versionfile, versions) 2031 | 2032 | _py2exe.run(self) 2033 | os.unlink(target_versionfile) 2034 | with open(cfg.versionfile_source, "w") as f: 2035 | LONG = LONG_VERSION_PY[cfg.VCS] 2036 | f.write(LONG % 2037 | {"DOLLAR": "$", 2038 | "STYLE": cfg.style, 2039 | "TAG_PREFIX": cfg.tag_prefix, 2040 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 2041 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 2042 | }) 2043 | cmds["py2exe"] = cmd_py2exe 2044 | 2045 | # sdist farms its file list building out to egg_info 2046 | if 'egg_info' in cmds: 2047 | _egg_info = cmds['egg_info'] 2048 | else: 2049 | from setuptools.command.egg_info import egg_info as _egg_info 2050 | 2051 | class cmd_egg_info(_egg_info, object): 2052 | def find_sources(self): 2053 | # egg_info.find_sources builds the manifest list and writes it 2054 | # in one shot 2055 | super(cmd_egg_info, self).find_sources() 2056 | 2057 | # Modify the filelist and normalize it 2058 | root = get_root() 2059 | cfg = get_config_from_root(root) 2060 | self.filelist.append('versioneer.py') 2061 | if cfg.versionfile_source: 2062 | # There are rare cases where versionfile_source might not be 2063 | # included by default, so we must be explicit 2064 | self.filelist.append(cfg.versionfile_source) 2065 | self.filelist.sort() 2066 | self.filelist.remove_duplicates() 2067 | 2068 | # The write method is hidden in the manifest_maker instance that 2069 | # generated the filelist and was thrown away 2070 | # We will instead replicate their final normalization (to unicode, 2071 | # and POSIX-style paths) 2072 | from setuptools import unicode_utils 2073 | normalized = [unicode_utils.filesys_decode(f).replace(os.sep, '/') 2074 | for f in self.filelist.files] 2075 | 2076 | manifest_filename = os.path.join(self.egg_info, 'SOURCES.txt') 2077 | with open(manifest_filename, 'w') as fobj: 2078 | fobj.write('\n'.join(normalized)) 2079 | 2080 | cmds['egg_info'] = cmd_egg_info 2081 | 2082 | # we override different "sdist" commands for both environments 2083 | if 'sdist' in cmds: 2084 | _sdist = cmds['sdist'] 2085 | else: 2086 | from setuptools.command.sdist import sdist as _sdist 2087 | 2088 | class cmd_sdist(_sdist): 2089 | def run(self): 2090 | versions = get_versions() 2091 | self._versioneer_generated_versions = versions 2092 | # unless we update this, the command will keep using the old 2093 | # version 2094 | self.distribution.metadata.version = versions["version"] 2095 | return _sdist.run(self) 2096 | 2097 | def make_release_tree(self, base_dir, files): 2098 | root = get_root() 2099 | cfg = get_config_from_root(root) 2100 | _sdist.make_release_tree(self, base_dir, files) 2101 | # now locate _version.py in the new base_dir directory 2102 | # (remembering that it may be a hardlink) and replace it with an 2103 | # updated value 2104 | target_versionfile = os.path.join(base_dir, cfg.versionfile_source) 2105 | print("UPDATING %s" % target_versionfile) 2106 | write_to_version_file(target_versionfile, 2107 | self._versioneer_generated_versions) 2108 | cmds["sdist"] = cmd_sdist 2109 | 2110 | return cmds 2111 | 2112 | 2113 | CONFIG_ERROR = """ 2114 | setup.cfg is missing the necessary Versioneer configuration. You need 2115 | a section like: 2116 | 2117 | [versioneer] 2118 | VCS = git 2119 | style = pep440 2120 | versionfile_source = src/myproject/_version.py 2121 | versionfile_build = myproject/_version.py 2122 | tag_prefix = 2123 | parentdir_prefix = myproject- 2124 | 2125 | You will also need to edit your setup.py to use the results: 2126 | 2127 | import versioneer 2128 | setup(version=versioneer.get_version(), 2129 | cmdclass=versioneer.get_cmdclass(), ...) 2130 | 2131 | Please read the docstring in ./versioneer.py for configuration instructions, 2132 | edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. 2133 | """ 2134 | 2135 | SAMPLE_CONFIG = """ 2136 | # See the docstring in versioneer.py for instructions. Note that you must 2137 | # re-run 'versioneer.py setup' after changing this section, and commit the 2138 | # resulting files. 2139 | 2140 | [versioneer] 2141 | #VCS = git 2142 | #style = pep440 2143 | #versionfile_source = 2144 | #versionfile_build = 2145 | #tag_prefix = 2146 | #parentdir_prefix = 2147 | 2148 | """ 2149 | 2150 | OLD_SNIPPET = """ 2151 | from ._version import get_versions 2152 | __version__ = get_versions()['version'] 2153 | del get_versions 2154 | """ 2155 | 2156 | INIT_PY_SNIPPET = """ 2157 | from . import {0} 2158 | __version__ = {0}.get_versions()['version'] 2159 | """ 2160 | 2161 | 2162 | def do_setup(): 2163 | """Do main VCS-independent setup function for installing Versioneer.""" 2164 | root = get_root() 2165 | try: 2166 | cfg = get_config_from_root(root) 2167 | except (OSError, configparser.NoSectionError, 2168 | configparser.NoOptionError) as e: 2169 | if isinstance(e, (OSError, configparser.NoSectionError)): 2170 | print("Adding sample versioneer config to setup.cfg") 2171 | with open(os.path.join(root, "setup.cfg"), "a") as f: 2172 | f.write(SAMPLE_CONFIG) 2173 | print(CONFIG_ERROR) 2174 | return 1 2175 | 2176 | print(" creating %s" % cfg.versionfile_source) 2177 | with open(cfg.versionfile_source, "w") as f: 2178 | LONG = LONG_VERSION_PY[cfg.VCS] 2179 | f.write(LONG % {"DOLLAR": "$", 2180 | "STYLE": cfg.style, 2181 | "TAG_PREFIX": cfg.tag_prefix, 2182 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 2183 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 2184 | }) 2185 | 2186 | ipy = os.path.join(os.path.dirname(cfg.versionfile_source), 2187 | "__init__.py") 2188 | if os.path.exists(ipy): 2189 | try: 2190 | with open(ipy, "r") as f: 2191 | old = f.read() 2192 | except OSError: 2193 | old = "" 2194 | module = os.path.splitext(os.path.basename(cfg.versionfile_source))[0] 2195 | snippet = INIT_PY_SNIPPET.format(module) 2196 | if OLD_SNIPPET in old: 2197 | print(" replacing boilerplate in %s" % ipy) 2198 | with open(ipy, "w") as f: 2199 | f.write(old.replace(OLD_SNIPPET, snippet)) 2200 | elif snippet not in old: 2201 | print(" appending to %s" % ipy) 2202 | with open(ipy, "a") as f: 2203 | f.write(snippet) 2204 | else: 2205 | print(" %s unmodified" % ipy) 2206 | else: 2207 | print(" %s doesn't exist, ok" % ipy) 2208 | ipy = None 2209 | 2210 | # Make VCS-specific changes. For git, this means creating/changing 2211 | # .gitattributes to mark _version.py for export-subst keyword 2212 | # substitution. 2213 | do_vcs_install(cfg.versionfile_source, ipy) 2214 | return 0 2215 | 2216 | 2217 | def scan_setup_py(): 2218 | """Validate the contents of setup.py against Versioneer's expectations.""" 2219 | found = set() 2220 | setters = False 2221 | errors = 0 2222 | with open("setup.py", "r") as f: 2223 | for line in f.readlines(): 2224 | if "import versioneer" in line: 2225 | found.add("import") 2226 | if "versioneer.get_cmdclass()" in line: 2227 | found.add("cmdclass") 2228 | if "versioneer.get_version()" in line: 2229 | found.add("get_version") 2230 | if "versioneer.VCS" in line: 2231 | setters = True 2232 | if "versioneer.versionfile_source" in line: 2233 | setters = True 2234 | if len(found) != 3: 2235 | print("") 2236 | print("Your setup.py appears to be missing some important items") 2237 | print("(but I might be wrong). Please make sure it has something") 2238 | print("roughly like the following:") 2239 | print("") 2240 | print(" import versioneer") 2241 | print(" setup( version=versioneer.get_version(),") 2242 | print(" cmdclass=versioneer.get_cmdclass(), ...)") 2243 | print("") 2244 | errors += 1 2245 | if setters: 2246 | print("You should remove lines like 'versioneer.VCS = ' and") 2247 | print("'versioneer.versionfile_source = ' . This configuration") 2248 | print("now lives in setup.cfg, and should be removed from setup.py") 2249 | print("") 2250 | errors += 1 2251 | return errors 2252 | 2253 | 2254 | def setup_command(): 2255 | """Set up Versioneer and exit with appropriate error code.""" 2256 | errors = do_setup() 2257 | errors += scan_setup_py() 2258 | sys.exit(1 if errors else 0) 2259 | 2260 | 2261 | def get_metadata(): 2262 | setup_cfg = os.path.join(get_root(), "setup.cfg") 2263 | parser = configparser.RawConfigParser() 2264 | with open(setup_cfg, "r") as f: 2265 | parser.read_file(f) 2266 | 2267 | def get(section, parser, name): 2268 | if parser.has_option(section, name): 2269 | return parser.get(section, name) 2270 | return None 2271 | 2272 | return { 2273 | "name": parser.get("metadata", "name"), 2274 | "version": parser.get("metadata", "version") 2275 | } 2276 | 2277 | 2278 | def package_name(): 2279 | return get_metadata()["name"] 2280 | 2281 | 2282 | def working_version(): 2283 | return get_metadata()["version"] 2284 | 2285 | if __name__ == "__main__": 2286 | cmd = sys.argv[1] 2287 | if cmd == "setup": 2288 | setup_command() 2289 | --------------------------------------------------------------------------------