├── .codacy.yml ├── .coveragerc ├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── conda-recipe.yml ├── environment.yml ├── pyosim ├── __init__.py ├── _version.py ├── analogs.py ├── analyse_tool.py ├── conf.py ├── inverse_dynamics.py ├── inverse_kinematics.py ├── joint_reaction.py ├── markers.py ├── model.py ├── muscle_analysis.py ├── project.py ├── scale.py └── static_optimization.py ├── setup.cfg ├── setup.py ├── tests ├── _conf.csv ├── data │ ├── forces_calibration_matrix.csv │ ├── markers.trc │ └── markers_analogs.c3d ├── pytest.ini ├── test_fileio.py └── test_tools.py └── versioneer.py /.codacy.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at codacy.com 2 | --- 3 | exclude_paths: 4 | - 'pyosim/_version.py' 5 | - 'tests/**/*' 6 | - 'tests/*' 7 | - 'benchmarks/**/*' 8 | - 'setup.py' 9 | - 'versioneer.py' 10 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | setup.py 4 | pyosim/_version.py 5 | versioneer.py 6 | tests/* 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | pyomeca/_version.py export-subst 2 | pyosim/_version.py export-subst 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 | 64 | # pycharm 65 | .idea 66 | 67 | # Misc 68 | Misc 69 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | language: python 3 | 4 | before_install: 5 | - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh 6 | - bash miniconda.sh -b -p $HOME/miniconda 7 | - export PATH="$HOME/miniconda/bin:$PATH" 8 | - hash -r 9 | - conda config --set always_yes yes --set changeps1 no 10 | - conda config --set auto_update_conda no 11 | - conda update -q conda 12 | 13 | install: 14 | - curl -L https://github.com/pyomeca/conda_recipes/raw/master/pyosim/meta.yaml --create-dirs -o ./conda.recipe/meta.yaml 15 | - cd conda.recipe 16 | - conda install conda-build anaconda-client pytest pytest-cov 17 | - conda build . --no-test -c pyomeca 18 | - cd .. 19 | - conda install --use-local pyosim -c pyomeca 20 | - conda info -a 21 | 22 | script: 23 | - pytest -v --color=yes --cov=pyosim tests 24 | 25 | after_success: 26 | - conda install -c conda-forge codecov 27 | - codecov 28 | 29 | notifications: 30 | email: 31 | on_success: never 32 | on_failure: always 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include versioneer.py 2 | include pyosim/_version.py 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test cover clean lint create_env 2 | 3 | ################################################################################# 4 | # GLOBALS # 5 | ################################################################################# 6 | 7 | REPO_NAME = pyosim 8 | EXCLUDES_LINT = --exclude=bin/,src/rebuydsutils/,docs/conf.py 9 | EXCLUDES_PYTEST = --ignore src/rebuydsutils 10 | SHELL=/bin/bash 11 | 12 | ifeq (,$(shell which conda)) 13 | $(error conda must be installed) 14 | endif 15 | 16 | # Define utility variable to help calling Python from the virtual environment 17 | ifeq ($(CONDA_DEFAULT_ENV),$(REPO_NAME)) 18 | ACTIVATE_ENV := true 19 | else 20 | ACTIVATE_ENV := source activate $(REPO_NAME) 21 | endif 22 | 23 | # Execute python related functionalities from within the project's environment 24 | define execute_in_env 25 | $(ACTIVATE_ENV) && $1 26 | endef 27 | 28 | ################################################################################# 29 | # PROJECT RULES # 30 | ################################################################################# 31 | 32 | ## Run pytest on the project 33 | test: 34 | $(call execute_in_env, python -m pytest --color=yes tests $(EXCLUDES_PYTEST)) 35 | 36 | ## Run coverage test on the project 37 | cover: 38 | $(call execute_in_env, python -m pytest --color=yes --cov=pyosim tests $(EXCLUDES_PYTEST)) 39 | 40 | ## Delete all compiled Python files 41 | clean: 42 | find . -name "*.pyc" -exec rm {} \; 43 | 44 | ## Lint using flake8; Excluding $EXCLUDES_LINT 45 | lint: 46 | $(call execute_in_env, flake8 $(EXCLUDES_LINT) .) 47 | 48 | ## Set up python interpreter environment 49 | create_env: 50 | conda env create -n $(REPO_NAME) -f environment.yml 51 | rm -rf *.egg-info 52 | 53 | ################################################################################# 54 | # Self Documenting Commands # 55 | ################################################################################# 56 | 57 | .DEFAULT_GOAL := help 58 | 59 | # Inspired by 60 | # sed script explained: 61 | # /^##/: 62 | # * save line in hold space 63 | # * purge line 64 | # * Loop: 65 | # * append newline + line to hold space 66 | # * go to next line 67 | # * if line starts with doc comment, strip comment character off and loop 68 | # * remove target prerequisites 69 | # * append hold space (+ newline) to line 70 | # * replace newline plus comments by `---` 71 | # * print line 72 | # Separate expressions are necessary because labels cannot be delimited by 73 | # semicolon; see 74 | .PHONY: help 75 | help: 76 | @echo "$$(tput bold)Available rules:$$(tput sgr0)" 77 | @echo 78 | @sed -n -e "/^## / { \ 79 | h; \ 80 | s/.*//; \ 81 | :doc" \ 82 | -e "H; \ 83 | n; \ 84 | s/^## //; \ 85 | t doc" \ 86 | -e "s/:.*//; \ 87 | G; \ 88 | s/\\n## /---/; \ 89 | s/\\n/ /g; \ 90 | p; \ 91 | }" ${MAKEFILE_LIST} \ 92 | | LC_ALL='C' sort --ignore-case \ 93 | | awk -F '---' \ 94 | -v ncol=$$(tput cols) \ 95 | -v indent=19 \ 96 | -v col_on="$$(tput setaf 6)" \ 97 | -v col_off="$$(tput sgr0)" \ 98 | '{ \ 99 | printf "%s%*s%s ", col_on, -indent, $$1, col_off; \ 100 | n = split($$2, words, " "); \ 101 | line_length = ncol - indent; \ 102 | for (i = 1; i <= n; i++) { \ 103 | line_length -= length(words[i]) + 1; \ 104 | if (line_length <= 0) { \ 105 | line_length = ncol - indent - length(words[i]) - 1; \ 106 | printf "\n%*s ", -indent, " "; \ 107 | } \ 108 | printf "%s ", words[i]; \ 109 | } \ 110 | printf "\n"; \ 111 | }' \ 112 | | more $(shell test $(shell uname) = Darwin && echo '--no-init --raw-control-chars') 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Pyosim 4 | 5 | > Pyosim is a python library building on top of [pyomeca](https://github.com/pyomeca/pyomeca) allowing you to batch process multiple musculoskeletal analysis performed by the OpenSim software. 6 | 7 | ## Example 8 | 9 | You can find some examples in the [`examples`](https://github.com/pyomeca/pyosim/tree/master/examples) folder. 10 | 11 | ## Installation 12 | 13 | ### Using Conda 14 | 15 | First, install [miniconda](https://conda.io/miniconda.html) or [anaconda](https://www.anaconda.com/download/). 16 | Then type: 17 | 18 | ```bash 19 | conda install pyosim -c pyomeca -c conda-forge 20 | ``` 21 | 22 | ## Integration with other modules 23 | 24 | Pyosim is designed to work well with other libraries that we have developed: 25 | 26 | - [pyomeca](https://github.com/pyomeca/pyomeca): Python toolbox for biomechanics analysis 27 | - [ezc3d](https://github.com/pyomeca/ezc3d): Easy to use C3D reader/writer in C++, Python and Matlab 28 | - [biorbd](https://github.com/pyomeca/biorbd): C++ interface and add-ons to the Rigid Body Dynamics Library, with Python and Matlab binders. 29 | - [pyoviz](https://github.com/pyomeca/pyoviz): pyomeca visualization toolkit and GUIs (still in development and not usable yet) 30 | 31 | 32 | ## Bug Reports & Questions 33 | 34 | Pyosim is Apache-licensed and the source code is available on [GitHub](https://github.com/pyomeca/pyomeca). 35 | 36 | If any questions or issues come up as you use pyosim, please get in touch via [GitHub issues](https://github.com/pyomeca/pyosim/issues). 37 | 38 | We welcome any input, feedback, bug reports, and contributions. 39 | -------------------------------------------------------------------------------- /conda-recipe.yml: -------------------------------------------------------------------------------- 1 | {% set data = load_setup_py_data() %} 2 | 3 | package: 4 | name: pyosim 5 | version: {{ data['version'] }} 6 | 7 | source: 8 | git_url: https://github.com/pyomeca/pyosim 9 | 10 | build: 11 | # If the installation is complex, or different between Unix and Windows, use 12 | # separate bld.bat and build.sh files instead of this key. Add the line 13 | # "skip: True # [py<35]" (for example) to limit to Python 3.5 and newer, or 14 | # "skip: True # [not win]" to limit to Windows. 15 | script: python setup.py install --single-version-externally-managed --record=record.txt 16 | 17 | requirements: 18 | build: 19 | - python 20 | - setuptools 21 | - pyyaml 22 | run: 23 | - python 24 | # dependencies are defined in setup.py 25 | {% for dep in data['install_requires'] %} 26 | - {{ dep.lower() }} 27 | {% endfor %} 28 | 29 | test: 30 | imports: 31 | - pyosim 32 | 33 | about: 34 | home: https://github.com/pyomeca/pyosim 35 | summary: Interface between OpenSim and the Pyomeca library 36 | 37 | extra: 38 | recipe-maintainers: 39 | - mrtnz 40 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | # create a dev conda environment with: `make create_env` or `conda env create -f environment.yml` 2 | name: pyosim 3 | channels: 4 | - defaults 5 | - conda-forge 6 | - pyomeca 7 | dependencies: 8 | - python 9 | - numpy 10 | - pandas 11 | - matplotlib 12 | - pyomeca 13 | - opensim 14 | -------------------------------------------------------------------------------- /pyosim/__init__.py: -------------------------------------------------------------------------------- 1 | from .scale import * 2 | from .project import * 3 | from .model import * 4 | from .conf import * 5 | from .inverse_kinematics import * 6 | from .inverse_dynamics import * 7 | from .analyse_tool import * 8 | from .static_optimization import * 9 | from .muscle_analysis import * 10 | from .joint_reaction import * 11 | from .analogs import * 12 | from .markers import * 13 | 14 | __author__ = "Romain Martinez" 15 | __version__ = "0.1.0" 16 | 17 | from ._version import get_versions 18 | 19 | __version__ = get_versions()["version"] 20 | del get_versions 21 | -------------------------------------------------------------------------------- /pyosim/_version.py: -------------------------------------------------------------------------------- 1 | 2 | # This file helps to compute a version number in source trees obtained from 3 | # git-archive tarball (such as those provided by githubs download-from-tag 4 | # feature). Distribution tarballs (built by setup.py sdist) and build 5 | # directories (produced by setup.py build) will contain a much shorter file 6 | # that just contains the computed version number. 7 | 8 | # This file is released into the public domain. Generated by 9 | # versioneer-0.18 (https://github.com/warner/python-versioneer) 10 | 11 | """Git implementation of _version.py.""" 12 | 13 | import errno 14 | import os 15 | import re 16 | import subprocess 17 | import sys 18 | 19 | 20 | def get_keywords(): 21 | """Get the keywords needed to look up the version information.""" 22 | # these strings will be replaced by git during git-archive. 23 | # setup.py/versioneer.py will grep for the variable names, so they must 24 | # each be defined on a line of their own. _version.py will just call 25 | # get_keywords(). 26 | git_refnames = " (HEAD -> master)" 27 | git_full = "84b418ba276a9e5c499c44acd3fc4531103ad6c0" 28 | git_date = "2021-08-03 10:20:28 -0400" 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 = "pyosim-" 46 | cfg.versionfile_source = "pyosim/_version.py" 47 | cfg.verbose = False 48 | return cfg 49 | 50 | 51 | class NotThisMethod(Exception): 52 | """Exception raised if a method is not valid for the current scenario.""" 53 | 54 | 55 | LONG_VERSION_PY = {} 56 | HANDLERS = {} 57 | 58 | 59 | def register_vcs_handler(vcs, method): # decorator 60 | """Decorator to mark a method as the handler for a particular VCS.""" 61 | def decorate(f): 62 | """Store f in HANDLERS[vcs][method].""" 63 | if vcs not in HANDLERS: 64 | HANDLERS[vcs] = {} 65 | HANDLERS[vcs][method] = f 66 | return f 67 | return decorate 68 | 69 | 70 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 71 | env=None): 72 | """Call the given command(s).""" 73 | assert isinstance(commands, list) 74 | p = None 75 | for c in commands: 76 | try: 77 | dispcmd = str([c] + args) 78 | # remember shell=False, so use git.cmd on windows, not just git 79 | p = subprocess.Popen([c] + args, cwd=cwd, env=env, 80 | stdout=subprocess.PIPE, 81 | stderr=(subprocess.PIPE if hide_stderr 82 | else None)) 83 | break 84 | except EnvironmentError: 85 | e = sys.exc_info()[1] 86 | if e.errno == errno.ENOENT: 87 | continue 88 | if verbose: 89 | print("unable to run %s" % dispcmd) 90 | print(e) 91 | return None, None 92 | else: 93 | if verbose: 94 | print("unable to find command, tried %s" % (commands,)) 95 | return None, None 96 | stdout = p.communicate()[0].strip() 97 | if sys.version_info[0] >= 3: 98 | stdout = stdout.decode() 99 | if p.returncode != 0: 100 | if verbose: 101 | print("unable to run %s (error)" % dispcmd) 102 | print("stdout was %s" % stdout) 103 | return None, p.returncode 104 | return stdout, p.returncode 105 | 106 | 107 | def versions_from_parentdir(parentdir_prefix, root, verbose): 108 | """Try to determine the version from the parent directory name. 109 | 110 | Source tarballs conventionally unpack into a directory that includes both 111 | the project name and a version string. We will also support searching up 112 | two directory levels for an appropriately named parent directory 113 | """ 114 | rootdirs = [] 115 | 116 | for i in range(3): 117 | dirname = os.path.basename(root) 118 | if dirname.startswith(parentdir_prefix): 119 | return {"version": dirname[len(parentdir_prefix):], 120 | "full-revisionid": None, 121 | "dirty": False, "error": None, "date": None} 122 | else: 123 | rootdirs.append(root) 124 | root = os.path.dirname(root) # up a level 125 | 126 | if verbose: 127 | print("Tried directories %s but none started with prefix %s" % 128 | (str(rootdirs), parentdir_prefix)) 129 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 130 | 131 | 132 | @register_vcs_handler("git", "get_keywords") 133 | def git_get_keywords(versionfile_abs): 134 | """Extract version information from the given file.""" 135 | # the code embedded in _version.py can just fetch the value of these 136 | # keywords. When used from setup.py, we don't want to import _version.py, 137 | # so we do it with a regexp instead. This function is not used from 138 | # _version.py. 139 | keywords = {} 140 | try: 141 | f = open(versionfile_abs, "r") 142 | for line in f.readlines(): 143 | if line.strip().startswith("git_refnames ="): 144 | mo = re.search(r'=\s*"(.*)"', line) 145 | if mo: 146 | keywords["refnames"] = mo.group(1) 147 | if line.strip().startswith("git_full ="): 148 | mo = re.search(r'=\s*"(.*)"', line) 149 | if mo: 150 | keywords["full"] = mo.group(1) 151 | if line.strip().startswith("git_date ="): 152 | mo = re.search(r'=\s*"(.*)"', line) 153 | if mo: 154 | keywords["date"] = mo.group(1) 155 | f.close() 156 | except EnvironmentError: 157 | pass 158 | return keywords 159 | 160 | 161 | @register_vcs_handler("git", "keywords") 162 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 163 | """Get version information from git keywords.""" 164 | if not keywords: 165 | raise NotThisMethod("no keywords at all, weird") 166 | date = keywords.get("date") 167 | if date is not None: 168 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 169 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 170 | # -like" string, which we must then edit to make compliant), because 171 | # it's been around since git-1.5.3, and it's too difficult to 172 | # discover which version we're using, or to work around using an 173 | # older one. 174 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 175 | refnames = keywords["refnames"].strip() 176 | if refnames.startswith("$Format"): 177 | if verbose: 178 | print("keywords are unexpanded, not using") 179 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 180 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 181 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 182 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 183 | TAG = "tag: " 184 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 185 | if not tags: 186 | # Either we're using git < 1.8.3, or there really are no tags. We use 187 | # a heuristic: assume all version tags have a digit. The old git %d 188 | # expansion behaves like git log --decorate=short and strips out the 189 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 190 | # between branches and tags. By ignoring refnames without digits, we 191 | # filter out many common branch names like "release" and 192 | # "stabilization", as well as "HEAD" and "master". 193 | tags = set([r for r in refs if re.search(r'\d', r)]) 194 | if verbose: 195 | print("discarding '%s', no digits" % ",".join(refs - tags)) 196 | if verbose: 197 | print("likely tags: %s" % ",".join(sorted(tags))) 198 | for ref in sorted(tags): 199 | # sorting will prefer e.g. "2.0" over "2.0rc1" 200 | if ref.startswith(tag_prefix): 201 | r = ref[len(tag_prefix):] 202 | if verbose: 203 | print("picking %s" % r) 204 | return {"version": r, 205 | "full-revisionid": keywords["full"].strip(), 206 | "dirty": False, "error": None, 207 | "date": date} 208 | # no suitable tags, so version is "0+unknown", but full hex is still there 209 | if verbose: 210 | print("no suitable tags, using unknown + full revision id") 211 | return {"version": "0+unknown", 212 | "full-revisionid": keywords["full"].strip(), 213 | "dirty": False, "error": "no suitable tags", "date": None} 214 | 215 | 216 | @register_vcs_handler("git", "pieces_from_vcs") 217 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 218 | """Get version from 'git describe' in the root of the source tree. 219 | 220 | This only gets called if the git-archive 'subst' keywords were *not* 221 | expanded, and _version.py hasn't already been rewritten with a short 222 | version string, meaning we're inside a checked out source tree. 223 | """ 224 | GITS = ["git"] 225 | if sys.platform == "win32": 226 | GITS = ["git.cmd", "git.exe"] 227 | 228 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, 229 | hide_stderr=True) 230 | if rc != 0: 231 | if verbose: 232 | print("Directory %s not under git control" % root) 233 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 234 | 235 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 236 | # if there isn't one, this yields HEX[-dirty] (no NUM) 237 | describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", 238 | "--always", "--long", 239 | "--match", "%s*" % tag_prefix], 240 | cwd=root) 241 | # --long was added in git-1.5.5 242 | if describe_out is None: 243 | raise NotThisMethod("'git describe' failed") 244 | describe_out = describe_out.strip() 245 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 246 | if full_out is None: 247 | raise NotThisMethod("'git rev-parse' failed") 248 | full_out = full_out.strip() 249 | 250 | pieces = {} 251 | pieces["long"] = full_out 252 | pieces["short"] = full_out[:7] # maybe improved later 253 | pieces["error"] = None 254 | 255 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 256 | # TAG might have hyphens. 257 | git_describe = describe_out 258 | 259 | # look for -dirty suffix 260 | dirty = git_describe.endswith("-dirty") 261 | pieces["dirty"] = dirty 262 | if dirty: 263 | git_describe = git_describe[:git_describe.rindex("-dirty")] 264 | 265 | # now we have TAG-NUM-gHEX or HEX 266 | 267 | if "-" in git_describe: 268 | # TAG-NUM-gHEX 269 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 270 | if not mo: 271 | # unparseable. Maybe git-describe is misbehaving? 272 | pieces["error"] = ("unable to parse git-describe output: '%s'" 273 | % describe_out) 274 | return pieces 275 | 276 | # tag 277 | full_tag = mo.group(1) 278 | if not full_tag.startswith(tag_prefix): 279 | if verbose: 280 | fmt = "tag '%s' doesn't start with prefix '%s'" 281 | print(fmt % (full_tag, tag_prefix)) 282 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 283 | % (full_tag, tag_prefix)) 284 | return pieces 285 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 286 | 287 | # distance: number of commits since tag 288 | pieces["distance"] = int(mo.group(2)) 289 | 290 | # commit: short hex revision ID 291 | pieces["short"] = mo.group(3) 292 | 293 | else: 294 | # HEX: no tags 295 | pieces["closest-tag"] = None 296 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], 297 | cwd=root) 298 | pieces["distance"] = int(count_out) # total number of commits 299 | 300 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 301 | date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], 302 | cwd=root)[0].strip() 303 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 304 | 305 | return pieces 306 | 307 | 308 | def plus_or_dot(pieces): 309 | """Return a + if we don't already have one, else return a .""" 310 | if "+" in pieces.get("closest-tag", ""): 311 | return "." 312 | return "+" 313 | 314 | 315 | def render_pep440(pieces): 316 | """Build up version string, with post-release "local version identifier". 317 | 318 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 319 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 320 | 321 | Exceptions: 322 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 323 | """ 324 | if pieces["closest-tag"]: 325 | rendered = pieces["closest-tag"] 326 | if pieces["distance"] or pieces["dirty"]: 327 | rendered += plus_or_dot(pieces) 328 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 329 | if pieces["dirty"]: 330 | rendered += ".dirty" 331 | else: 332 | # exception #1 333 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 334 | pieces["short"]) 335 | if pieces["dirty"]: 336 | rendered += ".dirty" 337 | return rendered 338 | 339 | 340 | def render_pep440_pre(pieces): 341 | """TAG[.post.devDISTANCE] -- No -dirty. 342 | 343 | Exceptions: 344 | 1: no tags. 0.post.devDISTANCE 345 | """ 346 | if pieces["closest-tag"]: 347 | rendered = pieces["closest-tag"] 348 | if pieces["distance"]: 349 | rendered += ".post.dev%d" % pieces["distance"] 350 | else: 351 | # exception #1 352 | rendered = "0.post.dev%d" % pieces["distance"] 353 | return rendered 354 | 355 | 356 | def render_pep440_post(pieces): 357 | """TAG[.postDISTANCE[.dev0]+gHEX] . 358 | 359 | The ".dev0" means dirty. Note that .dev0 sorts backwards 360 | (a dirty tree will appear "older" than the corresponding clean one), 361 | but you shouldn't be releasing software with -dirty anyways. 362 | 363 | Exceptions: 364 | 1: no tags. 0.postDISTANCE[.dev0] 365 | """ 366 | if pieces["closest-tag"]: 367 | rendered = pieces["closest-tag"] 368 | if pieces["distance"] or pieces["dirty"]: 369 | rendered += ".post%d" % pieces["distance"] 370 | if pieces["dirty"]: 371 | rendered += ".dev0" 372 | rendered += plus_or_dot(pieces) 373 | rendered += "g%s" % pieces["short"] 374 | else: 375 | # exception #1 376 | rendered = "0.post%d" % pieces["distance"] 377 | if pieces["dirty"]: 378 | rendered += ".dev0" 379 | rendered += "+g%s" % pieces["short"] 380 | return rendered 381 | 382 | 383 | def render_pep440_old(pieces): 384 | """TAG[.postDISTANCE[.dev0]] . 385 | 386 | The ".dev0" means dirty. 387 | 388 | Eexceptions: 389 | 1: no tags. 0.postDISTANCE[.dev0] 390 | """ 391 | if pieces["closest-tag"]: 392 | rendered = pieces["closest-tag"] 393 | if pieces["distance"] or pieces["dirty"]: 394 | rendered += ".post%d" % pieces["distance"] 395 | if pieces["dirty"]: 396 | rendered += ".dev0" 397 | else: 398 | # exception #1 399 | rendered = "0.post%d" % pieces["distance"] 400 | if pieces["dirty"]: 401 | rendered += ".dev0" 402 | return rendered 403 | 404 | 405 | def render_git_describe(pieces): 406 | """TAG[-DISTANCE-gHEX][-dirty]. 407 | 408 | Like 'git describe --tags --dirty --always'. 409 | 410 | Exceptions: 411 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 412 | """ 413 | if pieces["closest-tag"]: 414 | rendered = pieces["closest-tag"] 415 | if pieces["distance"]: 416 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 417 | else: 418 | # exception #1 419 | rendered = pieces["short"] 420 | if pieces["dirty"]: 421 | rendered += "-dirty" 422 | return rendered 423 | 424 | 425 | def render_git_describe_long(pieces): 426 | """TAG-DISTANCE-gHEX[-dirty]. 427 | 428 | Like 'git describe --tags --dirty --always -long'. 429 | The distance/hash is unconditional. 430 | 431 | Exceptions: 432 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 433 | """ 434 | if pieces["closest-tag"]: 435 | rendered = pieces["closest-tag"] 436 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 437 | else: 438 | # exception #1 439 | rendered = pieces["short"] 440 | if pieces["dirty"]: 441 | rendered += "-dirty" 442 | return rendered 443 | 444 | 445 | def render(pieces, style): 446 | """Render the given version pieces into the requested style.""" 447 | if pieces["error"]: 448 | return {"version": "unknown", 449 | "full-revisionid": pieces.get("long"), 450 | "dirty": None, 451 | "error": pieces["error"], 452 | "date": None} 453 | 454 | if not style or style == "default": 455 | style = "pep440" # the default 456 | 457 | if style == "pep440": 458 | rendered = render_pep440(pieces) 459 | elif style == "pep440-pre": 460 | rendered = render_pep440_pre(pieces) 461 | elif style == "pep440-post": 462 | rendered = render_pep440_post(pieces) 463 | elif style == "pep440-old": 464 | rendered = render_pep440_old(pieces) 465 | elif style == "git-describe": 466 | rendered = render_git_describe(pieces) 467 | elif style == "git-describe-long": 468 | rendered = render_git_describe_long(pieces) 469 | else: 470 | raise ValueError("unknown style '%s'" % style) 471 | 472 | return {"version": rendered, "full-revisionid": pieces["long"], 473 | "dirty": pieces["dirty"], "error": None, 474 | "date": pieces.get("date")} 475 | 476 | 477 | def get_versions(): 478 | """Get version information or return default if unable to do so.""" 479 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 480 | # __file__, we can work backwards from there to the root. Some 481 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 482 | # case we can only use expanded keywords. 483 | 484 | cfg = get_config() 485 | verbose = cfg.verbose 486 | 487 | try: 488 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 489 | verbose) 490 | except NotThisMethod: 491 | pass 492 | 493 | try: 494 | root = os.path.realpath(__file__) 495 | # versionfile_source is the relative path from the top of the source 496 | # tree (where the .git directory might live) to this file. Invert 497 | # this to find the root from __file__. 498 | for i in cfg.versionfile_source.split('/'): 499 | root = os.path.dirname(root) 500 | except NameError: 501 | return {"version": "0+unknown", "full-revisionid": None, 502 | "dirty": None, 503 | "error": "unable to find root of source tree", 504 | "date": None} 505 | 506 | try: 507 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 508 | return render(pieces, cfg.style) 509 | except NotThisMethod: 510 | pass 511 | 512 | try: 513 | if cfg.parentdir_prefix: 514 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 515 | except NotThisMethod: 516 | pass 517 | 518 | return {"version": "0+unknown", "full-revisionid": None, 519 | "dirty": None, 520 | "error": "unable to compute version", "date": None} 521 | -------------------------------------------------------------------------------- /pyosim/analogs.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import numpy as np 4 | import opensim as osim 5 | 6 | from pyomeca import Analogs 7 | 8 | 9 | class Analogs3dOsim(Analogs): 10 | def __new__(cls, *args, **kwargs): 11 | """Convenient wrapper around Analogs3d from the pyomeca library""" 12 | return super(Analogs3dOsim, cls).__new__(cls, *args, **kwargs) 13 | 14 | def __array_finalize__(self, obj): 15 | super().__array_finalize__(obj) 16 | # Allow slicing 17 | if obj is None or not isinstance(obj, Analogs3dOsim): 18 | return 19 | 20 | def to_sto(self, filename, metadata=None): 21 | """ 22 | Write a sto file from a Analogs3dOsim 23 | Parameters 24 | ---------- 25 | filename : string 26 | path of the file to write 27 | metadata : dict, optional 28 | dict with optional metadata to add in the output file 29 | """ 30 | filename = Path(filename) 31 | # Make sure the directory exists, otherwise create it 32 | if not filename.parents[0].is_dir(): 33 | filename.parents[0].mkdir() 34 | 35 | table = osim.TimeSeriesTable() 36 | 37 | # set metadata 38 | table.setColumnLabels(self.get_labels) 39 | if metadata: 40 | for key, value in metadata.items(): 41 | table.addTableMetaDataString(key, str(value)) 42 | if 'nColumns' not in metadata: 43 | table.addTableMetaDataString('nColumns', str(self.shape[1])) 44 | else: 45 | table.addTableMetaDataString('nColumns', str(self.shape[1])) 46 | table.addTableMetaDataString('nRows', str(self.shape[-1])) 47 | 48 | time_vector = np.arange(start=0, stop=1 / self.get_rate * self.shape[2], step=1 / self.get_rate) 49 | 50 | for iframe in range(self.shape[-1]): 51 | a = self.get_frame(iframe) 52 | row = osim.RowVector(a.ravel().tolist()) 53 | table.appendRow(time_vector[iframe], row) 54 | 55 | adapter = osim.STOFileAdapter() 56 | adapter.write(table, str(filename)) 57 | -------------------------------------------------------------------------------- /pyosim/analyse_tool.py: -------------------------------------------------------------------------------- 1 | """ 2 | Analyze tool class in pyosim. 3 | Used in static optimization, muscle analysis and joint reaction analysis. 4 | """ 5 | from pathlib import Path 6 | 7 | import opensim as osim 8 | 9 | 10 | class AnalyzeTool: 11 | """ 12 | Analyze tool in pyosim. 13 | Used in static optimization, muscle analysis and joint reaction analysis. 14 | 15 | Parameters 16 | ---------- 17 | model_input : str 18 | Path to the osim model 19 | xml_input : str 20 | Path to the generic so xml 21 | xml_output : str 22 | Output path of the so xml 23 | xml_forces : str, optional 24 | Path to the generic forces sensor xml (Optional) 25 | ext_forces_dir : str, optional 26 | Path of the directory containing the external forces files (`.sto`) (Optional) 27 | muscle_forces_dir : str, optional 28 | Path of the directory containing the muscle forces files (`.sto`) (Optional) 29 | mot_files : str, Path, list 30 | Path or list of path to the directory containing the motion files (`.mot`) 31 | sto_output : Path, str 32 | Output directory 33 | xml_actuators: Path, str 34 | Actuators (Optional) 35 | prefix : str, optional 36 | Optional prefix to put in front of the output filename (typically model name) (Optional) 37 | low_pass : int, optional 38 | Cutoff frequency for an optional low pass filter on coordinates (Optional) 39 | remove_empty_files : bool, optional 40 | remove empty files i in `sto_output` if True (Optional) 41 | multi : bool, optional 42 | Launch AnalyzeTool in multiprocessing if True 43 | 44 | Examples 45 | -------- 46 | >>> from pathlib import Path 47 | >>> 48 | >>> from pyosim import AnalyzeTool 49 | >>> 50 | >>> PROJECT_PATH = Path('../Misc/project_sample') 51 | >>> TEMPLATES_PATH = PROJECT_PATH / '_templates' 52 | >>> 53 | >>> participant = 'dapo' 54 | >>> model = 'wu' 55 | >>> trials = [ifile for ifile in (PROJECT_PATH / participant / '1_inverse_kinematic').glob('*.mot')] 56 | >>> 57 | >>> path_kwargs = { 58 | >>> 'model_input': f"{(PROJECT_PATH / participant / '_models' / model).resolve()}_scaled_markers.osim", 59 | >>> 'xml_input': f"{(TEMPLATES_PATH / model).resolve()}_so.xml", 60 | >>> 'xml_output': f"{(PROJECT_PATH / participant / '_xml' / model).resolve()}_so.xml", 61 | >>> 'xml_forces': f"{(TEMPLATES_PATH / 'forces_sensor.xml').resolve()}", 62 | >>> 'xml_actuators': f"{(TEMPLATES_PATH / f'{model}_actuators.xml').resolve()}", 63 | >>> 'ext_forces_dir': f"{(PROJECT_PATH / participant / '0_forces').resolve()}", 64 | >>> 'muscle_forces_dir': f"{(PROJECT_PATH / participant / '3_static_optimization').resolve()}", 65 | >>> 'sto_output': f"{(PROJECT_PATH / participant / '3_static_optimization').resolve()}", 66 | >>> } 67 | >>> 68 | >>> AnalyzeTool( 69 | >>> **path_kwargs, 70 | >>> mot_files=trials, 71 | >>> prefix=model, 72 | >>> low_pass=5 73 | >>> ) 74 | """ 75 | 76 | def __init__( 77 | self, 78 | model_input, 79 | xml_input, 80 | xml_output, 81 | sto_output, 82 | mot_files, 83 | forces_file=None, 84 | xml_forces=None, 85 | ext_forces_dir=None, 86 | muscle_forces_dir=None, 87 | xml_actuators=None, 88 | prefix=None, 89 | low_pass=None, 90 | remove_empty_files=False, 91 | multi=False, 92 | contains=None, 93 | print_to_xml=False, 94 | ): 95 | self.model_input = model_input 96 | self.xml_input = xml_input 97 | self.xml_output = xml_output 98 | self.sto_output = sto_output 99 | self.xml_forces = xml_forces 100 | self.forces_file = forces_file 101 | self.ext_forces_dir = ext_forces_dir 102 | self.muscle_forces_dir = muscle_forces_dir 103 | self.xml_actuators = xml_actuators 104 | self.prefix = prefix 105 | self.low_pass = low_pass 106 | self.remove_empty_files = remove_empty_files 107 | self.multi = multi 108 | self.contains = contains 109 | self.print_to_xml = print_to_xml 110 | 111 | if not isinstance(mot_files, list): 112 | self.mot_files = [mot_files] 113 | else: 114 | self.mot_files = mot_files 115 | 116 | if not isinstance(self.mot_files[0], Path): 117 | self.mot_files = [Path(i) for i in self.mot_files] 118 | 119 | self.main_loop() 120 | 121 | def main_loop(self): 122 | if self.multi: 123 | import os 124 | from multiprocessing import Pool 125 | 126 | pool = Pool(os.cpu_count()) 127 | pool.map(self.run_analyze_tool, self.mot_files) 128 | else: 129 | for itrial in self.mot_files: 130 | self.run_analyze_tool(itrial) 131 | 132 | def run_analyze_tool(self, trial): 133 | if self.prefix and not trial.stem.startswith(self.prefix): 134 | # skip file if user specified a prefix and prefix is not present in current file 135 | pass 136 | else: 137 | print(f"\t{trial.stem}") 138 | 139 | # model 140 | model = osim.Model(self.model_input) if isinstance(self.model_input, str) is True else self.model_input 141 | model.initSystem() 142 | 143 | # get starting and ending time 144 | motion = osim.Storage(f"{trial.resolve()}") 145 | first_time = motion.getFirstTime() 146 | last_time = motion.getLastTime() 147 | 148 | # prepare external forces xml file 149 | if self.xml_forces: 150 | external_loads = osim.ExternalLoads(self.xml_forces, True) 151 | if self.prefix: 152 | external_loads.setDataFileName( 153 | f"{Path(self.ext_forces_dir, trial.stem.replace(f'{self.prefix}_', '')).resolve()}.sto" 154 | ) 155 | else: 156 | external_loads.setDataFileName( 157 | f"{Path(self.ext_forces_dir, trial.stem).resolve()}.sto" 158 | ) 159 | external_loads.setExternalLoadsModelKinematicsFileName( 160 | f"{trial.resolve()}" 161 | ) 162 | if self.low_pass: 163 | external_loads.setLowpassCutoffFrequencyForLoadKinematics( 164 | self.low_pass 165 | ) 166 | temp_xml = Path(f"{trial.stem}_temp.xml") 167 | external_loads.printToXML(f"{temp_xml.resolve()}") # temporary xml file 168 | 169 | current_class = self.get_class_name() 170 | params = self.parse_analyze_set_xml(self.xml_input, node=current_class) 171 | solve_for_equilibrium = False 172 | if current_class == "StaticOptimization": 173 | analysis = osim.StaticOptimization(model) 174 | analysis.setUseModelForceSet(params["use_model_force_set"]) 175 | analysis.setActivationExponent(params["activation_exponent"]) 176 | analysis.setUseMusclePhysiology(params["use_muscle_physiology"]) 177 | analysis.setConvergenceCriterion( 178 | params["optimizer_convergence_criterion"] 179 | ) 180 | analysis.setMaxIterations(int(params["optimizer_max_iterations"])) 181 | elif current_class == "MuscleAnalysis": 182 | solve_for_equilibrium = True 183 | analysis = osim.MuscleAnalysis(model) 184 | coord = osim.ArrayStr() 185 | for c in params["moment_arm_coordinate_list"]: 186 | coord.append(c) 187 | analysis.setCoordinates(coord) 188 | 189 | mus = osim.ArrayStr() 190 | for m in params["muscle_list"]: 191 | mus.append(m) 192 | analysis.setMuscles(mus) 193 | # analysis.setComputeMoments(params["compute_moments"]) 194 | elif current_class == "JointReaction": 195 | # construct joint reaction analysis 196 | analysis = osim.JointReaction(model) 197 | if params["forces_file"] or self.forces_file: 198 | force_file = self.forces_file if self.forces_file else params["forces_file"] 199 | analysis.setForcesFileName(force_file) 200 | 201 | joint = osim.ArrayStr() 202 | for j in params["joint_names"]: 203 | joint.append(j) 204 | analysis.setJointNames(joint) 205 | 206 | body = osim.ArrayStr() 207 | for b in params["apply_on_bodies"]: 208 | body.append(b) 209 | analysis.setOnBody(body) 210 | 211 | frame = osim.ArrayStr() 212 | for f in params["express_in_frame"]: 213 | frame.append(f) 214 | analysis.setInFrame(frame) 215 | else: 216 | raise ValueError("AnalyzeTool must be called from a child class") 217 | analysis.setModel(model) 218 | analysis.setName(current_class) 219 | analysis.setOn(params["on"]) 220 | analysis.setStepInterval(int(params["step_interval"])) 221 | analysis.setInDegrees(params["in_degrees"]) 222 | analysis.setStartTime(first_time) 223 | analysis.setEndTime(last_time) 224 | model.addAnalysis(analysis) 225 | 226 | if self.print_to_xml is True: 227 | analysis.printToXML(f"{self.xml_output}/{current_class}_analysis.xml") 228 | 229 | # analysis tool 230 | analyze_tool = osim.AnalyzeTool(model) 231 | analyze_tool.setName(trial.stem) 232 | analyze_tool.setModel(model) 233 | analyze_tool.setModelFilename(Path(model.toString()).stem) 234 | analyze_tool.setSolveForEquilibrium(solve_for_equilibrium) 235 | 236 | if self.xml_actuators: 237 | force_set = osim.ArrayStr() 238 | force_set.append(self.xml_actuators) 239 | analyze_tool.setForceSetFiles(force_set) 240 | analyze_tool.updateModelForces(model, self.xml_actuators) 241 | 242 | analyze_tool.setInitialTime(first_time) 243 | analyze_tool.setFinalTime(last_time) 244 | 245 | if self.low_pass: 246 | analyze_tool.setLowpassCutoffFrequency(self.low_pass) 247 | 248 | analyze_tool.setCoordinatesFileName(f"{trial.resolve()}") 249 | if self.xml_forces: 250 | analyze_tool.setExternalLoadsFileName(f"{temp_xml}") 251 | analyze_tool.setLoadModelAndInput(True) 252 | analyze_tool.setResultsDir(f"{self.sto_output}") 253 | 254 | analyze_tool.run() 255 | 256 | if self.xml_forces: 257 | temp_xml.unlink() # delete temporary xml file 258 | 259 | if self.remove_empty_files: 260 | self._remove_empty_files(directory=self.sto_output) 261 | 262 | if self.contains: 263 | self._subset_output(directory=self.sto_output, contains=self.contains) 264 | 265 | def parse_analyze_set_xml(self, filename, node): 266 | from xml.etree import ElementTree 267 | 268 | tree = ElementTree.parse(filename) 269 | root = tree.getroot() 270 | 271 | def isfloat(value): 272 | try: 273 | float(value) 274 | return True 275 | except ValueError: 276 | return False 277 | 278 | out = {} 279 | for t in root.findall(f".//{node}/*"): 280 | if t.text == "true": 281 | out.update({t.tag: True}) 282 | elif t.text == "false": 283 | out.update({t.tag: False}) 284 | elif t.text is None: 285 | out.update({t.tag: t.text}) 286 | elif isfloat(t.text) is True: 287 | out.update({t.tag: float(t.text)}) 288 | else: 289 | out.update({t.tag: self._str_to_list(t.text)}) 290 | 291 | 292 | return out 293 | 294 | @staticmethod 295 | def _str_to_list(string): 296 | count = 0 297 | car = string[count] 298 | while car == " ": 299 | count += 1 300 | car = string[count] 301 | 302 | string = string[count:] 303 | li = list(string.split(" ")) 304 | return li 305 | 306 | @staticmethod 307 | def _remove_empty_files(directory, threshold=1000): 308 | """ 309 | Remove empty files from a directory. 310 | 311 | Parameters 312 | ---------- 313 | directory : str, Path 314 | directory 315 | threshold : int 316 | threshold in bytes 317 | """ 318 | for ifile in Path(directory).iterdir(): 319 | if ifile.stat().st_size < threshold: 320 | ifile.unlink() 321 | 322 | @staticmethod 323 | def _subset_output(directory, contains): 324 | """ 325 | Keep only files that contains `contains` string 326 | 327 | Parameters 328 | ---------- 329 | directory : str, Path 330 | directory 331 | contains : str 332 | string 333 | """ 334 | for ifile in Path(directory).iterdir(): 335 | if contains not in ifile.stem: 336 | ifile.unlink() 337 | 338 | @classmethod 339 | def get_class_name(cls): 340 | return cls.__name__ 341 | 342 | -------------------------------------------------------------------------------- /pyosim/conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration class in pyosim 3 | """ 4 | import json 5 | from pathlib import Path 6 | 7 | import pandas as pd 8 | 9 | 10 | class Conf: 11 | """ 12 | Configuration class in pyosim 13 | 14 | Parameters 15 | ---------- 16 | project_path : str, Path 17 | Path to the project 18 | conf_file : str 19 | Filename of the configuration file 20 | """ 21 | 22 | def __init__(self, project_path, conf_file='_conf.csv'): 23 | # load project dir 24 | self.project_path = Path(project_path) 25 | if not self.project_path.is_dir(): 26 | raise ValueError(f'{self.project_path} does not exist') 27 | else: 28 | print('Project loaded') 29 | 30 | # load project conf 31 | self.conf_path = self.project_path / conf_file 32 | if not self.conf_path.is_file(): 33 | raise ValueError(f'{self.conf_path} does not exist') 34 | else: 35 | self.project_conf = pd.read_csv(self.conf_path) 36 | print('Configuration file loaded') 37 | 38 | def get_participants_to_process(self): 39 | """ 40 | Get a list of participants with the flag 'process' to one or true in project configuration file 41 | Returns 42 | ------- 43 | list 44 | """ 45 | to_process = self.project_conf['process'] == True 46 | return self.project_conf['participant'].loc[to_process].tolist() 47 | 48 | def get_project_conf_column(self, col): 49 | """ 50 | Get column(s) from the conf file 51 | 52 | Parameters 53 | ---------- 54 | col : str 55 | column to return 56 | 57 | Returns 58 | ------- 59 | pandas series 60 | """ 61 | return self.project_conf[col] 62 | 63 | def check_confs(self, verbose=False): 64 | """check if all participants have a configuration file and update it in the project's configuration file""" 65 | 66 | for index, irow in self.project_conf.iterrows(): 67 | if not irow['process']: 68 | continue 69 | default = (self.project_path / irow['participant'] / '_conf.json') 70 | is_nan = irow['conf_file'] != irow['conf_file'] 71 | if not is_nan and Path(irow['conf_file']).is_file(): 72 | if verbose: 73 | print(f'{irow["participant"]}: checked') 74 | if (is_nan and default.is_file()) or default.is_file(): # check if nan or file exist in default location 75 | conf_file = str(default.resolve()) 76 | self.project_conf.loc[index, 'conf_file'] = conf_file 77 | self.update_conf(conf_file, {'conf_file': conf_file}) 78 | if verbose: 79 | print(f'{irow["participant"]}: updated in project conf') 80 | else: 81 | raise ValueError(f'{irow["participant"]} does not have a configuration file in {irow["conf_file"]}') 82 | 83 | # update conf file 84 | self.project_conf.to_csv(self.conf_path, index=False) 85 | 86 | @classmethod 87 | def update_conf(cls, filename, d): 88 | """ 89 | Update a json file with the dictionary `d` 90 | Parameters 91 | ---------- 92 | filename : str 93 | Path to the json file 94 | d : dict 95 | Dictionary to add in configuration file 96 | """ 97 | 98 | def dict_merge(dct, merge_dct): 99 | """Recursive dict merge. Inspired by :meth:`dict.update()`, instead of 100 | updating only top-level keys, dict_merge recurses down into dicts nested 101 | to an arbitrary depth, updating keys. The `merge_dct` is merged into 102 | `dct`. 103 | 104 | 105 | Parameters 106 | ---------- 107 | dct : dict 108 | dict onto which the merge is executed 109 | merge_dct : dict 110 | dct merged into dct 111 | """ 112 | from collections import Mapping 113 | for k, v in merge_dct.items(): 114 | if (k in dct and isinstance(dct[k], dict) 115 | and isinstance(merge_dct[k], Mapping)): 116 | dict_merge(dct[k], merge_dct[k]) 117 | else: 118 | dct[k] = merge_dct[k] 119 | return dct 120 | 121 | file = open(filename, 'r') 122 | data = json.load(file) 123 | 124 | # data.update(d) 125 | file = open(filename, 'w+') 126 | json.dump(dict_merge(data, d), file) 127 | file.close() 128 | 129 | @classmethod 130 | def get_conf_file(cls, filename): 131 | """ 132 | Get a configuration file 133 | 134 | Parameters 135 | ---------- 136 | filename : str 137 | Path to the json file 138 | 139 | Returns 140 | ------- 141 | dict 142 | """ 143 | with open(filename) as file: 144 | data = json.load(file) 145 | return data 146 | 147 | def add_conf_field(self, d): 148 | """ 149 | Update configurations files from a dictionary. The keys should be the participant's pseudo 150 | 151 | Parameters 152 | ---------- 153 | d : dict 154 | Dictionary to add in configuration file 155 | 156 | Examples 157 | ------- 158 | # add some data path 159 | d = { 160 | 'dapo': {'data': '/home/romain/Downloads/conf-files/DapO/mvc'}, 161 | 'davo': {'data': '/home/romain/Downloads/conf-files/DavO/mvc'}, 162 | 'fabd': {'data': '/home/romain/Downloads/conf-files/FabD/mvc'} 163 | } 164 | project.add_conf_field(d) 165 | """ 166 | for iparticipant, ivalue in d.items(): 167 | conf_file = self.get_conf_path(iparticipant) 168 | self.update_conf(conf_file, ivalue) 169 | print(f"{iparticipant}'s conf file updated") 170 | 171 | def get_conf_path(self, participant): 172 | """ 173 | Get participant's configuration file path 174 | 175 | Parameters 176 | ---------- 177 | participant : str 178 | Participant 179 | """ 180 | conf_path = self.project_conf[self.project_conf['participant'] == participant]['conf_file'].values[0] 181 | return conf_path 182 | 183 | def get_conf_field(self, participant, field): 184 | """ 185 | Get participant's specific configuration field 186 | Parameters 187 | ---------- 188 | participant : str 189 | Participant 190 | field : str, list 191 | Field(s) to search in the configuration file 192 | 193 | Returns 194 | ------- 195 | str 196 | """ 197 | conf_path = self.get_conf_path(participant) 198 | conf_file = self.get_conf_file(conf_path) 199 | 200 | def get_from_dict(d, keys): 201 | for k in keys: 202 | d = d[k] 203 | return d 204 | 205 | return get_from_dict(conf_file, field) 206 | -------------------------------------------------------------------------------- /pyosim/inverse_dynamics.py: -------------------------------------------------------------------------------- 1 | """ 2 | Inverse dynamic class in pyosim 3 | """ 4 | from pathlib import Path 5 | 6 | import opensim as osim 7 | 8 | 9 | class InverseDynamics: 10 | """ 11 | Inverse dynamic in pyosim 12 | 13 | Parameters 14 | ---------- 15 | model_input : str 16 | Path to the osim model 17 | xml_input : str 18 | Path to the generic id xml 19 | xml_output : str 20 | Output path of the id xml 21 | xml_forces : str 22 | Path to the generic forces sensor xml 23 | forces_dir : str 24 | Path of the directory containing the forces files (`.sto`) 25 | mot_files : str, Path, list 26 | Path or list of path to the directory containing the motion files (`.mot`) 27 | sto_output : Path, str 28 | Output directory 29 | prefix : str, optional 30 | Optional prefix to put in front of the output filename (typically model name) 31 | low_pass : int, optional 32 | Cutoff frequency for an optional low pass filter on coordinates 33 | multi : bool, optional 34 | Launch InverseDynamics in multiprocessing if True 35 | 36 | Examples 37 | -------- 38 | >>> from pyosim import Conf 39 | >>> from pyosim import InverseDynamics 40 | >>> from pathlib import Path 41 | >>> 42 | >>> PROJECT_PATH = Path('../Misc/project_sample') 43 | >>> TEMPLATES_PATH = PROJECT_PATH / '_templates' 44 | >>> 45 | >>> participant = 'dapo' 46 | >>> model = 'wu' 47 | >>> 48 | >>> trials = [ifile for ifile in (PROJECT_PATH / participant / '1_inverse_kinematic').glob('*.mot')] 49 | >>> conf = Conf(project_path=PROJECT_PATH) 50 | >>> onsets = conf.get_conf_field(participant, ['onset']) 51 | >>> 52 | >>> idyn = InverseDynamics( 53 | >>> model_input=f"{PROJECT_PATH / participant / '_models' / model}_scaled_markers.osim", 54 | >>> xml_input=f'{TEMPLATES_PATH / model}_ik.xml', 55 | >>> xml_output=f"{PROJECT_PATH / participant / '_xml' / model}_ik.xml", 56 | >>> xml_forces=f'{TEMPLATES_PATH}/forces_sensor.xml', 57 | >>> forces_dir=f"{PROJECT_PATH / participant / '0_forces'}", 58 | >>> mot_files=trials, 59 | >>> sto_output=f"{(PROJECT_PATH / participant / '2_inverse_dynamic').resolve()}", 60 | >>> prefix=model, 61 | >>> low_pass=10 62 | >>> ) 63 | """ 64 | 65 | def __init__( 66 | self, 67 | model_input, 68 | xml_input, 69 | xml_output, 70 | mot_files, 71 | sto_output, 72 | xml_forces=None, 73 | forces_dir=None, 74 | prefix=None, 75 | low_pass=None, 76 | multi=False 77 | ): 78 | self.model_input = model_input 79 | self.xml_input = xml_input 80 | self.xml_output = xml_output 81 | self.sto_output = sto_output 82 | self.xml_forces = xml_forces 83 | self.forces_dir = forces_dir 84 | self.low_pass = low_pass 85 | self.multi = multi 86 | 87 | if prefix: 88 | self.prefix = prefix 89 | 90 | if not isinstance(mot_files, list): 91 | self.mot_files = [mot_files] 92 | else: 93 | self.mot_files = mot_files 94 | 95 | if not isinstance(self.mot_files[0], Path): 96 | self.mot_files = [Path(i) for i in self.mot_files] 97 | 98 | self.main_loop() 99 | 100 | def main_loop(self): 101 | if self.multi: 102 | import os 103 | from multiprocessing import Pool 104 | 105 | pool = Pool(os.cpu_count()) 106 | pool.map(self.run_id_tool, self.mot_files) 107 | else: 108 | for itrial in self.mot_files: 109 | self.run_id_tool(itrial) 110 | 111 | def run_id_tool(self, trial): 112 | if self.prefix and not trial.stem.startswith(self.prefix): 113 | # skip file if user specified a prefix and prefix is not present in current file 114 | pass 115 | else: 116 | print(f'\t{trial.stem}') 117 | 118 | # initialize inverse dynamic tool from setup file 119 | model = osim.Model(self.model_input) 120 | id_tool = osim.InverseDynamicsTool(self.xml_input) 121 | id_tool.setModel(model) 122 | 123 | # get starting and ending time 124 | motion = osim.Storage(f'{trial.resolve()}') 125 | start = motion.getFirstTime() 126 | end = motion.getLastTime() 127 | 128 | # inverse dynamics tool 129 | id_tool.setStartTime(start) 130 | id_tool.setEndTime(end) 131 | id_tool.setCoordinatesFileName(f'{trial.resolve()}') 132 | 133 | if self.low_pass: 134 | id_tool.setLowpassCutoffFrequency(self.low_pass) 135 | 136 | # set name of input (mot) file and output (sto) 137 | filename = f'{trial.stem}' 138 | id_tool.setName(filename) 139 | id_tool.setOutputGenForceFileName(f"{filename}.sto") 140 | id_tool.setResultsDir(f'{self.sto_output}') 141 | 142 | # external loads file 143 | if self.forces_dir: 144 | loads = osim.ExternalLoads(self.xml_forces, True) 145 | if self.prefix: 146 | loads.setDataFileName( 147 | f"{Path(self.forces_dir, trial.stem.replace(f'{self.prefix}_', '')).resolve()}.sto" 148 | ) 149 | else: 150 | loads.setDataFileName( 151 | f"{Path(self.forces_dir, trial.stem).resolve()}.sto" 152 | ) 153 | loads.setExternalLoadsModelKinematicsFileName(f'{trial.resolve()}') 154 | 155 | temp_xml = Path(f'{trial.stem}_temp.xml') 156 | loads.printToXML(f'{temp_xml.resolve()}') # temporary xml file 157 | id_tool.setExternalLoadsFileName(f'{temp_xml}') 158 | 159 | id_tool.printToXML(self.xml_output) 160 | id_tool.run() 161 | 162 | if self.forces_dir: 163 | temp_xml.unlink() # delete temporary xml file 164 | -------------------------------------------------------------------------------- /pyosim/inverse_kinematics.py: -------------------------------------------------------------------------------- 1 | """ 2 | Inverse kinematic class in pyosim 3 | """ 4 | from pathlib import Path 5 | 6 | import opensim as osim 7 | 8 | 9 | class InverseKinematics: 10 | """ 11 | Inverse kinematic in pyosim 12 | 13 | Parameters 14 | ---------- 15 | model_input : str 16 | Path to the osim model 17 | xml_input : str 18 | Path to the generic ik xml 19 | xml_output : str 20 | Output path of the ik xml 21 | trc_files : str, list 22 | Path or list of path to the marker files (`.trc`) 23 | mot_output : str 24 | Output directory 25 | onsets : dict, optional 26 | Dictionary which contains the starting and ending point in second as values and trial name as keys 27 | prefix : str, optional 28 | Optional prefix to put in front of the output filename (typically model name) 29 | multi : bool, optional 30 | Launch InverseKinematics in multiprocessing if True 31 | 32 | Examples 33 | -------- 34 | >>> from pathlib import Path 35 | >>> 36 | >>> from pyosim import Conf 37 | >>> from pyosim import InverseKinematics 38 | >>> 39 | >>> PROJECT_PATH = Path('../Misc/project_sample') 40 | >>> TEMPLATES_PATH = PROJECT_PATH / '_templates' 41 | >>> 42 | >>> participant = 'dapo' 43 | >>> model = 'wu' 44 | >>> 45 | >>> trials = [ifile for ifile in (PROJECT_PATH / participant / '0_markers').glob('*.trc')] 46 | >>> conf = Conf(project_path=PROJECT_PATH) 47 | >>> onsets = conf.get_conf_field(participant, ['onset']) 48 | >>> 49 | >>> ik = InverseKinematics( 50 | >>> model_input=f"{PROJECT_PATH / participant / '_models' / model}_scaled_markers.osim", 51 | >>> xml_input=f'{TEMPLATES_PATH / model}_ik.xml', 52 | >>> xml_output=f"{PROJECT_PATH / participant / '_xml' / model}_ik.xml", 53 | >>> trc_files=trials, 54 | >>> mot_output=f"{PROJECT_PATH / participant / '1_inverse_kinematic'}", 55 | >>> onsets=onsets, 56 | >>> prefix=model 57 | >>> ) 58 | """ 59 | 60 | def __init__( 61 | self, 62 | model_input, 63 | xml_input, 64 | xml_output, 65 | trc_files, 66 | mot_output, 67 | onsets=None, 68 | prefix=None, 69 | multi=False 70 | ): 71 | self.model_input = model_input 72 | self.mot_output = mot_output 73 | self.onsets = onsets 74 | self.xml_input = xml_input 75 | self.xml_output = xml_output 76 | self.multi = multi 77 | 78 | if prefix: 79 | self.prefix = prefix 80 | 81 | if not isinstance(trc_files, list): 82 | self.trc_files = [trc_files] 83 | else: 84 | self.trc_files = trc_files 85 | 86 | if not isinstance(self.trc_files[0], Path): 87 | self.trc_files = [Path(i) for i in self.trc_files] 88 | 89 | self.main_loop() 90 | 91 | def main_loop(self): 92 | if self.multi: 93 | import os 94 | from multiprocessing import Pool 95 | 96 | pool = Pool(os.cpu_count()) 97 | pool.map(self.run_ik_tool, self.trc_files) 98 | else: 99 | for itrial in self.trc_files: 100 | self.run_ik_tool(itrial) 101 | 102 | def run_ik_tool(self, trial): 103 | model = osim.Model(self.model_input) 104 | # initialize inverse kinematic tool from setup file 105 | ik_tool = osim.InverseKinematicsTool(self.xml_input) 106 | ik_tool.setModel(model) 107 | 108 | print(f'\t{trial.stem}') 109 | # initialize inverse kinematic tool from setup file 110 | ik_tool = osim.InverseKinematicsTool(self.xml_input) 111 | ik_tool.setModel(model) 112 | 113 | # set name of input (trc) file and output (mot) 114 | if self.prefix: 115 | filename = f"{self.prefix}_{trial.stem}" 116 | else: 117 | filename = trial.stem 118 | ik_tool.setName(filename) 119 | ik_tool.setMarkerDataFileName(f'{trial}') 120 | ik_tool.setOutputMotionFileName(f"{Path(self.mot_output) / filename}.mot") 121 | ik_tool.setResultsDir(self.mot_output) 122 | 123 | if trial.stem in self.onsets: 124 | # set start and end times from configuration file 125 | start = self.onsets[trial.stem][0] 126 | end = self.onsets[trial.stem][1] 127 | else: 128 | # use the trc file to get the start and end times 129 | m = osim.MarkerData(f'{trial}') 130 | start = m.getStartFrameTime() 131 | end = m.getLastFrameTime() - 1e-2 # -1e-2 because removing last frame resolves some bug 132 | ik_tool.setStartTime(start) 133 | ik_tool.setEndTime(end) 134 | 135 | ik_tool.printToXML(self.xml_output) 136 | ik_tool.run() 137 | -------------------------------------------------------------------------------- /pyosim/joint_reaction.py: -------------------------------------------------------------------------------- 1 | """ 2 | Joint reaction class in pyosim 3 | """ 4 | from pyosim import AnalyzeTool 5 | 6 | 7 | class JointReaction(AnalyzeTool): 8 | """ 9 | Static Optimization in pyosim 10 | 11 | Parameters 12 | ---------- 13 | model_input : str 14 | Path to the osim model 15 | xml_input : str 16 | Path to the generic so xml 17 | xml_output : str 18 | Output path of the so xml 19 | xml_forces : str, optional 20 | Path to the generic forces sensor xml (Optional) 21 | ext_forces_dir : str, optional 22 | Path of the directory containing the external forces files (`.sto`) (Optional) 23 | muscle_forces_dir : str, optional 24 | Path of the directory containing the muscle forces files (`.sto`) (Optional) 25 | mot_files : str, Path, list 26 | Path or list of path to the directory containing the motion files (`.mot`) 27 | sto_output : Path, str 28 | Output directory 29 | xml_actuators: Path, str 30 | Actuators (Optional) 31 | prefix : str, optional 32 | Optional prefix to put in front of the output filename (typically model name) (Optional) 33 | low_pass : int, optional 34 | Cutoff frequency for an optional low pass filter on coordinates (Optional) 35 | remove_empty_files : bool, optional 36 | remove empty files i in `sto_output` if True (Optional) 37 | 38 | Examples 39 | -------- 40 | >>> from pathlib import Path 41 | >>> 42 | >>> from pyosim import MuscleAnalysis 43 | >>> 44 | >>> PROJECT_PATH = Path('../Misc/project_sample') 45 | >>> TEMPLATES_PATH = PROJECT_PATH / '_templates' 46 | >>> 47 | >>> participant = 'dapo' 48 | >>> model = 'wu' 49 | >>> trials = [ifile for ifile in (PROJECT_PATH / participant / '1_inverse_kinematic').glob('*.mot')] 50 | >>> 51 | >>> path_kwargs = { 52 | >>> 'model_input': f"{(PROJECT_PATH / participant / '_models' / model).resolve()}_scaled_markers.osim", 53 | >>> 'xml_input': f"{(TEMPLATES_PATH / model).resolve()}_ma.xml", 54 | >>> 'xml_output': f"{(PROJECT_PATH / participant / '_xml' / model).resolve()}_ma.xml", 55 | >>> 'xml_forces': f"{(TEMPLATES_PATH / 'forces_sensor.xml').resolve()}", 56 | >>> 'xml_actuators': f"{(TEMPLATES_PATH / f'{model}_actuators.xml').resolve()}", 57 | >>> 'forces_dir': f"{(PROJECT_PATH / participant / '0_forces').resolve()}", 58 | >>> 'sto_output': f"{(PROJECT_PATH / participant / '4_muscle_analysis').resolve()}", 59 | >>> } 60 | >>> 61 | >>> MuscleAnalysis( 62 | >>> **path_kwargs, 63 | >>> mot_files=trials, 64 | >>> prefix=model, 65 | >>> low_pass=5, 66 | >>> remove_empty_files=True 67 | >>> ) 68 | """ 69 | pass 70 | -------------------------------------------------------------------------------- /pyosim/markers.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import numpy as np 4 | import opensim as osim 5 | 6 | from pyomeca import Markers 7 | 8 | 9 | class Markers3dOsim(Markers): 10 | def __new__(cls, *args, **kwargs): 11 | """Convenient wrapper around Markers3d from the pyomeca library""" 12 | return super(Markers3dOsim, cls).__new__(cls, *args, **kwargs) 13 | 14 | def __array_finalize__(self, obj): 15 | super().__array_finalize__(obj) 16 | # Allow slicing 17 | if obj is None or not isinstance(obj, Markers3dOsim): 18 | return 19 | 20 | def to_trc(self, filename): 21 | """ 22 | Write a trc file from a Markers3dOsim 23 | Parameters 24 | ---------- 25 | filename : string 26 | path of the file to write 27 | """ 28 | filename = Path(filename) 29 | # Make sure the directory exists, otherwise create it 30 | if not filename.parents[0].is_dir(): 31 | filename.parents[0].mkdir() 32 | 33 | # Make sure the metadata are set 34 | if not self.get_rate: 35 | raise ValueError('get_rate is empty. Please fill with `your_variable.get_rate = 100.0` for example') 36 | if not self.get_unit: 37 | raise ValueError('get_unit is empty. Please fill with `your_variable.get_unit = "mm"` for example') 38 | if not self.get_labels: 39 | raise ValueError( 40 | 'get_labels is empty. Please fill with `your_variable.get_labels = ["M1", "M2"]` for example') 41 | 42 | table = osim.TimeSeriesTableVec3() 43 | 44 | # set metadata 45 | table.setColumnLabels(self.get_labels) 46 | table.addTableMetaDataString('DataRate', str(self.get_rate)) 47 | table.addTableMetaDataString('Units', self.get_unit) 48 | 49 | time_vector = np.arange(start=0, stop=1 / self.get_rate * self.shape[2], step=1 / self.get_rate) 50 | 51 | for iframe in range(self.shape[-1]): 52 | a = np.round(self.get_frame(iframe)[:-1, ...], decimals=4) 53 | row = osim.RowVectorOfVec3( 54 | [osim.Vec3(a[0, i], a[1, i], a[2, i]) for i in range(a.shape[-1])] 55 | ) 56 | table.appendRow(time_vector[iframe], row) 57 | 58 | adapter = osim.TRCFileAdapter() 59 | adapter.write(table, str(filename)) 60 | -------------------------------------------------------------------------------- /pyosim/model.py: -------------------------------------------------------------------------------- 1 | """ 2 | Model class in pyosim 3 | """ 4 | import opensim as osim 5 | 6 | 7 | class Model(osim.Model): 8 | """Wrapper around opensim's osim models.""" 9 | 10 | def strengthen(self, factor, output): 11 | """ 12 | Strengthens a model by multiplying the maximum isometric forces of each muscle by the `factor` parameter 13 | 14 | Parameters 15 | ---------- 16 | factor : int 17 | Factor with which to multiply the max. iso. forces of each muscles 18 | output : str 19 | New model path 20 | """ 21 | new_model = self.clone() 22 | output = str(output) 23 | for i in range(new_model.getMuscles().getSize()): 24 | current_muscle = new_model.getMuscles().get(i) 25 | current_muscle.setMaxIsometricForce(current_muscle.getMaxIsometricForce() * factor) 26 | new_model.printToXML(output) 27 | print(f'{output} created') 28 | -------------------------------------------------------------------------------- /pyosim/muscle_analysis.py: -------------------------------------------------------------------------------- 1 | """ 2 | Muscle analysis class in pyosim 3 | """ 4 | from pyosim import AnalyzeTool 5 | 6 | 7 | class MuscleAnalysis(AnalyzeTool): 8 | """ 9 | Static Optimization in pyosim 10 | 11 | Parameters 12 | ---------- 13 | model_input : str 14 | Path to the osim model 15 | xml_input : str 16 | Path to the generic so xml 17 | xml_output : str 18 | Output path of the so xml 19 | xml_forces : str, optional 20 | Path to the generic forces sensor xml (Optional) 21 | ext_forces_dir : str, optional 22 | Path of the directory containing the external forces files (`.sto`) (Optional) 23 | muscle_forces_dir : str, optional 24 | Path of the directory containing the muscle forces files (`.sto`) (Optional) 25 | mot_files : str, Path, list 26 | Path or list of path to the directory containing the motion files (`.mot`) 27 | sto_output : Path, str 28 | Output directory 29 | xml_actuators: Path, str 30 | Actuators (Optional) 31 | prefix : str, optional 32 | Optional prefix to put in front of the output filename (typically model name) (Optional) 33 | low_pass : int, optional 34 | Cutoff frequency for an optional low pass filter on coordinates (Optional) 35 | remove_empty_files : bool, optional 36 | remove empty files i in `sto_output` if True (Optional) 37 | 38 | Examples 39 | -------- 40 | >>> from pathlib import Path 41 | >>> 42 | >>> from pyosim import MuscleAnalysis 43 | >>> 44 | >>> PROJECT_PATH = Path('../Misc/project_sample') 45 | >>> TEMPLATES_PATH = PROJECT_PATH / '_templates' 46 | >>> 47 | >>> participant = 'dapo' 48 | >>> model = 'wu' 49 | >>> trials = [ifile for ifile in (PROJECT_PATH / participant / '1_inverse_kinematic').glob('*.mot')] 50 | >>> 51 | >>> path_kwargs = { 52 | >>> 'model_input': f"{(PROJECT_PATH / participant / '_models' / model).resolve()}_scaled_markers.osim", 53 | >>> 'xml_input': f"{(TEMPLATES_PATH / model).resolve()}_ma.xml", 54 | >>> 'xml_output': f"{(PROJECT_PATH / participant / '_xml' / model).resolve()}_ma.xml", 55 | >>> 'xml_forces': f"{(TEMPLATES_PATH / 'forces_sensor.xml').resolve()}", 56 | >>> 'xml_actuators': f"{(TEMPLATES_PATH / f'{model}_actuators.xml').resolve()}", 57 | >>> 'ext_forces_dir': f"{(PROJECT_PATH / participant / '0_forces').resolve()}", 58 | >>> 'sto_output': f"{(PROJECT_PATH / participant / '4_muscle_analysis').resolve()}", 59 | >>> } 60 | >>> 61 | >>> MuscleAnalysis( 62 | >>> **path_kwargs, 63 | >>> mot_files=trials, 64 | >>> prefix=model, 65 | >>> low_pass=5, 66 | >>> remove_empty_files=True 67 | >>> ) 68 | """ 69 | pass 70 | -------------------------------------------------------------------------------- /pyosim/project.py: -------------------------------------------------------------------------------- 1 | """ 2 | Project class in pyosim 3 | """ 4 | 5 | from pathlib import Path 6 | 7 | import pandas as pd 8 | 9 | 10 | class Project: 11 | """ 12 | Project manager 13 | 14 | Parameters 15 | ---------- 16 | path : str, Path 17 | Path to the project 18 | """ 19 | 20 | def __init__(self, path): 21 | self.path = Path(path) 22 | 23 | def create_project(self): 24 | """create a new project pyosim project. 25 | 26 | 1. Check if folder is empty or non existent (else error) 27 | 2. Add project directories 28 | 3. Create a configuration file 29 | """ 30 | 31 | # check directory 32 | if not self.path.is_dir(): 33 | self.path.mkdir() 34 | print(f'{self.path} created\n') 35 | else: 36 | files = [ifile for ifile in self.path.rglob('*')] 37 | if files: 38 | raise IsADirectoryError(f'{self.path} is not empty. Please choose an empty directory') 39 | else: 40 | print(f'{self.path} selected') 41 | 42 | # create project directories 43 | project_dirs = [ 44 | '_templates', # generic XML 45 | '_models' # generic models* 46 | ] 47 | 48 | for idir in project_dirs: 49 | (self.path / idir).mkdir() 50 | 51 | conf_cols = ['participant', 'sex', 'laterality', 'group', 'mass', 'height', 'conf_file', 'process'] 52 | 53 | pd.DataFrame(columns=conf_cols).to_csv(self.path / '_conf.csv', index=False) 54 | 55 | print( 56 | f'You should now:\n' 57 | f'\t1. Put one or several models into: `{self.path}/_models`\n' 58 | f'\t2. Put your generic XMLs into: `{self.path}/_templates`\n' 59 | f'\t3. Fill the conf file: {self.path}/_conf.csv\n' 60 | ) 61 | 62 | def update_participants(self): 63 | """add participants in project. 64 | 65 | 1. Read the project configuration file 66 | 2. Check if there is participant(s) added in the configuration file and not yet imported in the project 67 | 3. Add these participants and add participant directories 68 | 4. write a configuration file in each participant directory 69 | """ 70 | conf = pd.read_csv(self.path / '_conf.csv') 71 | 72 | participant_dirs = [ 73 | '_xml', # generated XMLs 74 | '_models', # generated models 75 | '0_markers', # markers in TRC format 76 | '0_emg', # emg in STO format 77 | '0_forces', # forces in STO format 78 | '1_inverse_kinematic', # generated MOT motion files from inverse kinematic 79 | '2_inverse_dynamic', # generated STO files from inverse dynamic 80 | '3_static_optimization', # generated STO files from static optimization 81 | '4_muscle_analysis', # generated STO files from muscle analysis 82 | '5_joint_reaction_force' # generated STO files from joint reaction force analysis 83 | ] 84 | 85 | count = 0 86 | for index, irow in conf.iterrows(): 87 | if irow['process'] and not list(self.path.glob(f"{irow['participant']}")): 88 | count += 1 89 | for idir in participant_dirs: 90 | (self.path / irow['participant'] / idir).mkdir(parents=True) 91 | 92 | # create conf file 93 | irow.to_json(self.path / irow['participant'] / '_conf.json') 94 | 95 | print(f'{count} participants added\n') 96 | -------------------------------------------------------------------------------- /pyosim/scale.py: -------------------------------------------------------------------------------- 1 | """ 2 | Scale class in pyosim 3 | """ 4 | 5 | import opensim as osim 6 | 7 | 8 | class Scale: 9 | """ 10 | Scale tool in pyosim 11 | 12 | Parameters 13 | ---------- 14 | model_input : str 15 | Path to the generic model 16 | model_output : str 17 | Output path of the scaled model 18 | xml_input : str 19 | Path to the generic scaling xml 20 | xml_output : str 21 | Output path of the scaling xml 22 | static_path : str 23 | Path to the static trial (must be .trc) 24 | mass : double 25 | Participant's mass (kg) 26 | height : double 27 | Participant's height (mm) 28 | age : int 29 | Participant's age (year) 30 | add_model: str (optional) 31 | Append the specified model 32 | remove_unused : bool 33 | If unused markers have to be removed (default = True in OpenSim) 34 | 35 | Examples 36 | -------- 37 | >>> from pathlib import Path 38 | >>> 39 | >>> from pyosim import Conf 40 | >>> from pyosim import Scale 41 | >>> 42 | >>> # path 43 | >>> PROJECT_PATH = Path('../Misc/project_sample') 44 | >>> MODELS_PATH = PROJECT_PATH / '_models' 45 | >>> TEMPLATES_PATH = PROJECT_PATH / '_templates' 46 | >>> 47 | >>> model = 'wu' 48 | >>> participant = 'dapo' 49 | >>> static_path = f"{PROJECT_PATH / participant / '0_markers' / 'IRSST_'}DapOd0.trc" 50 | >>> 51 | >>> conf = Conf(project_path=PROJECT_PATH) 52 | >>> mass = conf.get_conf_field(participant, ['mass']) 53 | >>> height = conf.get_conf_field(participant, ['height']) 54 | >>> 55 | >>> 56 | >>> path_kwargs = { 57 | >>> 'model_input': f'{MODELS_PATH / model}.osim', 58 | >>> 'model_output': f"{PROJECT_PATH / participant / '_models' / model}_scaled.osim", 59 | >>> 'xml_input': f'{TEMPLATES_PATH / model}_scaling.xml', 60 | >>> 'xml_output': f"{PROJECT_PATH / participant / '_xml' / model}_scaled.xml", 61 | >>> 'static_path': static_path 62 | >>> } 63 | >>> 64 | >>> Scale( 65 | >>> **path_kwargs, 66 | >>> mass=mass, 67 | >>> height=height * 10, 68 | >>> remove_unused=False 69 | >>> ) 70 | """ 71 | 72 | def __init__( 73 | self, 74 | model_input, 75 | model_output, 76 | xml_input, 77 | xml_output, 78 | static_path, 79 | mass=-1, 80 | height=-1, 81 | age=-1, 82 | add_model=None, 83 | remove_unused=True, 84 | ): 85 | self.model = osim.Model(model_input) 86 | self.model_output = model_output 87 | self.model_with_markers_output = model_output.replace(".osim", "_markers.osim") 88 | self.static_path = static_path 89 | self.xml_output = xml_output 90 | 91 | self.time_range = self.time_range_from_static() 92 | 93 | # initialize scale tool from setup file 94 | self.scale_tool = osim.ScaleTool(xml_input) 95 | self.set_anthropometry(mass, height, age) 96 | # Tell scale tool to use the loaded model 97 | self.scale_tool.getGenericModelMaker().setModelFileName(model_input) 98 | 99 | self.run_model_scaler(mass) 100 | self.run_marker_placer() 101 | 102 | if add_model: 103 | self.combine_models(add_model) 104 | 105 | if not remove_unused: 106 | self.add_unused_markers() 107 | 108 | def time_range_from_static(self): 109 | static = osim.MarkerData(self.static_path) 110 | initial_time = static.getStartFrameTime() 111 | final_time = static.getLastFrameTime() 112 | range_time = osim.ArrayDouble() 113 | range_time.set(0, initial_time) 114 | range_time.set(1, final_time) 115 | return range_time 116 | 117 | def set_anthropometry(self, mass, height, age): 118 | """ 119 | Set basic anthropometric parameters in scaling model 120 | Parameters 121 | ---------- 122 | mass : Double 123 | mass (kg) 124 | height : Double 125 | height (mm) 126 | age : int 127 | age (year) 128 | """ 129 | self.scale_tool.setSubjectMass(mass) 130 | self.scale_tool.setSubjectHeight(height) 131 | self.scale_tool.setSubjectAge(age) 132 | 133 | def run_model_scaler(self, mass): 134 | model_scaler = self.scale_tool.getModelScaler() 135 | # Whether or not to use the model scaler during scale 136 | model_scaler.setApply(True) 137 | # Set the marker file to be used for scaling 138 | model_scaler.setMarkerFileName(self.static_path) 139 | 140 | # set time range 141 | model_scaler.setTimeRange(self.time_range) 142 | 143 | # Indicating whether or not to preserve relative mass between segments 144 | model_scaler.setPreserveMassDist(True) 145 | 146 | # Name of model file (.osim) to write when done scaling 147 | model_scaler.setOutputModelFileName(self.model_output) 148 | 149 | # Filename to write scale factors that were applied to the unscaled model (optional) 150 | model_scaler.setOutputScaleFileName( 151 | self.xml_output.replace(".xml", "_scaling_factor.xml") 152 | ) 153 | 154 | model_scaler.processModel(self.model, "", mass) 155 | 156 | def run_marker_placer(self): 157 | # load a scaled model 158 | scaled_model = osim.Model(self.model_output) 159 | 160 | marker_placer = self.scale_tool.getMarkerPlacer() 161 | # Whether or not to use the model scaler during scale` 162 | marker_placer.setApply(True) 163 | marker_placer.setTimeRange(self.time_range) 164 | 165 | marker_placer.setStaticPoseFileName(self.static_path) 166 | 167 | # Name of model file (.osim) to write when done scaling 168 | marker_placer.setOutputModelFileName(self.model_with_markers_output) 169 | 170 | # Maximum amount of movement allowed in marker data when averaging 171 | marker_placer.setMaxMarkerMovement(-1) 172 | 173 | marker_placer.processModel(scaled_model) 174 | 175 | # save processed model 176 | scaled_model.printToXML(self.model_output) 177 | 178 | # print scale config to xml 179 | self.scale_tool.printToXML(self.xml_output) 180 | 181 | def add_unused_markers(self): 182 | with_unused = osim.Model(self.model_output) 183 | without_unused = osim.Model(self.model_with_markers_output) 184 | 185 | with_unused_markerset = with_unused.getMarkerSet() 186 | without_unused_markerset = without_unused.getMarkerSet() 187 | 188 | with_unused_l = [ 189 | with_unused_markerset.get(imarker).getName() 190 | for imarker in range(with_unused.getNumMarkers()) 191 | ] 192 | without_unused_l = [ 193 | without_unused_markerset.get(imarker).getName() 194 | for imarker in range(without_unused.getNumMarkers()) 195 | ] 196 | 197 | differences = set(with_unused_l).difference(without_unused_l) 198 | 199 | for idiff in differences: 200 | m = with_unused_markerset.get(idiff).clone() 201 | without_unused.addMarker(m) 202 | 203 | without_unused.printToXML(self.model_with_markers_output) 204 | 205 | def combine_models(self, model_to_add): 206 | # Open the models 207 | osim_base = osim.Model(self.model_output) 208 | osim_to_add = osim.Model(model_to_add) 209 | 210 | bodies = osim_to_add.getBodySet() 211 | joints = osim_to_add.getJointSet() 212 | controls = osim_to_add.getControllerSet() 213 | constraints = osim_to_add.getConstraintSet() 214 | markers = osim_to_add.getMarkerSet() 215 | 216 | print("Add bodies:") 217 | for body in bodies: 218 | print(f"\t{body.getName()}") 219 | osim_base.addBody(body.clone()) 220 | 221 | print("Add joints:") 222 | for joint in joints: 223 | print(f"\t{joint.getName()}") 224 | osim_base.addJoint(joint.clone()) 225 | 226 | print("Add controls:") 227 | for control in controls: 228 | print(f"\t{control.getName()}") 229 | osim_base.addControl(control.clone()) 230 | 231 | print("Add constraints:") 232 | for constraint in constraints: 233 | print(f"\t{constraint.getName()}") 234 | osim_base.addConstraint(constraint.clone()) 235 | 236 | print("Add markers") 237 | for marker in markers: 238 | print(f"\t{marker.getName()}") 239 | osim_base.addMarker(marker.clone()) 240 | 241 | osim_base.initSystem() 242 | 243 | osim_base.printToXML(self.model_with_markers_output) 244 | print(f"{model_to_add} added to {self.model_with_markers_output}") 245 | -------------------------------------------------------------------------------- /pyosim/static_optimization.py: -------------------------------------------------------------------------------- 1 | """ 2 | Static optimization class in pyosim 3 | """ 4 | 5 | from pyosim import AnalyzeTool 6 | 7 | 8 | class StaticOptimization(AnalyzeTool): 9 | """ 10 | Static Optimization in pyosim 11 | 12 | Parameters 13 | ---------- 14 | model_input : str 15 | Path to the osim model 16 | xml_input : str 17 | Path to the generic so xml 18 | xml_output : str 19 | Output path of the so xml 20 | xml_forces : str, optional 21 | Path to the generic forces sensor xml (Optional) 22 | ext_forces_dir : str, optional 23 | Path of the directory containing the external forces files (`.sto`) (Optional) 24 | muscle_forces_dir : str, optional 25 | Path of the directory containing the muscle forces files (`.sto`) (Optional) 26 | mot_files : str, Path, list 27 | Path or list of path to the directory containing the motion files (`.mot`) 28 | sto_output : Path, str 29 | Output directory 30 | xml_actuators: Path, str 31 | Actuators (Optional) 32 | prefix : str, optional 33 | Optional prefix to put in front of the output filename (typically model name) (Optional) 34 | low_pass : int, optional 35 | Cutoff frequency for an optional low pass filter on coordinates (Optional) 36 | remove_empty_files : bool, optional 37 | remove empty files i in `sto_output` if True (Optional) 38 | 39 | Examples 40 | -------- 41 | >>> from pathlib import Path 42 | >>> 43 | >>> from pyosim import StaticOptimization 44 | >>> 45 | >>> PROJECT_PATH = Path('../Misc/project_sample') 46 | >>> TEMPLATES_PATH = PROJECT_PATH / '_templates' 47 | >>> 48 | >>> participant = 'dapo' 49 | >>> model = 'wu' 50 | >>> trials = [ifile for ifile in (PROJECT_PATH / participant / '1_inverse_kinematic').glob('*.mot')] 51 | >>> 52 | >>> path_kwargs = { 53 | >>> 'model_input': f"{(PROJECT_PATH / participant / '_models' / model).resolve()}_scaled_markers.osim", 54 | >>> 'xml_input': f"{(TEMPLATES_PATH / model).resolve()}_so.xml", 55 | >>> 'xml_output': f"{(PROJECT_PATH / participant / '_xml' / model).resolve()}_so.xml", 56 | >>> 'xml_forces': f"{(TEMPLATES_PATH / 'forces_sensor.xml').resolve()}", 57 | >>> 'xml_actuators': f"{(TEMPLATES_PATH / f'{model}_actuators.xml').resolve()}", 58 | >>> 'ext_forces_dir': f"{(PROJECT_PATH / participant / '0_forces').resolve()}", 59 | >>> 'sto_output': f"{(PROJECT_PATH / participant / '3_static_optimization').resolve()}", 60 | >>> } 61 | >>> 62 | >>> StaticOptimization( 63 | >>> **path_kwargs, 64 | >>> mot_files=trials, 65 | >>> prefix=model, 66 | >>> low_pass=5 67 | >>> ) 68 | """ 69 | pass 70 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | ignore = E122,E123,E126,E127,E128,E731,E722 4 | exclude = build,pyosim/_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 = pyosim/_version.py 24 | versionfile_build = pyosim/_version.py 25 | tag_prefix = 26 | parentdir_prefix = pyosim- 27 | 28 | [bdist_wheel] 29 | universal=1 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from setuptools import setup 3 | 4 | import versioneer 5 | 6 | with open("environment.yml", 'r') as stream: 7 | out = yaml.load(stream) 8 | requirements = out['dependencies'][1:] # we do not return python 9 | 10 | setup( 11 | name='pyosim', 12 | version=versioneer.get_version(), 13 | cmdclass=versioneer.get_cmdclass(), 14 | description="Interface between OpenSim and the Pyomeca library", 15 | author="Romain Martinez", 16 | author_email='martinez.staps@gmail.com', 17 | url='https://github.com/pyomeca/pyosim', 18 | license='Apache 2.0', 19 | packages=['pyosim'], 20 | install_requires=requirements, 21 | keywords='pyosim', 22 | classifiers=[ 23 | 'Programming Language :: Python :: 3.6', 24 | 'Programming Language :: Python :: 3.7', 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /tests/_conf.csv: -------------------------------------------------------------------------------- 1 | participant,sex,laterality,group,mass,height,conf_file,process 2 | sarc,0,1,0,55.5,170,,1 3 | inea,0,1,0,55.8,167,,1 4 | dapo,0,1,0,61.4,176,,1 5 | laug,0,1,0,66,180,,1 6 | mara,0,1,0,56.7,163,,1 7 | mars,0,1,0,54,163,,1 8 | yosc,0,1,0,63,161,,1 9 | marc,0,1,0,70,157,,1 10 | benl,1,1,1,67,175,,1 11 | davo,1,1,1,86,185,,1 12 | marb,1,1,1,68,178,,1 13 | gatb,1,1,1,94,173,,1 14 | yoap,1,1,1,79,181,,1 15 | romm,1,1,1,91,193,,1 16 | camb,0,1,0,65.9,171,,0 17 | eved,0,1,0,65,165,,0 18 | naus,0,1,0,87.5,174,,0 19 | emid,0,1,0,51,165,,0 20 | verc,0,1,0,68,164,,0 21 | marh,0,1,0,61,176,,0 22 | samn,0,1,0,51.5,167,,0 23 | alef,0,1,0,52.3,152.4,,0 24 | danf,0,1,0,60,170,,0 25 | aimq,0,1,0,54,165,,0 26 | ameg,0,1,0,72,167,,0 27 | noel,0,1,0,63.5,159,,0 28 | karm,1,1,1,65,180,,0 29 | patm,1,1,1,85,186,,0 30 | vicj,1,1,1,69,170,,0 31 | luia,1,1,1,72,175,,0 32 | gabf,1,1,1,76,183,,0 33 | aleb,1,1,1,47,165,,0 34 | humm,1,1,1,70,173,,0 35 | emmb,1,1,1,69.1,185,,0 36 | fabg,1,1,1,74,175,,0 37 | nicl,1,1,1,78,173,,0 38 | sylg,1,1,1,79,183,,0 39 | daml,1,1,1,73,176,,0 40 | land,1,1,1,73,189,,0 41 | emyc,0,1,0,61.5,167,,0 42 | amia,0,1,0,50,157,,0 43 | romr,0,1,0,63,172,,0 44 | carb,0,1,0,60,170,,0 45 | anns,0,1,0,62,172,,0 46 | steb,0,1,0,57,175,,0 47 | chac,0,1,0,58,169,,0 48 | roxd,0,1,0,56,165,,0 49 | vivs,0,1,0,70,177,,0 50 | adrc,1,1,1,72,174,,0 51 | geoa,1,1,1,64,169,,0 52 | yoab,1,1,1,70,180,,0 53 | nemk,1,1,1,75,175,,0 54 | matr,1,1,1,68,177,,0 55 | doca,1,1,1,65,168,,0 56 | jawr,1,1,1,83,190,,0 57 | fabd,1,1,1,73,179,,0 58 | damg,1,1,1,100,194,,0 59 | arst,1,1,1,75,181,,0 60 | phil,1,1,1,65,169,,0 61 | marm,1,1,1,70,180,,0 62 | -------------------------------------------------------------------------------- /tests/data/forces_calibration_matrix.csv: -------------------------------------------------------------------------------- 1 | 15.7377,-178.4176,172.9822,7.6998,-192.7411,174.184 2 | 208.3629,-109.1685,-110.3583,209.3269,-104.9032,-103.5278 3 | 227.6774,222.8613,219.1087,234.3732,217.1453,221.2831 4 | 5.6472,-0.7266,-0.3242,5.465,-8.9705,-8.4179 5 | 5.77,6.7466,-6.9682,-4.1899,1.5741,-2.4571 6 | -1.2722,1.6912,-3.0543,5.1092,-5.6222,3.3049 7 | -------------------------------------------------------------------------------- /tests/data/markers_analogs.c3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyomeca/pyosim/84b418ba276a9e5c499c44acd3fc4531103ad6c0/tests/data/markers_analogs.c3d -------------------------------------------------------------------------------- /tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | # add verbose param to pytest 3 | addopts = -v -------------------------------------------------------------------------------- /tests/test_fileio.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyomeca/pyosim/84b418ba276a9e5c499c44acd3fc4531103ad6c0/tests/test_fileio.py -------------------------------------------------------------------------------- /tests/test_tools.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyomeca/pyosim/84b418ba276a9e5c499c44acd3fc4531103ad6c0/tests/test_tools.py -------------------------------------------------------------------------------- /versioneer.py: -------------------------------------------------------------------------------- 1 | 2 | # Version: 0.18 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/warner/python-versioneer 11 | * Brian Warner 12 | * License: Public Domain 13 | * Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, and pypy 14 | * [![Latest Version] 15 | (https://pypip.in/version/versioneer/badge.svg?style=flat) 16 | ](https://pypi.python.org/pypi/versioneer/) 17 | * [![Build Status] 18 | (https://travis-ci.org/warner/python-versioneer.png?branch=master) 19 | ](https://travis-ci.org/warner/python-versioneer) 20 | 21 | This is a tool for managing a recorded version number in distutils-based 22 | python projects. The goal is to remove the tedious and error-prone "update 23 | the embedded version string" step from your release process. Making a new 24 | release should be as easy as recording a new tag in your version-control 25 | system, and maybe making new tarballs. 26 | 27 | 28 | ## Quick Install 29 | 30 | * `pip install versioneer` to somewhere to your $PATH 31 | * add a `[versioneer]` section to your setup.cfg (see below) 32 | * run `versioneer install` in your source tree, commit the results 33 | 34 | ## Version Identifiers 35 | 36 | Source trees come from a variety of places: 37 | 38 | * a version-control system checkout (mostly used by developers) 39 | * a nightly tarball, produced by build automation 40 | * a snapshot tarball, produced by a web-based VCS browser, like github's 41 | "tarball from tag" feature 42 | * a release tarball, produced by "setup.py sdist", distributed through PyPI 43 | 44 | Within each source tree, the version identifier (either a string or a number, 45 | this tool is format-agnostic) can come from a variety of places: 46 | 47 | * ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows 48 | about recent "tags" and an absolute revision-id 49 | * the name of the directory into which the tarball was unpacked 50 | * an expanded VCS keyword ($Id$, etc) 51 | * a `_version.py` created by some earlier build step 52 | 53 | For released software, the version identifier is closely related to a VCS 54 | tag. Some projects use tag names that include more than just the version 55 | string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool 56 | needs to strip the tag prefix to extract the version identifier. For 57 | unreleased software (between tags), the version identifier should provide 58 | enough information to help developers recreate the same tree, while also 59 | giving them an idea of roughly how old the tree is (after version 1.2, before 60 | version 1.3). Many VCS systems can report a description that captures this, 61 | for example `git describe --tags --dirty --always` reports things like 62 | "0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the 63 | 0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has 64 | uncommitted changes. 65 | 66 | The version identifier is used for multiple purposes: 67 | 68 | * to allow the module to self-identify its version: `myproject.__version__` 69 | * to choose a name and prefix for a 'setup.py sdist' tarball 70 | 71 | ## Theory of Operation 72 | 73 | Versioneer works by adding a special `_version.py` file into your source 74 | tree, where your `__init__.py` can import it. This `_version.py` knows how to 75 | dynamically ask the VCS tool for version information at import time. 76 | 77 | `_version.py` also contains `$Revision$` markers, and the installation 78 | process marks `_version.py` to have this marker rewritten with a tag name 79 | during the `git archive` command. As a result, generated tarballs will 80 | contain enough information to get the proper version. 81 | 82 | To allow `setup.py` to compute a version too, a `versioneer.py` is added to 83 | the top level of your source tree, next to `setup.py` and the `setup.cfg` 84 | that configures it. This overrides several distutils/setuptools commands to 85 | compute the version when invoked, and changes `setup.py build` and `setup.py 86 | sdist` to replace `_version.py` with a small static file that contains just 87 | the generated version data. 88 | 89 | ## Installation 90 | 91 | See [INSTALL.md](./INSTALL.md) for detailed installation instructions. 92 | 93 | ## Version-String Flavors 94 | 95 | Code which uses Versioneer can learn about its version string at runtime by 96 | importing `_version` from your main `__init__.py` file and running the 97 | `get_versions()` function. From the "outside" (e.g. in `setup.py`), you can 98 | import the top-level `versioneer.py` and run `get_versions()`. 99 | 100 | Both functions return a dictionary with different flavors of version 101 | information: 102 | 103 | * `['version']`: A condensed version string, rendered using the selected 104 | style. This is the most commonly used value for the project's version 105 | string. The default "pep440" style yields strings like `0.11`, 106 | `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section 107 | below for alternative styles. 108 | 109 | * `['full-revisionid']`: detailed revision identifier. For Git, this is the 110 | full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". 111 | 112 | * `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the 113 | commit date in ISO 8601 format. This will be None if the date is not 114 | available. 115 | 116 | * `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that 117 | this is only accurate if run in a VCS checkout, otherwise it is likely to 118 | be False or None 119 | 120 | * `['error']`: if the version string could not be computed, this will be set 121 | to a string describing the problem, otherwise it will be None. It may be 122 | useful to throw an exception in setup.py if this is set, to avoid e.g. 123 | creating tarballs with a version string of "unknown". 124 | 125 | Some variants are more useful than others. Including `full-revisionid` in a 126 | bug report should allow developers to reconstruct the exact code being tested 127 | (or indicate the presence of local changes that should be shared with the 128 | developers). `version` is suitable for display in an "about" box or a CLI 129 | `--version` output: it can be easily compared against release notes and lists 130 | of bugs fixed in various releases. 131 | 132 | The installer adds the following text to your `__init__.py` to place a basic 133 | version in `YOURPROJECT.__version__`: 134 | 135 | from ._version import get_versions 136 | __version__ = get_versions()['version'] 137 | del get_versions 138 | 139 | ## Styles 140 | 141 | The setup.cfg `style=` configuration controls how the VCS information is 142 | rendered into a version string. 143 | 144 | The default style, "pep440", produces a PEP440-compliant string, equal to the 145 | un-prefixed tag name for actual releases, and containing an additional "local 146 | version" section with more detail for in-between builds. For Git, this is 147 | TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags 148 | --dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the 149 | tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and 150 | that this commit is two revisions ("+2") beyond the "0.11" tag. For released 151 | software (exactly equal to a known tag), the identifier will only contain the 152 | stripped tag, e.g. "0.11". 153 | 154 | Other styles are available. See [details.md](details.md) in the Versioneer 155 | source tree for descriptions. 156 | 157 | ## Debugging 158 | 159 | Versioneer tries to avoid fatal errors: if something goes wrong, it will tend 160 | to return a version of "0+unknown". To investigate the problem, run `setup.py 161 | version`, which will run the version-lookup code in a verbose mode, and will 162 | display the full contents of `get_versions()` (including the `error` string, 163 | which may help identify what went wrong). 164 | 165 | ## Known Limitations 166 | 167 | Some situations are known to cause problems for Versioneer. This details the 168 | most significant ones. More can be found on Github 169 | [issues page](https://github.com/warner/python-versioneer/issues). 170 | 171 | ### Subprojects 172 | 173 | Versioneer has limited support for source trees in which `setup.py` is not in 174 | the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are 175 | two common reasons why `setup.py` might not be in the root: 176 | 177 | * Source trees which contain multiple subprojects, such as 178 | [Buildbot](https://github.com/buildbot/buildbot), which contains both 179 | "master" and "slave" subprojects, each with their own `setup.py`, 180 | `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI 181 | distributions (and upload multiple independently-installable tarballs). 182 | * Source trees whose main purpose is to contain a C library, but which also 183 | provide bindings to Python (and perhaps other langauges) in subdirectories. 184 | 185 | Versioneer will look for `.git` in parent directories, and most operations 186 | should get the right version string. However `pip` and `setuptools` have bugs 187 | and implementation details which frequently cause `pip install .` from a 188 | subproject directory to fail to find a correct version string (so it usually 189 | defaults to `0+unknown`). 190 | 191 | `pip install --editable .` should work correctly. `setup.py install` might 192 | work too. 193 | 194 | Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in 195 | some later version. 196 | 197 | [Bug #38](https://github.com/warner/python-versioneer/issues/38) is tracking 198 | this issue. The discussion in 199 | [PR #61](https://github.com/warner/python-versioneer/pull/61) describes the 200 | issue from the Versioneer side in more detail. 201 | [pip PR#3176](https://github.com/pypa/pip/pull/3176) and 202 | [pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve 203 | pip to let Versioneer work correctly. 204 | 205 | Versioneer-0.16 and earlier only looked for a `.git` directory next to the 206 | `setup.cfg`, so subprojects were completely unsupported with those releases. 207 | 208 | ### Editable installs with setuptools <= 18.5 209 | 210 | `setup.py develop` and `pip install --editable .` allow you to install a 211 | project into a virtualenv once, then continue editing the source code (and 212 | test) without re-installing after every change. 213 | 214 | "Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a 215 | convenient way to specify executable scripts that should be installed along 216 | with the python package. 217 | 218 | These both work as expected when using modern setuptools. When using 219 | setuptools-18.5 or earlier, however, certain operations will cause 220 | `pkg_resources.DistributionNotFound` errors when running the entrypoint 221 | script, which must be resolved by re-installing the package. This happens 222 | when the install happens with one version, then the egg_info data is 223 | regenerated while a different version is checked out. Many setup.py commands 224 | cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into 225 | a different virtualenv), so this can be surprising. 226 | 227 | [Bug #83](https://github.com/warner/python-versioneer/issues/83) describes 228 | this one, but upgrading to a newer version of setuptools should probably 229 | resolve it. 230 | 231 | ### Unicode version strings 232 | 233 | While Versioneer works (and is continually tested) with both Python 2 and 234 | Python 3, it is not entirely consistent with bytes-vs-unicode distinctions. 235 | Newer releases probably generate unicode version strings on py2. It's not 236 | clear that this is wrong, but it may be surprising for applications when then 237 | write these strings to a network connection or include them in bytes-oriented 238 | APIs like cryptographic checksums. 239 | 240 | [Bug #71](https://github.com/warner/python-versioneer/issues/71) investigates 241 | this question. 242 | 243 | 244 | ## Updating Versioneer 245 | 246 | To upgrade your project to a new release of Versioneer, do the following: 247 | 248 | * install the new Versioneer (`pip install -U versioneer` or equivalent) 249 | * edit `setup.cfg`, if necessary, to include any new configuration settings 250 | indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. 251 | * re-run `versioneer install` in your source tree, to replace 252 | `SRC/_version.py` 253 | * commit any changed files 254 | 255 | ## Future Directions 256 | 257 | This tool is designed to make it easily extended to other version-control 258 | systems: all VCS-specific components are in separate directories like 259 | src/git/ . The top-level `versioneer.py` script is assembled from these 260 | components by running make-versioneer.py . In the future, make-versioneer.py 261 | will take a VCS name as an argument, and will construct a version of 262 | `versioneer.py` that is specific to the given VCS. It might also take the 263 | configuration arguments that are currently provided manually during 264 | installation by editing setup.py . Alternatively, it might go the other 265 | direction and include code from all supported VCS systems, reducing the 266 | number of intermediate scripts. 267 | 268 | 269 | ## License 270 | 271 | To make Versioneer easier to embed, all its code is dedicated to the public 272 | domain. The `_version.py` that it creates is also in the public domain. 273 | Specifically, both are released under the Creative Commons "Public Domain 274 | Dedication" license (CC0-1.0), as described in 275 | https://creativecommons.org/publicdomain/zero/1.0/ . 276 | 277 | """ 278 | 279 | from __future__ import print_function 280 | try: 281 | import configparser 282 | except ImportError: 283 | import ConfigParser as configparser 284 | import errno 285 | import json 286 | import os 287 | import re 288 | import subprocess 289 | import sys 290 | 291 | 292 | class VersioneerConfig: 293 | """Container for Versioneer configuration parameters.""" 294 | 295 | 296 | def get_root(): 297 | """Get the project root directory. 298 | 299 | We require that all commands are run from the project root, i.e. the 300 | directory that contains setup.py, setup.cfg, and versioneer.py . 301 | """ 302 | root = os.path.realpath(os.path.abspath(os.getcwd())) 303 | setup_py = os.path.join(root, "setup.py") 304 | versioneer_py = os.path.join(root, "versioneer.py") 305 | if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): 306 | # allow 'python path/to/setup.py COMMAND' 307 | root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) 308 | setup_py = os.path.join(root, "setup.py") 309 | versioneer_py = os.path.join(root, "versioneer.py") 310 | if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): 311 | err = ("Versioneer was unable to run the project root directory. " 312 | "Versioneer requires setup.py to be executed from " 313 | "its immediate directory (like 'python setup.py COMMAND'), " 314 | "or in a way that lets it use sys.argv[0] to find the root " 315 | "(like 'python path/to/setup.py COMMAND').") 316 | raise VersioneerBadRootError(err) 317 | try: 318 | # Certain runtime workflows (setup.py install/develop in a setuptools 319 | # tree) execute all dependencies in a single python process, so 320 | # "versioneer" may be imported multiple times, and python's shared 321 | # module-import table will cache the first one. So we can't use 322 | # os.path.dirname(__file__), as that will find whichever 323 | # versioneer.py was first imported, even in later projects. 324 | me = os.path.realpath(os.path.abspath(__file__)) 325 | me_dir = os.path.normcase(os.path.splitext(me)[0]) 326 | vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) 327 | if me_dir != vsr_dir: 328 | print("Warning: build in %s is using versioneer.py from %s" 329 | % (os.path.dirname(me), versioneer_py)) 330 | except NameError: 331 | pass 332 | return root 333 | 334 | 335 | def get_config_from_root(root): 336 | """Read the project setup.cfg file to determine Versioneer config.""" 337 | # This might raise EnvironmentError (if setup.cfg is missing), or 338 | # configparser.NoSectionError (if it lacks a [versioneer] section), or 339 | # configparser.NoOptionError (if it lacks "VCS="). See the docstring at 340 | # the top of versioneer.py for instructions on writing your setup.cfg . 341 | setup_cfg = os.path.join(root, "setup.cfg") 342 | parser = configparser.SafeConfigParser() 343 | with open(setup_cfg, "r") as f: 344 | parser.readfp(f) 345 | VCS = parser.get("versioneer", "VCS") # mandatory 346 | 347 | def get(parser, name): 348 | if parser.has_option("versioneer", name): 349 | return parser.get("versioneer", name) 350 | return None 351 | cfg = VersioneerConfig() 352 | cfg.VCS = VCS 353 | cfg.style = get(parser, "style") or "" 354 | cfg.versionfile_source = get(parser, "versionfile_source") 355 | cfg.versionfile_build = get(parser, "versionfile_build") 356 | cfg.tag_prefix = get(parser, "tag_prefix") 357 | if cfg.tag_prefix in ("''", '""'): 358 | cfg.tag_prefix = "" 359 | cfg.parentdir_prefix = get(parser, "parentdir_prefix") 360 | cfg.verbose = get(parser, "verbose") 361 | return cfg 362 | 363 | 364 | class NotThisMethod(Exception): 365 | """Exception raised if a method is not valid for the current scenario.""" 366 | 367 | 368 | # these dictionaries contain VCS-specific tools 369 | LONG_VERSION_PY = {} 370 | HANDLERS = {} 371 | 372 | 373 | def register_vcs_handler(vcs, method): # decorator 374 | """Decorator to mark a method as the handler for a particular VCS.""" 375 | def decorate(f): 376 | """Store f in HANDLERS[vcs][method].""" 377 | if vcs not in HANDLERS: 378 | HANDLERS[vcs] = {} 379 | HANDLERS[vcs][method] = f 380 | return f 381 | return decorate 382 | 383 | 384 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 385 | env=None): 386 | """Call the given command(s).""" 387 | assert isinstance(commands, list) 388 | p = None 389 | for c in commands: 390 | try: 391 | dispcmd = str([c] + args) 392 | # remember shell=False, so use git.cmd on windows, not just git 393 | p = subprocess.Popen([c] + args, cwd=cwd, env=env, 394 | stdout=subprocess.PIPE, 395 | stderr=(subprocess.PIPE if hide_stderr 396 | else None)) 397 | break 398 | except EnvironmentError: 399 | e = sys.exc_info()[1] 400 | if e.errno == errno.ENOENT: 401 | continue 402 | if verbose: 403 | print("unable to run %s" % dispcmd) 404 | print(e) 405 | return None, None 406 | else: 407 | if verbose: 408 | print("unable to find command, tried %s" % (commands,)) 409 | return None, None 410 | stdout = p.communicate()[0].strip() 411 | if sys.version_info[0] >= 3: 412 | stdout = stdout.decode() 413 | if p.returncode != 0: 414 | if verbose: 415 | print("unable to run %s (error)" % dispcmd) 416 | print("stdout was %s" % stdout) 417 | return None, p.returncode 418 | return stdout, p.returncode 419 | 420 | 421 | LONG_VERSION_PY['git'] = ''' 422 | # This file helps to compute a version number in source trees obtained from 423 | # git-archive tarball (such as those provided by githubs download-from-tag 424 | # feature). Distribution tarballs (built by setup.py sdist) and build 425 | # directories (produced by setup.py build) will contain a much shorter file 426 | # that just contains the computed version number. 427 | 428 | # This file is released into the public domain. Generated by 429 | # versioneer-0.18 (https://github.com/warner/python-versioneer) 430 | 431 | """Git implementation of _version.py.""" 432 | 433 | import errno 434 | import os 435 | import re 436 | import subprocess 437 | import sys 438 | 439 | 440 | def get_keywords(): 441 | """Get the keywords needed to look up the version information.""" 442 | # these strings will be replaced by git during git-archive. 443 | # setup.py/versioneer.py will grep for the variable names, so they must 444 | # each be defined on a line of their own. _version.py will just call 445 | # get_keywords(). 446 | git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" 447 | git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" 448 | git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s" 449 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 450 | return keywords 451 | 452 | 453 | class VersioneerConfig: 454 | """Container for Versioneer configuration parameters.""" 455 | 456 | 457 | def get_config(): 458 | """Create, populate and return the VersioneerConfig() object.""" 459 | # these strings are filled in when 'setup.py versioneer' creates 460 | # _version.py 461 | cfg = VersioneerConfig() 462 | cfg.VCS = "git" 463 | cfg.style = "%(STYLE)s" 464 | cfg.tag_prefix = "%(TAG_PREFIX)s" 465 | cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" 466 | cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" 467 | cfg.verbose = False 468 | return cfg 469 | 470 | 471 | class NotThisMethod(Exception): 472 | """Exception raised if a method is not valid for the current scenario.""" 473 | 474 | 475 | LONG_VERSION_PY = {} 476 | HANDLERS = {} 477 | 478 | 479 | def register_vcs_handler(vcs, method): # decorator 480 | """Decorator to mark a method as the handler for a particular VCS.""" 481 | def decorate(f): 482 | """Store f in HANDLERS[vcs][method].""" 483 | if vcs not in HANDLERS: 484 | HANDLERS[vcs] = {} 485 | HANDLERS[vcs][method] = f 486 | return f 487 | return decorate 488 | 489 | 490 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 491 | env=None): 492 | """Call the given command(s).""" 493 | assert isinstance(commands, list) 494 | p = None 495 | for c in commands: 496 | try: 497 | dispcmd = str([c] + args) 498 | # remember shell=False, so use git.cmd on windows, not just git 499 | p = subprocess.Popen([c] + args, cwd=cwd, env=env, 500 | stdout=subprocess.PIPE, 501 | stderr=(subprocess.PIPE if hide_stderr 502 | else None)) 503 | break 504 | except EnvironmentError: 505 | e = sys.exc_info()[1] 506 | if e.errno == errno.ENOENT: 507 | continue 508 | if verbose: 509 | print("unable to run %%s" %% dispcmd) 510 | print(e) 511 | return None, None 512 | else: 513 | if verbose: 514 | print("unable to find command, tried %%s" %% (commands,)) 515 | return None, None 516 | stdout = p.communicate()[0].strip() 517 | if sys.version_info[0] >= 3: 518 | stdout = stdout.decode() 519 | if p.returncode != 0: 520 | if verbose: 521 | print("unable to run %%s (error)" %% dispcmd) 522 | print("stdout was %%s" %% stdout) 523 | return None, p.returncode 524 | return stdout, p.returncode 525 | 526 | 527 | def versions_from_parentdir(parentdir_prefix, root, verbose): 528 | """Try to determine the version from the parent directory name. 529 | 530 | Source tarballs conventionally unpack into a directory that includes both 531 | the project name and a version string. We will also support searching up 532 | two directory levels for an appropriately named parent directory 533 | """ 534 | rootdirs = [] 535 | 536 | for i in range(3): 537 | dirname = os.path.basename(root) 538 | if dirname.startswith(parentdir_prefix): 539 | return {"version": dirname[len(parentdir_prefix):], 540 | "full-revisionid": None, 541 | "dirty": False, "error": None, "date": None} 542 | else: 543 | rootdirs.append(root) 544 | root = os.path.dirname(root) # up a level 545 | 546 | if verbose: 547 | print("Tried directories %%s but none started with prefix %%s" %% 548 | (str(rootdirs), parentdir_prefix)) 549 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 550 | 551 | 552 | @register_vcs_handler("git", "get_keywords") 553 | def git_get_keywords(versionfile_abs): 554 | """Extract version information from the given file.""" 555 | # the code embedded in _version.py can just fetch the value of these 556 | # keywords. When used from setup.py, we don't want to import _version.py, 557 | # so we do it with a regexp instead. This function is not used from 558 | # _version.py. 559 | keywords = {} 560 | try: 561 | f = open(versionfile_abs, "r") 562 | for line in f.readlines(): 563 | if line.strip().startswith("git_refnames ="): 564 | mo = re.search(r'=\s*"(.*)"', line) 565 | if mo: 566 | keywords["refnames"] = mo.group(1) 567 | if line.strip().startswith("git_full ="): 568 | mo = re.search(r'=\s*"(.*)"', line) 569 | if mo: 570 | keywords["full"] = mo.group(1) 571 | if line.strip().startswith("git_date ="): 572 | mo = re.search(r'=\s*"(.*)"', line) 573 | if mo: 574 | keywords["date"] = mo.group(1) 575 | f.close() 576 | except EnvironmentError: 577 | pass 578 | return keywords 579 | 580 | 581 | @register_vcs_handler("git", "keywords") 582 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 583 | """Get version information from git keywords.""" 584 | if not keywords: 585 | raise NotThisMethod("no keywords at all, weird") 586 | date = keywords.get("date") 587 | if date is not None: 588 | # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant 589 | # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 590 | # -like" string, which we must then edit to make compliant), because 591 | # it's been around since git-1.5.3, and it's too difficult to 592 | # discover which version we're using, or to work around using an 593 | # older one. 594 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 595 | refnames = keywords["refnames"].strip() 596 | if refnames.startswith("$Format"): 597 | if verbose: 598 | print("keywords are unexpanded, not using") 599 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 600 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 601 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 602 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 603 | TAG = "tag: " 604 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 605 | if not tags: 606 | # Either we're using git < 1.8.3, or there really are no tags. We use 607 | # a heuristic: assume all version tags have a digit. The old git %%d 608 | # expansion behaves like git log --decorate=short and strips out the 609 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 610 | # between branches and tags. By ignoring refnames without digits, we 611 | # filter out many common branch names like "release" and 612 | # "stabilization", as well as "HEAD" and "master". 613 | tags = set([r for r in refs if re.search(r'\d', r)]) 614 | if verbose: 615 | print("discarding '%%s', no digits" %% ",".join(refs - tags)) 616 | if verbose: 617 | print("likely tags: %%s" %% ",".join(sorted(tags))) 618 | for ref in sorted(tags): 619 | # sorting will prefer e.g. "2.0" over "2.0rc1" 620 | if ref.startswith(tag_prefix): 621 | r = ref[len(tag_prefix):] 622 | if verbose: 623 | print("picking %%s" %% r) 624 | return {"version": r, 625 | "full-revisionid": keywords["full"].strip(), 626 | "dirty": False, "error": None, 627 | "date": date} 628 | # no suitable tags, so version is "0+unknown", but full hex is still there 629 | if verbose: 630 | print("no suitable tags, using unknown + full revision id") 631 | return {"version": "0+unknown", 632 | "full-revisionid": keywords["full"].strip(), 633 | "dirty": False, "error": "no suitable tags", "date": None} 634 | 635 | 636 | @register_vcs_handler("git", "pieces_from_vcs") 637 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 638 | """Get version from 'git describe' in the root of the source tree. 639 | 640 | This only gets called if the git-archive 'subst' keywords were *not* 641 | expanded, and _version.py hasn't already been rewritten with a short 642 | version string, meaning we're inside a checked out source tree. 643 | """ 644 | GITS = ["git"] 645 | if sys.platform == "win32": 646 | GITS = ["git.cmd", "git.exe"] 647 | 648 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, 649 | hide_stderr=True) 650 | if rc != 0: 651 | if verbose: 652 | print("Directory %%s not under git control" %% root) 653 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 654 | 655 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 656 | # if there isn't one, this yields HEX[-dirty] (no NUM) 657 | describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", 658 | "--always", "--long", 659 | "--match", "%%s*" %% tag_prefix], 660 | cwd=root) 661 | # --long was added in git-1.5.5 662 | if describe_out is None: 663 | raise NotThisMethod("'git describe' failed") 664 | describe_out = describe_out.strip() 665 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 666 | if full_out is None: 667 | raise NotThisMethod("'git rev-parse' failed") 668 | full_out = full_out.strip() 669 | 670 | pieces = {} 671 | pieces["long"] = full_out 672 | pieces["short"] = full_out[:7] # maybe improved later 673 | pieces["error"] = None 674 | 675 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 676 | # TAG might have hyphens. 677 | git_describe = describe_out 678 | 679 | # look for -dirty suffix 680 | dirty = git_describe.endswith("-dirty") 681 | pieces["dirty"] = dirty 682 | if dirty: 683 | git_describe = git_describe[:git_describe.rindex("-dirty")] 684 | 685 | # now we have TAG-NUM-gHEX or HEX 686 | 687 | if "-" in git_describe: 688 | # TAG-NUM-gHEX 689 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 690 | if not mo: 691 | # unparseable. Maybe git-describe is misbehaving? 692 | pieces["error"] = ("unable to parse git-describe output: '%%s'" 693 | %% describe_out) 694 | return pieces 695 | 696 | # tag 697 | full_tag = mo.group(1) 698 | if not full_tag.startswith(tag_prefix): 699 | if verbose: 700 | fmt = "tag '%%s' doesn't start with prefix '%%s'" 701 | print(fmt %% (full_tag, tag_prefix)) 702 | pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" 703 | %% (full_tag, tag_prefix)) 704 | return pieces 705 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 706 | 707 | # distance: number of commits since tag 708 | pieces["distance"] = int(mo.group(2)) 709 | 710 | # commit: short hex revision ID 711 | pieces["short"] = mo.group(3) 712 | 713 | else: 714 | # HEX: no tags 715 | pieces["closest-tag"] = None 716 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], 717 | cwd=root) 718 | pieces["distance"] = int(count_out) # total number of commits 719 | 720 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 721 | date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"], 722 | cwd=root)[0].strip() 723 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 724 | 725 | return pieces 726 | 727 | 728 | def plus_or_dot(pieces): 729 | """Return a + if we don't already have one, else return a .""" 730 | if "+" in pieces.get("closest-tag", ""): 731 | return "." 732 | return "+" 733 | 734 | 735 | def render_pep440(pieces): 736 | """Build up version string, with post-release "local version identifier". 737 | 738 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 739 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 740 | 741 | Exceptions: 742 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 743 | """ 744 | if pieces["closest-tag"]: 745 | rendered = pieces["closest-tag"] 746 | if pieces["distance"] or pieces["dirty"]: 747 | rendered += plus_or_dot(pieces) 748 | rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) 749 | if pieces["dirty"]: 750 | rendered += ".dirty" 751 | else: 752 | # exception #1 753 | rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], 754 | pieces["short"]) 755 | if pieces["dirty"]: 756 | rendered += ".dirty" 757 | return rendered 758 | 759 | 760 | def render_pep440_pre(pieces): 761 | """TAG[.post.devDISTANCE] -- No -dirty. 762 | 763 | Exceptions: 764 | 1: no tags. 0.post.devDISTANCE 765 | """ 766 | if pieces["closest-tag"]: 767 | rendered = pieces["closest-tag"] 768 | if pieces["distance"]: 769 | rendered += ".post.dev%%d" %% pieces["distance"] 770 | else: 771 | # exception #1 772 | rendered = "0.post.dev%%d" %% pieces["distance"] 773 | return rendered 774 | 775 | 776 | def render_pep440_post(pieces): 777 | """TAG[.postDISTANCE[.dev0]+gHEX] . 778 | 779 | The ".dev0" means dirty. Note that .dev0 sorts backwards 780 | (a dirty tree will appear "older" than the corresponding clean one), 781 | but you shouldn't be releasing software with -dirty anyways. 782 | 783 | Exceptions: 784 | 1: no tags. 0.postDISTANCE[.dev0] 785 | """ 786 | if pieces["closest-tag"]: 787 | rendered = pieces["closest-tag"] 788 | if pieces["distance"] or pieces["dirty"]: 789 | rendered += ".post%%d" %% pieces["distance"] 790 | if pieces["dirty"]: 791 | rendered += ".dev0" 792 | rendered += plus_or_dot(pieces) 793 | rendered += "g%%s" %% pieces["short"] 794 | else: 795 | # exception #1 796 | rendered = "0.post%%d" %% pieces["distance"] 797 | if pieces["dirty"]: 798 | rendered += ".dev0" 799 | rendered += "+g%%s" %% pieces["short"] 800 | return rendered 801 | 802 | 803 | def render_pep440_old(pieces): 804 | """TAG[.postDISTANCE[.dev0]] . 805 | 806 | The ".dev0" means dirty. 807 | 808 | Eexceptions: 809 | 1: no tags. 0.postDISTANCE[.dev0] 810 | """ 811 | if pieces["closest-tag"]: 812 | rendered = pieces["closest-tag"] 813 | if pieces["distance"] or pieces["dirty"]: 814 | rendered += ".post%%d" %% pieces["distance"] 815 | if pieces["dirty"]: 816 | rendered += ".dev0" 817 | else: 818 | # exception #1 819 | rendered = "0.post%%d" %% pieces["distance"] 820 | if pieces["dirty"]: 821 | rendered += ".dev0" 822 | return rendered 823 | 824 | 825 | def render_git_describe(pieces): 826 | """TAG[-DISTANCE-gHEX][-dirty]. 827 | 828 | Like 'git describe --tags --dirty --always'. 829 | 830 | Exceptions: 831 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 832 | """ 833 | if pieces["closest-tag"]: 834 | rendered = pieces["closest-tag"] 835 | if pieces["distance"]: 836 | rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) 837 | else: 838 | # exception #1 839 | rendered = pieces["short"] 840 | if pieces["dirty"]: 841 | rendered += "-dirty" 842 | return rendered 843 | 844 | 845 | def render_git_describe_long(pieces): 846 | """TAG-DISTANCE-gHEX[-dirty]. 847 | 848 | Like 'git describe --tags --dirty --always -long'. 849 | The distance/hash is unconditional. 850 | 851 | Exceptions: 852 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 853 | """ 854 | if pieces["closest-tag"]: 855 | rendered = pieces["closest-tag"] 856 | rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) 857 | else: 858 | # exception #1 859 | rendered = pieces["short"] 860 | if pieces["dirty"]: 861 | rendered += "-dirty" 862 | return rendered 863 | 864 | 865 | def render(pieces, style): 866 | """Render the given version pieces into the requested style.""" 867 | if pieces["error"]: 868 | return {"version": "unknown", 869 | "full-revisionid": pieces.get("long"), 870 | "dirty": None, 871 | "error": pieces["error"], 872 | "date": None} 873 | 874 | if not style or style == "default": 875 | style = "pep440" # the default 876 | 877 | if style == "pep440": 878 | rendered = render_pep440(pieces) 879 | elif style == "pep440-pre": 880 | rendered = render_pep440_pre(pieces) 881 | elif style == "pep440-post": 882 | rendered = render_pep440_post(pieces) 883 | elif style == "pep440-old": 884 | rendered = render_pep440_old(pieces) 885 | elif style == "git-describe": 886 | rendered = render_git_describe(pieces) 887 | elif style == "git-describe-long": 888 | rendered = render_git_describe_long(pieces) 889 | else: 890 | raise ValueError("unknown style '%%s'" %% style) 891 | 892 | return {"version": rendered, "full-revisionid": pieces["long"], 893 | "dirty": pieces["dirty"], "error": None, 894 | "date": pieces.get("date")} 895 | 896 | 897 | def get_versions(): 898 | """Get version information or return default if unable to do so.""" 899 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 900 | # __file__, we can work backwards from there to the root. Some 901 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 902 | # case we can only use expanded keywords. 903 | 904 | cfg = get_config() 905 | verbose = cfg.verbose 906 | 907 | try: 908 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 909 | verbose) 910 | except NotThisMethod: 911 | pass 912 | 913 | try: 914 | root = os.path.realpath(__file__) 915 | # versionfile_source is the relative path from the top of the source 916 | # tree (where the .git directory might live) to this file. Invert 917 | # this to find the root from __file__. 918 | for i in cfg.versionfile_source.split('/'): 919 | root = os.path.dirname(root) 920 | except NameError: 921 | return {"version": "0+unknown", "full-revisionid": None, 922 | "dirty": None, 923 | "error": "unable to find root of source tree", 924 | "date": None} 925 | 926 | try: 927 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 928 | return render(pieces, cfg.style) 929 | except NotThisMethod: 930 | pass 931 | 932 | try: 933 | if cfg.parentdir_prefix: 934 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 935 | except NotThisMethod: 936 | pass 937 | 938 | return {"version": "0+unknown", "full-revisionid": None, 939 | "dirty": None, 940 | "error": "unable to compute version", "date": None} 941 | ''' 942 | 943 | 944 | @register_vcs_handler("git", "get_keywords") 945 | def git_get_keywords(versionfile_abs): 946 | """Extract version information from the given file.""" 947 | # the code embedded in _version.py can just fetch the value of these 948 | # keywords. When used from setup.py, we don't want to import _version.py, 949 | # so we do it with a regexp instead. This function is not used from 950 | # _version.py. 951 | keywords = {} 952 | try: 953 | f = open(versionfile_abs, "r") 954 | for line in f.readlines(): 955 | if line.strip().startswith("git_refnames ="): 956 | mo = re.search(r'=\s*"(.*)"', line) 957 | if mo: 958 | keywords["refnames"] = mo.group(1) 959 | if line.strip().startswith("git_full ="): 960 | mo = re.search(r'=\s*"(.*)"', line) 961 | if mo: 962 | keywords["full"] = mo.group(1) 963 | if line.strip().startswith("git_date ="): 964 | mo = re.search(r'=\s*"(.*)"', line) 965 | if mo: 966 | keywords["date"] = mo.group(1) 967 | f.close() 968 | except EnvironmentError: 969 | pass 970 | return keywords 971 | 972 | 973 | @register_vcs_handler("git", "keywords") 974 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 975 | """Get version information from git keywords.""" 976 | if not keywords: 977 | raise NotThisMethod("no keywords at all, weird") 978 | date = keywords.get("date") 979 | if date is not None: 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 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 1116 | 1117 | return pieces 1118 | 1119 | 1120 | def do_vcs_install(manifest_in, versionfile_source, ipy): 1121 | """Git-specific installation logic for Versioneer. 1122 | 1123 | For Git, this means creating/changing .gitattributes to mark _version.py 1124 | for export-subst keyword substitution. 1125 | """ 1126 | GITS = ["git"] 1127 | if sys.platform == "win32": 1128 | GITS = ["git.cmd", "git.exe"] 1129 | files = [manifest_in, versionfile_source] 1130 | if ipy: 1131 | files.append(ipy) 1132 | try: 1133 | me = __file__ 1134 | if me.endswith(".pyc") or me.endswith(".pyo"): 1135 | me = os.path.splitext(me)[0] + ".py" 1136 | versioneer_file = os.path.relpath(me) 1137 | except NameError: 1138 | versioneer_file = "versioneer.py" 1139 | files.append(versioneer_file) 1140 | present = False 1141 | try: 1142 | f = open(".gitattributes", "r") 1143 | for line in f.readlines(): 1144 | if line.strip().startswith(versionfile_source): 1145 | if "export-subst" in line.strip().split()[1:]: 1146 | present = True 1147 | f.close() 1148 | except EnvironmentError: 1149 | pass 1150 | if not present: 1151 | f = open(".gitattributes", "a+") 1152 | f.write("%s export-subst\n" % versionfile_source) 1153 | f.close() 1154 | files.append(".gitattributes") 1155 | run_command(GITS, ["add", "--"] + files) 1156 | 1157 | 1158 | def versions_from_parentdir(parentdir_prefix, root, verbose): 1159 | """Try to determine the version from the parent directory name. 1160 | 1161 | Source tarballs conventionally unpack into a directory that includes both 1162 | the project name and a version string. We will also support searching up 1163 | two directory levels for an appropriately named parent directory 1164 | """ 1165 | rootdirs = [] 1166 | 1167 | for i in range(3): 1168 | dirname = os.path.basename(root) 1169 | if dirname.startswith(parentdir_prefix): 1170 | return {"version": dirname[len(parentdir_prefix):], 1171 | "full-revisionid": None, 1172 | "dirty": False, "error": None, "date": None} 1173 | else: 1174 | rootdirs.append(root) 1175 | root = os.path.dirname(root) # up a level 1176 | 1177 | if verbose: 1178 | print("Tried directories %s but none started with prefix %s" % 1179 | (str(rootdirs), parentdir_prefix)) 1180 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 1181 | 1182 | 1183 | SHORT_VERSION_PY = """ 1184 | # This file was generated by 'versioneer.py' (0.18) from 1185 | # revision-control system data, or from the parent directory name of an 1186 | # unpacked source archive. Distribution tarballs contain a pre-generated copy 1187 | # of this file. 1188 | 1189 | import json 1190 | 1191 | version_json = ''' 1192 | %s 1193 | ''' # END VERSION_JSON 1194 | 1195 | 1196 | def get_versions(): 1197 | return json.loads(version_json) 1198 | """ 1199 | 1200 | 1201 | def versions_from_file(filename): 1202 | """Try to determine the version from _version.py if present.""" 1203 | try: 1204 | with open(filename) as f: 1205 | contents = f.read() 1206 | except EnvironmentError: 1207 | raise NotThisMethod("unable to read _version.py") 1208 | mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", 1209 | contents, re.M | re.S) 1210 | if not mo: 1211 | mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", 1212 | contents, re.M | re.S) 1213 | if not mo: 1214 | raise NotThisMethod("no version_json in _version.py") 1215 | return json.loads(mo.group(1)) 1216 | 1217 | 1218 | def write_to_version_file(filename, versions): 1219 | """Write the given version number to the given _version.py file.""" 1220 | os.unlink(filename) 1221 | contents = json.dumps(versions, sort_keys=True, 1222 | indent=1, separators=(",", ": ")) 1223 | with open(filename, "w") as f: 1224 | f.write(SHORT_VERSION_PY % contents) 1225 | 1226 | print("set %s to '%s'" % (filename, versions["version"])) 1227 | 1228 | 1229 | def plus_or_dot(pieces): 1230 | """Return a + if we don't already have one, else return a .""" 1231 | if "+" in pieces.get("closest-tag", ""): 1232 | return "." 1233 | return "+" 1234 | 1235 | 1236 | def render_pep440(pieces): 1237 | """Build up version string, with post-release "local version identifier". 1238 | 1239 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 1240 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 1241 | 1242 | Exceptions: 1243 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 1244 | """ 1245 | if pieces["closest-tag"]: 1246 | rendered = pieces["closest-tag"] 1247 | if pieces["distance"] or pieces["dirty"]: 1248 | rendered += plus_or_dot(pieces) 1249 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 1250 | if pieces["dirty"]: 1251 | rendered += ".dirty" 1252 | else: 1253 | # exception #1 1254 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 1255 | pieces["short"]) 1256 | if pieces["dirty"]: 1257 | rendered += ".dirty" 1258 | return rendered 1259 | 1260 | 1261 | def render_pep440_pre(pieces): 1262 | """TAG[.post.devDISTANCE] -- No -dirty. 1263 | 1264 | Exceptions: 1265 | 1: no tags. 0.post.devDISTANCE 1266 | """ 1267 | if pieces["closest-tag"]: 1268 | rendered = pieces["closest-tag"] 1269 | if pieces["distance"]: 1270 | rendered += ".post.dev%d" % pieces["distance"] 1271 | else: 1272 | # exception #1 1273 | rendered = "0.post.dev%d" % pieces["distance"] 1274 | return rendered 1275 | 1276 | 1277 | def render_pep440_post(pieces): 1278 | """TAG[.postDISTANCE[.dev0]+gHEX] . 1279 | 1280 | The ".dev0" means dirty. Note that .dev0 sorts backwards 1281 | (a dirty tree will appear "older" than the corresponding clean one), 1282 | but you shouldn't be releasing software with -dirty anyways. 1283 | 1284 | Exceptions: 1285 | 1: no tags. 0.postDISTANCE[.dev0] 1286 | """ 1287 | if pieces["closest-tag"]: 1288 | rendered = pieces["closest-tag"] 1289 | if pieces["distance"] or pieces["dirty"]: 1290 | rendered += ".post%d" % pieces["distance"] 1291 | if pieces["dirty"]: 1292 | rendered += ".dev0" 1293 | rendered += plus_or_dot(pieces) 1294 | rendered += "g%s" % pieces["short"] 1295 | else: 1296 | # exception #1 1297 | rendered = "0.post%d" % pieces["distance"] 1298 | if pieces["dirty"]: 1299 | rendered += ".dev0" 1300 | rendered += "+g%s" % pieces["short"] 1301 | return rendered 1302 | 1303 | 1304 | def render_pep440_old(pieces): 1305 | """TAG[.postDISTANCE[.dev0]] . 1306 | 1307 | The ".dev0" means dirty. 1308 | 1309 | Eexceptions: 1310 | 1: no tags. 0.postDISTANCE[.dev0] 1311 | """ 1312 | if pieces["closest-tag"]: 1313 | rendered = pieces["closest-tag"] 1314 | if pieces["distance"] or pieces["dirty"]: 1315 | rendered += ".post%d" % pieces["distance"] 1316 | if pieces["dirty"]: 1317 | rendered += ".dev0" 1318 | else: 1319 | # exception #1 1320 | rendered = "0.post%d" % pieces["distance"] 1321 | if pieces["dirty"]: 1322 | rendered += ".dev0" 1323 | return rendered 1324 | 1325 | 1326 | def render_git_describe(pieces): 1327 | """TAG[-DISTANCE-gHEX][-dirty]. 1328 | 1329 | Like 'git describe --tags --dirty --always'. 1330 | 1331 | Exceptions: 1332 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 1333 | """ 1334 | if pieces["closest-tag"]: 1335 | rendered = pieces["closest-tag"] 1336 | if pieces["distance"]: 1337 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 1338 | else: 1339 | # exception #1 1340 | rendered = pieces["short"] 1341 | if pieces["dirty"]: 1342 | rendered += "-dirty" 1343 | return rendered 1344 | 1345 | 1346 | def render_git_describe_long(pieces): 1347 | """TAG-DISTANCE-gHEX[-dirty]. 1348 | 1349 | Like 'git describe --tags --dirty --always -long'. 1350 | The distance/hash is unconditional. 1351 | 1352 | Exceptions: 1353 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 1354 | """ 1355 | if pieces["closest-tag"]: 1356 | rendered = pieces["closest-tag"] 1357 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 1358 | else: 1359 | # exception #1 1360 | rendered = pieces["short"] 1361 | if pieces["dirty"]: 1362 | rendered += "-dirty" 1363 | return rendered 1364 | 1365 | 1366 | def render(pieces, style): 1367 | """Render the given version pieces into the requested style.""" 1368 | if pieces["error"]: 1369 | return {"version": "unknown", 1370 | "full-revisionid": pieces.get("long"), 1371 | "dirty": None, 1372 | "error": pieces["error"], 1373 | "date": None} 1374 | 1375 | if not style or style == "default": 1376 | style = "pep440" # the default 1377 | 1378 | if style == "pep440": 1379 | rendered = render_pep440(pieces) 1380 | elif style == "pep440-pre": 1381 | rendered = render_pep440_pre(pieces) 1382 | elif style == "pep440-post": 1383 | rendered = render_pep440_post(pieces) 1384 | elif style == "pep440-old": 1385 | rendered = render_pep440_old(pieces) 1386 | elif style == "git-describe": 1387 | rendered = render_git_describe(pieces) 1388 | elif style == "git-describe-long": 1389 | rendered = render_git_describe_long(pieces) 1390 | else: 1391 | raise ValueError("unknown style '%s'" % style) 1392 | 1393 | return {"version": rendered, "full-revisionid": pieces["long"], 1394 | "dirty": pieces["dirty"], "error": None, 1395 | "date": pieces.get("date")} 1396 | 1397 | 1398 | class VersioneerBadRootError(Exception): 1399 | """The project root directory is unknown or missing key files.""" 1400 | 1401 | 1402 | def get_versions(verbose=False): 1403 | """Get the project version from whatever source is available. 1404 | 1405 | Returns dict with two keys: 'version' and 'full'. 1406 | """ 1407 | if "versioneer" in sys.modules: 1408 | # see the discussion in cmdclass.py:get_cmdclass() 1409 | del sys.modules["versioneer"] 1410 | 1411 | root = get_root() 1412 | cfg = get_config_from_root(root) 1413 | 1414 | assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" 1415 | handlers = HANDLERS.get(cfg.VCS) 1416 | assert handlers, "unrecognized VCS '%s'" % cfg.VCS 1417 | verbose = verbose or cfg.verbose 1418 | assert cfg.versionfile_source is not None, \ 1419 | "please set versioneer.versionfile_source" 1420 | assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" 1421 | 1422 | versionfile_abs = os.path.join(root, cfg.versionfile_source) 1423 | 1424 | # extract version from first of: _version.py, VCS command (e.g. 'git 1425 | # describe'), parentdir. This is meant to work for developers using a 1426 | # source checkout, for users of a tarball created by 'setup.py sdist', 1427 | # and for users of a tarball/zipball created by 'git archive' or github's 1428 | # download-from-tag feature or the equivalent in other VCSes. 1429 | 1430 | get_keywords_f = handlers.get("get_keywords") 1431 | from_keywords_f = handlers.get("keywords") 1432 | if get_keywords_f and from_keywords_f: 1433 | try: 1434 | keywords = get_keywords_f(versionfile_abs) 1435 | ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) 1436 | if verbose: 1437 | print("got version from expanded keyword %s" % ver) 1438 | return ver 1439 | except NotThisMethod: 1440 | pass 1441 | 1442 | try: 1443 | ver = versions_from_file(versionfile_abs) 1444 | if verbose: 1445 | print("got version from file %s %s" % (versionfile_abs, ver)) 1446 | return ver 1447 | except NotThisMethod: 1448 | pass 1449 | 1450 | from_vcs_f = handlers.get("pieces_from_vcs") 1451 | if from_vcs_f: 1452 | try: 1453 | pieces = from_vcs_f(cfg.tag_prefix, root, verbose) 1454 | ver = render(pieces, cfg.style) 1455 | if verbose: 1456 | print("got version from VCS %s" % ver) 1457 | return ver 1458 | except NotThisMethod: 1459 | pass 1460 | 1461 | try: 1462 | if cfg.parentdir_prefix: 1463 | ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 1464 | if verbose: 1465 | print("got version from parentdir %s" % ver) 1466 | return ver 1467 | except NotThisMethod: 1468 | pass 1469 | 1470 | if verbose: 1471 | print("unable to compute version") 1472 | 1473 | return {"version": "0+unknown", "full-revisionid": None, 1474 | "dirty": None, "error": "unable to compute version", 1475 | "date": None} 1476 | 1477 | 1478 | def get_version(): 1479 | """Get the short version string for this project.""" 1480 | return get_versions()["version"] 1481 | 1482 | 1483 | def get_cmdclass(): 1484 | """Get the custom setuptools/distutils subclasses used by Versioneer.""" 1485 | if "versioneer" in sys.modules: 1486 | del sys.modules["versioneer"] 1487 | # this fixes the "python setup.py develop" case (also 'install' and 1488 | # 'easy_install .'), in which subdependencies of the main project are 1489 | # built (using setup.py bdist_egg) in the same python process. Assume 1490 | # a main project A and a dependency B, which use different versions 1491 | # of Versioneer. A's setup.py imports A's Versioneer, leaving it in 1492 | # sys.modules by the time B's setup.py is executed, causing B to run 1493 | # with the wrong versioneer. Setuptools wraps the sub-dep builds in a 1494 | # sandbox that restores sys.modules to it's pre-build state, so the 1495 | # parent is protected against the child's "import versioneer". By 1496 | # removing ourselves from sys.modules here, before the child build 1497 | # happens, we protect the child from the parent's versioneer too. 1498 | # Also see https://github.com/warner/python-versioneer/issues/52 1499 | 1500 | cmds = {} 1501 | 1502 | # we add "version" to both distutils and setuptools 1503 | from distutils.core import Command 1504 | 1505 | class cmd_version(Command): 1506 | description = "report generated version string" 1507 | user_options = [] 1508 | boolean_options = [] 1509 | 1510 | def initialize_options(self): 1511 | pass 1512 | 1513 | def finalize_options(self): 1514 | pass 1515 | 1516 | def run(self): 1517 | vers = get_versions(verbose=True) 1518 | print("Version: %s" % vers["version"]) 1519 | print(" full-revisionid: %s" % vers.get("full-revisionid")) 1520 | print(" dirty: %s" % vers.get("dirty")) 1521 | print(" date: %s" % vers.get("date")) 1522 | if vers["error"]: 1523 | print(" error: %s" % vers["error"]) 1524 | cmds["version"] = cmd_version 1525 | 1526 | # we override "build_py" in both distutils and setuptools 1527 | # 1528 | # most invocation pathways end up running build_py: 1529 | # distutils/build -> build_py 1530 | # distutils/install -> distutils/build ->.. 1531 | # setuptools/bdist_wheel -> distutils/install ->.. 1532 | # setuptools/bdist_egg -> distutils/install_lib -> build_py 1533 | # setuptools/install -> bdist_egg ->.. 1534 | # setuptools/develop -> ? 1535 | # pip install: 1536 | # copies source tree to a tempdir before running egg_info/etc 1537 | # if .git isn't copied too, 'git describe' will fail 1538 | # then does setup.py bdist_wheel, or sometimes setup.py install 1539 | # setup.py egg_info -> ? 1540 | 1541 | # we override different "build_py" commands for both environments 1542 | if "setuptools" in sys.modules: 1543 | from setuptools.command.build_py import build_py as _build_py 1544 | else: 1545 | from distutils.command.build_py import build_py as _build_py 1546 | 1547 | class cmd_build_py(_build_py): 1548 | def run(self): 1549 | root = get_root() 1550 | cfg = get_config_from_root(root) 1551 | versions = get_versions() 1552 | _build_py.run(self) 1553 | # now locate _version.py in the new build/ directory and replace 1554 | # it with an updated value 1555 | if cfg.versionfile_build: 1556 | target_versionfile = os.path.join(self.build_lib, 1557 | cfg.versionfile_build) 1558 | print("UPDATING %s" % target_versionfile) 1559 | write_to_version_file(target_versionfile, versions) 1560 | cmds["build_py"] = cmd_build_py 1561 | 1562 | if "cx_Freeze" in sys.modules: # cx_freeze enabled? 1563 | from cx_Freeze.dist import build_exe as _build_exe 1564 | # nczeczulin reports that py2exe won't like the pep440-style string 1565 | # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. 1566 | # setup(console=[{ 1567 | # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION 1568 | # "product_version": versioneer.get_version(), 1569 | # ... 1570 | 1571 | class cmd_build_exe(_build_exe): 1572 | def run(self): 1573 | root = get_root() 1574 | cfg = get_config_from_root(root) 1575 | versions = get_versions() 1576 | target_versionfile = cfg.versionfile_source 1577 | print("UPDATING %s" % target_versionfile) 1578 | write_to_version_file(target_versionfile, versions) 1579 | 1580 | _build_exe.run(self) 1581 | os.unlink(target_versionfile) 1582 | with open(cfg.versionfile_source, "w") as f: 1583 | LONG = LONG_VERSION_PY[cfg.VCS] 1584 | f.write(LONG % 1585 | {"DOLLAR": "$", 1586 | "STYLE": cfg.style, 1587 | "TAG_PREFIX": cfg.tag_prefix, 1588 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1589 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 1590 | }) 1591 | cmds["build_exe"] = cmd_build_exe 1592 | del cmds["build_py"] 1593 | 1594 | if 'py2exe' in sys.modules: # py2exe enabled? 1595 | try: 1596 | from py2exe.distutils_buildexe import py2exe as _py2exe # py3 1597 | except ImportError: 1598 | from py2exe.build_exe import py2exe as _py2exe # py2 1599 | 1600 | class cmd_py2exe(_py2exe): 1601 | def run(self): 1602 | root = get_root() 1603 | cfg = get_config_from_root(root) 1604 | versions = get_versions() 1605 | target_versionfile = cfg.versionfile_source 1606 | print("UPDATING %s" % target_versionfile) 1607 | write_to_version_file(target_versionfile, versions) 1608 | 1609 | _py2exe.run(self) 1610 | os.unlink(target_versionfile) 1611 | with open(cfg.versionfile_source, "w") as f: 1612 | LONG = LONG_VERSION_PY[cfg.VCS] 1613 | f.write(LONG % 1614 | {"DOLLAR": "$", 1615 | "STYLE": cfg.style, 1616 | "TAG_PREFIX": cfg.tag_prefix, 1617 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1618 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 1619 | }) 1620 | cmds["py2exe"] = cmd_py2exe 1621 | 1622 | # we override different "sdist" commands for both environments 1623 | if "setuptools" in sys.modules: 1624 | from setuptools.command.sdist import sdist as _sdist 1625 | else: 1626 | from distutils.command.sdist import sdist as _sdist 1627 | 1628 | class cmd_sdist(_sdist): 1629 | def run(self): 1630 | versions = get_versions() 1631 | self._versioneer_generated_versions = versions 1632 | # unless we update this, the command will keep using the old 1633 | # version 1634 | self.distribution.metadata.version = versions["version"] 1635 | return _sdist.run(self) 1636 | 1637 | def make_release_tree(self, base_dir, files): 1638 | root = get_root() 1639 | cfg = get_config_from_root(root) 1640 | _sdist.make_release_tree(self, base_dir, files) 1641 | # now locate _version.py in the new base_dir directory 1642 | # (remembering that it may be a hardlink) and replace it with an 1643 | # updated value 1644 | target_versionfile = os.path.join(base_dir, cfg.versionfile_source) 1645 | print("UPDATING %s" % target_versionfile) 1646 | write_to_version_file(target_versionfile, 1647 | self._versioneer_generated_versions) 1648 | cmds["sdist"] = cmd_sdist 1649 | 1650 | return cmds 1651 | 1652 | 1653 | CONFIG_ERROR = """ 1654 | setup.cfg is missing the necessary Versioneer configuration. You need 1655 | a section like: 1656 | 1657 | [versioneer] 1658 | VCS = git 1659 | style = pep440 1660 | versionfile_source = src/myproject/_version.py 1661 | versionfile_build = myproject/_version.py 1662 | tag_prefix = 1663 | parentdir_prefix = myproject- 1664 | 1665 | You will also need to edit your setup.py to use the results: 1666 | 1667 | import versioneer 1668 | setup(version=versioneer.get_version(), 1669 | cmdclass=versioneer.get_cmdclass(), ...) 1670 | 1671 | Please read the docstring in ./versioneer.py for configuration instructions, 1672 | edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. 1673 | """ 1674 | 1675 | SAMPLE_CONFIG = """ 1676 | # See the docstring in versioneer.py for instructions. Note that you must 1677 | # re-run 'versioneer.py setup' after changing this section, and commit the 1678 | # resulting files. 1679 | 1680 | [versioneer] 1681 | #VCS = git 1682 | #style = pep440 1683 | #versionfile_source = 1684 | #versionfile_build = 1685 | #tag_prefix = 1686 | #parentdir_prefix = 1687 | 1688 | """ 1689 | 1690 | INIT_PY_SNIPPET = """ 1691 | from ._version import get_versions 1692 | __version__ = get_versions()['version'] 1693 | del get_versions 1694 | """ 1695 | 1696 | 1697 | def do_setup(): 1698 | """Main VCS-independent setup function for installing Versioneer.""" 1699 | root = get_root() 1700 | try: 1701 | cfg = get_config_from_root(root) 1702 | except (EnvironmentError, configparser.NoSectionError, 1703 | configparser.NoOptionError) as e: 1704 | if isinstance(e, (EnvironmentError, configparser.NoSectionError)): 1705 | print("Adding sample versioneer config to setup.cfg", 1706 | file=sys.stderr) 1707 | with open(os.path.join(root, "setup.cfg"), "a") as f: 1708 | f.write(SAMPLE_CONFIG) 1709 | print(CONFIG_ERROR, file=sys.stderr) 1710 | return 1 1711 | 1712 | print(" creating %s" % cfg.versionfile_source) 1713 | with open(cfg.versionfile_source, "w") as f: 1714 | LONG = LONG_VERSION_PY[cfg.VCS] 1715 | f.write(LONG % {"DOLLAR": "$", 1716 | "STYLE": cfg.style, 1717 | "TAG_PREFIX": cfg.tag_prefix, 1718 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1719 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 1720 | }) 1721 | 1722 | ipy = os.path.join(os.path.dirname(cfg.versionfile_source), 1723 | "__init__.py") 1724 | if os.path.exists(ipy): 1725 | try: 1726 | with open(ipy, "r") as f: 1727 | old = f.read() 1728 | except EnvironmentError: 1729 | old = "" 1730 | if INIT_PY_SNIPPET not in old: 1731 | print(" appending to %s" % ipy) 1732 | with open(ipy, "a") as f: 1733 | f.write(INIT_PY_SNIPPET) 1734 | else: 1735 | print(" %s unmodified" % ipy) 1736 | else: 1737 | print(" %s doesn't exist, ok" % ipy) 1738 | ipy = None 1739 | 1740 | # Make sure both the top-level "versioneer.py" and versionfile_source 1741 | # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so 1742 | # they'll be copied into source distributions. Pip won't be able to 1743 | # install the package without this. 1744 | manifest_in = os.path.join(root, "MANIFEST.in") 1745 | simple_includes = set() 1746 | try: 1747 | with open(manifest_in, "r") as f: 1748 | for line in f: 1749 | if line.startswith("include "): 1750 | for include in line.split()[1:]: 1751 | simple_includes.add(include) 1752 | except EnvironmentError: 1753 | pass 1754 | # That doesn't cover everything MANIFEST.in can do 1755 | # (http://docs.python.org/2/distutils/sourcedist.html#commands), so 1756 | # it might give some false negatives. Appending redundant 'include' 1757 | # lines is safe, though. 1758 | if "versioneer.py" not in simple_includes: 1759 | print(" appending 'versioneer.py' to MANIFEST.in") 1760 | with open(manifest_in, "a") as f: 1761 | f.write("include versioneer.py\n") 1762 | else: 1763 | print(" 'versioneer.py' already in MANIFEST.in") 1764 | if cfg.versionfile_source not in simple_includes: 1765 | print(" appending versionfile_source ('%s') to MANIFEST.in" % 1766 | cfg.versionfile_source) 1767 | with open(manifest_in, "a") as f: 1768 | f.write("include %s\n" % cfg.versionfile_source) 1769 | else: 1770 | print(" versionfile_source already in MANIFEST.in") 1771 | 1772 | # Make VCS-specific changes. For git, this means creating/changing 1773 | # .gitattributes to mark _version.py for export-subst keyword 1774 | # substitution. 1775 | do_vcs_install(manifest_in, cfg.versionfile_source, ipy) 1776 | return 0 1777 | 1778 | 1779 | def scan_setup_py(): 1780 | """Validate the contents of setup.py against Versioneer's expectations.""" 1781 | found = set() 1782 | setters = False 1783 | errors = 0 1784 | with open("setup.py", "r") as f: 1785 | for line in f.readlines(): 1786 | if "import versioneer" in line: 1787 | found.add("import") 1788 | if "versioneer.get_cmdclass()" in line: 1789 | found.add("cmdclass") 1790 | if "versioneer.get_version()" in line: 1791 | found.add("get_version") 1792 | if "versioneer.VCS" in line: 1793 | setters = True 1794 | if "versioneer.versionfile_source" in line: 1795 | setters = True 1796 | if len(found) != 3: 1797 | print("") 1798 | print("Your setup.py appears to be missing some important items") 1799 | print("(but I might be wrong). Please make sure it has something") 1800 | print("roughly like the following:") 1801 | print("") 1802 | print(" import versioneer") 1803 | print(" setup( version=versioneer.get_version(),") 1804 | print(" cmdclass=versioneer.get_cmdclass(), ...)") 1805 | print("") 1806 | errors += 1 1807 | if setters: 1808 | print("You should remove lines like 'versioneer.VCS = ' and") 1809 | print("'versioneer.versionfile_source = ' . This configuration") 1810 | print("now lives in setup.cfg, and should be removed from setup.py") 1811 | print("") 1812 | errors += 1 1813 | return errors 1814 | 1815 | 1816 | if __name__ == "__main__": 1817 | cmd = sys.argv[1] 1818 | if cmd == "setup": 1819 | errors = do_setup() 1820 | errors += scan_setup_py() 1821 | if errors: 1822 | sys.exit(1) 1823 | --------------------------------------------------------------------------------