├── .circleci └── config.yml ├── .codacy.yml ├── .coveragerc ├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── appveyor.yml ├── conda.recipe └── meta.yaml ├── images └── sample.PNG ├── lazyprofiler ├── GetStats.py ├── __init__.py ├── __main__.py ├── _version.py └── cli.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py └── test_cli.py └── versioneer.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: continuumio/miniconda3 6 | 7 | working_directory: ~/repo 8 | 9 | steps: 10 | - checkout 11 | 12 | 13 | # - restore_cache: 14 | # keys: 15 | # - v1-dependencies-{{ checksum "environment.yml" }} 16 | # - v1-dependencies- 17 | 18 | 19 | - run: 20 | name: install dependencies 21 | command: | 22 | # conda env create -q || conda env update -q 23 | # source activate adj 24 | conda install -qy conda-build anaconda-client pytest pytest-cov 25 | conda config --set auto_update_conda no 26 | conda info -a 27 | conda build conda.recipe --no-test 28 | conda install --use-local lazyprofiler 29 | 30 | 31 | # - save_cache: 32 | # paths: 33 | # - /opt/conda 34 | # key: v1-dependencies-{{ checksum "environment.yml" }} 35 | 36 | 37 | - run: 38 | name: run tests 39 | command: | 40 | # source activate adj 41 | pytest --color=yes -v --cov=lazyprofiler tests 42 | conda install -c conda-forge codecov 43 | codecov 44 | 45 | - store_artifacts: 46 | path: test-reports 47 | destination: test-reports 48 | -------------------------------------------------------------------------------- /.codacy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - 'lazyprofiler/_version.py' 4 | - 'tests/**/*' 5 | - 'tests/*' 6 | - 'benchmarks/**/*' 7 | - 'setup.py' 8 | - 'versioneer.py' 9 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | setup.py 4 | lazyprofiler/__main__.py 5 | lazyprofiler/_version.py 6 | versioneer.py 7 | tests/* 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | lazyprofiler/_version.py export-subst 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | # pyenv python configuration file 62 | .python-version 63 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '3.6' 4 | - '3.7' 5 | - '3.8' 6 | - '3.9' 7 | install: 8 | - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then wget https://repo.continuum.io/miniconda/Miniconda2-latest-Linux-x86_64.sh 9 | -O miniconda.sh; else wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh 10 | -O miniconda.sh; fi 11 | - bash miniconda.sh -b -p $HOME/miniconda 12 | - export PATH="$HOME/miniconda/bin:$PATH" 13 | - hash -r 14 | - conda config --set always_yes yes 15 | - conda update -q conda 16 | - conda install conda-build anaconda-client pytest pytest-cov 17 | - conda config --set auto_update_conda no 18 | - conda build conda.recipe --no-test 19 | - conda install --use-local lazyprofiler 20 | - conda info -a 21 | script: 22 | - pytest -v --color=yes --cov=lazyprofiler tests 23 | after_success: 24 | - conda install -c conda-forge codecov 25 | - codecov 26 | deploy: 27 | provider: pypi 28 | distributions: sdist bdist_wheel 29 | skip_existing: true 30 | user: __token__ 31 | password: 32 | secure: S/S0Z8JxptmIzu7ro4Y1OJY04KDc94Cm8T3T4b+x1xfbS8QnjIS4mtDLFq2qtEKEc0fL7Kl+ZzdAruVwd96eNIwdxCOOtCkAuKlSzORHVqa/GQmtUpRLPzb1cGc98uFavFqbwM1odvf2rltuH3HjCrokaDXbFwFIVonKh/fldeDCfqcJt0hQTHtINwmAIUdNNo7HYDSr44HLEsPmCcyWLR4f4l2D+JQm4Lv4ZiaJnPMWHJR4F5gh33ZRQ8WPI6eO8yveMAI+7T71YyQ3afZl3Na5XxN219jsuH4A/1+tCSE/7xQWmCCmIrFYXZjzsKO2XE77P0DvBrFGm/15jX3rSdDPdoWRCtToCrlPACa7g0p8uJ0X61mXdXRY2ozcLCKgj2BsfalEste5Z61WKzO2pmj7Pp4P6gp4XOWtJoCYVNO+hnlfR/KppudwdnMhTQ4mLGTJjdmMPp6gdoOZme717fsGMwxMApgSNXYYNeI8OmVCpbtnsTCH6lwvrC6H2yqUT4Uk07nqf6c8K322mdtMlktXKAqMJaHJea+x++bWvhyQCYpH6bnoWYSxD1DsrY55nKPMt0sQLhTFHAcnQJ2Q5XH3HbinEWi7eVCkQ3aDcZ4Mq/KR0qdCbasvGmdwM+kii44aqgoNiSXG9OWLgNGjD0Uc4YTai4PgUrZBsHA6tf8= 33 | on: 34 | tags: true 35 | repo: shankarpandala/lazyprofiler 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021, Shankar Rao Pandala 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include versioneer.py 2 | include lazyprofiler/_version.py 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lazyprofiler 2 | [![TravisCI](https://img.shields.io/travis/shankarpandala/lazyprofiler.svg)](https://travis-ci.org/shankarpandala/lazyprofiler) 3 | [![CircleCI](https://circleci.com/gh/shankarpandala/lazyprofiler.svg?style=svg)](https://circleci.com/gh/shankarpandala/lazyprofiler) 4 | [![CodeCov](https://codecov.io/gh/shankarpandala/lazyprofiler/branch/master/graph/badge.svg)](https://codecov.io/gh/shankarpandala/lazyprofiler) 5 | [![Downloads](https://pepy.tech/badge/lazyprofiler)](https://pepy.tech/project/lazyprofiler) 6 | ------------------------------------------- 7 | 8 | Lazy Profiler is a simple utility to collect CPU, GPU, RAM and GPU Memory stats while the program is running. 9 | 10 | 11 | ## Installation 12 | 13 | Use the package manager [pip](https://pip.pypa.io/en/stable/) to install foobar. 14 | 15 | ```bash 16 | pip install lazyprofiler 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```python 22 | import lazyprofiler.GetStats as gs 23 | pid = gs.start_log("test") 24 | """ 25 | Do something in between 26 | """ 27 | gs.stop_log(pid=pid) 28 | gs.plot_stats('test') 29 | ``` 30 | ![Sample Output](/images/sample.PNG) 31 | 32 | ## Contributing 33 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 34 | 35 | Please make sure to update tests as appropriate. 36 | 37 | ## License 38 | [MIT](https://choosealicense.com/licenses/mit/) 39 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | global: 3 | # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the 4 | # /E:ON and /V:ON options are not enabled in the batch script intepreter 5 | # See: http://stackoverflow.com/a/13751649/163740 6 | CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\tools\\appveyor\\run_with_env.cmd" 7 | 8 | matrix: 9 | - PYTHON: "C:\\Miniconda36-x64" 10 | PYTHON_VERSION: "3.6" 11 | PYTHON_ARCH: "64" 12 | 13 | - PYTHON: "C:\\Miniconda36-x64" 14 | PYTHON_VERSION: "3.7" 15 | PYTHON_ARCH: "64" 16 | 17 | - PYTHON: "C:\\Miniconda36-x64" 18 | PYTHON_VERSION: "3.8" 19 | PYTHON_ARCH: "64" 20 | 21 | - PYTHON: "C:\\Miniconda36-x64" 22 | PYTHON_VERSION: "3.9" 23 | PYTHON_ARCH: "64" 24 | 25 | init: 26 | - ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH% %HOME% 27 | 28 | 29 | install: 30 | # If there is a newer build queued for the same PR, cancel this one. 31 | # The AppVeyor 'rollout builds' option is supposed to serve the same 32 | # purpose but it is problematic because it tends to cancel builds pushed 33 | # directly to master instead of just PR builds (or the converse). 34 | # credits: JuliaLang developers. 35 | - ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod ` 36 | https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | ` 37 | Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { ` 38 | throw "There are newer queued builds for this pull request, failing early." } 39 | # these correspond to folder naming of miniconda installs on appveyor. See 40 | # https://www.appveyor.com/docs/installed-software#python 41 | - if "%PYTHON_VERSION%" == "3.6" set "BASE_PYTHON_VERSION=36" 42 | - if "%PYTHON_ARCH%" == "64" set "ARCH_LABEL=-x64" 43 | - call "C:\Miniconda%BASE_PYTHON_VERSION%%ARCH_LABEL%\Scripts\activate.bat" 44 | - conda config --set always_yes yes 45 | - conda update -q conda 46 | - conda config --set auto_update_conda no 47 | - conda update -q --all 48 | - conda install -q pytest pytest-cov conda-build anaconda-client 49 | - conda info 50 | # this is to ensure dependencies 51 | - conda build conda.recipe --no-test 52 | - conda install --use-local lazyprofiler 53 | 54 | 55 | # Not a .NET project, we build package in the install step instead 56 | build: false 57 | 58 | test_script: 59 | - py.test --color=yes -v --cov lazyprofiler --cov-report xml tests 60 | 61 | on_success: 62 | - conda install -c conda-forge codecov 63 | - codecov --env PYTHON_VERSION --file C:\projects\lazyprofiler\coverage.xml 64 | -------------------------------------------------------------------------------- /conda.recipe/meta.yaml: -------------------------------------------------------------------------------- 1 | 2 | {% set data = load_setup_py_data() %} 3 | 4 | package: 5 | name: lazyprofiler 6 | 7 | version: {{ data['version'] }} 8 | 9 | source: 10 | path: .. 11 | 12 | build: 13 | # If the installation is complex, or different between Unix and Windows, use 14 | # separate bld.bat and build.sh files instead of this key. Add the line 15 | # "skip: True # [py<35]" (for example) to limit to Python 3.5 and newer, or 16 | # "skip: True # [not win]" to limit to Windows. 17 | script: {{ PYTHON }} -m pip install --no-deps --ignore-installed -vv . 18 | noarch: python 19 | 20 | entry_points: 21 | {% for entry in data['entry_points']['console_scripts'] %} 22 | - {{ entry.split('=')[0].strip() }} = {{ entry.split('=')[1].strip() }} 23 | {% endfor %} 24 | 25 | 26 | requirements: 27 | # if you need compilers, uncomment these 28 | # read more at https://docs.conda.io/projects/conda-build/en/latest/resources/compiler-tools.html 29 | # build: 30 | # - {{ compilers('c') }} 31 | host: 32 | - python 33 | - pip 34 | run: 35 | - python 36 | # dependencies are defined in setup.py 37 | {% for dep in data['install_requires'] %} 38 | - {{ dep.lower() }} 39 | {% endfor %} 40 | {# raw is for ignoring templating with cookiecutter, leaving it for use with conda-build #} 41 | 42 | test: 43 | source_files: 44 | - tests 45 | requires: 46 | - pytest 47 | - pytest-cov 48 | commands: 49 | - pytest tests 50 | 51 | about: 52 | home: https://github.com/shankarpandala/lazyprofiler 53 | summary: Lazy Profiler is a simple utility to collect CPU, GPU, RAM and GPU Memorystats while the program is running. 54 | license: {{ data.get('license') }} 55 | license_file: LICENSE 56 | -------------------------------------------------------------------------------- /images/sample.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shankarpandala/lazyprofiler/64427a7243c6f1b8c560fefe7a916237be37c4a4/images/sample.PNG -------------------------------------------------------------------------------- /lazyprofiler/GetStats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # log_gpu_cpu_stats 4 | # Logs GPU and CPU stats either to stdout, or to a CSV file. 5 | # See usage below. 6 | 7 | # Copyright (c) 2019, Scott C. Lowe 8 | # All rights reserved. 9 | # 10 | # Redistribution and use in source and binary forms, with or without 11 | # modification, are permitted provided that the following conditions are 12 | # met: 13 | # * Redistributions of source code must retain the above copyright 14 | # notice, this list of conditions and the following disclaimer. 15 | # * Redistributions in binary form must reproduce the above copyright 16 | # notice, this list of conditions and the following disclaimer in 17 | # the documentation and/or other materials provided with the distribution 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 23 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | # POSSIBILITY OF SUCH DAMAGE. 30 | 31 | from __future__ import print_function 32 | 33 | import datetime 34 | import psutil 35 | import subprocess 36 | import sys 37 | import time 38 | import contextlib 39 | import os 40 | import pandas as pd 41 | import math 42 | import multiprocessing as mp 43 | 44 | 45 | @contextlib.contextmanager 46 | def smart_open(filename=None, mode="a"): 47 | """ 48 | Context manager which can handle writing to stdout as well as files. 49 | 50 | If filename is None, the handle yielded is to stdout. If writing to 51 | stdout, the connection is not closed when leaving the context. 52 | """ 53 | if filename: 54 | hf = open(filename, mode) 55 | else: 56 | hf = sys.stdout 57 | 58 | try: 59 | yield hf 60 | finally: 61 | if hf is not sys.stdout: 62 | hf.close() 63 | 64 | 65 | def check_nvidia_smi(): 66 | try: 67 | subprocess.check_output(["nvidia-smi"]) 68 | except FileNotFoundError: 69 | raise EnvironmentError("The nvidia-smi command could not be found.") 70 | 71 | 72 | try: 73 | check_nvidia_smi() 74 | except: 75 | pass 76 | 77 | 78 | class Logger: 79 | def __init__( 80 | self, 81 | fname=None, 82 | style=None, 83 | date_format=None, 84 | refresh_interval=1, 85 | iter_limit=None, 86 | show_header=True, 87 | header_only_once=True, 88 | show_units=True, 89 | sep=",", 90 | ): 91 | 92 | self.fname = fname if fname else None 93 | if style is not None: 94 | self.style = style 95 | elif self.fname is None: 96 | self.style = "tabular" 97 | else: 98 | self.style = "csv" 99 | self.date_format = date_format 100 | self.refresh_interval = refresh_interval 101 | self.iter_limit = iter_limit 102 | self.show_header = show_header 103 | self.header_only_once = header_only_once 104 | self.header_count = 0 105 | self.show_units = show_units 106 | self.sep = sep 107 | self.col_width = 10 108 | self.time_field_width = ( 109 | 15 110 | if self.date_format is None 111 | else max(self.col_width, len(time.strftime(self.date_format))) 112 | ) 113 | 114 | if self.date_format is None: 115 | self.time_field_name = "Timestamp" + (" (s)" if self.show_units else "") 116 | else: 117 | self.time_field_name = "Time" 118 | 119 | self.cpu_field_names = [ 120 | "CPU" + (" (%)" if self.show_units else ""), 121 | "RAM" + (" (%)" if self.show_units else ""), 122 | "Swap" + (" (%)" if self.show_units else ""), 123 | ] 124 | self.gpu_field_names = [ 125 | "GPU" + (" (%)" if self.show_units else ""), 126 | "Mem" + (" (%)" if self.show_units else ""), 127 | "Temp" + (" (C)" if self.show_units else ""), 128 | ] 129 | self.gpu_queries = [ 130 | "utilization.gpu", 131 | "utilization.memory", 132 | "temperature.gpu", 133 | ] 134 | self.gpu_query = ",".join(self.gpu_queries) 135 | 136 | self.gpu_names = self.get_gpu_names() 137 | 138 | def get_gpu_names(self): 139 | try: 140 | res = subprocess.check_output(["nvidia-smi", "-L"]) 141 | return [i_res for i_res in res.decode().split("\n") if i_res != ""] 142 | except: 143 | pass 144 | 145 | @property 146 | def tabular_format(self): 147 | fmt = "{:>" + str(self.time_field_width) + "} |" 148 | fmt += ("|{:>" + str(self.col_width) + "} ") * len(self.cpu_field_names) 149 | try: 150 | for i_gpu in range(len(self.gpu_names)): 151 | fmt += "|" 152 | fmt += ("|{:>" + str(self.col_width) + "} ") * len(self.gpu_field_names) 153 | except: 154 | pass 155 | return fmt 156 | 157 | def write_header_csv(self): 158 | """ 159 | Write header in CSV format. 160 | """ 161 | with smart_open(self.fname, "a") as hf: 162 | print(self.time_field_name + self.sep, end="", file=hf) 163 | print(*self.cpu_field_names, sep=self.sep, end="", file=hf) 164 | try: 165 | for i_gpu in range(len(self.gpu_names)): 166 | print(self.sep, end="", file=hf) 167 | print( 168 | *["{}:{}".format(i_gpu, fn) for fn in self.gpu_field_names], 169 | sep=self.sep, 170 | end="", 171 | file=hf 172 | ) 173 | except: 174 | pass 175 | print("\n", end="", file=hf) # add a newline 176 | 177 | def write_header_tabular(self): 178 | """ 179 | Write header in tabular format. 180 | """ 181 | with smart_open(self.fname, "a") as hf: 182 | cols = [self.time_field_name] 183 | cols += self.cpu_field_names 184 | try: 185 | for i_gpu in range(len(self.gpu_names)): 186 | cols += ["{}:{}".format(i_gpu, fn) for fn in self.gpu_field_names] 187 | except: 188 | pass 189 | 190 | print(self.tabular_format.format(*cols), file=hf) 191 | 192 | # Add separating line 193 | print("-" * (self.time_field_width + 1), end="", file=hf) 194 | print( 195 | "+", 196 | ("+" + "-" * (self.col_width + 1)) * len(self.cpu_field_names), 197 | sep="", 198 | end="", 199 | file=hf, 200 | ) 201 | try: 202 | for i_gpu in range(len(self.gpu_names)): 203 | print( 204 | "+", 205 | ("+" + "-" * (self.col_width + 1)) * len(self.gpu_field_names), 206 | sep="", 207 | end="", 208 | file=hf, 209 | ) 210 | except: 211 | pass 212 | print("\n", end="", file=hf) # add a newline 213 | 214 | def write_header(self): 215 | if self.style == "csv": 216 | self.write_header_csv() 217 | elif self.style == "tabular": 218 | self.write_header_tabular() 219 | else: 220 | raise ValueError("Unrecognised style: {}".format(self.style)) 221 | self.header_count += 1 222 | 223 | def poll_cpu(self): 224 | """ 225 | Fetch current CPU, RAM and Swap utilisation 226 | 227 | Returns 228 | ------- 229 | float 230 | CPU utilisation (percentage) 231 | float 232 | RAM utilisation (percentage) 233 | float 234 | Swap utilisation (percentage) 235 | """ 236 | return ( 237 | psutil.cpu_percent(), 238 | psutil.virtual_memory().percent, 239 | psutil.swap_memory().percent, 240 | ) 241 | 242 | def poll_gpus(self, flatten=False): 243 | """ 244 | Query GPU utilisation, and sanitise results 245 | 246 | Returns 247 | ------- 248 | list of lists of utilisation stats 249 | For each GPU (outer list), there is a list of utilisations 250 | corresponding to each query (inner list), as a string. 251 | """ 252 | res = subprocess.check_output( 253 | [ 254 | "nvidia-smi", 255 | "--query-gpu=" + self.gpu_query, 256 | "--format=csv,nounits,noheader", 257 | ] 258 | ) 259 | lines = [i_res for i_res in res.decode().split("\n") if i_res != ""] 260 | data = [ 261 | [ 262 | val.strip() if "Not Supported" not in val else "N/A" 263 | for val in line.split(",") 264 | ] 265 | for line in lines 266 | ] 267 | if flatten: 268 | data = [y for row in data for y in row] 269 | return data 270 | 271 | def write_record(self): 272 | with smart_open(self.fname, "a") as hf: 273 | stats = list(self.poll_cpu()) 274 | if self.date_format is None: 275 | t = "{:.3f}".format(time.time()) 276 | else: 277 | t = time.strftime(self.date_format) 278 | stats.insert(0, t) 279 | try: 280 | stats += self.poll_gpus(flatten=True) 281 | except: 282 | pass 283 | if self.style == "csv": 284 | print(",".join([str(stat) for stat in stats]), file=hf) 285 | elif self.style == "tabular": 286 | print(self.tabular_format.format(*stats), file=hf) 287 | else: 288 | raise ValueError("Unrecognised style: {}".format(self.style)) 289 | 290 | def __call__(self, n_iter=None): 291 | if self.show_header and (self.header_count < 1 or not self.header_only_once): 292 | self.write_header() 293 | n_iter = self.iter_limit if n_iter is None else n_iter 294 | i_iter = 0 295 | while True: 296 | t_begin = time.time() 297 | self.write_record() 298 | i_iter += 1 299 | if n_iter is not None and n_iter > 0 and i_iter >= n_iter: 300 | break 301 | t_sleep = self.refresh_interval + t_begin - time.time() - 0.001 302 | if t_sleep > 0: 303 | time.sleep(t_sleep) 304 | 305 | def start_log(file_name=None): 306 | if file_name is not None: 307 | if os.path.isfile(file_name + ".csv"): 308 | os.remove(file_name + ".csv") 309 | logger_fname = file_name + ".csv" 310 | else: 311 | if os.path.isfile("log_compute.csv"): 312 | os.remove("log_compute.csv") 313 | logger_fname = "log_compute.csv" 314 | 315 | proc = mp.Process(target=Logger(fname=logger_fname, refresh_interval=0.2)) 316 | proc.start() 317 | 318 | print("Started logging compute utilisation") 319 | return proc 320 | 321 | 322 | def stop_log(pid=None): 323 | pid.terminate() 324 | print("Terminated the compute utilisation logger background process") 325 | 326 | 327 | def plot_stats(file_name=None, save_plot=False): 328 | if file_name is not None: 329 | logger_fname = file_name + ".csv" 330 | else: 331 | logger_fname = "log_compute.csv" 332 | log_data = pd.read_csv(logger_fname) 333 | log_data["Timestamp (s)"] = pd.to_datetime(log_data["Timestamp (s)"], unit="s") 334 | log_data.set_index("Timestamp (s)", inplace=True) 335 | plot_rows = int(math.ceil(len(log_data.columns) / 3)) 336 | 337 | if save_plot is True: 338 | return log_data.plot( 339 | subplots=True, layout=(plot_rows, 3), figsize=(16, 4 * plot_rows) 340 | ) 341 | else: 342 | log_data.plot(subplots=True, layout=(plot_rows, 3), figsize=(16, 4 * plot_rows)) 343 | -------------------------------------------------------------------------------- /lazyprofiler/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import get_versions 2 | __version__ = get_versions()['version'] 3 | del get_versions 4 | -------------------------------------------------------------------------------- /lazyprofiler/__main__.py: -------------------------------------------------------------------------------- 1 | from lazyprofiler import cli 2 | 3 | cli.cli() 4 | -------------------------------------------------------------------------------- /lazyprofiler/_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.19 (https://github.com/python-versioneer/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 = " (HEAD -> master, dev)" 27 | git_full = "64427a7243c6f1b8c560fefe7a916237be37c4a4" 28 | git_date = "2021-02-17 14:05:37 +0530" 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 = "" 44 | cfg.tag_prefix = "" 45 | cfg.parentdir_prefix = "lazyprofiler-" 46 | cfg.versionfile_source = "lazyprofiler/_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 | """Create decorator to mark a method as the handler of a 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().decode() 97 | if p.returncode != 0: 98 | if verbose: 99 | print("unable to run %s (error)" % dispcmd) 100 | print("stdout was %s" % stdout) 101 | return None, p.returncode 102 | return stdout, p.returncode 103 | 104 | 105 | def versions_from_parentdir(parentdir_prefix, root, verbose): 106 | """Try to determine the version from the parent directory name. 107 | 108 | Source tarballs conventionally unpack into a directory that includes both 109 | the project name and a version string. We will also support searching up 110 | two directory levels for an appropriately named parent directory 111 | """ 112 | rootdirs = [] 113 | 114 | for i in range(3): 115 | dirname = os.path.basename(root) 116 | if dirname.startswith(parentdir_prefix): 117 | return {"version": dirname[len(parentdir_prefix):], 118 | "full-revisionid": None, 119 | "dirty": False, "error": None, "date": None} 120 | else: 121 | rootdirs.append(root) 122 | root = os.path.dirname(root) # up a level 123 | 124 | if verbose: 125 | print("Tried directories %s but none started with prefix %s" % 126 | (str(rootdirs), parentdir_prefix)) 127 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 128 | 129 | 130 | @register_vcs_handler("git", "get_keywords") 131 | def git_get_keywords(versionfile_abs): 132 | """Extract version information from the given file.""" 133 | # the code embedded in _version.py can just fetch the value of these 134 | # keywords. When used from setup.py, we don't want to import _version.py, 135 | # so we do it with a regexp instead. This function is not used from 136 | # _version.py. 137 | keywords = {} 138 | try: 139 | f = open(versionfile_abs, "r") 140 | for line in f.readlines(): 141 | if line.strip().startswith("git_refnames ="): 142 | mo = re.search(r'=\s*"(.*)"', line) 143 | if mo: 144 | keywords["refnames"] = mo.group(1) 145 | if line.strip().startswith("git_full ="): 146 | mo = re.search(r'=\s*"(.*)"', line) 147 | if mo: 148 | keywords["full"] = mo.group(1) 149 | if line.strip().startswith("git_date ="): 150 | mo = re.search(r'=\s*"(.*)"', line) 151 | if mo: 152 | keywords["date"] = mo.group(1) 153 | f.close() 154 | except EnvironmentError: 155 | pass 156 | return keywords 157 | 158 | 159 | @register_vcs_handler("git", "keywords") 160 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 161 | """Get version information from git keywords.""" 162 | if not keywords: 163 | raise NotThisMethod("no keywords at all, weird") 164 | date = keywords.get("date") 165 | if date is not None: 166 | # Use only the last line. Previous lines may contain GPG signature 167 | # information. 168 | date = date.splitlines()[-1] 169 | 170 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 171 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 172 | # -like" string, which we must then edit to make compliant), because 173 | # it's been around since git-1.5.3, and it's too difficult to 174 | # discover which version we're using, or to work around using an 175 | # older one. 176 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 177 | refnames = keywords["refnames"].strip() 178 | if refnames.startswith("$Format"): 179 | if verbose: 180 | print("keywords are unexpanded, not using") 181 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 182 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 183 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 184 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 185 | TAG = "tag: " 186 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 187 | if not tags: 188 | # Either we're using git < 1.8.3, or there really are no tags. We use 189 | # a heuristic: assume all version tags have a digit. The old git %d 190 | # expansion behaves like git log --decorate=short and strips out the 191 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 192 | # between branches and tags. By ignoring refnames without digits, we 193 | # filter out many common branch names like "release" and 194 | # "stabilization", as well as "HEAD" and "master". 195 | tags = set([r for r in refs if re.search(r'\d', r)]) 196 | if verbose: 197 | print("discarding '%s', no digits" % ",".join(refs - tags)) 198 | if verbose: 199 | print("likely tags: %s" % ",".join(sorted(tags))) 200 | for ref in sorted(tags): 201 | # sorting will prefer e.g. "2.0" over "2.0rc1" 202 | if ref.startswith(tag_prefix): 203 | r = ref[len(tag_prefix):] 204 | if verbose: 205 | print("picking %s" % r) 206 | return {"version": r, 207 | "full-revisionid": keywords["full"].strip(), 208 | "dirty": False, "error": None, 209 | "date": date} 210 | # no suitable tags, so version is "0+unknown", but full hex is still there 211 | if verbose: 212 | print("no suitable tags, using unknown + full revision id") 213 | return {"version": "0+unknown", 214 | "full-revisionid": keywords["full"].strip(), 215 | "dirty": False, "error": "no suitable tags", "date": None} 216 | 217 | 218 | @register_vcs_handler("git", "pieces_from_vcs") 219 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 220 | """Get version from 'git describe' in the root of the source tree. 221 | 222 | This only gets called if the git-archive 'subst' keywords were *not* 223 | expanded, and _version.py hasn't already been rewritten with a short 224 | version string, meaning we're inside a checked out source tree. 225 | """ 226 | GITS = ["git"] 227 | if sys.platform == "win32": 228 | GITS = ["git.cmd", "git.exe"] 229 | 230 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, 231 | hide_stderr=True) 232 | if rc != 0: 233 | if verbose: 234 | print("Directory %s not under git control" % root) 235 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 236 | 237 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 238 | # if there isn't one, this yields HEX[-dirty] (no NUM) 239 | describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", 240 | "--always", "--long", 241 | "--match", "%s*" % tag_prefix], 242 | cwd=root) 243 | # --long was added in git-1.5.5 244 | if describe_out is None: 245 | raise NotThisMethod("'git describe' failed") 246 | describe_out = describe_out.strip() 247 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 248 | if full_out is None: 249 | raise NotThisMethod("'git rev-parse' failed") 250 | full_out = full_out.strip() 251 | 252 | pieces = {} 253 | pieces["long"] = full_out 254 | pieces["short"] = full_out[:7] # maybe improved later 255 | pieces["error"] = None 256 | 257 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 258 | # TAG might have hyphens. 259 | git_describe = describe_out 260 | 261 | # look for -dirty suffix 262 | dirty = git_describe.endswith("-dirty") 263 | pieces["dirty"] = dirty 264 | if dirty: 265 | git_describe = git_describe[:git_describe.rindex("-dirty")] 266 | 267 | # now we have TAG-NUM-gHEX or HEX 268 | 269 | if "-" in git_describe: 270 | # TAG-NUM-gHEX 271 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 272 | if not mo: 273 | # unparseable. Maybe git-describe is misbehaving? 274 | pieces["error"] = ("unable to parse git-describe output: '%s'" 275 | % describe_out) 276 | return pieces 277 | 278 | # tag 279 | full_tag = mo.group(1) 280 | if not full_tag.startswith(tag_prefix): 281 | if verbose: 282 | fmt = "tag '%s' doesn't start with prefix '%s'" 283 | print(fmt % (full_tag, tag_prefix)) 284 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 285 | % (full_tag, tag_prefix)) 286 | return pieces 287 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 288 | 289 | # distance: number of commits since tag 290 | pieces["distance"] = int(mo.group(2)) 291 | 292 | # commit: short hex revision ID 293 | pieces["short"] = mo.group(3) 294 | 295 | else: 296 | # HEX: no tags 297 | pieces["closest-tag"] = None 298 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], 299 | cwd=root) 300 | pieces["distance"] = int(count_out) # total number of commits 301 | 302 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 303 | date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], 304 | cwd=root)[0].strip() 305 | # Use only the last line. Previous lines may contain GPG signature 306 | # information. 307 | date = date.splitlines()[-1] 308 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 309 | 310 | return pieces 311 | 312 | 313 | def plus_or_dot(pieces): 314 | """Return a + if we don't already have one, else return a .""" 315 | if "+" in pieces.get("closest-tag", ""): 316 | return "." 317 | return "+" 318 | 319 | 320 | def render_pep440(pieces): 321 | """Build up version string, with post-release "local version identifier". 322 | 323 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 324 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 325 | 326 | Exceptions: 327 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 328 | """ 329 | if pieces["closest-tag"]: 330 | rendered = pieces["closest-tag"] 331 | if pieces["distance"] or pieces["dirty"]: 332 | rendered += plus_or_dot(pieces) 333 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 334 | if pieces["dirty"]: 335 | rendered += ".dirty" 336 | else: 337 | # exception #1 338 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 339 | pieces["short"]) 340 | if pieces["dirty"]: 341 | rendered += ".dirty" 342 | return rendered 343 | 344 | 345 | def render_pep440_pre(pieces): 346 | """TAG[.post0.devDISTANCE] -- No -dirty. 347 | 348 | Exceptions: 349 | 1: no tags. 0.post0.devDISTANCE 350 | """ 351 | if pieces["closest-tag"]: 352 | rendered = pieces["closest-tag"] 353 | if pieces["distance"]: 354 | rendered += ".post0.dev%d" % pieces["distance"] 355 | else: 356 | # exception #1 357 | rendered = "0.post0.dev%d" % pieces["distance"] 358 | return rendered 359 | 360 | 361 | def render_pep440_post(pieces): 362 | """TAG[.postDISTANCE[.dev0]+gHEX] . 363 | 364 | The ".dev0" means dirty. Note that .dev0 sorts backwards 365 | (a dirty tree will appear "older" than the corresponding clean one), 366 | but you shouldn't be releasing software with -dirty anyways. 367 | 368 | Exceptions: 369 | 1: no tags. 0.postDISTANCE[.dev0] 370 | """ 371 | if pieces["closest-tag"]: 372 | rendered = pieces["closest-tag"] 373 | if pieces["distance"] or pieces["dirty"]: 374 | rendered += ".post%d" % pieces["distance"] 375 | if pieces["dirty"]: 376 | rendered += ".dev0" 377 | rendered += plus_or_dot(pieces) 378 | rendered += "g%s" % pieces["short"] 379 | else: 380 | # exception #1 381 | rendered = "0.post%d" % pieces["distance"] 382 | if pieces["dirty"]: 383 | rendered += ".dev0" 384 | rendered += "+g%s" % pieces["short"] 385 | return rendered 386 | 387 | 388 | def render_pep440_old(pieces): 389 | """TAG[.postDISTANCE[.dev0]] . 390 | 391 | The ".dev0" means dirty. 392 | 393 | Exceptions: 394 | 1: no tags. 0.postDISTANCE[.dev0] 395 | """ 396 | if pieces["closest-tag"]: 397 | rendered = pieces["closest-tag"] 398 | if pieces["distance"] or pieces["dirty"]: 399 | rendered += ".post%d" % pieces["distance"] 400 | if pieces["dirty"]: 401 | rendered += ".dev0" 402 | else: 403 | # exception #1 404 | rendered = "0.post%d" % pieces["distance"] 405 | if pieces["dirty"]: 406 | rendered += ".dev0" 407 | return rendered 408 | 409 | 410 | def render_git_describe(pieces): 411 | """TAG[-DISTANCE-gHEX][-dirty]. 412 | 413 | Like 'git describe --tags --dirty --always'. 414 | 415 | Exceptions: 416 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 417 | """ 418 | if pieces["closest-tag"]: 419 | rendered = pieces["closest-tag"] 420 | if pieces["distance"]: 421 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 422 | else: 423 | # exception #1 424 | rendered = pieces["short"] 425 | if pieces["dirty"]: 426 | rendered += "-dirty" 427 | return rendered 428 | 429 | 430 | def render_git_describe_long(pieces): 431 | """TAG-DISTANCE-gHEX[-dirty]. 432 | 433 | Like 'git describe --tags --dirty --always -long'. 434 | The distance/hash is unconditional. 435 | 436 | Exceptions: 437 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 438 | """ 439 | if pieces["closest-tag"]: 440 | rendered = pieces["closest-tag"] 441 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 442 | else: 443 | # exception #1 444 | rendered = pieces["short"] 445 | if pieces["dirty"]: 446 | rendered += "-dirty" 447 | return rendered 448 | 449 | 450 | def render(pieces, style): 451 | """Render the given version pieces into the requested style.""" 452 | if pieces["error"]: 453 | return {"version": "unknown", 454 | "full-revisionid": pieces.get("long"), 455 | "dirty": None, 456 | "error": pieces["error"], 457 | "date": None} 458 | 459 | if not style or style == "default": 460 | style = "pep440" # the default 461 | 462 | if style == "pep440": 463 | rendered = render_pep440(pieces) 464 | elif style == "pep440-pre": 465 | rendered = render_pep440_pre(pieces) 466 | elif style == "pep440-post": 467 | rendered = render_pep440_post(pieces) 468 | elif style == "pep440-old": 469 | rendered = render_pep440_old(pieces) 470 | elif style == "git-describe": 471 | rendered = render_git_describe(pieces) 472 | elif style == "git-describe-long": 473 | rendered = render_git_describe_long(pieces) 474 | else: 475 | raise ValueError("unknown style '%s'" % style) 476 | 477 | return {"version": rendered, "full-revisionid": pieces["long"], 478 | "dirty": pieces["dirty"], "error": None, 479 | "date": pieces.get("date")} 480 | 481 | 482 | def get_versions(): 483 | """Get version information or return default if unable to do so.""" 484 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 485 | # __file__, we can work backwards from there to the root. Some 486 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 487 | # case we can only use expanded keywords. 488 | 489 | cfg = get_config() 490 | verbose = cfg.verbose 491 | 492 | try: 493 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 494 | verbose) 495 | except NotThisMethod: 496 | pass 497 | 498 | try: 499 | root = os.path.realpath(__file__) 500 | # versionfile_source is the relative path from the top of the source 501 | # tree (where the .git directory might live) to this file. Invert 502 | # this to find the root from __file__. 503 | for i in cfg.versionfile_source.split('/'): 504 | root = os.path.dirname(root) 505 | except NameError: 506 | return {"version": "0+unknown", "full-revisionid": None, 507 | "dirty": None, 508 | "error": "unable to find root of source tree", 509 | "date": None} 510 | 511 | try: 512 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 513 | return render(pieces, cfg.style) 514 | except NotThisMethod: 515 | pass 516 | 517 | try: 518 | if cfg.parentdir_prefix: 519 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 520 | except NotThisMethod: 521 | pass 522 | 523 | return {"version": "0+unknown", "full-revisionid": None, 524 | "dirty": None, 525 | "error": "unable to compute version", "date": None} 526 | -------------------------------------------------------------------------------- /lazyprofiler/cli.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from lazyprofiler import __version__ 3 | 4 | def cli(args=None): 5 | p = ArgumentParser( 6 | description="Lazy Profiler is a simple utility to collect CPU, GPU, RAM and GPU Memorystats while the program is running.", 7 | conflict_handler='resolve' 8 | ) 9 | p.add_argument( 10 | '-V', '--version', 11 | action='version', 12 | help='Show the conda-prefix-replacement version number and exit.', 13 | version="lazyprofiler %s" % __version__, 14 | ) 15 | 16 | args = p.parse_args(args) 17 | 18 | # do something with the args 19 | print("CLI template - fix me up!") 20 | 21 | # No return value means no error. 22 | # Return a value of 1 or higher to signify an error. 23 | # See https://docs.python.org/3/library/sys.html#sys.exit 24 | 25 | 26 | if __name__ == '__main__': 27 | import sys 28 | cli(sys.argv[1:]) 29 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | ignore = E122,E123,E126,E127,E128,E731,E722 4 | exclude = build,lazyprofiler/_version.py,tests,conda.recipe,.git,versioneer.py,benchmarks,.asv 5 | 6 | [tool:pytest] 7 | norecursedirs= .* *.egg* build dist conda.recipe 8 | addopts = 9 | --junitxml=junit.xml 10 | --ignore setup.py 11 | --ignore run_test.py 12 | --cov-report term-missing 13 | --tb native 14 | --strict 15 | --durations=20 16 | env = 17 | PYTHONHASHSEED=0 18 | markers = 19 | serial: execute test serially (to avoid race conditions) 20 | 21 | [versioneer] 22 | VCS = git 23 | versionfile_source = lazyprofiler/_version.py 24 | versionfile_build = lazyprofiler/_version.py 25 | tag_prefix = 26 | parentdir_prefix = lazyprofiler- 27 | 28 | [bdist_wheel] 29 | universal=1 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import versioneer 3 | 4 | requirements = [ 5 | # package requirements go here 6 | ] 7 | 8 | setup( 9 | name='lazyprofiler', 10 | version=versioneer.get_version(), 11 | cmdclass=versioneer.get_cmdclass(), 12 | description="Lazy Profiler is a simple utility to collect CPU, GPU, RAM and GPU Memorystats while the program is running.", 13 | license="MIT", 14 | author="Shankar Rao Pandala", 15 | author_email='shankar.pandala@gmail.com', 16 | url='https://github.com/shankarpandala/lazyprofiler', 17 | packages=['lazyprofiler'], 18 | entry_points={ 19 | 'console_scripts': [ 20 | 'lazyprofiler=lazyprofiler.cli:cli' 21 | ] 22 | }, 23 | install_requires=requirements, 24 | keywords='lazyprofiler', 25 | classifiers=[ 26 | 'Programming Language :: Python :: 3.6', 27 | 'Programming Language :: Python :: 3.7', 28 | 'Programming Language :: Python :: 3.8', 29 | 'Programming Language :: Python :: 3.9', 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shankarpandala/lazyprofiler/64427a7243c6f1b8c560fefe7a916237be37c4a4/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | from lazyprofiler import cli 2 | 3 | def test_cli_template(): 4 | pass 5 | # assert cli.cli() is None -------------------------------------------------------------------------------- /versioneer.py: -------------------------------------------------------------------------------- 1 | 2 | # Version: 0.19 3 | 4 | """The Versioneer - like a rocketeer, but for versions. 5 | 6 | The Versioneer 7 | ============== 8 | 9 | * like a rocketeer, but for versions! 10 | * https://github.com/python-versioneer/python-versioneer 11 | * Brian Warner 12 | * License: Public Domain 13 | * Compatible with: Python 3.6, 3.7, 3.8, 3.9 and pypy3 14 | * [![Latest Version][pypi-image]][pypi-url] 15 | * [![Build Status][travis-image]][travis-url] 16 | 17 | This is a tool for managing a recorded version number in distutils-based 18 | python projects. The goal is to remove the tedious and error-prone "update 19 | the embedded version string" step from your release process. Making a new 20 | release should be as easy as recording a new tag in your version-control 21 | system, and maybe making new tarballs. 22 | 23 | 24 | ## Quick Install 25 | 26 | * `pip install versioneer` to somewhere in your $PATH 27 | * add a `[versioneer]` section to your setup.cfg (see [Install](INSTALL.md)) 28 | * run `versioneer install` in your source tree, commit the results 29 | * Verify version information with `python setup.py version` 30 | 31 | ## Version Identifiers 32 | 33 | Source trees come from a variety of places: 34 | 35 | * a version-control system checkout (mostly used by developers) 36 | * a nightly tarball, produced by build automation 37 | * a snapshot tarball, produced by a web-based VCS browser, like github's 38 | "tarball from tag" feature 39 | * a release tarball, produced by "setup.py sdist", distributed through PyPI 40 | 41 | Within each source tree, the version identifier (either a string or a number, 42 | this tool is format-agnostic) can come from a variety of places: 43 | 44 | * ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows 45 | about recent "tags" and an absolute revision-id 46 | * the name of the directory into which the tarball was unpacked 47 | * an expanded VCS keyword ($Id$, etc) 48 | * a `_version.py` created by some earlier build step 49 | 50 | For released software, the version identifier is closely related to a VCS 51 | tag. Some projects use tag names that include more than just the version 52 | string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool 53 | needs to strip the tag prefix to extract the version identifier. For 54 | unreleased software (between tags), the version identifier should provide 55 | enough information to help developers recreate the same tree, while also 56 | giving them an idea of roughly how old the tree is (after version 1.2, before 57 | version 1.3). Many VCS systems can report a description that captures this, 58 | for example `git describe --tags --dirty --always` reports things like 59 | "0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the 60 | 0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has 61 | uncommitted changes). 62 | 63 | The version identifier is used for multiple purposes: 64 | 65 | * to allow the module to self-identify its version: `myproject.__version__` 66 | * to choose a name and prefix for a 'setup.py sdist' tarball 67 | 68 | ## Theory of Operation 69 | 70 | Versioneer works by adding a special `_version.py` file into your source 71 | tree, where your `__init__.py` can import it. This `_version.py` knows how to 72 | dynamically ask the VCS tool for version information at import time. 73 | 74 | `_version.py` also contains `$Revision$` markers, and the installation 75 | process marks `_version.py` to have this marker rewritten with a tag name 76 | during the `git archive` command. As a result, generated tarballs will 77 | contain enough information to get the proper version. 78 | 79 | To allow `setup.py` to compute a version too, a `versioneer.py` is added to 80 | the top level of your source tree, next to `setup.py` and the `setup.cfg` 81 | that configures it. This overrides several distutils/setuptools commands to 82 | compute the version when invoked, and changes `setup.py build` and `setup.py 83 | sdist` to replace `_version.py` with a small static file that contains just 84 | the generated version data. 85 | 86 | ## Installation 87 | 88 | See [INSTALL.md](./INSTALL.md) for detailed installation instructions. 89 | 90 | ## Version-String Flavors 91 | 92 | Code which uses Versioneer can learn about its version string at runtime by 93 | importing `_version` from your main `__init__.py` file and running the 94 | `get_versions()` function. From the "outside" (e.g. in `setup.py`), you can 95 | import the top-level `versioneer.py` and run `get_versions()`. 96 | 97 | Both functions return a dictionary with different flavors of version 98 | information: 99 | 100 | * `['version']`: A condensed version string, rendered using the selected 101 | style. This is the most commonly used value for the project's version 102 | string. The default "pep440" style yields strings like `0.11`, 103 | `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section 104 | below for alternative styles. 105 | 106 | * `['full-revisionid']`: detailed revision identifier. For Git, this is the 107 | full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". 108 | 109 | * `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the 110 | commit date in ISO 8601 format. This will be None if the date is not 111 | available. 112 | 113 | * `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that 114 | this is only accurate if run in a VCS checkout, otherwise it is likely to 115 | be False or None 116 | 117 | * `['error']`: if the version string could not be computed, this will be set 118 | to a string describing the problem, otherwise it will be None. It may be 119 | useful to throw an exception in setup.py if this is set, to avoid e.g. 120 | creating tarballs with a version string of "unknown". 121 | 122 | Some variants are more useful than others. Including `full-revisionid` in a 123 | bug report should allow developers to reconstruct the exact code being tested 124 | (or indicate the presence of local changes that should be shared with the 125 | developers). `version` is suitable for display in an "about" box or a CLI 126 | `--version` output: it can be easily compared against release notes and lists 127 | of bugs fixed in various releases. 128 | 129 | The installer adds the following text to your `__init__.py` to place a basic 130 | version in `YOURPROJECT.__version__`: 131 | 132 | from ._version import get_versions 133 | __version__ = get_versions()['version'] 134 | del get_versions 135 | 136 | ## Styles 137 | 138 | The setup.cfg `style=` configuration controls how the VCS information is 139 | rendered into a version string. 140 | 141 | The default style, "pep440", produces a PEP440-compliant string, equal to the 142 | un-prefixed tag name for actual releases, and containing an additional "local 143 | version" section with more detail for in-between builds. For Git, this is 144 | TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags 145 | --dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the 146 | tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and 147 | that this commit is two revisions ("+2") beyond the "0.11" tag. For released 148 | software (exactly equal to a known tag), the identifier will only contain the 149 | stripped tag, e.g. "0.11". 150 | 151 | Other styles are available. See [details.md](details.md) in the Versioneer 152 | source tree for descriptions. 153 | 154 | ## Debugging 155 | 156 | Versioneer tries to avoid fatal errors: if something goes wrong, it will tend 157 | to return a version of "0+unknown". To investigate the problem, run `setup.py 158 | version`, which will run the version-lookup code in a verbose mode, and will 159 | display the full contents of `get_versions()` (including the `error` string, 160 | which may help identify what went wrong). 161 | 162 | ## Known Limitations 163 | 164 | Some situations are known to cause problems for Versioneer. This details the 165 | most significant ones. More can be found on Github 166 | [issues page](https://github.com/python-versioneer/python-versioneer/issues). 167 | 168 | ### Subprojects 169 | 170 | Versioneer has limited support for source trees in which `setup.py` is not in 171 | the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are 172 | two common reasons why `setup.py` might not be in the root: 173 | 174 | * Source trees which contain multiple subprojects, such as 175 | [Buildbot](https://github.com/buildbot/buildbot), which contains both 176 | "master" and "slave" subprojects, each with their own `setup.py`, 177 | `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI 178 | distributions (and upload multiple independently-installable tarballs). 179 | * Source trees whose main purpose is to contain a C library, but which also 180 | provide bindings to Python (and perhaps other languages) in subdirectories. 181 | 182 | Versioneer will look for `.git` in parent directories, and most operations 183 | should get the right version string. However `pip` and `setuptools` have bugs 184 | and implementation details which frequently cause `pip install .` from a 185 | subproject directory to fail to find a correct version string (so it usually 186 | defaults to `0+unknown`). 187 | 188 | `pip install --editable .` should work correctly. `setup.py install` might 189 | work too. 190 | 191 | Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in 192 | some later version. 193 | 194 | [Bug #38](https://github.com/python-versioneer/python-versioneer/issues/38) is tracking 195 | this issue. The discussion in 196 | [PR #61](https://github.com/python-versioneer/python-versioneer/pull/61) describes the 197 | issue from the Versioneer side in more detail. 198 | [pip PR#3176](https://github.com/pypa/pip/pull/3176) and 199 | [pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve 200 | pip to let Versioneer work correctly. 201 | 202 | Versioneer-0.16 and earlier only looked for a `.git` directory next to the 203 | `setup.cfg`, so subprojects were completely unsupported with those releases. 204 | 205 | ### Editable installs with setuptools <= 18.5 206 | 207 | `setup.py develop` and `pip install --editable .` allow you to install a 208 | project into a virtualenv once, then continue editing the source code (and 209 | test) without re-installing after every change. 210 | 211 | "Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a 212 | convenient way to specify executable scripts that should be installed along 213 | with the python package. 214 | 215 | These both work as expected when using modern setuptools. When using 216 | setuptools-18.5 or earlier, however, certain operations will cause 217 | `pkg_resources.DistributionNotFound` errors when running the entrypoint 218 | script, which must be resolved by re-installing the package. This happens 219 | when the install happens with one version, then the egg_info data is 220 | regenerated while a different version is checked out. Many setup.py commands 221 | cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into 222 | a different virtualenv), so this can be surprising. 223 | 224 | [Bug #83](https://github.com/python-versioneer/python-versioneer/issues/83) describes 225 | this one, but upgrading to a newer version of setuptools should probably 226 | resolve it. 227 | 228 | 229 | ## Updating Versioneer 230 | 231 | To upgrade your project to a new release of Versioneer, do the following: 232 | 233 | * install the new Versioneer (`pip install -U versioneer` or equivalent) 234 | * edit `setup.cfg`, if necessary, to include any new configuration settings 235 | indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. 236 | * re-run `versioneer install` in your source tree, to replace 237 | `SRC/_version.py` 238 | * commit any changed files 239 | 240 | ## Future Directions 241 | 242 | This tool is designed to make it easily extended to other version-control 243 | systems: all VCS-specific components are in separate directories like 244 | src/git/ . The top-level `versioneer.py` script is assembled from these 245 | components by running make-versioneer.py . In the future, make-versioneer.py 246 | will take a VCS name as an argument, and will construct a version of 247 | `versioneer.py` that is specific to the given VCS. It might also take the 248 | configuration arguments that are currently provided manually during 249 | installation by editing setup.py . Alternatively, it might go the other 250 | direction and include code from all supported VCS systems, reducing the 251 | number of intermediate scripts. 252 | 253 | ## Similar projects 254 | 255 | * [setuptools_scm](https://github.com/pypa/setuptools_scm/) - a non-vendored build-time 256 | dependency 257 | * [minver](https://github.com/jbweston/miniver) - a lightweight reimplementation of 258 | versioneer 259 | 260 | ## License 261 | 262 | To make Versioneer easier to embed, all its code is dedicated to the public 263 | domain. The `_version.py` that it creates is also in the public domain. 264 | Specifically, both are released under the Creative Commons "Public Domain 265 | Dedication" license (CC0-1.0), as described in 266 | https://creativecommons.org/publicdomain/zero/1.0/ . 267 | 268 | [pypi-image]: https://img.shields.io/pypi/v/versioneer.svg 269 | [pypi-url]: https://pypi.python.org/pypi/versioneer/ 270 | [travis-image]: 271 | https://img.shields.io/travis/com/python-versioneer/python-versioneer.svg 272 | [travis-url]: https://travis-ci.com/github/python-versioneer/python-versioneer 273 | 274 | """ 275 | 276 | import configparser 277 | import errno 278 | import json 279 | import os 280 | import re 281 | import subprocess 282 | import sys 283 | 284 | 285 | class VersioneerConfig: 286 | """Container for Versioneer configuration parameters.""" 287 | 288 | 289 | def get_root(): 290 | """Get the project root directory. 291 | 292 | We require that all commands are run from the project root, i.e. the 293 | directory that contains setup.py, setup.cfg, and versioneer.py . 294 | """ 295 | root = os.path.realpath(os.path.abspath(os.getcwd())) 296 | setup_py = os.path.join(root, "setup.py") 297 | versioneer_py = os.path.join(root, "versioneer.py") 298 | if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): 299 | # allow 'python path/to/setup.py COMMAND' 300 | root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) 301 | setup_py = os.path.join(root, "setup.py") 302 | versioneer_py = os.path.join(root, "versioneer.py") 303 | if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): 304 | err = ("Versioneer was unable to run the project root directory. " 305 | "Versioneer requires setup.py to be executed from " 306 | "its immediate directory (like 'python setup.py COMMAND'), " 307 | "or in a way that lets it use sys.argv[0] to find the root " 308 | "(like 'python path/to/setup.py COMMAND').") 309 | raise VersioneerBadRootError(err) 310 | try: 311 | # Certain runtime workflows (setup.py install/develop in a setuptools 312 | # tree) execute all dependencies in a single python process, so 313 | # "versioneer" may be imported multiple times, and python's shared 314 | # module-import table will cache the first one. So we can't use 315 | # os.path.dirname(__file__), as that will find whichever 316 | # versioneer.py was first imported, even in later projects. 317 | me = os.path.realpath(os.path.abspath(__file__)) 318 | me_dir = os.path.normcase(os.path.splitext(me)[0]) 319 | vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) 320 | if me_dir != vsr_dir: 321 | print("Warning: build in %s is using versioneer.py from %s" 322 | % (os.path.dirname(me), versioneer_py)) 323 | except NameError: 324 | pass 325 | return root 326 | 327 | 328 | def get_config_from_root(root): 329 | """Read the project setup.cfg file to determine Versioneer config.""" 330 | # This might raise EnvironmentError (if setup.cfg is missing), or 331 | # configparser.NoSectionError (if it lacks a [versioneer] section), or 332 | # configparser.NoOptionError (if it lacks "VCS="). See the docstring at 333 | # the top of versioneer.py for instructions on writing your setup.cfg . 334 | setup_cfg = os.path.join(root, "setup.cfg") 335 | parser = configparser.ConfigParser() 336 | with open(setup_cfg, "r") as f: 337 | parser.read_file(f) 338 | VCS = parser.get("versioneer", "VCS") # mandatory 339 | 340 | def get(parser, name): 341 | if parser.has_option("versioneer", name): 342 | return parser.get("versioneer", name) 343 | return None 344 | cfg = VersioneerConfig() 345 | cfg.VCS = VCS 346 | cfg.style = get(parser, "style") or "" 347 | cfg.versionfile_source = get(parser, "versionfile_source") 348 | cfg.versionfile_build = get(parser, "versionfile_build") 349 | cfg.tag_prefix = get(parser, "tag_prefix") 350 | if cfg.tag_prefix in ("''", '""'): 351 | cfg.tag_prefix = "" 352 | cfg.parentdir_prefix = get(parser, "parentdir_prefix") 353 | cfg.verbose = get(parser, "verbose") 354 | return cfg 355 | 356 | 357 | class NotThisMethod(Exception): 358 | """Exception raised if a method is not valid for the current scenario.""" 359 | 360 | 361 | # these dictionaries contain VCS-specific tools 362 | LONG_VERSION_PY = {} 363 | HANDLERS = {} 364 | 365 | 366 | def register_vcs_handler(vcs, method): # decorator 367 | """Create decorator to mark a method as the handler of a VCS.""" 368 | def decorate(f): 369 | """Store f in HANDLERS[vcs][method].""" 370 | if vcs not in HANDLERS: 371 | HANDLERS[vcs] = {} 372 | HANDLERS[vcs][method] = f 373 | return f 374 | return decorate 375 | 376 | 377 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 378 | env=None): 379 | """Call the given command(s).""" 380 | assert isinstance(commands, list) 381 | p = None 382 | for c in commands: 383 | try: 384 | dispcmd = str([c] + args) 385 | # remember shell=False, so use git.cmd on windows, not just git 386 | p = subprocess.Popen([c] + args, cwd=cwd, env=env, 387 | stdout=subprocess.PIPE, 388 | stderr=(subprocess.PIPE if hide_stderr 389 | else None)) 390 | break 391 | except EnvironmentError: 392 | e = sys.exc_info()[1] 393 | if e.errno == errno.ENOENT: 394 | continue 395 | if verbose: 396 | print("unable to run %s" % dispcmd) 397 | print(e) 398 | return None, None 399 | else: 400 | if verbose: 401 | print("unable to find command, tried %s" % (commands,)) 402 | return None, None 403 | stdout = p.communicate()[0].strip().decode() 404 | if p.returncode != 0: 405 | if verbose: 406 | print("unable to run %s (error)" % dispcmd) 407 | print("stdout was %s" % stdout) 408 | return None, p.returncode 409 | return stdout, p.returncode 410 | 411 | 412 | LONG_VERSION_PY['git'] = r''' 413 | # This file helps to compute a version number in source trees obtained from 414 | # git-archive tarball (such as those provided by githubs download-from-tag 415 | # feature). Distribution tarballs (built by setup.py sdist) and build 416 | # directories (produced by setup.py build) will contain a much shorter file 417 | # that just contains the computed version number. 418 | 419 | # This file is released into the public domain. Generated by 420 | # versioneer-0.19 (https://github.com/python-versioneer/python-versioneer) 421 | 422 | """Git implementation of _version.py.""" 423 | 424 | import errno 425 | import os 426 | import re 427 | import subprocess 428 | import sys 429 | 430 | 431 | def get_keywords(): 432 | """Get the keywords needed to look up the version information.""" 433 | # these strings will be replaced by git during git-archive. 434 | # setup.py/versioneer.py will grep for the variable names, so they must 435 | # each be defined on a line of their own. _version.py will just call 436 | # get_keywords(). 437 | git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" 438 | git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" 439 | git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s" 440 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 441 | return keywords 442 | 443 | 444 | class VersioneerConfig: 445 | """Container for Versioneer configuration parameters.""" 446 | 447 | 448 | def get_config(): 449 | """Create, populate and return the VersioneerConfig() object.""" 450 | # these strings are filled in when 'setup.py versioneer' creates 451 | # _version.py 452 | cfg = VersioneerConfig() 453 | cfg.VCS = "git" 454 | cfg.style = "%(STYLE)s" 455 | cfg.tag_prefix = "%(TAG_PREFIX)s" 456 | cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" 457 | cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" 458 | cfg.verbose = False 459 | return cfg 460 | 461 | 462 | class NotThisMethod(Exception): 463 | """Exception raised if a method is not valid for the current scenario.""" 464 | 465 | 466 | LONG_VERSION_PY = {} 467 | HANDLERS = {} 468 | 469 | 470 | def register_vcs_handler(vcs, method): # decorator 471 | """Create decorator to mark a method as the handler of a VCS.""" 472 | def decorate(f): 473 | """Store f in HANDLERS[vcs][method].""" 474 | if vcs not in HANDLERS: 475 | HANDLERS[vcs] = {} 476 | HANDLERS[vcs][method] = f 477 | return f 478 | return decorate 479 | 480 | 481 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 482 | env=None): 483 | """Call the given command(s).""" 484 | assert isinstance(commands, list) 485 | p = None 486 | for c in commands: 487 | try: 488 | dispcmd = str([c] + args) 489 | # remember shell=False, so use git.cmd on windows, not just git 490 | p = subprocess.Popen([c] + args, cwd=cwd, env=env, 491 | stdout=subprocess.PIPE, 492 | stderr=(subprocess.PIPE if hide_stderr 493 | else None)) 494 | break 495 | except EnvironmentError: 496 | e = sys.exc_info()[1] 497 | if e.errno == errno.ENOENT: 498 | continue 499 | if verbose: 500 | print("unable to run %%s" %% dispcmd) 501 | print(e) 502 | return None, None 503 | else: 504 | if verbose: 505 | print("unable to find command, tried %%s" %% (commands,)) 506 | return None, None 507 | stdout = p.communicate()[0].strip().decode() 508 | if p.returncode != 0: 509 | if verbose: 510 | print("unable to run %%s (error)" %% dispcmd) 511 | print("stdout was %%s" %% stdout) 512 | return None, p.returncode 513 | return stdout, p.returncode 514 | 515 | 516 | def versions_from_parentdir(parentdir_prefix, root, verbose): 517 | """Try to determine the version from the parent directory name. 518 | 519 | Source tarballs conventionally unpack into a directory that includes both 520 | the project name and a version string. We will also support searching up 521 | two directory levels for an appropriately named parent directory 522 | """ 523 | rootdirs = [] 524 | 525 | for i in range(3): 526 | dirname = os.path.basename(root) 527 | if dirname.startswith(parentdir_prefix): 528 | return {"version": dirname[len(parentdir_prefix):], 529 | "full-revisionid": None, 530 | "dirty": False, "error": None, "date": None} 531 | else: 532 | rootdirs.append(root) 533 | root = os.path.dirname(root) # up a level 534 | 535 | if verbose: 536 | print("Tried directories %%s but none started with prefix %%s" %% 537 | (str(rootdirs), parentdir_prefix)) 538 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 539 | 540 | 541 | @register_vcs_handler("git", "get_keywords") 542 | def git_get_keywords(versionfile_abs): 543 | """Extract version information from the given file.""" 544 | # the code embedded in _version.py can just fetch the value of these 545 | # keywords. When used from setup.py, we don't want to import _version.py, 546 | # so we do it with a regexp instead. This function is not used from 547 | # _version.py. 548 | keywords = {} 549 | try: 550 | f = open(versionfile_abs, "r") 551 | for line in f.readlines(): 552 | if line.strip().startswith("git_refnames ="): 553 | mo = re.search(r'=\s*"(.*)"', line) 554 | if mo: 555 | keywords["refnames"] = mo.group(1) 556 | if line.strip().startswith("git_full ="): 557 | mo = re.search(r'=\s*"(.*)"', line) 558 | if mo: 559 | keywords["full"] = mo.group(1) 560 | if line.strip().startswith("git_date ="): 561 | mo = re.search(r'=\s*"(.*)"', line) 562 | if mo: 563 | keywords["date"] = mo.group(1) 564 | f.close() 565 | except EnvironmentError: 566 | pass 567 | return keywords 568 | 569 | 570 | @register_vcs_handler("git", "keywords") 571 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 572 | """Get version information from git keywords.""" 573 | if not keywords: 574 | raise NotThisMethod("no keywords at all, weird") 575 | date = keywords.get("date") 576 | if date is not None: 577 | # Use only the last line. Previous lines may contain GPG signature 578 | # information. 579 | date = date.splitlines()[-1] 580 | 581 | # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant 582 | # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 583 | # -like" string, which we must then edit to make compliant), because 584 | # it's been around since git-1.5.3, and it's too difficult to 585 | # discover which version we're using, or to work around using an 586 | # older one. 587 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 588 | refnames = keywords["refnames"].strip() 589 | if refnames.startswith("$Format"): 590 | if verbose: 591 | print("keywords are unexpanded, not using") 592 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 593 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 594 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 595 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 596 | TAG = "tag: " 597 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 598 | if not tags: 599 | # Either we're using git < 1.8.3, or there really are no tags. We use 600 | # a heuristic: assume all version tags have a digit. The old git %%d 601 | # expansion behaves like git log --decorate=short and strips out the 602 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 603 | # between branches and tags. By ignoring refnames without digits, we 604 | # filter out many common branch names like "release" and 605 | # "stabilization", as well as "HEAD" and "master". 606 | tags = set([r for r in refs if re.search(r'\d', r)]) 607 | if verbose: 608 | print("discarding '%%s', no digits" %% ",".join(refs - tags)) 609 | if verbose: 610 | print("likely tags: %%s" %% ",".join(sorted(tags))) 611 | for ref in sorted(tags): 612 | # sorting will prefer e.g. "2.0" over "2.0rc1" 613 | if ref.startswith(tag_prefix): 614 | r = ref[len(tag_prefix):] 615 | if verbose: 616 | print("picking %%s" %% r) 617 | return {"version": r, 618 | "full-revisionid": keywords["full"].strip(), 619 | "dirty": False, "error": None, 620 | "date": date} 621 | # no suitable tags, so version is "0+unknown", but full hex is still there 622 | if verbose: 623 | print("no suitable tags, using unknown + full revision id") 624 | return {"version": "0+unknown", 625 | "full-revisionid": keywords["full"].strip(), 626 | "dirty": False, "error": "no suitable tags", "date": None} 627 | 628 | 629 | @register_vcs_handler("git", "pieces_from_vcs") 630 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 631 | """Get version from 'git describe' in the root of the source tree. 632 | 633 | This only gets called if the git-archive 'subst' keywords were *not* 634 | expanded, and _version.py hasn't already been rewritten with a short 635 | version string, meaning we're inside a checked out source tree. 636 | """ 637 | GITS = ["git"] 638 | if sys.platform == "win32": 639 | GITS = ["git.cmd", "git.exe"] 640 | 641 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, 642 | hide_stderr=True) 643 | if rc != 0: 644 | if verbose: 645 | print("Directory %%s not under git control" %% root) 646 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 647 | 648 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 649 | # if there isn't one, this yields HEX[-dirty] (no NUM) 650 | describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", 651 | "--always", "--long", 652 | "--match", "%%s*" %% tag_prefix], 653 | cwd=root) 654 | # --long was added in git-1.5.5 655 | if describe_out is None: 656 | raise NotThisMethod("'git describe' failed") 657 | describe_out = describe_out.strip() 658 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 659 | if full_out is None: 660 | raise NotThisMethod("'git rev-parse' failed") 661 | full_out = full_out.strip() 662 | 663 | pieces = {} 664 | pieces["long"] = full_out 665 | pieces["short"] = full_out[:7] # maybe improved later 666 | pieces["error"] = None 667 | 668 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 669 | # TAG might have hyphens. 670 | git_describe = describe_out 671 | 672 | # look for -dirty suffix 673 | dirty = git_describe.endswith("-dirty") 674 | pieces["dirty"] = dirty 675 | if dirty: 676 | git_describe = git_describe[:git_describe.rindex("-dirty")] 677 | 678 | # now we have TAG-NUM-gHEX or HEX 679 | 680 | if "-" in git_describe: 681 | # TAG-NUM-gHEX 682 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 683 | if not mo: 684 | # unparseable. Maybe git-describe is misbehaving? 685 | pieces["error"] = ("unable to parse git-describe output: '%%s'" 686 | %% describe_out) 687 | return pieces 688 | 689 | # tag 690 | full_tag = mo.group(1) 691 | if not full_tag.startswith(tag_prefix): 692 | if verbose: 693 | fmt = "tag '%%s' doesn't start with prefix '%%s'" 694 | print(fmt %% (full_tag, tag_prefix)) 695 | pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" 696 | %% (full_tag, tag_prefix)) 697 | return pieces 698 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 699 | 700 | # distance: number of commits since tag 701 | pieces["distance"] = int(mo.group(2)) 702 | 703 | # commit: short hex revision ID 704 | pieces["short"] = mo.group(3) 705 | 706 | else: 707 | # HEX: no tags 708 | pieces["closest-tag"] = None 709 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], 710 | cwd=root) 711 | pieces["distance"] = int(count_out) # total number of commits 712 | 713 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 714 | date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"], 715 | cwd=root)[0].strip() 716 | # Use only the last line. Previous lines may contain GPG signature 717 | # information. 718 | date = date.splitlines()[-1] 719 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 720 | 721 | return pieces 722 | 723 | 724 | def plus_or_dot(pieces): 725 | """Return a + if we don't already have one, else return a .""" 726 | if "+" in pieces.get("closest-tag", ""): 727 | return "." 728 | return "+" 729 | 730 | 731 | def render_pep440(pieces): 732 | """Build up version string, with post-release "local version identifier". 733 | 734 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 735 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 736 | 737 | Exceptions: 738 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 739 | """ 740 | if pieces["closest-tag"]: 741 | rendered = pieces["closest-tag"] 742 | if pieces["distance"] or pieces["dirty"]: 743 | rendered += plus_or_dot(pieces) 744 | rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) 745 | if pieces["dirty"]: 746 | rendered += ".dirty" 747 | else: 748 | # exception #1 749 | rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], 750 | pieces["short"]) 751 | if pieces["dirty"]: 752 | rendered += ".dirty" 753 | return rendered 754 | 755 | 756 | def render_pep440_pre(pieces): 757 | """TAG[.post0.devDISTANCE] -- No -dirty. 758 | 759 | Exceptions: 760 | 1: no tags. 0.post0.devDISTANCE 761 | """ 762 | if pieces["closest-tag"]: 763 | rendered = pieces["closest-tag"] 764 | if pieces["distance"]: 765 | rendered += ".post0.dev%%d" %% pieces["distance"] 766 | else: 767 | # exception #1 768 | rendered = "0.post0.dev%%d" %% pieces["distance"] 769 | return rendered 770 | 771 | 772 | def render_pep440_post(pieces): 773 | """TAG[.postDISTANCE[.dev0]+gHEX] . 774 | 775 | The ".dev0" means dirty. Note that .dev0 sorts backwards 776 | (a dirty tree will appear "older" than the corresponding clean one), 777 | but you shouldn't be releasing software with -dirty anyways. 778 | 779 | Exceptions: 780 | 1: no tags. 0.postDISTANCE[.dev0] 781 | """ 782 | if pieces["closest-tag"]: 783 | rendered = pieces["closest-tag"] 784 | if pieces["distance"] or pieces["dirty"]: 785 | rendered += ".post%%d" %% pieces["distance"] 786 | if pieces["dirty"]: 787 | rendered += ".dev0" 788 | rendered += plus_or_dot(pieces) 789 | rendered += "g%%s" %% pieces["short"] 790 | else: 791 | # exception #1 792 | rendered = "0.post%%d" %% pieces["distance"] 793 | if pieces["dirty"]: 794 | rendered += ".dev0" 795 | rendered += "+g%%s" %% pieces["short"] 796 | return rendered 797 | 798 | 799 | def render_pep440_old(pieces): 800 | """TAG[.postDISTANCE[.dev0]] . 801 | 802 | The ".dev0" means dirty. 803 | 804 | Exceptions: 805 | 1: no tags. 0.postDISTANCE[.dev0] 806 | """ 807 | if pieces["closest-tag"]: 808 | rendered = pieces["closest-tag"] 809 | if pieces["distance"] or pieces["dirty"]: 810 | rendered += ".post%%d" %% pieces["distance"] 811 | if pieces["dirty"]: 812 | rendered += ".dev0" 813 | else: 814 | # exception #1 815 | rendered = "0.post%%d" %% pieces["distance"] 816 | if pieces["dirty"]: 817 | rendered += ".dev0" 818 | return rendered 819 | 820 | 821 | def render_git_describe(pieces): 822 | """TAG[-DISTANCE-gHEX][-dirty]. 823 | 824 | Like 'git describe --tags --dirty --always'. 825 | 826 | Exceptions: 827 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 828 | """ 829 | if pieces["closest-tag"]: 830 | rendered = pieces["closest-tag"] 831 | if pieces["distance"]: 832 | rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) 833 | else: 834 | # exception #1 835 | rendered = pieces["short"] 836 | if pieces["dirty"]: 837 | rendered += "-dirty" 838 | return rendered 839 | 840 | 841 | def render_git_describe_long(pieces): 842 | """TAG-DISTANCE-gHEX[-dirty]. 843 | 844 | Like 'git describe --tags --dirty --always -long'. 845 | The distance/hash is unconditional. 846 | 847 | Exceptions: 848 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 849 | """ 850 | if pieces["closest-tag"]: 851 | rendered = pieces["closest-tag"] 852 | rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) 853 | else: 854 | # exception #1 855 | rendered = pieces["short"] 856 | if pieces["dirty"]: 857 | rendered += "-dirty" 858 | return rendered 859 | 860 | 861 | def render(pieces, style): 862 | """Render the given version pieces into the requested style.""" 863 | if pieces["error"]: 864 | return {"version": "unknown", 865 | "full-revisionid": pieces.get("long"), 866 | "dirty": None, 867 | "error": pieces["error"], 868 | "date": None} 869 | 870 | if not style or style == "default": 871 | style = "pep440" # the default 872 | 873 | if style == "pep440": 874 | rendered = render_pep440(pieces) 875 | elif style == "pep440-pre": 876 | rendered = render_pep440_pre(pieces) 877 | elif style == "pep440-post": 878 | rendered = render_pep440_post(pieces) 879 | elif style == "pep440-old": 880 | rendered = render_pep440_old(pieces) 881 | elif style == "git-describe": 882 | rendered = render_git_describe(pieces) 883 | elif style == "git-describe-long": 884 | rendered = render_git_describe_long(pieces) 885 | else: 886 | raise ValueError("unknown style '%%s'" %% style) 887 | 888 | return {"version": rendered, "full-revisionid": pieces["long"], 889 | "dirty": pieces["dirty"], "error": None, 890 | "date": pieces.get("date")} 891 | 892 | 893 | def get_versions(): 894 | """Get version information or return default if unable to do so.""" 895 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 896 | # __file__, we can work backwards from there to the root. Some 897 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 898 | # case we can only use expanded keywords. 899 | 900 | cfg = get_config() 901 | verbose = cfg.verbose 902 | 903 | try: 904 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 905 | verbose) 906 | except NotThisMethod: 907 | pass 908 | 909 | try: 910 | root = os.path.realpath(__file__) 911 | # versionfile_source is the relative path from the top of the source 912 | # tree (where the .git directory might live) to this file. Invert 913 | # this to find the root from __file__. 914 | for i in cfg.versionfile_source.split('/'): 915 | root = os.path.dirname(root) 916 | except NameError: 917 | return {"version": "0+unknown", "full-revisionid": None, 918 | "dirty": None, 919 | "error": "unable to find root of source tree", 920 | "date": None} 921 | 922 | try: 923 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 924 | return render(pieces, cfg.style) 925 | except NotThisMethod: 926 | pass 927 | 928 | try: 929 | if cfg.parentdir_prefix: 930 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 931 | except NotThisMethod: 932 | pass 933 | 934 | return {"version": "0+unknown", "full-revisionid": None, 935 | "dirty": None, 936 | "error": "unable to compute version", "date": None} 937 | ''' 938 | 939 | 940 | @register_vcs_handler("git", "get_keywords") 941 | def git_get_keywords(versionfile_abs): 942 | """Extract version information from the given file.""" 943 | # the code embedded in _version.py can just fetch the value of these 944 | # keywords. When used from setup.py, we don't want to import _version.py, 945 | # so we do it with a regexp instead. This function is not used from 946 | # _version.py. 947 | keywords = {} 948 | try: 949 | f = open(versionfile_abs, "r") 950 | for line in f.readlines(): 951 | if line.strip().startswith("git_refnames ="): 952 | mo = re.search(r'=\s*"(.*)"', line) 953 | if mo: 954 | keywords["refnames"] = mo.group(1) 955 | if line.strip().startswith("git_full ="): 956 | mo = re.search(r'=\s*"(.*)"', line) 957 | if mo: 958 | keywords["full"] = mo.group(1) 959 | if line.strip().startswith("git_date ="): 960 | mo = re.search(r'=\s*"(.*)"', line) 961 | if mo: 962 | keywords["date"] = mo.group(1) 963 | f.close() 964 | except EnvironmentError: 965 | pass 966 | return keywords 967 | 968 | 969 | @register_vcs_handler("git", "keywords") 970 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 971 | """Get version information from git keywords.""" 972 | if not keywords: 973 | raise NotThisMethod("no keywords at all, weird") 974 | date = keywords.get("date") 975 | if date is not None: 976 | # Use only the last line. Previous lines may contain GPG signature 977 | # information. 978 | date = date.splitlines()[-1] 979 | 980 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 981 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 982 | # -like" string, which we must then edit to make compliant), because 983 | # it's been around since git-1.5.3, and it's too difficult to 984 | # discover which version we're using, or to work around using an 985 | # older one. 986 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 987 | refnames = keywords["refnames"].strip() 988 | if refnames.startswith("$Format"): 989 | if verbose: 990 | print("keywords are unexpanded, not using") 991 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 992 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 993 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 994 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 995 | TAG = "tag: " 996 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 997 | if not tags: 998 | # Either we're using git < 1.8.3, or there really are no tags. We use 999 | # a heuristic: assume all version tags have a digit. The old git %d 1000 | # expansion behaves like git log --decorate=short and strips out the 1001 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 1002 | # between branches and tags. By ignoring refnames without digits, we 1003 | # filter out many common branch names like "release" and 1004 | # "stabilization", as well as "HEAD" and "master". 1005 | tags = set([r for r in refs if re.search(r'\d', r)]) 1006 | if verbose: 1007 | print("discarding '%s', no digits" % ",".join(refs - tags)) 1008 | if verbose: 1009 | print("likely tags: %s" % ",".join(sorted(tags))) 1010 | for ref in sorted(tags): 1011 | # sorting will prefer e.g. "2.0" over "2.0rc1" 1012 | if ref.startswith(tag_prefix): 1013 | r = ref[len(tag_prefix):] 1014 | if verbose: 1015 | print("picking %s" % r) 1016 | return {"version": r, 1017 | "full-revisionid": keywords["full"].strip(), 1018 | "dirty": False, "error": None, 1019 | "date": date} 1020 | # no suitable tags, so version is "0+unknown", but full hex is still there 1021 | if verbose: 1022 | print("no suitable tags, using unknown + full revision id") 1023 | return {"version": "0+unknown", 1024 | "full-revisionid": keywords["full"].strip(), 1025 | "dirty": False, "error": "no suitable tags", "date": None} 1026 | 1027 | 1028 | @register_vcs_handler("git", "pieces_from_vcs") 1029 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 1030 | """Get version from 'git describe' in the root of the source tree. 1031 | 1032 | This only gets called if the git-archive 'subst' keywords were *not* 1033 | expanded, and _version.py hasn't already been rewritten with a short 1034 | version string, meaning we're inside a checked out source tree. 1035 | """ 1036 | GITS = ["git"] 1037 | if sys.platform == "win32": 1038 | GITS = ["git.cmd", "git.exe"] 1039 | 1040 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, 1041 | hide_stderr=True) 1042 | if rc != 0: 1043 | if verbose: 1044 | print("Directory %s not under git control" % root) 1045 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 1046 | 1047 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 1048 | # if there isn't one, this yields HEX[-dirty] (no NUM) 1049 | describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", 1050 | "--always", "--long", 1051 | "--match", "%s*" % tag_prefix], 1052 | cwd=root) 1053 | # --long was added in git-1.5.5 1054 | if describe_out is None: 1055 | raise NotThisMethod("'git describe' failed") 1056 | describe_out = describe_out.strip() 1057 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 1058 | if full_out is None: 1059 | raise NotThisMethod("'git rev-parse' failed") 1060 | full_out = full_out.strip() 1061 | 1062 | pieces = {} 1063 | pieces["long"] = full_out 1064 | pieces["short"] = full_out[:7] # maybe improved later 1065 | pieces["error"] = None 1066 | 1067 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 1068 | # TAG might have hyphens. 1069 | git_describe = describe_out 1070 | 1071 | # look for -dirty suffix 1072 | dirty = git_describe.endswith("-dirty") 1073 | pieces["dirty"] = dirty 1074 | if dirty: 1075 | git_describe = git_describe[:git_describe.rindex("-dirty")] 1076 | 1077 | # now we have TAG-NUM-gHEX or HEX 1078 | 1079 | if "-" in git_describe: 1080 | # TAG-NUM-gHEX 1081 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 1082 | if not mo: 1083 | # unparseable. Maybe git-describe is misbehaving? 1084 | pieces["error"] = ("unable to parse git-describe output: '%s'" 1085 | % describe_out) 1086 | return pieces 1087 | 1088 | # tag 1089 | full_tag = mo.group(1) 1090 | if not full_tag.startswith(tag_prefix): 1091 | if verbose: 1092 | fmt = "tag '%s' doesn't start with prefix '%s'" 1093 | print(fmt % (full_tag, tag_prefix)) 1094 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 1095 | % (full_tag, tag_prefix)) 1096 | return pieces 1097 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 1098 | 1099 | # distance: number of commits since tag 1100 | pieces["distance"] = int(mo.group(2)) 1101 | 1102 | # commit: short hex revision ID 1103 | pieces["short"] = mo.group(3) 1104 | 1105 | else: 1106 | # HEX: no tags 1107 | pieces["closest-tag"] = None 1108 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], 1109 | cwd=root) 1110 | pieces["distance"] = int(count_out) # total number of commits 1111 | 1112 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 1113 | date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], 1114 | cwd=root)[0].strip() 1115 | # Use only the last line. Previous lines may contain GPG signature 1116 | # information. 1117 | date = date.splitlines()[-1] 1118 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 1119 | 1120 | return pieces 1121 | 1122 | 1123 | def do_vcs_install(manifest_in, versionfile_source, ipy): 1124 | """Git-specific installation logic for Versioneer. 1125 | 1126 | For Git, this means creating/changing .gitattributes to mark _version.py 1127 | for export-subst keyword substitution. 1128 | """ 1129 | GITS = ["git"] 1130 | if sys.platform == "win32": 1131 | GITS = ["git.cmd", "git.exe"] 1132 | files = [manifest_in, versionfile_source] 1133 | if ipy: 1134 | files.append(ipy) 1135 | try: 1136 | me = __file__ 1137 | if me.endswith(".pyc") or me.endswith(".pyo"): 1138 | me = os.path.splitext(me)[0] + ".py" 1139 | versioneer_file = os.path.relpath(me) 1140 | except NameError: 1141 | versioneer_file = "versioneer.py" 1142 | files.append(versioneer_file) 1143 | present = False 1144 | try: 1145 | f = open(".gitattributes", "r") 1146 | for line in f.readlines(): 1147 | if line.strip().startswith(versionfile_source): 1148 | if "export-subst" in line.strip().split()[1:]: 1149 | present = True 1150 | f.close() 1151 | except EnvironmentError: 1152 | pass 1153 | if not present: 1154 | f = open(".gitattributes", "a+") 1155 | f.write("%s export-subst\n" % versionfile_source) 1156 | f.close() 1157 | files.append(".gitattributes") 1158 | run_command(GITS, ["add", "--"] + files) 1159 | 1160 | 1161 | def versions_from_parentdir(parentdir_prefix, root, verbose): 1162 | """Try to determine the version from the parent directory name. 1163 | 1164 | Source tarballs conventionally unpack into a directory that includes both 1165 | the project name and a version string. We will also support searching up 1166 | two directory levels for an appropriately named parent directory 1167 | """ 1168 | rootdirs = [] 1169 | 1170 | for i in range(3): 1171 | dirname = os.path.basename(root) 1172 | if dirname.startswith(parentdir_prefix): 1173 | return {"version": dirname[len(parentdir_prefix):], 1174 | "full-revisionid": None, 1175 | "dirty": False, "error": None, "date": None} 1176 | else: 1177 | rootdirs.append(root) 1178 | root = os.path.dirname(root) # up a level 1179 | 1180 | if verbose: 1181 | print("Tried directories %s but none started with prefix %s" % 1182 | (str(rootdirs), parentdir_prefix)) 1183 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 1184 | 1185 | 1186 | SHORT_VERSION_PY = """ 1187 | # This file was generated by 'versioneer.py' (0.19) from 1188 | # revision-control system data, or from the parent directory name of an 1189 | # unpacked source archive. Distribution tarballs contain a pre-generated copy 1190 | # of this file. 1191 | 1192 | import json 1193 | 1194 | version_json = ''' 1195 | %s 1196 | ''' # END VERSION_JSON 1197 | 1198 | 1199 | def get_versions(): 1200 | return json.loads(version_json) 1201 | """ 1202 | 1203 | 1204 | def versions_from_file(filename): 1205 | """Try to determine the version from _version.py if present.""" 1206 | try: 1207 | with open(filename) as f: 1208 | contents = f.read() 1209 | except EnvironmentError: 1210 | raise NotThisMethod("unable to read _version.py") 1211 | mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", 1212 | contents, re.M | re.S) 1213 | if not mo: 1214 | mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", 1215 | contents, re.M | re.S) 1216 | if not mo: 1217 | raise NotThisMethod("no version_json in _version.py") 1218 | return json.loads(mo.group(1)) 1219 | 1220 | 1221 | def write_to_version_file(filename, versions): 1222 | """Write the given version number to the given _version.py file.""" 1223 | os.unlink(filename) 1224 | contents = json.dumps(versions, sort_keys=True, 1225 | indent=1, separators=(",", ": ")) 1226 | with open(filename, "w") as f: 1227 | f.write(SHORT_VERSION_PY % contents) 1228 | 1229 | print("set %s to '%s'" % (filename, versions["version"])) 1230 | 1231 | 1232 | def plus_or_dot(pieces): 1233 | """Return a + if we don't already have one, else return a .""" 1234 | if "+" in pieces.get("closest-tag", ""): 1235 | return "." 1236 | return "+" 1237 | 1238 | 1239 | def render_pep440(pieces): 1240 | """Build up version string, with post-release "local version identifier". 1241 | 1242 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 1243 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 1244 | 1245 | Exceptions: 1246 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 1247 | """ 1248 | if pieces["closest-tag"]: 1249 | rendered = pieces["closest-tag"] 1250 | if pieces["distance"] or pieces["dirty"]: 1251 | rendered += plus_or_dot(pieces) 1252 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 1253 | if pieces["dirty"]: 1254 | rendered += ".dirty" 1255 | else: 1256 | # exception #1 1257 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 1258 | pieces["short"]) 1259 | if pieces["dirty"]: 1260 | rendered += ".dirty" 1261 | return rendered 1262 | 1263 | 1264 | def render_pep440_pre(pieces): 1265 | """TAG[.post0.devDISTANCE] -- No -dirty. 1266 | 1267 | Exceptions: 1268 | 1: no tags. 0.post0.devDISTANCE 1269 | """ 1270 | if pieces["closest-tag"]: 1271 | rendered = pieces["closest-tag"] 1272 | if pieces["distance"]: 1273 | rendered += ".post0.dev%d" % pieces["distance"] 1274 | else: 1275 | # exception #1 1276 | rendered = "0.post0.dev%d" % pieces["distance"] 1277 | return rendered 1278 | 1279 | 1280 | def render_pep440_post(pieces): 1281 | """TAG[.postDISTANCE[.dev0]+gHEX] . 1282 | 1283 | The ".dev0" means dirty. Note that .dev0 sorts backwards 1284 | (a dirty tree will appear "older" than the corresponding clean one), 1285 | but you shouldn't be releasing software with -dirty anyways. 1286 | 1287 | Exceptions: 1288 | 1: no tags. 0.postDISTANCE[.dev0] 1289 | """ 1290 | if pieces["closest-tag"]: 1291 | rendered = pieces["closest-tag"] 1292 | if pieces["distance"] or pieces["dirty"]: 1293 | rendered += ".post%d" % pieces["distance"] 1294 | if pieces["dirty"]: 1295 | rendered += ".dev0" 1296 | rendered += plus_or_dot(pieces) 1297 | rendered += "g%s" % pieces["short"] 1298 | else: 1299 | # exception #1 1300 | rendered = "0.post%d" % pieces["distance"] 1301 | if pieces["dirty"]: 1302 | rendered += ".dev0" 1303 | rendered += "+g%s" % pieces["short"] 1304 | return rendered 1305 | 1306 | 1307 | def render_pep440_old(pieces): 1308 | """TAG[.postDISTANCE[.dev0]] . 1309 | 1310 | The ".dev0" means dirty. 1311 | 1312 | Exceptions: 1313 | 1: no tags. 0.postDISTANCE[.dev0] 1314 | """ 1315 | if pieces["closest-tag"]: 1316 | rendered = pieces["closest-tag"] 1317 | if pieces["distance"] or pieces["dirty"]: 1318 | rendered += ".post%d" % pieces["distance"] 1319 | if pieces["dirty"]: 1320 | rendered += ".dev0" 1321 | else: 1322 | # exception #1 1323 | rendered = "0.post%d" % pieces["distance"] 1324 | if pieces["dirty"]: 1325 | rendered += ".dev0" 1326 | return rendered 1327 | 1328 | 1329 | def render_git_describe(pieces): 1330 | """TAG[-DISTANCE-gHEX][-dirty]. 1331 | 1332 | Like 'git describe --tags --dirty --always'. 1333 | 1334 | Exceptions: 1335 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 1336 | """ 1337 | if pieces["closest-tag"]: 1338 | rendered = pieces["closest-tag"] 1339 | if pieces["distance"]: 1340 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 1341 | else: 1342 | # exception #1 1343 | rendered = pieces["short"] 1344 | if pieces["dirty"]: 1345 | rendered += "-dirty" 1346 | return rendered 1347 | 1348 | 1349 | def render_git_describe_long(pieces): 1350 | """TAG-DISTANCE-gHEX[-dirty]. 1351 | 1352 | Like 'git describe --tags --dirty --always -long'. 1353 | The distance/hash is unconditional. 1354 | 1355 | Exceptions: 1356 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 1357 | """ 1358 | if pieces["closest-tag"]: 1359 | rendered = pieces["closest-tag"] 1360 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 1361 | else: 1362 | # exception #1 1363 | rendered = pieces["short"] 1364 | if pieces["dirty"]: 1365 | rendered += "-dirty" 1366 | return rendered 1367 | 1368 | 1369 | def render(pieces, style): 1370 | """Render the given version pieces into the requested style.""" 1371 | if pieces["error"]: 1372 | return {"version": "unknown", 1373 | "full-revisionid": pieces.get("long"), 1374 | "dirty": None, 1375 | "error": pieces["error"], 1376 | "date": None} 1377 | 1378 | if not style or style == "default": 1379 | style = "pep440" # the default 1380 | 1381 | if style == "pep440": 1382 | rendered = render_pep440(pieces) 1383 | elif style == "pep440-pre": 1384 | rendered = render_pep440_pre(pieces) 1385 | elif style == "pep440-post": 1386 | rendered = render_pep440_post(pieces) 1387 | elif style == "pep440-old": 1388 | rendered = render_pep440_old(pieces) 1389 | elif style == "git-describe": 1390 | rendered = render_git_describe(pieces) 1391 | elif style == "git-describe-long": 1392 | rendered = render_git_describe_long(pieces) 1393 | else: 1394 | raise ValueError("unknown style '%s'" % style) 1395 | 1396 | return {"version": rendered, "full-revisionid": pieces["long"], 1397 | "dirty": pieces["dirty"], "error": None, 1398 | "date": pieces.get("date")} 1399 | 1400 | 1401 | class VersioneerBadRootError(Exception): 1402 | """The project root directory is unknown or missing key files.""" 1403 | 1404 | 1405 | def get_versions(verbose=False): 1406 | """Get the project version from whatever source is available. 1407 | 1408 | Returns dict with two keys: 'version' and 'full'. 1409 | """ 1410 | if "versioneer" in sys.modules: 1411 | # see the discussion in cmdclass.py:get_cmdclass() 1412 | del sys.modules["versioneer"] 1413 | 1414 | root = get_root() 1415 | cfg = get_config_from_root(root) 1416 | 1417 | assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" 1418 | handlers = HANDLERS.get(cfg.VCS) 1419 | assert handlers, "unrecognized VCS '%s'" % cfg.VCS 1420 | verbose = verbose or cfg.verbose 1421 | assert cfg.versionfile_source is not None, \ 1422 | "please set versioneer.versionfile_source" 1423 | assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" 1424 | 1425 | versionfile_abs = os.path.join(root, cfg.versionfile_source) 1426 | 1427 | # extract version from first of: _version.py, VCS command (e.g. 'git 1428 | # describe'), parentdir. This is meant to work for developers using a 1429 | # source checkout, for users of a tarball created by 'setup.py sdist', 1430 | # and for users of a tarball/zipball created by 'git archive' or github's 1431 | # download-from-tag feature or the equivalent in other VCSes. 1432 | 1433 | get_keywords_f = handlers.get("get_keywords") 1434 | from_keywords_f = handlers.get("keywords") 1435 | if get_keywords_f and from_keywords_f: 1436 | try: 1437 | keywords = get_keywords_f(versionfile_abs) 1438 | ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) 1439 | if verbose: 1440 | print("got version from expanded keyword %s" % ver) 1441 | return ver 1442 | except NotThisMethod: 1443 | pass 1444 | 1445 | try: 1446 | ver = versions_from_file(versionfile_abs) 1447 | if verbose: 1448 | print("got version from file %s %s" % (versionfile_abs, ver)) 1449 | return ver 1450 | except NotThisMethod: 1451 | pass 1452 | 1453 | from_vcs_f = handlers.get("pieces_from_vcs") 1454 | if from_vcs_f: 1455 | try: 1456 | pieces = from_vcs_f(cfg.tag_prefix, root, verbose) 1457 | ver = render(pieces, cfg.style) 1458 | if verbose: 1459 | print("got version from VCS %s" % ver) 1460 | return ver 1461 | except NotThisMethod: 1462 | pass 1463 | 1464 | try: 1465 | if cfg.parentdir_prefix: 1466 | ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 1467 | if verbose: 1468 | print("got version from parentdir %s" % ver) 1469 | return ver 1470 | except NotThisMethod: 1471 | pass 1472 | 1473 | if verbose: 1474 | print("unable to compute version") 1475 | 1476 | return {"version": "0+unknown", "full-revisionid": None, 1477 | "dirty": None, "error": "unable to compute version", 1478 | "date": None} 1479 | 1480 | 1481 | def get_version(): 1482 | """Get the short version string for this project.""" 1483 | return get_versions()["version"] 1484 | 1485 | 1486 | def get_cmdclass(cmdclass=None): 1487 | """Get the custom setuptools/distutils subclasses used by Versioneer. 1488 | 1489 | If the package uses a different cmdclass (e.g. one from numpy), it 1490 | should be provide as an argument. 1491 | """ 1492 | if "versioneer" in sys.modules: 1493 | del sys.modules["versioneer"] 1494 | # this fixes the "python setup.py develop" case (also 'install' and 1495 | # 'easy_install .'), in which subdependencies of the main project are 1496 | # built (using setup.py bdist_egg) in the same python process. Assume 1497 | # a main project A and a dependency B, which use different versions 1498 | # of Versioneer. A's setup.py imports A's Versioneer, leaving it in 1499 | # sys.modules by the time B's setup.py is executed, causing B to run 1500 | # with the wrong versioneer. Setuptools wraps the sub-dep builds in a 1501 | # sandbox that restores sys.modules to it's pre-build state, so the 1502 | # parent is protected against the child's "import versioneer". By 1503 | # removing ourselves from sys.modules here, before the child build 1504 | # happens, we protect the child from the parent's versioneer too. 1505 | # Also see https://github.com/python-versioneer/python-versioneer/issues/52 1506 | 1507 | cmds = {} if cmdclass is None else cmdclass.copy() 1508 | 1509 | # we add "version" to both distutils and setuptools 1510 | from distutils.core import Command 1511 | 1512 | class cmd_version(Command): 1513 | description = "report generated version string" 1514 | user_options = [] 1515 | boolean_options = [] 1516 | 1517 | def initialize_options(self): 1518 | pass 1519 | 1520 | def finalize_options(self): 1521 | pass 1522 | 1523 | def run(self): 1524 | vers = get_versions(verbose=True) 1525 | print("Version: %s" % vers["version"]) 1526 | print(" full-revisionid: %s" % vers.get("full-revisionid")) 1527 | print(" dirty: %s" % vers.get("dirty")) 1528 | print(" date: %s" % vers.get("date")) 1529 | if vers["error"]: 1530 | print(" error: %s" % vers["error"]) 1531 | cmds["version"] = cmd_version 1532 | 1533 | # we override "build_py" in both distutils and setuptools 1534 | # 1535 | # most invocation pathways end up running build_py: 1536 | # distutils/build -> build_py 1537 | # distutils/install -> distutils/build ->.. 1538 | # setuptools/bdist_wheel -> distutils/install ->.. 1539 | # setuptools/bdist_egg -> distutils/install_lib -> build_py 1540 | # setuptools/install -> bdist_egg ->.. 1541 | # setuptools/develop -> ? 1542 | # pip install: 1543 | # copies source tree to a tempdir before running egg_info/etc 1544 | # if .git isn't copied too, 'git describe' will fail 1545 | # then does setup.py bdist_wheel, or sometimes setup.py install 1546 | # setup.py egg_info -> ? 1547 | 1548 | # we override different "build_py" commands for both environments 1549 | if 'build_py' in cmds: 1550 | _build_py = cmds['build_py'] 1551 | elif "setuptools" in sys.modules: 1552 | from setuptools.command.build_py import build_py as _build_py 1553 | else: 1554 | from distutils.command.build_py import build_py as _build_py 1555 | 1556 | class cmd_build_py(_build_py): 1557 | def run(self): 1558 | root = get_root() 1559 | cfg = get_config_from_root(root) 1560 | versions = get_versions() 1561 | _build_py.run(self) 1562 | # now locate _version.py in the new build/ directory and replace 1563 | # it with an updated value 1564 | if cfg.versionfile_build: 1565 | target_versionfile = os.path.join(self.build_lib, 1566 | cfg.versionfile_build) 1567 | print("UPDATING %s" % target_versionfile) 1568 | write_to_version_file(target_versionfile, versions) 1569 | cmds["build_py"] = cmd_build_py 1570 | 1571 | if "setuptools" in sys.modules: 1572 | from setuptools.command.build_ext import build_ext as _build_ext 1573 | else: 1574 | from distutils.command.build_ext import build_ext as _build_ext 1575 | 1576 | class cmd_build_ext(_build_ext): 1577 | def run(self): 1578 | root = get_root() 1579 | cfg = get_config_from_root(root) 1580 | versions = get_versions() 1581 | _build_ext.run(self) 1582 | if self.inplace: 1583 | # build_ext --inplace will only build extensions in 1584 | # build/lib<..> dir with no _version.py to write to. 1585 | # As in place builds will already have a _version.py 1586 | # in the module dir, we do not need to write one. 1587 | return 1588 | # now locate _version.py in the new build/ directory and replace 1589 | # it with an updated value 1590 | target_versionfile = os.path.join(self.build_lib, 1591 | cfg.versionfile_source) 1592 | print("UPDATING %s" % target_versionfile) 1593 | write_to_version_file(target_versionfile, versions) 1594 | cmds["build_ext"] = cmd_build_ext 1595 | 1596 | if "cx_Freeze" in sys.modules: # cx_freeze enabled? 1597 | from cx_Freeze.dist import build_exe as _build_exe 1598 | # nczeczulin reports that py2exe won't like the pep440-style string 1599 | # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. 1600 | # setup(console=[{ 1601 | # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION 1602 | # "product_version": versioneer.get_version(), 1603 | # ... 1604 | 1605 | class cmd_build_exe(_build_exe): 1606 | def run(self): 1607 | root = get_root() 1608 | cfg = get_config_from_root(root) 1609 | versions = get_versions() 1610 | target_versionfile = cfg.versionfile_source 1611 | print("UPDATING %s" % target_versionfile) 1612 | write_to_version_file(target_versionfile, versions) 1613 | 1614 | _build_exe.run(self) 1615 | os.unlink(target_versionfile) 1616 | with open(cfg.versionfile_source, "w") as f: 1617 | LONG = LONG_VERSION_PY[cfg.VCS] 1618 | f.write(LONG % 1619 | {"DOLLAR": "$", 1620 | "STYLE": cfg.style, 1621 | "TAG_PREFIX": cfg.tag_prefix, 1622 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1623 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 1624 | }) 1625 | cmds["build_exe"] = cmd_build_exe 1626 | del cmds["build_py"] 1627 | 1628 | if 'py2exe' in sys.modules: # py2exe enabled? 1629 | from py2exe.distutils_buildexe import py2exe as _py2exe 1630 | 1631 | class cmd_py2exe(_py2exe): 1632 | def run(self): 1633 | root = get_root() 1634 | cfg = get_config_from_root(root) 1635 | versions = get_versions() 1636 | target_versionfile = cfg.versionfile_source 1637 | print("UPDATING %s" % target_versionfile) 1638 | write_to_version_file(target_versionfile, versions) 1639 | 1640 | _py2exe.run(self) 1641 | os.unlink(target_versionfile) 1642 | with open(cfg.versionfile_source, "w") as f: 1643 | LONG = LONG_VERSION_PY[cfg.VCS] 1644 | f.write(LONG % 1645 | {"DOLLAR": "$", 1646 | "STYLE": cfg.style, 1647 | "TAG_PREFIX": cfg.tag_prefix, 1648 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1649 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 1650 | }) 1651 | cmds["py2exe"] = cmd_py2exe 1652 | 1653 | # we override different "sdist" commands for both environments 1654 | if 'sdist' in cmds: 1655 | _sdist = cmds['sdist'] 1656 | elif "setuptools" in sys.modules: 1657 | from setuptools.command.sdist import sdist as _sdist 1658 | else: 1659 | from distutils.command.sdist import sdist as _sdist 1660 | 1661 | class cmd_sdist(_sdist): 1662 | def run(self): 1663 | versions = get_versions() 1664 | self._versioneer_generated_versions = versions 1665 | # unless we update this, the command will keep using the old 1666 | # version 1667 | self.distribution.metadata.version = versions["version"] 1668 | return _sdist.run(self) 1669 | 1670 | def make_release_tree(self, base_dir, files): 1671 | root = get_root() 1672 | cfg = get_config_from_root(root) 1673 | _sdist.make_release_tree(self, base_dir, files) 1674 | # now locate _version.py in the new base_dir directory 1675 | # (remembering that it may be a hardlink) and replace it with an 1676 | # updated value 1677 | target_versionfile = os.path.join(base_dir, cfg.versionfile_source) 1678 | print("UPDATING %s" % target_versionfile) 1679 | write_to_version_file(target_versionfile, 1680 | self._versioneer_generated_versions) 1681 | cmds["sdist"] = cmd_sdist 1682 | 1683 | return cmds 1684 | 1685 | 1686 | CONFIG_ERROR = """ 1687 | setup.cfg is missing the necessary Versioneer configuration. You need 1688 | a section like: 1689 | 1690 | [versioneer] 1691 | VCS = git 1692 | style = pep440 1693 | versionfile_source = src/myproject/_version.py 1694 | versionfile_build = myproject/_version.py 1695 | tag_prefix = 1696 | parentdir_prefix = myproject- 1697 | 1698 | You will also need to edit your setup.py to use the results: 1699 | 1700 | import versioneer 1701 | setup(version=versioneer.get_version(), 1702 | cmdclass=versioneer.get_cmdclass(), ...) 1703 | 1704 | Please read the docstring in ./versioneer.py for configuration instructions, 1705 | edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. 1706 | """ 1707 | 1708 | SAMPLE_CONFIG = """ 1709 | # See the docstring in versioneer.py for instructions. Note that you must 1710 | # re-run 'versioneer.py setup' after changing this section, and commit the 1711 | # resulting files. 1712 | 1713 | [versioneer] 1714 | #VCS = git 1715 | #style = pep440 1716 | #versionfile_source = 1717 | #versionfile_build = 1718 | #tag_prefix = 1719 | #parentdir_prefix = 1720 | 1721 | """ 1722 | 1723 | INIT_PY_SNIPPET = """ 1724 | from ._version import get_versions 1725 | __version__ = get_versions()['version'] 1726 | del get_versions 1727 | """ 1728 | 1729 | 1730 | def do_setup(): 1731 | """Do main VCS-independent setup function for installing Versioneer.""" 1732 | root = get_root() 1733 | try: 1734 | cfg = get_config_from_root(root) 1735 | except (EnvironmentError, configparser.NoSectionError, 1736 | configparser.NoOptionError) as e: 1737 | if isinstance(e, (EnvironmentError, configparser.NoSectionError)): 1738 | print("Adding sample versioneer config to setup.cfg", 1739 | file=sys.stderr) 1740 | with open(os.path.join(root, "setup.cfg"), "a") as f: 1741 | f.write(SAMPLE_CONFIG) 1742 | print(CONFIG_ERROR, file=sys.stderr) 1743 | return 1 1744 | 1745 | print(" creating %s" % cfg.versionfile_source) 1746 | with open(cfg.versionfile_source, "w") as f: 1747 | LONG = LONG_VERSION_PY[cfg.VCS] 1748 | f.write(LONG % {"DOLLAR": "$", 1749 | "STYLE": cfg.style, 1750 | "TAG_PREFIX": cfg.tag_prefix, 1751 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1752 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 1753 | }) 1754 | 1755 | ipy = os.path.join(os.path.dirname(cfg.versionfile_source), 1756 | "__init__.py") 1757 | if os.path.exists(ipy): 1758 | try: 1759 | with open(ipy, "r") as f: 1760 | old = f.read() 1761 | except EnvironmentError: 1762 | old = "" 1763 | if INIT_PY_SNIPPET not in old: 1764 | print(" appending to %s" % ipy) 1765 | with open(ipy, "a") as f: 1766 | f.write(INIT_PY_SNIPPET) 1767 | else: 1768 | print(" %s unmodified" % ipy) 1769 | else: 1770 | print(" %s doesn't exist, ok" % ipy) 1771 | ipy = None 1772 | 1773 | # Make sure both the top-level "versioneer.py" and versionfile_source 1774 | # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so 1775 | # they'll be copied into source distributions. Pip won't be able to 1776 | # install the package without this. 1777 | manifest_in = os.path.join(root, "MANIFEST.in") 1778 | simple_includes = set() 1779 | try: 1780 | with open(manifest_in, "r") as f: 1781 | for line in f: 1782 | if line.startswith("include "): 1783 | for include in line.split()[1:]: 1784 | simple_includes.add(include) 1785 | except EnvironmentError: 1786 | pass 1787 | # That doesn't cover everything MANIFEST.in can do 1788 | # (http://docs.python.org/2/distutils/sourcedist.html#commands), so 1789 | # it might give some false negatives. Appending redundant 'include' 1790 | # lines is safe, though. 1791 | if "versioneer.py" not in simple_includes: 1792 | print(" appending 'versioneer.py' to MANIFEST.in") 1793 | with open(manifest_in, "a") as f: 1794 | f.write("include versioneer.py\n") 1795 | else: 1796 | print(" 'versioneer.py' already in MANIFEST.in") 1797 | if cfg.versionfile_source not in simple_includes: 1798 | print(" appending versionfile_source ('%s') to MANIFEST.in" % 1799 | cfg.versionfile_source) 1800 | with open(manifest_in, "a") as f: 1801 | f.write("include %s\n" % cfg.versionfile_source) 1802 | else: 1803 | print(" versionfile_source already in MANIFEST.in") 1804 | 1805 | # Make VCS-specific changes. For git, this means creating/changing 1806 | # .gitattributes to mark _version.py for export-subst keyword 1807 | # substitution. 1808 | do_vcs_install(manifest_in, cfg.versionfile_source, ipy) 1809 | return 0 1810 | 1811 | 1812 | def scan_setup_py(): 1813 | """Validate the contents of setup.py against Versioneer's expectations.""" 1814 | found = set() 1815 | setters = False 1816 | errors = 0 1817 | with open("setup.py", "r") as f: 1818 | for line in f.readlines(): 1819 | if "import versioneer" in line: 1820 | found.add("import") 1821 | if "versioneer.get_cmdclass()" in line: 1822 | found.add("cmdclass") 1823 | if "versioneer.get_version()" in line: 1824 | found.add("get_version") 1825 | if "versioneer.VCS" in line: 1826 | setters = True 1827 | if "versioneer.versionfile_source" in line: 1828 | setters = True 1829 | if len(found) != 3: 1830 | print("") 1831 | print("Your setup.py appears to be missing some important items") 1832 | print("(but I might be wrong). Please make sure it has something") 1833 | print("roughly like the following:") 1834 | print("") 1835 | print(" import versioneer") 1836 | print(" setup( version=versioneer.get_version(),") 1837 | print(" cmdclass=versioneer.get_cmdclass(), ...)") 1838 | print("") 1839 | errors += 1 1840 | if setters: 1841 | print("You should remove lines like 'versioneer.VCS = ' and") 1842 | print("'versioneer.versionfile_source = ' . This configuration") 1843 | print("now lives in setup.cfg, and should be removed from setup.py") 1844 | print("") 1845 | errors += 1 1846 | return errors 1847 | 1848 | 1849 | if __name__ == "__main__": 1850 | cmd = sys.argv[1] 1851 | if cmd == "setup": 1852 | errors = do_setup() 1853 | errors += scan_setup_py() 1854 | if errors: 1855 | sys.exit(1) 1856 | --------------------------------------------------------------------------------