├── .github ├── ISSUE_TEMPLATE │ ├── bug.md │ └── feature.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── main.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── codecov.yml ├── deploying.md ├── pyproject.toml ├── requirements-dev.txt ├── setup.cfg ├── setup.py ├── tests ├── conftest.py ├── test_compile_source.py ├── test_install.py ├── test_to_vyper_version.py └── test_versioning.py ├── tox.ini └── vvm ├── __init__.py ├── exceptions.py ├── install.py ├── main.py ├── py.typed ├── utils ├── __init__.py ├── convert.py ├── lock.py └── versioning.py └── wrapper.py /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report an error that you've encountered. 4 | --- 5 | ### Environment information 6 | 7 | * `vvm` Version: x.x.x 8 | * `vyper` Version: x.x.x 9 | * Python Version: x.x.x 10 | * OS: macOS/linux/win 11 | 12 | ### What was wrong? 13 | 14 | Please include information like: 15 | 16 | * what command you ran 17 | * the code that caused the failure (see [this link](https://help.github.com/articles/basic-writing-and-formatting-syntax/) for help with formatting code) 18 | * full output of the error you received 19 | 20 | ### How can it be fixed? 21 | 22 | Fill this in if you know how the bug could be fixed. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Request a new feature, or an improvement to existing functionality. 4 | --- 5 | ### Overview 6 | Provide a simple overview of what you wish to see added. Please include: 7 | 8 | * What you are trying to do 9 | * Why vvm's current functionality is inadequate to address your goal 10 | 11 | ### Specification 12 | Describe the syntax and semantics of how you would like to see this feature implemented. The more detailed the better! 13 | 14 | Remember, your feature is much more likely to be included if it does not involve any breaking changes. 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What I did 2 | 3 | Related issue: # 4 | 5 | ### How I did it 6 | 7 | ### How to verify it 8 | 9 | ### Checklist 10 | 11 | - [ ] I have confirmed that my PR passes all linting checks 12 | - [ ] I have included test cases 13 | - [ ] I have updated the documentation (README.md) 14 | - [ ] I have added an entry to the changelog 15 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | - cron: 0 0 * * * # midnight every day 6 | 7 | env: 8 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 9 | 10 | name: vvm workflow 11 | 12 | jobs: 13 | 14 | lint: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Setup Python 3.9 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: 3.9 24 | 25 | - name: Install Tox 26 | run: pip install tox wheel 27 | 28 | - name: Run Tox 29 | run: tox -e lint 30 | 31 | test: 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | os: [ubuntu-latest] 36 | python-version: [["3.8", "38"], ["3.9", "39"], ["3.10", "310"], ["3.11", "311"], ["3.12", "312"]] 37 | include: 38 | - os: macos-latest 39 | python-version: ["3.11", "311"] 40 | - os: windows-latest 41 | python-version: ["3.11", "311"] 42 | 43 | runs-on: ${{ matrix.os }} 44 | 45 | steps: 46 | - uses: actions/checkout@v4 47 | 48 | - name: Setup Python ${{ matrix.python-version[0] }} 49 | uses: actions/setup-python@v5 50 | with: 51 | python-version: ${{ matrix.python-version[0] }} 52 | 53 | - name: Install Tox 54 | run: pip install tox wheel 55 | 56 | - name: Run Tox 57 | run: tox -e py${{ matrix.python-version[1] }} 58 | 59 | - name: Upload coverage to Codecov 60 | uses: codecov/codecov-action@v1 61 | with: 62 | file: ./coverage.xml 63 | # for now not failing, this may be reverted once we set it up properly 64 | fail_ci_if_error: false 65 | -------------------------------------------------------------------------------- /.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 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # PyCharm 132 | .idea/ 133 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: check-yaml 6 | - repo: https://github.com/psf/black 7 | rev: 19.3b0 8 | hooks: 9 | - id: black 10 | name: fmt 11 | - repo: https://github.com/pre-commit/pre-commit-hooks 12 | rev: v2.4.0 13 | hooks: 14 | - id: flake8 15 | - repo: https://github.com/pre-commit/mirrors-mypy 16 | rev: v0.720 17 | hooks: 18 | - id: mypy 19 | - repo: https://github.com/asottile/seed-isort-config 20 | rev: v1.9.2 21 | hooks: 22 | - id: seed-isort-config 23 | - repo: https://github.com/pre-commit/mirrors-isort 24 | rev: v4.3.21 25 | hooks: 26 | - id: isort 27 | 28 | default_language_version: 29 | python: python3 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased](https://github.com/vyperlang/vvm/) 8 | ### Changed 9 | - Update contact information in `CONTRIBUTING.md` ([#17](https://github.com/vyperlang/vvm/pull/17), [#18](https://github.com/vyperlang/vvm/pull/18)) 10 | - Update dependencies. Minimum python version is now 3.8 ([#22](https://github.com/vyperlang/vvm/pull/22)) 11 | - Add `output_format` argument to `compile_source` and `compile_files` ([#21](https://github.com/vyperlang/vvm/pull/21)) 12 | - New public function `detect_vyper_version_from_source` ([#23](https://github.com/vyperlang/vvm/pull/23)) 13 | - Fix `combine_json` for versions `>0.3.10` ([#29](https://github.com/vyperlang/vvm/pull/29)) 14 | - Relax version detection checks ([#30](https://github.com/vyperlang/vvm/pull/30)) 15 | 16 | ## [0.1.0](https://github.com/vyperlang/vvm/tree/v0.1.0) - 2020-10-07 17 | ### Added 18 | - Support for Python 3.9 19 | - Cache version information 20 | 21 | ## [0.0.2](https://github.com/vyperlang/vvm/tree/v0.0.2) - 2020-08-26 22 | ### Fixed 23 | - Ignore `.exe` when handling versions on Windows 24 | 25 | ## [0.0.1](https://github.com/vyperlang/vvm/tree/v0.0.1) - 2020-08-25 26 | - Initial release 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | To get started with working on the `vvm` codebase, use the following steps to prepare your local environment: 4 | 5 | ```bash 6 | # clone the github repo and navigate into the folder 7 | git clone https://github.com/vyperlang/vvm.git 8 | cd vvm 9 | 10 | # create and load a virtual environment 11 | python3 -m venv venv 12 | source venv/bin/activate 13 | 14 | # install vvm into the virtual environment 15 | python setup.py install 16 | 17 | # install the developer dependencies 18 | pip install -r requirements-dev.txt 19 | ``` 20 | 21 | ## Pre-Commit Hooks 22 | 23 | We use [`pre-commit`](https://pre-commit.com/) hooks to simplify linting and ensure consistent formatting among contributors. Use of `pre-commit` is not a requirement, but is highly recommended. 24 | 25 | Install `pre-commit` locally from the brownie root folder: 26 | 27 | ```bash 28 | pip install pre-commit 29 | pre-commit install 30 | ``` 31 | 32 | Commiting will now automatically run the local hooks and ensure that your commit passes all lint checks. 33 | 34 | ## Pull Requests 35 | 36 | Pull requests are welcomed! Please adhere to the following: 37 | 38 | - Ensure your pull request passes our linting checks (`tox -e lint`) 39 | - Include test cases for any new functionality 40 | - Include any relevant documentation updates 41 | 42 | It's a good idea to make pull requests early on. A pull request represents the start of a discussion, and doesn't necessarily need to be the final, finished submission. 43 | 44 | If you are opening a work-in-progress pull request to verify that it passes CI tests, please consider [marking it as a draft](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests). 45 | 46 | For further discussions and questions, talk to us on [Discord](https://discord.gg/6tw7PTM7C2) or on [Telegram](https://t.me/vyperlang). 47 | 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Ben Hauser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | include vvm/py.typed 4 | 5 | recursive-exclude * __pycache__ 6 | recursive-exclude * *.py[co] 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vvm 2 | 3 | [![Pypi Status](https://img.shields.io/pypi/v/vvm.svg)](https://pypi.org/project/vvm/) [![Build Status](https://img.shields.io/github/actions/workflow/status/vyperlang/vvm/main.yaml?branch=master)](https://github.com/vyperlang/vvm/actions) [![Coverage Status](https://img.shields.io/codecov/c/github/vyperlang/vvm)](https://codecov.io/gh/vyperlang/vvm) 4 | 5 | Vyper version management tool. 6 | 7 | ## Installation 8 | 9 | ### via `pip` 10 | 11 | ```bash 12 | pip install vvm 13 | ``` 14 | 15 | ### via `setuptools` 16 | 17 | ```bash 18 | git clone https://github.com/vyperlang/vvm.git 19 | cd vvm 20 | python3 setup.py install 21 | ``` 22 | 23 | ## Quickstart 24 | 25 | Use `vvm` to install versions of Vyper: 26 | 27 | ```python 28 | from vvm import install_vyper 29 | 30 | install_vyper(version="0.4.0") 31 | ``` 32 | 33 | **Note**: On macOS with the Apple chips, installing some versions of Vyper may fail if you have not first run this 34 | command: 35 | 36 | ```bash 37 | softwareupdate --install-rosetta 38 | ``` 39 | 40 | To install Vyper without validating the binary (useful for debugging), you can set `validate=False`. 41 | 42 | ```python 43 | from vvm import install_vyper 44 | 45 | install_vyper(version="0.4.0", validate=False) 46 | ``` 47 | 48 | ## Testing 49 | 50 | `vvm` is tested on Linux, macOS and Windows with Vyper versions `>=0.1.0-beta.16`. 51 | 52 | To run the test suite: 53 | 54 | ```bash 55 | pytest tests/ 56 | ``` 57 | 58 | By default, the test suite installs all available `vyper` versions for your OS. If you only wish to test against already installed versions, include the `--no-install` flag. Use the `--vyper-verions` flag to test against one or more specific versions. 59 | 60 | ## Contributing 61 | 62 | Help is always appreciated! Feel free to open an issue if you find a problem, or a pull request if you've solved an issue. 63 | 64 | Please check out our [Contribution Guide](CONTRIBUTING.md) prior to opening a pull request. 65 | 66 | ## License 67 | 68 | This project is licensed under the [MIT license](LICENSE). 69 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | coverage: 3 | status: 4 | project: 5 | default: 6 | target: 0 7 | patch: 8 | default: 9 | target: 0 10 | 11 | -------------------------------------------------------------------------------- /deploying.md: -------------------------------------------------------------------------------- 1 | # bump2version patch 2 | # (or, bump2version minor) 3 | # rm -r vvm.egg-info/ dist/ 4 | # python -m build 5 | # twine upload dist/* 6 | # or, 7 | # twine --repository vvm upload dist/* 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 100 3 | target-version = ['py38', 'py39', 'py310', 'py311', 'py312'] 4 | include = '\.pyi?$' 5 | exclude = ''' 6 | /( 7 | \.eggs 8 | | \.git 9 | | \.hg 10 | | \.mypy_cache 11 | | \.tox 12 | | \.venv 13 | | _build 14 | | buck-out 15 | | build 16 | | dist 17 | | env 18 | | venv 19 | )/ 20 | ''' 21 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | black==24.8.0 2 | bumpversion>=0.6.0,<1.0.0 3 | flake8>=7.1.1,<8.0.0 4 | isort>=5.13.2,<6.0.0 5 | mypy>=1.11.2,<2.0.0 6 | pytest>=8.3.2,<9.0.0 7 | pytest-cov>=5.0.0,<6.0.0 8 | tox>=4.18.0,<5.0.0 9 | tqdm>=4.66.5,<5.0.0 10 | twine>=5.1.1,<6.0.0 11 | wheel>=0.44.0,<1.0.0 12 | types-requests>2.32.0,<3.0.0 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.3.2 3 | 4 | [bumpversion:file:setup.py] 5 | 6 | [flake8] 7 | max-line-length = 100 8 | ignore = E203,W503 9 | per-file-ignores = 10 | */__init__.py: F401 11 | 12 | [mypy] 13 | ignore_missing_imports = True 14 | follow_imports = silent 15 | 16 | [tool:isort] 17 | force_grid_wrap = 0 18 | include_trailing_comma = True 19 | known_third_party = pytest,requests,packaging,setuptools 20 | line_length = 100 21 | multi_line_output = 3 22 | use_parentheses = True 23 | 24 | [tool:pytest] 25 | addopts = --cov=vvm --cov-branch --cov-report xml 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import find_packages, setup 4 | 5 | with open('README.md', encoding="utf-8") as f: 6 | long_description = f.read() 7 | 8 | setup( 9 | name="vvm", 10 | version="0.3.2", # don't change this manually, use bumpversion instead 11 | description="Vyper version management tool", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | author="Ben Hauser", 15 | author_email="ben@hauser.id", 16 | url="https://github.com/vyperlang/vvm", 17 | include_package_data=True, 18 | py_modules=["vvm"], 19 | python_requires=">=3.8, <4", 20 | install_requires=["requests>=2.32.3,<3", "packaging>=23.1"], 21 | license="MIT", 22 | zip_safe=False, 23 | keywords="ethereum vyper", 24 | packages=find_packages(exclude=["tests", "tests.*"]), 25 | classifiers=[ 26 | "Intended Audience :: Developers", 27 | "License :: OSI Approved :: MIT License", 28 | "Natural Language :: English", 29 | "Programming Language :: Python :: 3", 30 | "Programming Language :: Python :: 3.8", 31 | "Programming Language :: Python :: 3.9", 32 | "Programming Language :: Python :: 3.10", 33 | "Programming Language :: Python :: 3.11", 34 | "Programming Language :: Python :: 3.12", 35 | "Programming Language :: Python :: 3.13", 36 | ], 37 | ) 38 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | from packaging.version import Version 5 | from requests import ConnectionError 6 | 7 | import vvm 8 | 9 | 10 | def pytest_addoption(parser): 11 | parser.addoption( 12 | "--no-install", 13 | action="store_true", 14 | help="Only run vvm tests against already installed vyper versions", 15 | ) 16 | parser.addoption( 17 | "--vyper-versions", 18 | action="store", 19 | help="Only run tests against a specific version(s) of vyper", 20 | ) 21 | 22 | 23 | def pytest_configure(config): 24 | config.addinivalue_line("markers", "min_vyper: minimum version of vyper to run test against") 25 | config.addinivalue_line("markers", "max_vyper: maximum version of vyper to run test against") 26 | 27 | 28 | def pytest_collection(session): 29 | global VERSIONS 30 | if session.config.getoption("--vyper-versions"): 31 | VERSIONS = [Version(i) for i in session.config.getoption("--vyper-versions").split(",")] 32 | elif session.config.getoption("--no-install"): 33 | VERSIONS = vvm.get_installed_vyper_versions() 34 | else: 35 | try: 36 | VERSIONS = vvm.get_installable_vyper_versions() 37 | except ConnectionError: 38 | raise pytest.UsageError( 39 | "ConnectionError while attempting to get vyper versions.\n" 40 | "Use the --no-install flag to only run tests against already installed versions." 41 | ) 42 | for version in VERSIONS: 43 | vvm.install_vyper(version) 44 | 45 | 46 | # auto-parametrize the vyper_version fixture with all target vyper versions 47 | def pytest_generate_tests(metafunc): 48 | if "vyper_version" in metafunc.fixturenames: 49 | versions = VERSIONS.copy() 50 | for marker in metafunc.definition.iter_markers(name="min_vyper"): 51 | versions = [i for i in versions if i >= Version(marker.args[0])] 52 | for marker in metafunc.definition.iter_markers(name="max_vyper"): 53 | versions = [i for i in versions if i <= Version(marker.args[0])] 54 | metafunc.parametrize("vyper_version", versions, indirect=True) 55 | 56 | 57 | @pytest.fixture 58 | def vyper_version(request): 59 | """ 60 | Run a test against all vyper versions. 61 | """ 62 | version = request.param 63 | vvm.set_vyper_version(version) 64 | return version 65 | 66 | 67 | @pytest.fixture 68 | def latest_version(): 69 | global VERSIONS 70 | return VERSIONS[0] 71 | 72 | 73 | @pytest.fixture 74 | def foo_source(vyper_version): 75 | visibility = "external" if vyper_version >= Version("0.2.0") else "public" 76 | interface = "IERC20" if vyper_version >= Version("0.4.0a") else "ERC20" 77 | import_path = "ethereum.ercs" if vyper_version >= Version("0.4.0a") else "vyper.interfaces" 78 | pragma_version = "pragma version" if vyper_version >= Version("0.3.8") else "@version" 79 | yield f""" 80 | #{pragma_version} {vyper_version} 81 | from {import_path} import {interface} 82 | 83 | @{visibility} 84 | def foo() -> int128: 85 | return 13 86 | """ 87 | 88 | 89 | @pytest.fixture 90 | def foo_path(tmp_path_factory, foo_source, vyper_version): 91 | source = tmp_path_factory.getbasetemp().joinpath(f"Foo-{vyper_version}.vy") 92 | if not source.exists(): 93 | with source.open("w") as fp: 94 | fp.write(foo_source) 95 | return source 96 | 97 | 98 | @pytest.fixture 99 | def input_json(vyper_version): 100 | json = { 101 | "language": "Vyper", 102 | "sources": {}, 103 | "settings": {"outputSelection": {"*": {"*": ["evm.bytecode.object"]}}}, 104 | } 105 | yield json 106 | -------------------------------------------------------------------------------- /tests/test_compile_source.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from pathlib import Path 3 | 4 | import pytest 5 | from packaging.version import Version 6 | 7 | import vvm 8 | 9 | 10 | def test_compile_source(foo_source, vyper_version): 11 | if Version("0.4.0b1") <= vyper_version <= Version("0.4.0b5"): 12 | pytest.skip("vyper 0.4.0b1 to 0.4.0b5 have a bug with combined_json") 13 | output = vvm.compile_source(foo_source) 14 | assert "" in output 15 | assert "bytecode" in output[""] 16 | 17 | 18 | def test_compile_files(foo_path, vyper_version): 19 | if Version("0.4.0b1") <= vyper_version <= Version("0.4.0b5"): 20 | pytest.skip("vyper 0.4.0b1 to 0.4.0b5 have a bug with combined_json") 21 | output = vvm.compile_files([foo_path]) 22 | assert foo_path.as_posix() in output 23 | 24 | 25 | def test_compile_files_search_paths(foo_path, vyper_version): 26 | if Version("0.4.0b1") <= vyper_version <= Version("0.4.0b5"): 27 | pytest.skip("vyper 0.4.0b1 to 0.4.0b5 have a bug with combined_json") 28 | 29 | with tempfile.TemporaryDirectory() as tmpdir: 30 | output = vvm.compile_files([foo_path], search_paths=[Path.cwd(), tmpdir]) 31 | 32 | assert foo_path.as_posix() in output 33 | 34 | 35 | def test_compile_standard(input_json, foo_source): 36 | input_json["sources"] = {"contracts/Foo.vy": {"content": foo_source}} 37 | result = vvm.compile_standard(input_json) 38 | 39 | assert "contracts/Foo.vy" in result["contracts"] 40 | 41 | 42 | @pytest.mark.parametrize("version_str", ["0.1.0b16", "0.1.0beta17"]) 43 | def test_pragmas_in_vyper_010(version_str): 44 | source = f""" 45 | # @version {version_str} 46 | 47 | @public 48 | def foo() -> int128: 49 | return 42 50 | """ 51 | vvm.compile_source(source, vyper_version=version_str) 52 | 53 | 54 | def test_compile_metadata(foo_source, vyper_version): 55 | if vyper_version <= Version("0.3.1"): 56 | pytest.skip("metadata output not supported in vyper < 0.3.2") 57 | output = vvm.compile_source(foo_source, output_format="metadata") 58 | assert "function_info" in output 59 | 60 | 61 | def test_compile_metadata_from_file(foo_path, vyper_version): 62 | if vyper_version <= Version("0.3.1"): 63 | pytest.skip("metadata output not supported in vyper < 0.3.2") 64 | output = vvm.compile_files([foo_path], output_format="metadata") 65 | assert "function_info" in output 66 | -------------------------------------------------------------------------------- /tests/test_install.py: -------------------------------------------------------------------------------- 1 | import vvm 2 | 3 | 4 | def test_get_installed_vyper_versions(vyper_version): 5 | assert "exe" not in str(vyper_version) 6 | assert vyper_version in vvm.install.get_installed_vyper_versions() 7 | -------------------------------------------------------------------------------- /tests/test_to_vyper_version.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from packaging.version import Version 3 | 4 | from vvm.utils.convert import to_vyper_version 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "version_str", 9 | ["0.1.0b17", "0.1.0beta17", "0.1.0-beta17", "0.1.0.beta17", "0.1.0B17", "0.1.0.Beta.17"], 10 | ) 11 | def test_to_vyper_version(version_str): 12 | assert to_vyper_version(version_str) == Version("0.1.0-beta.17") 13 | -------------------------------------------------------------------------------- /tests/test_versioning.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from packaging.specifiers import InvalidSpecifier, SpecifierSet 3 | from packaging.version import Version 4 | 5 | from vvm import detect_vyper_version_from_source 6 | from vvm.exceptions import UnexpectedVersionError 7 | from vvm.utils.versioning import _pick_vyper_version, detect_version_specifier_set 8 | 9 | 10 | def test_foo_vyper_version(foo_source, vyper_version): 11 | specifier = detect_version_specifier_set(foo_source) 12 | assert str(specifier) == f"=={vyper_version}" 13 | assert vyper_version.major == 0 14 | assert _pick_vyper_version(specifier) == vyper_version 15 | 16 | 17 | @pytest.mark.parametrize( 18 | "version_str,decorator,pragma,expected_specifier_set,expected_version", 19 | [ 20 | # npm's ^ gets converted to ~= 21 | ("^0.2.0", "public", "@version", "~=0.2.0", "0.2.16"), 22 | ("^0.4.0", "external", "pragma version", "~=0.4.0", "0.4.0"), 23 | ("^0.1.0b16", "public", "@version", "~=0.1.0b16", "0.1.0b17"), 24 | # indented comment is supported 25 | ("0.4.0", "external", " pragma version", "==0.4.0", "0.4.0"), 26 | # pep440 >= and < are preserved 27 | (">=0.3.10, <0.4.0", "external", "pragma version", ">=0.3.10, <0.4.0", "0.3.10"), 28 | # beta and release candidate are supported 29 | ("0.1.0b17", "public", "@version", "==0.1.0b17", "0.1.0b17"), 30 | ("0.4.0rc6", "external", "pragma version", "==0.4.0rc6", "0.4.0rc6"), 31 | (">=0.3.0-beta17", "external", "@version", ">=0.3.0b17", "latest"), 32 | ], 33 | ) 34 | def test_vyper_version( 35 | version_str, decorator, pragma, expected_specifier_set, expected_version, latest_version 36 | ): 37 | source = f""" 38 | # {pragma} {version_str} 39 | 40 | @{decorator} 41 | def foo() -> int128: 42 | return 42 43 | """ 44 | detected = detect_version_specifier_set(source) 45 | assert detected == SpecifierSet(expected_specifier_set) 46 | if expected_version == "latest": 47 | expected_version = str(latest_version) 48 | assert detect_vyper_version_from_source(source) == Version(expected_version) 49 | 50 | 51 | @pytest.mark.parametrize( 52 | "version_str", 53 | [ 54 | "~0.2.0", 55 | ">= 0.3.1 < 0.4.0", 56 | "0.3.1 - 0.3.2", 57 | "0.3.1 || 0.3.2", 58 | "=0.3.1", 59 | ], 60 | ) 61 | def test_unsported_vyper_version(version_str): 62 | # npm's complex ranges are not supported although old vyper versions can handle them 63 | source = f""" 64 | # @version {version_str} 65 | """ 66 | with pytest.raises(InvalidSpecifier): 67 | detect_version_specifier_set(source) 68 | 69 | 70 | def test_no_version_in_source(): 71 | assert detect_vyper_version_from_source("def foo() -> int128: return 42") is None 72 | 73 | 74 | def test_version_does_not_exist(): 75 | with pytest.raises(UnexpectedVersionError) as excinfo: 76 | detect_vyper_version_from_source("# pragma version 2024.0.1") 77 | assert str(excinfo.value) == "No installable Vyper satisfies the specifier ==2024.0.1" 78 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | lint 4 | py{38,39,310,311,312} 5 | 6 | [testenv] 7 | passenv = 8 | GITHUB_TOKEN 9 | deps = -r{toxinidir}/requirements-dev.txt 10 | commands = 11 | py{38,39,310,311,312}: python -m pytest tests/ 12 | 13 | [testenv:lint] 14 | extras=linter 15 | commands = 16 | black --check {toxinidir}/vvm {toxinidir}/tests 17 | flake8 {toxinidir}/vvm {toxinidir}/tests 18 | isort --check-only --diff {toxinidir}/vvm {toxinidir}/tests 19 | mypy --disallow-untyped-defs {toxinidir}/vvm --implicit-optional 20 | -------------------------------------------------------------------------------- /vvm/__init__.py: -------------------------------------------------------------------------------- 1 | from vvm.install import ( 2 | get_installable_vyper_versions, 3 | get_installed_vyper_versions, 4 | get_vvm_install_folder, 5 | install_vyper, 6 | set_vyper_version, 7 | ) 8 | from vvm.main import compile_files, compile_source, compile_standard, get_vyper_version 9 | from vvm.utils.versioning import detect_vyper_version_from_source 10 | -------------------------------------------------------------------------------- /vvm/exceptions.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | # Exceptions 4 | 5 | 6 | class DownloadError(Exception): 7 | pass 8 | 9 | 10 | class UnexpectedVersionError(Exception): 11 | pass 12 | 13 | 14 | class UnknownOption(Exception): 15 | pass 16 | 17 | 18 | class UnknownValue(Exception): 19 | pass 20 | 21 | 22 | class VyperError(Exception): 23 | message = "An error occurred during execution" 24 | 25 | def __init__( 26 | self, 27 | message: str = None, 28 | command: List = None, 29 | return_code: int = None, 30 | stdin_data: str = None, 31 | stdout_data: str = None, 32 | stderr_data: str = None, 33 | error_dict: Dict = None, 34 | ) -> None: 35 | if message is not None: 36 | self.message = message 37 | self.command = command or [] 38 | self.return_code = return_code 39 | self.stdin_data = stdin_data 40 | self.stderr_data = stderr_data 41 | self.stdout_data = stdout_data 42 | self.error_dict = error_dict 43 | 44 | def __str__(self) -> str: 45 | return ( 46 | f"{self.message}" 47 | f"\n> command: `{' '.join(str(i) for i in self.command)}`" 48 | f"\n> return code: `{self.return_code}`" 49 | "\n> stdout:" 50 | f"\n{self.stdout_data}" 51 | "\n> stderr:" 52 | f"\n{self.stderr_data}" 53 | ).strip() 54 | 55 | 56 | class VyperInstallationError(Exception): 57 | pass 58 | 59 | 60 | class VyperNotInstalled(Exception): 61 | pass 62 | 63 | 64 | # Warnings 65 | 66 | 67 | class UnexpectedVersionWarning(Warning): 68 | pass 69 | -------------------------------------------------------------------------------- /vvm/install.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import stat 4 | import sys 5 | import warnings 6 | from base64 import b64encode 7 | from pathlib import Path 8 | from typing import Dict, List, Optional, Union 9 | 10 | from packaging.version import Version 11 | 12 | from vvm import wrapper 13 | from vvm.exceptions import ( 14 | DownloadError, 15 | UnexpectedVersionError, 16 | UnexpectedVersionWarning, 17 | VyperInstallationError, 18 | VyperNotInstalled, 19 | ) 20 | from vvm.utils.convert import to_vyper_version 21 | from vvm.utils.lock import get_process_lock 22 | 23 | try: 24 | from tqdm import tqdm 25 | except ImportError: 26 | tqdm = None 27 | 28 | try: 29 | from requests_cache import CachedSession 30 | 31 | SESSION = CachedSession( 32 | "~/.cache/vvm", 33 | allowable_codes=[200], 34 | cache_control=True, 35 | expire_after=3600, 36 | stale_if_error=True, 37 | ) 38 | except ImportError: 39 | from requests import Session 40 | 41 | SESSION = Session() 42 | 43 | GITHUB_RELEASES = "https://api.github.com/repos/vyperlang/vyper/releases?per_page=100" 44 | 45 | LOGGER = logging.getLogger("vvm") 46 | 47 | VVM_BINARY_PATH_VARIABLE = "VVM_BINARY_PATH" 48 | 49 | _default_vyper_binary = None 50 | _installable_vyper_versions: Optional[List[Version]] = None 51 | 52 | 53 | def _get_os_name() -> str: 54 | if sys.platform.startswith("linux"): 55 | return "linux" 56 | if sys.platform == "darwin": 57 | return "darwin" 58 | if sys.platform == "win32": 59 | return "windows" 60 | raise OSError(f"Unsupported OS: '{sys.platform}' - vvm supports Linux, OSX and Windows") 61 | 62 | 63 | def get_vvm_install_folder(vvm_binary_path: Union[Path, str] = None) -> Path: 64 | """ 65 | Return the directory where `vvm` stores installed `vyper` binaries. 66 | 67 | By default, this is `~/.vvm` 68 | 69 | Arguments 70 | --------- 71 | vvm_binary_path : Path | str, optional 72 | User-defined path, used to override the default installation directory. 73 | 74 | Returns 75 | ------- 76 | Path 77 | Subdirectory where `vyper` binaries are saved. 78 | """ 79 | if os.getenv(VVM_BINARY_PATH_VARIABLE): 80 | return Path(os.environ[VVM_BINARY_PATH_VARIABLE]) 81 | elif vvm_binary_path is not None: 82 | return Path(vvm_binary_path) 83 | else: 84 | path = Path.home().joinpath(".vvm") 85 | path.mkdir(exist_ok=True) 86 | return path 87 | 88 | 89 | def get_executable( 90 | version: Union[str, Version] = None, vvm_binary_path: Union[Path, str] = None 91 | ) -> Path: 92 | """ 93 | Return the Path to an installed `vyper` binary. 94 | 95 | Arguments 96 | --------- 97 | version : str | Version, optional 98 | Installed `vyper` version to get the path of. If not given, returns the 99 | path of the active version. 100 | vvm_binary_path : Path | str, optional 101 | User-defined path, used to override the default installation directory. 102 | 103 | Returns 104 | ------- 105 | Path 106 | `vyper` executable. 107 | """ 108 | if not version: 109 | if not _default_vyper_binary: 110 | raise VyperNotInstalled( 111 | "Vyper is not installed. Call vvm.get_available_vyper_versions()" 112 | " to view for available versions and vvm.install_vyper() to install." 113 | ) 114 | return _default_vyper_binary 115 | 116 | version = to_vyper_version(version) 117 | vyper_bin = get_vvm_install_folder(vvm_binary_path).joinpath(f"vyper-{version}") 118 | if _get_os_name() == "windows": 119 | vyper_bin = vyper_bin.with_name(f"{vyper_bin.name}.exe") 120 | 121 | if not vyper_bin.exists(): 122 | raise VyperNotInstalled( 123 | f"vyper {version} has not been installed." 124 | f" Use vvm.install_vyper('{version}') to install." 125 | ) 126 | return vyper_bin 127 | 128 | 129 | def set_vyper_version( 130 | version: Union[str, Version], silent: bool = False, vvm_binary_path: Union[Path, str] = None 131 | ) -> None: 132 | """ 133 | Set the currently active `vyper` binary. 134 | 135 | Arguments 136 | --------- 137 | version : str | Version, optional 138 | Installed `vyper` version to get the path of. If not given, returns the 139 | path of the active version. 140 | silent : bool, optional 141 | If True, do not generate any logger output. 142 | vvm_binary_path : Path | str, optional 143 | User-defined path, used to override the default installation directory. 144 | """ 145 | version = to_vyper_version(version) 146 | global _default_vyper_binary 147 | _default_vyper_binary = get_executable(version, vvm_binary_path) 148 | if not silent: 149 | LOGGER.info(f"Using vyper version {version}") 150 | 151 | 152 | def _get_headers(headers: Optional[Dict]) -> Dict: 153 | if headers is None and os.getenv("GITHUB_TOKEN") is not None: 154 | auth = b64encode(os.environ["GITHUB_TOKEN"].encode()).decode() 155 | headers = {"Authorization": f"Basic {auth}"} 156 | 157 | return headers or {} 158 | 159 | 160 | def _get_releases(headers: Optional[Dict]) -> Dict: 161 | data = SESSION.get(GITHUB_RELEASES, headers=headers) 162 | if data.status_code != 200: 163 | msg = ( 164 | f"Status {data.status_code} when getting Vyper versions from Github:" 165 | f" '{data.json()['message']}'" 166 | ) 167 | if data.status_code == 403: 168 | msg += ( 169 | "\n\nIf this issue persists, generate a Github API token and store" 170 | " it as the environment variable `GITHUB_TOKEN`:\n" 171 | "https://github.blog/2013-05-16-personal-api-tokens/" 172 | ) 173 | raise ConnectionError(msg) 174 | 175 | return data.json() 176 | 177 | 178 | def get_installable_vyper_versions(headers: Dict = None) -> List[Version]: 179 | """ 180 | Return a list of all `vyper` versions that can be installed by vvm. 181 | 182 | Note: this function is cached, so subsequent calls will not change the result. 183 | When new versions of vyper are released, the cache will need to be cleared 184 | manually or the application restarted. 185 | 186 | Returns 187 | ------- 188 | List 189 | List of Versions objects of installable `vyper` versions. 190 | """ 191 | global _installable_vyper_versions 192 | if _installable_vyper_versions is not None: 193 | return _installable_vyper_versions 194 | 195 | version_list = [] 196 | 197 | headers = _get_headers(headers) 198 | 199 | for release in _get_releases(headers): 200 | version = Version(release["tag_name"]) 201 | asset = next((i for i in release["assets"] if _get_os_name() in i["name"]), False) 202 | if asset: 203 | version_list.append(version) 204 | 205 | _installable_vyper_versions = sorted(version_list, reverse=True) 206 | return _installable_vyper_versions 207 | 208 | 209 | def get_installed_vyper_versions(vvm_binary_path: Union[Path, str] = None) -> List[Version]: 210 | """ 211 | Return a list of currently installed `vyper` versions. 212 | 213 | Arguments 214 | --------- 215 | vvm_binary_path : Path | str, optional 216 | User-defined path, used to override the default installation directory. 217 | 218 | Returns 219 | ------- 220 | List 221 | List of Version objects of installed `vyper` versions. 222 | """ 223 | install_path = get_vvm_install_folder(vvm_binary_path) 224 | if _get_os_name() == "windows": 225 | version_list = [i.stem[6:] for i in install_path.glob("vyper-*")] 226 | else: 227 | version_list = [i.name[6:] for i in install_path.glob("vyper-*")] 228 | return sorted([Version(i) for i in version_list], reverse=True) 229 | 230 | 231 | # TODO: maybe rename this function to `ensure_installed` 232 | def install_vyper( 233 | version: Union[str, Version] = "latest", 234 | show_progress: bool = False, 235 | vvm_binary_path: Union[Path, str] = None, 236 | headers: Dict = None, 237 | validate: bool = True, 238 | ) -> Version: 239 | """ 240 | Download and install a precompiled version of `vyper`. 241 | 242 | Arguments 243 | --------- 244 | version : str | Version, optional 245 | Version of `vyper` to install. Default is the newest available version. 246 | show_progress : bool, optional 247 | If True, display a progress bar while downloading. Requires installing 248 | the `tqdm` package. 249 | vvm_binary_path : Path | str, optional 250 | User-defined path, used to override the default installation directory. 251 | validate : bool 252 | Set to False to skip validating the downloaded binary. Defaults to True. 253 | Useful for when debugging why a binary fails to run on your OS (may need 254 | additional setup) or if managing binaries without needing to run them 255 | (such as a mirror). 256 | 257 | Returns 258 | ------- 259 | Version 260 | installed vyper version 261 | """ 262 | 263 | if version == "latest": 264 | version = get_installable_vyper_versions()[0] 265 | else: 266 | version = to_vyper_version(version) 267 | 268 | os_name = _get_os_name() 269 | process_lock = get_process_lock(str(version)) 270 | 271 | with process_lock: 272 | if _check_for_installed_version(version, vvm_binary_path): 273 | path = get_vvm_install_folder(vvm_binary_path).joinpath(f"vyper-{version}") 274 | LOGGER.info(f"vyper {version} already installed at: {path}") 275 | return version 276 | 277 | headers = _get_headers(headers) 278 | data = _get_releases(headers) 279 | try: 280 | release = next(i for i in data if Version(i["tag_name"]) == version) 281 | asset = next(i for i in release["assets"] if _get_os_name() in i["name"]) 282 | except StopIteration: 283 | raise VyperInstallationError(f"Vyper binary not available for v{version}") 284 | 285 | install_path = get_vvm_install_folder(vvm_binary_path).joinpath(f"vyper-{version}") 286 | if os_name == "windows": 287 | install_path = install_path.with_name(f"{install_path.name}.exe") 288 | 289 | url = asset["browser_download_url"] 290 | content = _download_vyper(url, headers, show_progress) 291 | with open(install_path, "wb") as fp: 292 | fp.write(content) 293 | 294 | if os_name != "windows": 295 | install_path.chmod(install_path.stat().st_mode | stat.S_IEXEC) 296 | 297 | if validate: 298 | _validate_installation(version, vvm_binary_path) 299 | 300 | return version 301 | 302 | 303 | def _check_for_installed_version( 304 | version: Version, vvm_binary_path: Union[Path, str] = None 305 | ) -> bool: 306 | path = get_vvm_install_folder(vvm_binary_path).joinpath(f"vyper-{version}") 307 | return path.exists() 308 | 309 | 310 | def _download_vyper(url: str, headers: Dict, show_progress: bool) -> bytes: 311 | LOGGER.info(f"Downloading from {url}") 312 | response = SESSION.get(url, headers=headers, stream=show_progress) 313 | if response.status_code == 404: 314 | raise DownloadError( 315 | "404 error when attempting to download from {} - are you sure this" 316 | " version of vyper is available?".format(url) 317 | ) 318 | if response.status_code != 200: 319 | raise DownloadError( 320 | f"Received status code {response.status_code} when attempting to download from {url}" 321 | ) 322 | if not show_progress: 323 | return response.content 324 | 325 | total_size = int(response.headers.get("content-length", 0)) 326 | progress_bar = tqdm(total=total_size, unit="iB", unit_scale=True) 327 | content = bytes() 328 | 329 | for data in response.iter_content(None, decode_unicode=True): 330 | progress_bar.update(len(data)) 331 | content += data 332 | progress_bar.close() 333 | 334 | return content 335 | 336 | 337 | def _validate_installation(version: Version, vvm_binary_path: Union[Path, str, None]) -> None: 338 | binary_path = get_executable(version, vvm_binary_path) 339 | try: 340 | installed_version = wrapper._get_vyper_version(binary_path) 341 | except Exception: 342 | binary_path.unlink() 343 | raise VyperInstallationError( 344 | "Downloaded binary would not execute, or returned unexpected output." 345 | ) 346 | 347 | if installed_version.base_version != version.base_version: 348 | # raise if the version of the tag is not the same as the binary version 349 | binary_path.unlink() 350 | raise UnexpectedVersionError( 351 | f"Attempted to install vyper v{version}, but got vyper v{installed_version}" 352 | ) 353 | if installed_version != version: 354 | # warn, but don't raise, when pre or post release is not the same. 355 | warnings.warn(f"Installed vyper version is v{installed_version}", UnexpectedVersionWarning) 356 | 357 | if not _default_vyper_binary: 358 | set_vyper_version(version) 359 | LOGGER.info(f"vyper {version} successfully installed at: {binary_path}") 360 | 361 | 362 | if get_installed_vyper_versions(): 363 | set_vyper_version(get_installed_vyper_versions()[0], silent=True) 364 | -------------------------------------------------------------------------------- /vvm/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import tempfile 3 | from pathlib import Path 4 | from typing import Any, Dict, List, Optional, Union 5 | 6 | from packaging.version import Version 7 | 8 | from vvm import wrapper 9 | from vvm.exceptions import VyperError 10 | from vvm.install import get_executable 11 | 12 | 13 | def get_vyper_version() -> Version: 14 | """ 15 | Get the version of the active `vyper` binary. 16 | 17 | Returns 18 | ------- 19 | Version 20 | vyper version 21 | """ 22 | vyper_binary = get_executable() 23 | return wrapper._get_vyper_version(vyper_binary) 24 | 25 | 26 | def compile_source( 27 | source: str, 28 | base_path: Union[Path, str] = None, 29 | evm_version: str = None, 30 | vyper_binary: Union[str, Path] = None, 31 | vyper_version: Union[str, Version, None] = None, 32 | output_format: str = None, 33 | ) -> Any: 34 | """ 35 | Compile a Vyper contract. 36 | 37 | Compilation is handled via the `--combined-json` flag. Depending on the vyper 38 | version used, some keyword arguments may not be available. 39 | 40 | Arguments 41 | --------- 42 | source: str 43 | Vyper contract to be compiled. 44 | base_path : Path | str, optional 45 | Use the given path as the root of the source tree instead of the root 46 | of the filesystem. 47 | evm_version: str, optional 48 | Select the desired EVM version. Valid options depend on the `vyper` version. 49 | vyper_binary : str | Path, optional 50 | Path of the `vyper` binary to use. If not given, the currently active 51 | version is used (as set by `vvm.set_vyper_version`) 52 | vyper_version: Version, optional 53 | `vyper` version to use. If not given, the currently active version is used. 54 | Ignored if `vyper_binary` is also given. 55 | output_format: str, optional 56 | Output format of the compiler. See `vyper --help` for more information. 57 | 58 | Returns 59 | ------- 60 | Any 61 | Compiler output (depends on `output_format`). 62 | For JSON output the return type is a dictionary, otherwise it is a string. 63 | """ 64 | 65 | with tempfile.NamedTemporaryFile(suffix=".vy", prefix="vyper-") as source_file: 66 | source_file.write(source.encode()) 67 | source_file.flush() 68 | 69 | compiler_data = _compile( 70 | vyper_binary=vyper_binary, 71 | vyper_version=vyper_version, 72 | source_files=[source_file.name], 73 | base_path=base_path, 74 | evm_version=evm_version, 75 | output_format=output_format, 76 | ) 77 | 78 | if output_format in ("combined_json", None): 79 | # Vyper 0.4.0 and up puts version at the front of the dict, which breaks 80 | # the `list(compiler_data.values())[0]` on the next line, so remove it. 81 | # Assumes the source file is not named `version` (without extension) 82 | compiler_data.pop("version", None) 83 | return {"": list(compiler_data.values())[0]} 84 | return compiler_data 85 | 86 | 87 | def compile_files( 88 | source_files: Union[List, Path, str], 89 | base_path: Optional[Union[Path, str]] = None, 90 | evm_version: str = None, 91 | vyper_binary: Union[str, Path] = None, 92 | vyper_version: Union[str, Version, None] = None, 93 | output_format: str = None, 94 | search_paths: Optional[List[Union[Path, str]]] = None, 95 | ) -> Any: 96 | """ 97 | Compile one or more Vyper source files. 98 | 99 | Compilation is handled via the `--combined-json` flag. Depending on the vyper 100 | version used, some keyword arguments may not be available. 101 | 102 | Arguments 103 | --------- 104 | source_files: List 105 | Path or list of paths of Vyper source files to be compiled. 106 | base_path : Path | str, optional 107 | Use the given path as the root of the source tree instead of the root 108 | of the filesystem. 109 | evm_version: str, optional 110 | Select the desired EVM version. Valid options depend on the `vyper` version. 111 | vyper_binary : str | Path, optional 112 | Path of the `vyper` binary to use. If not given, the currently active 113 | version is used (as set by `vvm.set_vyper_version`) 114 | vyper_version: Version, optional 115 | `vyper` version to use. If not given, the currently active version is used. 116 | Ignored if `vyper_binary` is also given. 117 | output_format: str, optional 118 | Output format of the compiler. See `vyper --help` for more information. 119 | search_paths: List[str | Path], optional 120 | Additional search paths. Only applicable for Vyper 0.4. Cannot use with 121 | `base_path` argument. 122 | 123 | Returns 124 | ------- 125 | Any 126 | Compiler output (depends on `output_format`). 127 | For JSON output the return type is a dictionary, otherwise it is a string. 128 | """ 129 | return _compile( 130 | vyper_binary=vyper_binary, 131 | vyper_version=vyper_version, 132 | source_files=source_files, 133 | base_path=base_path, 134 | evm_version=evm_version, 135 | output_format=output_format, 136 | search_paths=search_paths, 137 | ) 138 | 139 | 140 | def _compile( 141 | base_path: Union[str, Path, None], 142 | vyper_binary: Union[str, Path, None], 143 | vyper_version: Union[str, Version, None], 144 | output_format: Optional[str], 145 | search_paths: Optional[List[Union[Path, str]]] = None, 146 | **kwargs: Any, 147 | ) -> Any: 148 | if vyper_binary is None: 149 | vyper_binary = get_executable(vyper_version) 150 | if output_format is None: 151 | output_format = "combined_json" 152 | 153 | if base_path is not None and search_paths is not None: 154 | raise ValueError("Cannot specify both 'base_path' and 'search_paths'.") 155 | 156 | paths = search_paths if base_path is None else [base_path] 157 | stdoutdata, stderrdata, command, proc = wrapper.vyper_wrapper( 158 | vyper_binary=vyper_binary, f=output_format, paths=paths, **kwargs 159 | ) 160 | 161 | if output_format in ("combined_json", "standard_json", "metadata"): 162 | return json.loads(stdoutdata) 163 | return stdoutdata 164 | 165 | 166 | def compile_standard( 167 | input_data: Dict, 168 | base_path: str = None, 169 | vyper_binary: Union[str, Path] = None, 170 | vyper_version: Version = None, 171 | ) -> Dict: 172 | """ 173 | Compile Vyper contracts using the JSON-input-output interface. 174 | 175 | See the Vyper documentation for details on the expected JSON input and output formats. 176 | 177 | Arguments 178 | --------- 179 | input_data : Dict 180 | Compiler JSON input. 181 | base_path : Path | str, optional 182 | Use the given path as the root of the source tree instead of the root 183 | of the filesystem. 184 | vyper_binary : str | Path, optional 185 | Path of the `vyper` binary to use. If not given, the currently active 186 | version is used (as set by `vvm.set_vyper_version`) 187 | vyper_version: Version, optional 188 | `vyper` version to use. If not given, the currently active version is used. 189 | Ignored if `vyper_binary` is also given. 190 | 191 | Returns 192 | ------- 193 | Dict 194 | Compiler JSON output. 195 | """ 196 | 197 | if vyper_binary is None: 198 | vyper_binary = get_executable(vyper_version) 199 | 200 | stdoutdata, stderrdata, command, proc = wrapper.vyper_wrapper( 201 | vyper_binary=vyper_binary, stdin=json.dumps(input_data), standard_json=True, p=base_path 202 | ) 203 | 204 | compiler_output = json.loads(stdoutdata) 205 | if "errors" in compiler_output: 206 | has_errors = any(error["severity"] == "error" for error in compiler_output["errors"]) 207 | if has_errors: 208 | error_message = "\n".join( 209 | tuple( 210 | error.get("formattedMessage") or error["message"] 211 | for error in compiler_output["errors"] 212 | if error["severity"] == "error" 213 | ) 214 | ) 215 | raise VyperError( 216 | error_message, 217 | command=command, 218 | return_code=proc.returncode, 219 | stdin_data=json.dumps(input_data), 220 | stdout_data=stdoutdata, 221 | stderr_data=stderrdata, 222 | error_dict=compiler_output["errors"], 223 | ) 224 | return compiler_output 225 | -------------------------------------------------------------------------------- /vvm/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vyperlang/vvm/1d28c4a72f3c8c47c449f3c0dcc70fd9b6f4bf09/vvm/py.typed -------------------------------------------------------------------------------- /vvm/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vyperlang/vvm/1d28c4a72f3c8c47c449f3c0dcc70fd9b6f4bf09/vvm/utils/__init__.py -------------------------------------------------------------------------------- /vvm/utils/convert.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from packaging.version import Version 4 | 5 | 6 | def to_vyper_version(version: Union[str, Version]) -> Version: 7 | if not isinstance(version, Version): 8 | version = Version(version) 9 | 10 | return version 11 | -------------------------------------------------------------------------------- /vvm/utils/lock.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import tempfile 4 | import threading 5 | from pathlib import Path 6 | from typing import Any, Dict, Union 7 | 8 | if sys.platform == "win32": 9 | import msvcrt 10 | 11 | OPEN_MODE = os.O_RDWR | os.O_CREAT | os.O_TRUNC 12 | else: 13 | import fcntl 14 | 15 | NON_BLOCKING = fcntl.LOCK_EX | fcntl.LOCK_NB 16 | BLOCKING = fcntl.LOCK_EX 17 | 18 | _locks: Dict[str, Union["UnixLock", "WindowsLock"]] = {} 19 | _base_lock = threading.Lock() 20 | 21 | 22 | def get_process_lock(lock_id: str) -> Union["UnixLock", "WindowsLock"]: 23 | with _base_lock: 24 | if lock_id not in _locks: 25 | if sys.platform == "win32": 26 | _locks[lock_id] = WindowsLock(lock_id) 27 | else: 28 | _locks[lock_id] = UnixLock(lock_id) 29 | return _locks[lock_id] 30 | 31 | 32 | class _ProcessLock: 33 | """ 34 | Ensure an action is both thread-safe and process-safe. 35 | """ 36 | 37 | def __init__(self, lock_id: str) -> None: 38 | self._lock = threading.Lock() 39 | self._lock_path = Path(tempfile.gettempdir()).joinpath(f".vvm-lock-{lock_id}") 40 | self._lock_file = self._lock_path.open("w") 41 | 42 | 43 | class UnixLock(_ProcessLock): 44 | def __enter__(self) -> None: 45 | self.acquire(True) 46 | 47 | def __exit__(self, *args: Any) -> None: 48 | self.release() 49 | 50 | def acquire(self, blocking: bool) -> bool: 51 | if not self._lock.acquire(blocking): 52 | return False 53 | try: 54 | fcntl.flock(self._lock_file, BLOCKING if blocking else NON_BLOCKING) 55 | except BlockingIOError: 56 | self._lock.release() 57 | return False 58 | return True 59 | 60 | def release(self) -> None: 61 | fcntl.flock(self._lock_file, fcntl.LOCK_UN) 62 | self._lock.release() 63 | 64 | 65 | class WindowsLock(_ProcessLock): 66 | def __enter__(self) -> None: 67 | self.acquire(True) 68 | 69 | def __exit__(self, *args: Any) -> None: 70 | self.release() 71 | 72 | def acquire(self, blocking: bool) -> bool: 73 | if not self._lock.acquire(blocking): 74 | return False 75 | while True: 76 | try: 77 | fd = os.open(self._lock_path, OPEN_MODE) # type: ignore 78 | msvcrt.locking( # type: ignore 79 | fd, msvcrt.LK_LOCK if blocking else msvcrt.LK_NBLCK, 1 # type: ignore 80 | ) 81 | self._fd = fd 82 | return True 83 | except OSError: 84 | if not blocking: 85 | self._lock.release() 86 | return False 87 | 88 | def release(self) -> None: 89 | msvcrt.locking(self._fd, msvcrt.LK_UNLCK, 1) # type: ignore 90 | self._lock.release() 91 | -------------------------------------------------------------------------------- /vvm/utils/versioning.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import re 3 | from typing import Any, Optional 4 | 5 | from packaging.specifiers import SpecifierSet 6 | from packaging.version import Version 7 | 8 | from vvm.exceptions import UnexpectedVersionError 9 | from vvm.install import get_installable_vyper_versions, get_installed_vyper_versions 10 | 11 | # Find the first occurence of version specifier in the source code. 12 | # allow for indented comment (as the compiler allows it (as of 0.4.0)). 13 | # might have false positive if a triple quoted string contains a line 14 | # that looks like a version specifier and is before the actual version 15 | # specifier in the code, but this is accepted as it is an unlikely edge case. 16 | _VERSION_RE = re.compile(r"^\s*(?:#\s*(?:@version|pragma\s+version)\s+(.*))", re.MULTILINE) 17 | 18 | 19 | def detect_version_specifier_set(source_code: str) -> Optional[SpecifierSet]: 20 | """ 21 | Detect the specifier set given by the pragma version in the source code. 22 | 23 | Arguments 24 | --------- 25 | source_code : str 26 | Source code to detect the specifier set from. 27 | 28 | Returns 29 | ------- 30 | Optional[SpecifierSet] 31 | vyper version specifier set, or None if none could be detected. 32 | """ 33 | match = _VERSION_RE.search(source_code) 34 | if match is None: 35 | return None 36 | 37 | version_str = match.group(1) 38 | 39 | # X.Y.Z or vX.Y.Z => ==X.Y.Z, ==vX.Y.Z 40 | if re.match("[v0-9]", version_str): 41 | version_str = "==" + version_str 42 | # adapted from vyper/ast/pre_parse.py at commit c32b9b4c6f0d8 43 | # partially convert npm to pep440 44 | # - <0.4.0 contracts with complex npm version range might fail 45 | # - in versions >=1.0.0, the below conversion will be invalid 46 | version_str = re.sub("^\\^", "~=", version_str) 47 | 48 | return SpecifierSet(version_str) 49 | 50 | 51 | def _pick_vyper_version( 52 | specifier_set: SpecifierSet, 53 | prereleases: Optional[bool] = None, 54 | check_installed: bool = True, 55 | check_installable: bool = True, 56 | ) -> Version: 57 | """ 58 | Pick the latest vyper version that is installed and satisfies the given specifier set. 59 | If None of the installed versions satisfy the specifier set, pick the latest installable 60 | version. 61 | 62 | Arguments 63 | --------- 64 | specifier_set : SpecifierSet 65 | Specifier set to pick a version for. 66 | prereleases : bool, optional 67 | Whether to allow prereleases in the returned iterator. If set to 68 | ``None`` (the default), it will be intelligently decide whether to allow 69 | prereleases or not (based on the specifier.prereleases attribute, and 70 | whether the only versions matching are prereleases). 71 | check_installed : bool, optional 72 | Whether to check the installed versions. Defaults to True. 73 | check_installable : bool, optional 74 | Whether to check the installable versions. Defaults to True. 75 | 76 | Returns 77 | ------- 78 | Version 79 | Vyper version that satisfies the specifier set, or None if no version satisfies the set. 80 | """ 81 | versions = itertools.chain( 82 | get_installed_vyper_versions() if check_installed else [], 83 | get_installable_vyper_versions() if check_installable else [], 84 | ) 85 | if (ret := next(specifier_set.filter(versions, prereleases), None)) is None: 86 | raise UnexpectedVersionError( 87 | f"No installable Vyper satisfies the specifier {specifier_set}" 88 | ) 89 | return ret 90 | 91 | 92 | def detect_vyper_version_from_source(source_code: str, **kwargs: Any) -> Optional[Version]: 93 | """ 94 | Detect the version given by the pragma version in the source code. 95 | 96 | Arguments 97 | --------- 98 | source_code : str 99 | Source code to detect the version from. 100 | kwargs : Any 101 | Keyword arguments to pass to `pick_vyper_version`. 102 | 103 | Returns 104 | ------- 105 | Optional[Version] 106 | vyper version, or None if no version could be detected. 107 | """ 108 | specifier_set = detect_version_specifier_set(source_code) 109 | if specifier_set is None: 110 | return None 111 | return _pick_vyper_version(specifier_set, **kwargs) 112 | -------------------------------------------------------------------------------- /vvm/wrapper.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from pathlib import Path 3 | from typing import Any, Dict, List, Optional, Tuple, Union 4 | 5 | from packaging.version import Version 6 | 7 | from vvm import install 8 | from vvm.exceptions import UnknownOption, UnknownValue, VyperError 9 | from vvm.utils.convert import to_vyper_version 10 | 11 | _version_cache: Dict[str, Version] = {} 12 | 13 | 14 | def _get_vyper_version(vyper_binary: Union[Path, str]) -> Version: 15 | # private wrapper function to get `vyper` version 16 | cache_key = str(vyper_binary) 17 | 18 | if cache_key not in _version_cache: 19 | # cache the version info, because vyper binaries can be slow to load 20 | stdout_data = subprocess.check_output([vyper_binary, "--version"], encoding="utf8") 21 | version_str = stdout_data.split("+")[0] 22 | _version_cache[cache_key] = to_vyper_version(version_str) 23 | 24 | return _version_cache[cache_key] 25 | 26 | 27 | def _to_string(key: str, value: Any) -> str: 28 | if isinstance(value, (int, str)): 29 | return str(value) 30 | elif isinstance(value, Path): 31 | return value.as_posix() 32 | elif isinstance(value, (list, tuple)): 33 | return ",".join(_to_string(key, i) for i in value) 34 | else: 35 | raise TypeError(f"Invalid type for {key}: {type(value)}") 36 | 37 | 38 | def vyper_wrapper( 39 | vyper_binary: Union[Path, str] = None, 40 | stdin: str = None, 41 | source_files: Union[List, Path, str] = None, 42 | success_return_code: int = 0, 43 | paths: Optional[List[Union[Path, str]]] = None, 44 | **kwargs: Any, 45 | ) -> Tuple[str, str, List, subprocess.Popen]: 46 | """ 47 | Wrapper function for calling to `vyper`. 48 | 49 | Arguments 50 | --------- 51 | vyper_binary : Path | str, optional 52 | Location of the `vyper` binary. If not given, the current default binary is used. 53 | stdin : str, optional 54 | Input to pass to `vyper` via stdin 55 | source_files : list, optional 56 | Path or list of paths of source files to compile 57 | success_return_code : int, optional 58 | Expected exit code. Raises `VyperError` if the process returns a different value. 59 | 60 | Keyword Arguments 61 | ----------------- 62 | **kwargs : Any 63 | Flags to be passed to `vyper`. Keywords are converted to flags by prepending `--` and 64 | replacing `_` with `-`, for example the keyword `evm_version` becomes `--evm-version`. 65 | Values may be given in the following formats: 66 | 67 | * `False`, `None`: ignored 68 | * `True`: flag is used without any arguments 69 | * str: given as an argument without modification 70 | * int: given as an argument, converted to a string 71 | * Path: converted to a string via `Path.as_posix()` 72 | * List, Tuple: elements are converted to strings and joined with `,` 73 | 74 | Returns 75 | ------- 76 | str 77 | Process `stdout` output 78 | str 79 | Process `stderr` output 80 | List 81 | Full command executed by the function 82 | Popen 83 | Subprocess object used to call `vyper` 84 | """ 85 | if vyper_binary: 86 | vyper_binary = Path(vyper_binary) 87 | else: 88 | vyper_binary = install.get_executable() 89 | 90 | version = _get_vyper_version(vyper_binary) 91 | command: List = [vyper_binary] 92 | 93 | if source_files is not None: 94 | if isinstance(source_files, (str, Path)): 95 | command.append(_to_string("source_files", source_files)) 96 | else: 97 | command.extend([_to_string("source_files", i) for i in source_files]) 98 | 99 | if paths is not None: 100 | for path in paths: 101 | command.extend(("-p", f"{path}")) 102 | 103 | for key, value in kwargs.items(): 104 | if value is None or value is False: 105 | continue 106 | 107 | if len(key) == 1: 108 | key = f"-{key}" 109 | else: 110 | key = f"--{key.replace('_', '-')}" 111 | if value is True: 112 | command.append(key) 113 | else: 114 | command.extend([key, _to_string(key, value)]) 115 | 116 | if stdin is not None: 117 | stdin = str(stdin) 118 | 119 | proc = subprocess.Popen( 120 | command, 121 | stdin=subprocess.PIPE, 122 | stdout=subprocess.PIPE, 123 | stderr=subprocess.PIPE, 124 | encoding="utf8", 125 | ) 126 | 127 | stdoutdata, stderrdata = proc.communicate(stdin) 128 | 129 | if proc.returncode != success_return_code: 130 | if stderrdata.startswith("unrecognised option"): 131 | # unrecognised option '' 132 | flag = stderrdata.split("'")[1] 133 | raise UnknownOption(f"Vyper {version} does not support the '{flag}' option'") 134 | if stderrdata.startswith("Invalid option"): 135 | # Invalid option to :