Repository: sio/Makefile.venv Branch: master Commit: 6268cf2a1594 Files: 43 Total size: 80.5 KB Directory structure: gitextract_rrqsq90u/ ├── .gitattributes ├── .github/ │ └── workflows/ │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile.venv ├── README.md ├── docs/ │ ├── README.md │ ├── howto-custom-venv-recipe.md │ ├── howto-pip-compile.md │ ├── howto-pipenv.md │ └── howto-poetry.md ├── pypi/ │ ├── Makefile │ ├── README.md │ ├── pyproject.toml │ ├── setup.cfg │ ├── src/ │ │ └── Makefile_venv/ │ │ └── __init__.py │ ├── tests/ │ │ └── test_installable.py │ └── tox.ini └── tests/ ├── Makefile ├── README.md ├── __init__.py ├── common.py ├── data/ │ ├── dependencies.mk │ ├── hello.py │ ├── pip-compile.mk │ ├── pyproject.toml │ ├── recipe-override.mk │ ├── requirements-extra.txt │ ├── requirements.in │ ├── requirements.txt │ └── setup.py ├── test_dependencies.py ├── test_makefile.py ├── test_pip_compile.py ├── test_py_autodetect.py ├── test_pyproject_toml.py ├── test_recipe_override.py ├── test_release_checksum.py ├── test_release_version.py ├── test_requirements_txt.py ├── test_setup_py.py └── test_spaces.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ *.venv linguist-language=Makefile ================================================ FILE: .github/workflows/test.yml ================================================ name: Run automated tests on: push: paths-ignore: - '**.md' - '.git*' pull_request: schedule: - cron: '17 7 9,19,29 * *' jobs: linux: name: linux-py${{ matrix.py }} runs-on: ubuntu-latest container: python:${{ matrix.py }} strategy: matrix: py: - '3.9' - '3.10' - '3.11' - '3.12' steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: CI setup run: |- git config --global --add safe.directory $(pwd) - name: Run functional tests run: make -C tests test-verbose env: TEST_SUBPROCESS_TIMEOUT: 300 - name: Run packaging tests run: make -C pypi test other: name: ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: os: - windows-latest - macos-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Remove py.exe on Windows run: del /f %WINDIR%\py.exe shell: cmd if: matrix.os == 'windows-latest' - name: Configure Windows environment variables run: | echo "PY=python" >> $GITHUB_ENV echo "TEMP=C:\tmp" >> $GITHUB_ENV # remove when pip>22.0.3 is released mkdir -p "C:\tmp" # see https://github.com/sio/Makefile.venv/issues/17 shell: bash if: matrix.os == 'windows-latest' - name: Run functional tests run: make -C tests test-verbose env: TEST_SUBPROCESS_TIMEOUT: 300 - name: Run packaging tests run: make -C pypi test cygwin: name: windows-cygwin runs-on: windows-latest env: CYGWIN_ROOT: D:\cygwin CYGWIN_PACKAGES: "\ make,\ python3,\ git,\ bash,\ python-pip-wheel,\ python-setuptools-wheel,\ python-wheel-wheel" CYGWIN_MIRROR: http://mirrors.kernel.org/sourceware/cygwin/ LC_ALL: C.UTF-8 LANG: C.UTF-8 SHELLOPTS: igncr steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Install Cygwin run: > md %CYGWIN_ROOT% && choco install cygwin --params='"/InstallDir:%CYGWIN_ROOT%"' && %CYGWIN_ROOT%\cygwinsetup.exe --quiet-mode --no-desktop --download --local-install --no-verify --site "%CYGWIN_MIRROR%" --local-package-dir "%CYGWIN_ROOT%" --root "%CYGWIN_ROOT%" --packages "%CYGWIN_PACKAGES%" shell: cmd - name: Workaround for repo permissions mixup (https://stackoverflow.com/questions/7184941) shell: D:\cygwin\bin\bash.exe "{0}" run: | set -v export PATH=/bin set -euo pipefail chown $(id -u):$(id -g) . mkdir -p "$HOME" touch "$HOME/.gitconfig" git config --global --add safe.directory "$PWD" - name: Run automated tests shell: D:\cygwin\bin\bash.exe "{0}" run: | export PATH=/bin make -C tests test-verbose env: TEST_SUBPROCESS_TIMEOUT: 300 - name: Failure diagnostics run: | find "$CYGWIN_ROOT" -type f env shell: bash if: failure() ================================================ FILE: .gitignore ================================================ # Used for testing "include Makefile.venv" /Makefile # Python tests *.pyc # Packaging *.egg-info .venv/ .tox/ /pypi/src/Makefile_venv/LICENSE /pypi/src/Makefile_venv/Makefile.venv /pypi/src/Makefile_venv/README.md /pypi/dist/ ================================================ FILE: CHANGELOG.md ================================================ # Changelog for Makefile.venv ## v2023.04.17 * Added support for [pyproject.toml] (thanks to [@sla-te], issue [#22]) [pyproject.toml]: https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/ [@sla-te]: https://github.com/sla-te [#22]: https://github.com/sio/Makefile.venv/issues/22 [Source code tree](https://github.com/sio/Makefile.venv/tree/v2023.04.17) | [Tarball](https://github.com/sio/Makefile.venv/tarball/v2023.04.17) | [Commit history](https://github.com/sio/Makefile.venv/compare/v2022.07.20...v2023.04.17) ## v2022.07.20 * Support another edge case: "make -R" (running without builtin variables) * Minor documentation improvements [Source code tree](https://github.com/sio/Makefile.venv/tree/v2022.07.20) | [Tarball](https://github.com/sio/Makefile.venv/tarball/v2022.07.20) | [Commit history](https://github.com/sio/Makefile.venv/compare/v2022.04.13...v2022.07.20) ## v2022.04.13 * Makefile.venv is now installable from [PyPI](https://pypi.org/project/Makefile.venv/) * No changes to Makefile.venv itself [Source code tree](https://github.com/sio/Makefile.venv/tree/v2022.04.13) | [Tarball](https://github.com/sio/Makefile.venv/tarball/v2022.04.13) | [Commit history](https://github.com/sio/Makefile.venv/compare/v2021.12.16...v2022.04.13) ## v2021.12.16 * Added support for `py` entrypoint on Windows (thanks to [@gschwaer], issue [#15]) * Improved cross-platform support: now Windows without a POSIX shell should work too (wrappers for `cmd.exe` missing capabilities were added) * Minor improvements to debug messages and test suite common tools [@gschwaer]: https://github.com/gschwaer [#15]: https://github.com/sio/Makefile.venv/issues/15 [Source code tree](https://github.com/sio/Makefile.venv/tree/v2021.12.16) | [Tarball](https://github.com/sio/Makefile.venv/tarball/v2021.12.16) | [Commit history](https://github.com/sio/Makefile.venv/compare/v2021.12.01...v2021.12.16) ## v2021.12.01 * Move setup․py path to variable (SETUP_PY): now multiple paths are supported and setup․py processing may be skipped by providing empty value (issue [#14]) * Improve documentation and extend test suite [#14]: https://github.com/sio/Makefile.venv/issues/14 [Source code tree](https://github.com/sio/Makefile.venv/tree/v2021.12.01) | [Tarball](https://github.com/sio/Makefile.venv/tarball/v2021.12.01) | [Commit history](https://github.com/sio/Makefile.venv/compare/v2020.08.14...v2021.12.01) ## v2020.08.14 * Install 'wheel' package automatically when creating virtual environment [Source code tree](https://github.com/sio/Makefile.venv/tree/v2020.08.14) | [Tarball](https://github.com/sio/Makefile.venv/tarball/v2020.08.14) | [Commit history](https://github.com/sio/Makefile.venv/compare/v2020.08.04...v2020.08.14) ## v2020.08.04 * Allow REQUIREMENTS_TXT to be generated with a Makefile recipe * Improve documentation [Source code tree](https://github.com/sio/Makefile.venv/tree/v2020.08.04) | [Tarball](https://github.com/sio/Makefile.venv/tarball/v2020.08.04) | [Commit history](https://github.com/sio/Makefile.venv/compare/v2020.05.07...v2020.08.04) ## v2020.05.07 * Add `debug-venv` target for troubleshooting [Source code tree](https://github.com/sio/Makefile.venv/tree/v2020.05.07) | [Tarball](https://github.com/sio/Makefile.venv/tarball/v2020.05.07) | [Commit history](https://github.com/sio/Makefile.venv/compare/v2020.05.05...v2020.05.07) ## v2020.05.05 * Mark paths with spaces as unsupported * Do not convert WORKDIR into absolute path * Sanitize path when removing VENVDIR - avoid destructive consequences of paths with spaces [Source code tree](https://github.com/sio/Makefile.venv/tree/v2020.05.05) | [Tarball](https://github.com/sio/Makefile.venv/tarball/v2020.05.05) | [Commit history](https://github.com/sio/Makefile.venv/compare/v2020.02.26...v2020.05.05) ## v2020.02.26 * Update setuptools when creating venv * Trigger venv update on setup.cfg changes [Source code tree](https://github.com/sio/Makefile.venv/tree/v2020.02.26) | [Tarball](https://github.com/sio/Makefile.venv/tarball/v2020.02.26) | [Commit history](https://github.com/sio/Makefile.venv/compare/v2019.12.05...v2020.02.26) ## v2019.12.05 * Use Python to detect if Windows paths are required. This helps to avoid mistakes when using different combinations of Cygwin/native Windows environments. Thanks to [@jpc4242](https://github.com/jpc4242) [Source code tree](https://github.com/sio/Makefile.venv/tree/v2019.12.05) | [Tarball](https://github.com/sio/Makefile.venv/tarball/v2019.12.05) | [Commit history](https://github.com/sio/Makefile.venv/compare/v2019.12.04...v2019.12.05) ## v2019.12.04 * New configuration variable: FORCE_UNIX_PATHS. If this variable is set, unix-like file paths are assumed and no Windows detection takes place. Thanks to [@jpc4242](https://github.com/jpc4242) for reporting [the issue](https://github.com/sio/Makefile.venv/issues/2) with Cygwin. [Source code tree](https://github.com/sio/Makefile.venv/tree/v2019.12.04) | [Tarball](https://github.com/sio/Makefile.venv/tarball/v2019.12.04) | [Commit history](https://github.com/sio/Makefile.venv/compare/v2019.11.22...v2019.12.04) ## v2019.11.22 * Upgrade pip only at initial environment creation. This helps to avoid build failures with old Python versions where pip can not be upgraded to newer releases. [Example](https://circleci.com/gh/sio/bash-complete-partial-path/53) [Source code tree](https://github.com/sio/Makefile.venv/tree/v2019.11.22) | [Tarball](https://github.com/sio/Makefile.venv/tarball/v2019.11.22) | [Commit history](https://github.com/sio/Makefile.venv/compare/v2019.11.08...v2019.11.22) ## v2019.11.08 * Support multiple requirements.txt files via REQUIREMENTS_TXT environment variable [Source code tree](https://github.com/sio/Makefile.venv/tree/v2019.11.08) | [Tarball](https://github.com/sio/Makefile.venv/tarball/v2019.11.08) | [Commit history](https://github.com/sio/Makefile.venv/compare/v2019.11.07...v2019.11.08) ## v2019.11.07 * Virtual environment creation happens only once. Dependencies change does not trigger a redundant call to `-m venv` if environment already exists. [Source code tree](https://github.com/sio/Makefile.venv/tree/v2019.11.07) | [Tarball](https://github.com/sio/Makefile.venv/tarball/v2019.11.07) | [Commit history](https://github.com/sio/Makefile.venv/compare/v2019.11.06...v2019.11.07) ## v2019.11.06 * New pattern rule for rarely used dependencies (CLI tools in virtual environment) * Improved code readability and documentation [Source code tree](https://github.com/sio/Makefile.venv/tree/v2019.11.06) | [Tarball](https://github.com/sio/Makefile.venv/tarball/v2019.11.06) | [Commit history](https://github.com/sio/Makefile.venv/compare/v2019.10.04...v2019.11.06) ## v2019.10.04 * Automated testing for new releases with GitHub CI * Deduplicated code for interactive shell targets [Source code tree](https://github.com/sio/Makefile.venv/tree/v2019.10.04) | [Tarball](https://github.com/sio/Makefile.venv/tarball/v2019.10.04) | [Commit history](https://github.com/sio/Makefile.venv/compare/v2019.10.03...v2019.10.04) ## v2019.10.03 * Cleaner process tree thanks to launching interactive shells via `exec` [Source code tree](https://github.com/sio/Makefile.venv/tree/v2019.10.03) | [Tarball](https://github.com/sio/Makefile.venv/tarball/v2019.10.03) | [Commit history](https://github.com/sio/Makefile.venv/compare/v2019.10.01...v2019.10.03) ## v2019.10.01 * New targets for interactive shells in virtual environment: `make bash`, `make zsh` * Promotional post in author's blog: [https://potyarkin.com/...][blog] [blog]: https://potyarkin.com/posts/2019/manage-python-virtual-environment-from-your-makefile/ [Source code tree](https://github.com/sio/Makefile.venv/tree/v2019.10.01) | [Tarball](https://github.com/sio/Makefile.venv/tarball/v2019.10.01) | [Commit history](https://github.com/sio/Makefile.venv/compare/v2019.09.30...v2019.10.01) ## v2019.09.30 * First reusable version of Makefile.venv. All essential features are available. [Source code tree](https://github.com/sio/Makefile.venv/tree/v2019.09.30) | [Tarball](https://github.com/sio/Makefile.venv/tarball/v2019.09.30) | [Commit history](https://github.com/sio/Makefile.venv/compare/9c9b6d5aae8955d207d5c9d45b754c01c20be650...v2019.09.30) ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile.venv ================================================ # # SEAMLESSLY MANAGE PYTHON VIRTUAL ENVIRONMENT WITH A MAKEFILE # # https://github.com/sio/Makefile.venv v2023.04.17 # # # Insert `include Makefile.venv` at the bottom of your Makefile to enable these # rules. # # When writing your Makefile use '$(VENV)/python' to refer to the Python # interpreter within virtual environment and '$(VENV)/executablename' for any # other executable in venv. # # This Makefile provides the following targets: # venv # Use this as a dependency for any target that requires virtual # environment to be created and configured # python, ipython # Use these to launch interactive Python shell within virtual environment # shell, bash, zsh # Launch interactive command line shell. "shell" target launches the # default shell Makefile executes its rules in (usually /bin/sh). # "bash" and "zsh" can be used to refer to the specific desired shell. # show-venv # Show versions of Python and pip, and the path to the virtual environment # clean-venv # Remove virtual environment # $(VENV)/executable_name # Install `executable_name` with pip. Only packages with names matching # the name of the corresponding executable are supported. # Use this as a lightweight mechanism for development dependencies # tracking. E.g. for one-off tools that are not required in every # developer's environment, therefore are not included into # requirements.txt or setup.py. # Note: # Rules using such target or dependency MUST be defined below # `include` directive to make use of correct $(VENV) value. # Example: # codestyle: $(VENV)/pyflakes # $(VENV)/pyflakes . # See `ipython` target below for another example. # # This Makefile can be configured via following variables: # PY # Command name for system Python interpreter. It is used only initially to # create the virtual environment # Default: python3 # REQUIREMENTS_TXT # Space separated list of paths to requirements.txt files. # Paths are resolved relative to current working directory. # Default: requirements.txt # # Non-existent files are treated as hard dependencies, # recipes for creating such files must be provided by the main Makefile. # Providing empty value (REQUIREMENTS_TXT=) turns off processing of # requirements.txt even when the file exists. # SETUP_PY, SETUP_CFG, PYPROJECT_TOML, VENV_LOCAL_PACKAGE # Space separated list of paths to files that contain build instructions # for local Python packages. Corresponding packages will be installed # into venv in editable mode along with all their dependencies. # Default: setup.py setup.cfg pyproject.toml (whichever present) # # Non-existent and empty values are treated in the same way as for REQUIREMENTS_TXT. # WORKDIR # Parent directory for the virtual environment. # Default: current working directory. # VENVDIR # Python virtual environment directory. # Default: $(WORKDIR)/.venv # # This Makefile was written for GNU Make and may not work with other make # implementations. # # # Copyright (c) 2019-2023 Vitaly Potyarkin # # Licensed under the Apache License, Version 2.0 # # # # Configuration variables # WORKDIR?=. VENVDIR?=$(WORKDIR)/.venv REQUIREMENTS_TXT?=$(wildcard requirements.txt) # Multiple paths are supported (space separated) SETUP_PY?=$(wildcard setup.py) # Multiple paths are supported (space separated) SETUP_CFG?=$(foreach s,$(SETUP_PY),$(wildcard $(patsubst %setup.py,%setup.cfg,$(s)))) PYPROJECT_TOML?=$(wildcard pyproject.toml) VENV_LOCAL_PACKAGE?=$(SETUP_PY) $(SETUP_CFG) $(PYPROJECT_TOML) MARKER=.initialized-with-Makefile.venv # # Python interpreter detection # _PY_AUTODETECT_MSG=Detected Python interpreter: $(PY). Use PY environment variable to override ifeq (ok,$(shell test -e /dev/null 2>&1 && echo ok)) NULL_STDERR=2>/dev/null else NULL_STDERR=2>NUL endif ifndef PY _PY_OPTION:=python3 ifeq (ok,$(shell $(_PY_OPTION) -c "print('ok')" $(NULL_STDERR))) PY=$(_PY_OPTION) endif endif ifndef PY _PY_OPTION:=$(VENVDIR)/bin/python ifeq (ok,$(shell $(_PY_OPTION) -c "print('ok')" $(NULL_STDERR))) PY=$(_PY_OPTION) $(info $(_PY_AUTODETECT_MSG)) endif endif ifndef PY _PY_OPTION:=$(subst /,\,$(VENVDIR)/Scripts/python) ifeq (ok,$(shell $(_PY_OPTION) -c "print('ok')" $(NULL_STDERR))) PY=$(_PY_OPTION) $(info $(_PY_AUTODETECT_MSG)) endif endif ifndef PY _PY_OPTION:=py -3 ifeq (ok,$(shell $(_PY_OPTION) -c "print('ok')" $(NULL_STDERR))) PY=$(_PY_OPTION) $(info $(_PY_AUTODETECT_MSG)) endif endif ifndef PY _PY_OPTION:=python ifeq (ok,$(shell $(_PY_OPTION) -c "print('ok')" $(NULL_STDERR))) PY=$(_PY_OPTION) $(info $(_PY_AUTODETECT_MSG)) endif endif ifndef PY define _PY_AUTODETECT_ERR Could not detect Python interpreter automatically. Please specify path to interpreter via PY environment variable. endef $(error $(_PY_AUTODETECT_ERR)) endif # # Internal variable resolution # VENV=$(VENVDIR)/bin EXE= # Detect windows ifeq (win32,$(shell $(PY) -c "import __future__, sys; print(sys.platform)")) VENV=$(VENVDIR)/Scripts EXE=.exe endif touch=touch $(1) ifeq (,$(shell command -v touch $(NULL_STDERR))) # https://ss64.com/nt/touch.html touch=type nul >> $(subst /,\,$(1)) && copy /y /b $(subst /,\,$(1))+,, $(subst /,\,$(1)) endif RM?=rm -f ifeq (,$(shell command -v $(firstword $(RM)) $(NULL_STDERR))) RMDIR:=rd /s /q else RMDIR:=$(RM) -r endif # # Virtual environment # .PHONY: venv venv: $(VENV)/$(MARKER) .PHONY: clean-venv clean-venv: -$(RMDIR) "$(VENVDIR)" .PHONY: show-venv show-venv: venv @$(VENV)/python -c "import sys; print('Python ' + sys.version.replace('\n',''))" @$(VENV)/pip --version @echo venv: $(VENVDIR) .PHONY: debug-venv debug-venv: @echo "PATH (Shell)=$$PATH" @$(MAKE) --version $(info PATH (GNU Make)="$(PATH)") $(info SHELL="$(SHELL)") $(info PY="$(PY)") $(info REQUIREMENTS_TXT="$(REQUIREMENTS_TXT)") $(info VENV_LOCAL_PACKAGE="$(VENV_LOCAL_PACKAGE)") $(info VENVDIR="$(VENVDIR)") $(info VENVDEPENDS="$(VENVDEPENDS)") $(info WORKDIR="$(WORKDIR)") # # Dependencies # ifneq ($(strip $(REQUIREMENTS_TXT)),) VENVDEPENDS+=$(REQUIREMENTS_TXT) endif ifneq ($(strip $(VENV_LOCAL_PACKAGE)),) VENVDEPENDS+=$(VENV_LOCAL_PACKAGE) endif $(VENV): $(PY) -m venv $(VENVDIR) $(VENV)/python -m pip install --upgrade pip setuptools wheel $(VENV)/$(MARKER): $(VENVDEPENDS) | $(VENV) ifneq ($(strip $(REQUIREMENTS_TXT)),) $(VENV)/pip install $(foreach path,$(REQUIREMENTS_TXT),-r $(path)) endif ifneq ($(strip $(VENV_LOCAL_PACKAGE)),) $(VENV)/pip install $(foreach path,$(sort $(VENV_LOCAL_PACKAGE)),-e $(dir $(path))) endif $(call touch,$(VENV)/$(MARKER)) # # Interactive shells # .PHONY: python python: venv exec $(VENV)/python .PHONY: ipython ipython: $(VENV)/ipython exec $(VENV)/ipython .PHONY: shell shell: venv . $(VENV)/activate && exec $(notdir $(SHELL)) .PHONY: bash zsh bash zsh: venv . $(VENV)/activate && exec $@ # # Commandline tools (wildcard rule, executable name must match package name) # ifneq ($(EXE),) $(VENV)/%: $(VENV)/%$(EXE) ; .PHONY: $(VENV)/% .PRECIOUS: $(VENV)/%$(EXE) endif $(VENV)/%$(EXE): $(VENV)/$(MARKER) $(VENV)/pip install --upgrade $* $(call touch,$@) ================================================ FILE: README.md ================================================ # Seamlessly manage Python virtual environment with a Makefile *Makefile.venv* takes care of creating, updating and invoking Python virtual environment that you can use in your Makefiles. It will allow you to reduce venv related routines to almost zero! [![test status][badge]][tests] [badge]: https://github.com/sio/Makefile.venv/workflows/Run%20automated%20tests/badge.svg [tests]: https://github.com/sio/Makefile.venv/actions?query=branch%3Amaster+ *Makefile.venv* aims to be an one-stop solution for Python virtual environment management, regardless of the format used to define the venv: requirements.txt and setup․py are supported out of the box because they have become de-facto standards, but if anything else will take their place - Makefile.venv will support that too. [Pip-compile], pipenv and poetry are compatible but require some configuration. ## Installation ### Recommended method Copy [*Makefile.venv*](Makefile.venv) to your project directory and add include statement to the bottom of your `Makefile`: ```make include Makefile.venv ``` ### Alternative method Alternatively, you can add installation actions as the Makefile recipe: > **Note the checksum step!** Do not skip it, it would be as bad as [piping curl > to shell](https://0x46.net/thoughts/2019/04/27/piping-curl-to-shell/)! ```make include Makefile.venv Makefile.venv: curl \ -o Makefile.fetched \ -L "https://github.com/sio/Makefile.venv/raw/v2023.04.17/Makefile.venv" echo "fb48375ed1fd19e41e0cdcf51a4a0c6d1010dfe03b672ffc4c26a91878544f82 *Makefile.fetched" \ | sha256sum --check - \ && mv Makefile.fetched Makefile.venv ``` > Notes: > > * *curl* and/or *sha256sum* may not be available by default depending on what > OS and configuration is used > * To install *sha256sum* on macOS use `brew install coreutils` > * You can use Perl's *shasum -a 256* instead of *sha256sum*, as described > [here](https://github.com/sio/Makefile.venv/issues/11). ### Another alternative method If you want to use *Makefile.venv* in multiple projects and to be able to conveniently manage *Makefile.venv* version from one place you might find this [pip package] useful: - Install globally: `pip install Makefile.venv` or - Install for current user: `pip install --user Makefile.venv` This package will install *Makefile.venv* into your `site-packages/` and will add a command-line entrypoint which prints the full path to *Makefile.venv*. Include it it in your makefiles like this: ```make include $(shell Makefile.venv) ``` [pip package]: https://pypi.org/project/Makefile.venv/ ## Usage When writing your Makefile use `$(VENV)/python` to refer to the Python interpreter within virtual environment and `$(VENV)/executablename` for any other executable in venv. *Makefile.venv* is not meant to be used as a standalone tool, think of it more like a library that enables extra functionality. ## Demo screencast ## Targets *Makefile.venv* provides the following targets. Some are meant to be executed directly via `make $target`, some are meant to be dependencies for other targets written by you. ##### venv Use this as a dependency for any target that requires virtual environment to be created and configured. *venv* is a .PHONY target and rules that depend on it will be executed every time make is run. This behavior is sensible as default because most Python projects use Makefiles for running development chores, not for artifact building. In cases where that is not desirable use [order-only prerequisite] syntax: ```make artifacts.tar.gz: | venv ... ``` [order-only prerequisite]: https://www.gnu.org/software/make/manual/html_node/Prerequisite-Types.html ##### python, ipython Execute these targets to launch interactive Python shell within virtual environment. Ipython is not installed by default when creating the virtual environment but will be installed automatically when called for the first time. ##### shell, bash, zsh Execute these targets to launch interactive command line shell. `shell` target launches the default shell Makefile executes its rules in (usually /bin/sh). `bash` and `zsh` can be used to refer to the specific desired shell (if it's installed). ##### show-venv Execute this target to show versions of Python and pip, and the path to the virtual environment. Use this for debugging purposes. ##### clean-venv Execute this target to remove virtual environment. You can add this as a dependency to the `clean` target in your main Makefile. ##### $(VENV)/executablename Use this target as a dependency for tasks that need `executablename` to be installed if `executablename` is not listed as project's dependency neither in `requirements.txt` nor in `setup.py`. Only packages with names matching the name of the corresponding executable are supported. This can be a lightweight mechanism for development dependencies tracking. E.g. for one-off tools that are not required in every developer's environment, therefore are not included in formal dependency lists. **Note:** Rules using such dependency MUST be defined below `include` directive to make use of correct $(VENV) value. Example (see `ipython` target for another example): ```Makefile codestyle: $(VENV)/pyflakes # `venv` dependency is assumed and may be omitted $(VENV)/pyflakes . ``` ## Variables *Makefile.venv* can be configured via following variables: ##### PY Command name for system Python interpreter. It is used only initially to create the virtual environment. *Default: python3* ##### REQUIREMENTS_TXT Space separated list of paths to requirements.txt files. Paths are resolved relative to current working directory. *Default: requirements.txt* Non-existent files are treated as hard dependencies, recipes for creating such files must be provided by the main Makefile (sample usage: [pip-compile]). Providing empty value (`REQUIREMENTS_TXT=`) turns off processing of requirements.txt even when the file exists. [pip-compile]: docs/howto-pip-compile.md ##### SETUP_PY, SETUP_CFG, PYPROJECT_TOML, VENV_LOCAL_PACKAGE Space separated list of paths to files that contain build instructions for local Python packages. Corresponding packages will be installed into venv in [editable mode] along with all their dependencies. *Default: setup.py setup.cfg pyproject.toml (whichever present)* Non-existent and empty values are treated in the same way as for REQUIREMENTS_TXT. [editable mode]: https://pip.pypa.io/en/stable/cli/pip_install/#editable-installs ##### WORKDIR Parent directory for the virtual environment. *Default: current working directory* ##### VENVDIR Python virtual environment directory. *Default: $(WORKDIR)/.venv* ##### PIP_* Variables named starting with `PIP_` are not processed by *Makefile.venv* in any way and are passed to underlying pip calls as is. See [pip documentation](https://pip.pypa.io/en/stable/user_guide/#environment-variables) for more information. Use these variables to customize pip invocation, for example to provide custom package index url: ``` PIP_EXTRA_INDEX_URL="https://your.index/url" ``` ## Samples Makefile: ```make .PHONY: test test: venv $(VENV)/python -m unittest include Makefile.venv ``` Larger sample from a real project can be seen [here](https://github.com/sio/issyours/blob/master/Makefile). Also see [an introductory blog post](https://potyarkin.com/posts/2019/manage-python-virtual-environment-from-your-makefile/) from project author. Command line: ``` $ make test ...Skipped: creating and updating virtual environment... ... ---------------------------------------------------------------------- Ran 3 tests in 0.000s OK ``` ``` $ make show-venv Python 3.5.4 (v3.5.4:3f56838, Aug 8 2017, 02:07:06) [MSC v.1900 32 bit (Intel)] pip 19.2.3 from c:\users\99e7~1\appdata\local\temp\.venv\lib\site-packages\pip (python 3.5) venv: C:\Users\99E7~1\AppData\Local\Temp\.venv ``` ``` $ make python C:/Users/99E7~1/AppData/Local/Temp/.venv/Scripts/python Python 3.5.4 (v3.5.4:3f56838, Aug 8 2017, 02:07:06) [MSC v.1900 32 bit (Intel)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> _ ``` ## Compatibility *Makefile.venv* was written for GNU Make and may not work with other make implementations. Please be aware that GNU Make [can not correctly handle][spaces] whitespace characters in file paths. Such filepaths therefore are considered unsupported by *Makefile.venv* [spaces]: https://stackoverflow.com/questions/9838384/can-gnu-make-handle-filenames-with-spaces *Makefile.venv* is being [continuously tested][tests] on Linux, Windows and macOS. Any inconsistency encountered when running on Windows should be considered a bug and should be reported via [issues]. ## Support and contributing If you need help with using this Makefile or including it into your project, please create **[an issue][issues]**. Issues are also the primary venue for reporting bugs and posting feature requests. General discussion related to this project is also acceptable and very welcome! In case you wish to contribute code or documentation, feel free to open **[a pull request](https://github.com/sio/Makefile.venv/pulls)**. That would certainly make my day! I'm open to dialog and I promise to behave responsibly and treat all contributors with respect. Please try to do the same, and treat others the way you want to be treated. If for some reason you'd rather not use the issue tracker, contacting me via email is OK too. Please use a descriptive subject line to enhance visibility of your message. Also please keep in mind that public discussion channels are preferable because that way many other people may benefit from reading past conversations. My email is visible under the GitHub profile and in the commit log. [issues]: https://github.com/sio/Makefile.venv/issues ## License and copyright Copyright 2019-2023 Vitaly Potyarkin Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: docs/README.md ================================================ # Documentation for Makefile.venv ## Usage General usage is described in [README.md](../README.md). If you feel that some topics require a dedicated documentation entry please create an [issue] or better yet, a [pull request]. [issue]: https://github.com/sio/Makefile.venv/issues [pull request]: https://github.com/sio/Makefile.venv/pulls ## How-tos - [Integrating pip-compile into Makefile.venv workflow](howto-pip-compile.md) - [Using custom Makefile recipe to create virtual environment](howto-custom-venv-recipe.md) ================================================ FILE: docs/howto-custom-venv-recipe.md ================================================ # Using custom Makefile recipe to create virtual environment Sometimes assumptions made by *Makefile.venv* about how virtual environment should be created seem too rigid. Using venv module from standard library and always installing the latest version of pip, setuptools and wheel is a sane default, but users may want to: - Use another tool to create the virtual environment ([virtualenv] anyone?) *or* - Pass some [extra command line arguments] to venv module *or* - Specify which [versions of pip/setuptools/wheel] to use instead of latest. There is a way to do all of that. You can provide an alternative recipe for venv creation after you include *Makefile.venv*: ```makefile CUSTOM_INITIAL_PACKAGES=path/to/your/requirements-for-pip-wheel-setuptools.txt $(VENV): $(PY) -m venv $(VENVDIR) $(VENV)/python -m pip install -r $(CUSTOM_INITIAL_PACKAGES) ``` Here is [another example] where the last supported pip version is used when outdated Python interpreter is detected: ```makefile # Override default venv packages for older Python versions ifeq (True,$(shell $(PY) -c "import sys; print(sys.version_info < (3,5))")) $(VENV): $(PY) -m venv $(VENVDIR) $(VENV)/python -m pip install --upgrade "pip<19.2" "setuptools<44.0" "wheel<0.34" endif ``` It works because GNU Make allows to [redefine the recipe] for any target later in the Makefile. Downside is that this usage is not officially endorsed and GNU Make will spit out some warnings: ``` Makefile:46: warning: overriding recipe for target '.venv/bin' Makefile.venv:137: warning: ignoring old recipe for target '.venv/bin' ``` Despite the small drawback this solution allows for endless customization at practically zero cost. There is no way we could predict all oddly specific requests that users may come up with, let alone add variables and feature flags to support those. But with custom recipes users can define their venv whichever way they need. [Alternative suggestion] with `*-default` targets was considered and rejected because of being too invasive and producing unexpected outcomes in some scenarios. See more [here](https://github.com/sio/Makefile.venv/issues/13#issuecomment-928932526) [virtualenv]: https://pypi.org/project/virtualenv/ [extra command line arguments]: https://github.com/sio/Makefile.venv/pull/10 [versions of pip/setuptools/wheel]: https://github.com/sio/Makefile.venv/issues/13 [another example]: https://github.com/sio/bash-complete-partial-path/blob/2be6ef1f1885d3cb1ec2547ae41d78aa66f4ab78/Makefile#L42-L48 [redefine the recipe]: https://www.gnu.org/software/make/manual/html_node/Multiple-Rules.html [Alternative suggestion]: https://stackoverflow.com/questions/11958626/m/49804748#49804748 --- > Thanks to [@belm0] and [@simaoafonso-pwt] for bringing this to author's attention! [@belm0]: https://github.com/belm0 [@simaoafonso-pwt]: https://github.com/simaoafonso-pwt --- > Automated tests ensure that this workflow will not break in the future: > [tests/test_recipe_override.py] [tests/test_recipe_override.py]: ../tests/test_recipe_override.py ================================================ FILE: docs/howto-pip-compile.md ================================================ # Using pip-compile with Makefile.venv --- > **Note:** *Described workflow is possible only with v2020.08.04 and newer* --- *Makefile.venv* supports many non-trivial use cases. One such case is using `pip-compile` installed into a virtual environment to generate requirements file that defines that very environment. As always with *Makefile.venv* all venv related routines are handled automatically and are totally transparent to user. The task at hand is twofold: - We need to automatically generate `requirements.txt` with a Makefile recipe - We want to use a tool from virtual environment before that virtual environment is finalized by *Makefile.venv* Here is how it can be done: ```Makefile # We need to explicitly specify this value because by default it's ok for # requirements.txt to be missing. Providing any non-default value tells # Makefile.venv that the files listed must exist or be made from recipe. # This statement MUST come before `include Makefile.venv` REQUIREMENTS_TXT=requirements.txt include Makefile.venv # Save pip-compile path to variable for brevity # We may omit $(EXE) suffix if we're ok with Windows builds being broken PIP_COMPILE=$(VENV)/pip-compile$(EXE) # This and the next recipe MUST be defined after include statement. # We need to inject pip-compile into virtual environment # before Makefile.venv finishes working on it, but after venv is created. # There already exists a target we can add as dependency for this case: $(PIP_COMPILE): | $(VENV) $(VENV)/pip install pip-tools # or any other installation method touch $@ # Our requirements file directly depends upon *.in file and also requires # pip-compile to be available requirements.txt: requirements.in | $(PIP_COMPILE) $(PIP_COMPILE) --output-file $@ $< ``` And that's all. The rest of the Makefile should be written as usual when working with *Makefile.venv*: depend on `venv`, call `$(VENV)/entrypoints`, etc. The trick is to use `$(VENV)` as dependency for `$(PIP_COMPILE)` - that way we can avoid circular dependency and pip-compile will be installed after bare venv is created but before *Makefile.venv* attempts to fill it with packages from 'requirements.txt'. For example, after adding this rule to the Makefile above: ```Makefile all: venv $(VENV)/pip freeze ``` we can execute `make all` in a folder with only the makefiles and `requirements.in` - and both `requirements.txt` and full virtual environment described by it will be created automatically upon first invocation. --- > Thanks to [@belm0] for [bringing this](https://github.com/sio/Makefile.venv/issues/8) > to author's attention! [@belm0]: https://github.com/belm0 --- > Automated tests ensure that this workflow will not break in the future: > [tests/test_pip_compile.py] [tests/test_pip_compile.py]: ../tests/test_pip_compile.py ================================================ FILE: docs/howto-pipenv.md ================================================ # Integrating pipenv into Makefile.venv workflow It's possible, but this article has not been written yet. See [pip-compile how-to](howto-pip-compile.md) to get a rough idea on how to proceed. > TODO: write pipenv how-to ================================================ FILE: docs/howto-poetry.md ================================================ # Integrating poetry into Makefile.venv workflow It's possible, but this article has not been written yet. See [pip-compile how-to](howto-pip-compile.md) to get a rough idea on how to proceed. > TODO: write poetry how-to ================================================ FILE: pypi/Makefile ================================================ PACKAGE_ROOT=src/Makefile_venv PACKAGE_DATA=\ $(PACKAGE_ROOT)/LICENSE \ $(PACKAGE_ROOT)/Makefile.venv \ $(PACKAGE_ROOT)/README.md include ../Makefile.venv .PHONY: package build package build: dist dist: setup.cfg pyproject.toml Makefile dist: $(PACKAGE_DATA) dist: | $(VENV)/build -$(RM) -rv dist $(VENV)/python -m build $(PACKAGE_ROOT)/%: ../% cp $< $@ .PHONY: upload upload: dist | $(VENV)/twine $(VENV)/twine upload --repository testpypi $(TWINE_ARGS) dist/* $(VENV)/twine upload $(TWINE_ARGS) dist/* .PHONY: test test: $(PACKAGE_DATA) test: | $(VENV)/tox $(VENV)/tox $(TOX_ARGS) .PHONY: clean clean: git clean -idx ================================================ FILE: pypi/README.md ================================================ # Packaging Makefile.venv for PyPI This directory contains helper code and configuration files used to build a Python package for *Makefile.venv* and to upload that package to PyPI. It is assumed that *Makefile.venv* itself is in a working state when working on packaging, so we depend on it in multiple places here. This is also the reason why packaging tests are stored separately from the rest of test suite: we can not depend on *Makefile.venv* in the main test suite, but it's too useful to forego when doing auxiliary work. ================================================ FILE: pypi/pyproject.toml ================================================ [build-system] requires = [ "setuptools>=45", "setuptools_scm>=6.2", "wheel", ] build-backend = "setuptools.build_meta" [tool.setuptools_scm] root = ".." ================================================ FILE: pypi/setup.cfg ================================================ [metadata] name = Makefile.venv url = https://github.com/sio/Makefile.venv description = Seamlessly manage Python virtual environment with a Makefile long_description = file: src/Makefile_venv/README.md long_description_content_type = text/markdown keywords = makefile, virtualenv, env license = Apache-2.0 license_file = src/Makefile_venv/LICENSE classifiers = Environment :: Console Intended Audience :: Developers Intended Audience :: System Administrators License :: OSI Approved :: Apache Software License [options] zip_safe = False include_package_data = True packages = Makefile_venv package_dir = =src [options.package_data] * = Makefile.venv, *.md [options.entry_points] console_scripts = Makefile.venv = Makefile_venv:cli ================================================ FILE: pypi/src/Makefile_venv/__init__.py ================================================ ''' Python entrypoint that returns absolute path to Makefile.venv ''' try: from importlib.resources import as_file, files def path(): with as_file(files(__name__).joinpath('Makefile.venv')) as f: return str(f.resolve()) except (ImportError, ModuleNotFoundError) as e: import pkg_resources def path(): return pkg_resources.resource_filename(__name__, 'Makefile.venv') def cli(): '''Print Makefile.venv path to stdout''' print(path()) ================================================ FILE: pypi/tests/test_installable.py ================================================ import pytest from subprocess import run from pathlib import Path import Makefile_venv def test_makefile_is_installed(): makefile = Path(Makefile_venv.path()) assert makefile.name == 'Makefile.venv' assert makefile.exists() with open(makefile, 'r') as f: content = f.read() assert 'SEAMLESSLY MANAGE PYTHON VIRTUAL ENVIRONMENT WITH A MAKEFILE' in content def test_shell_entrypoint(): shell = run(['Makefile.venv'], capture_output=True, check=True, text=True, shell=True) assert Makefile_venv.path() == shell.stdout.strip() ================================================ FILE: pypi/tox.ini ================================================ [tox] envlist = pypi-latest isolated_build = True [testenv] deps = pytest commands = pytest -rA --color=yes -vv ================================================ FILE: tests/Makefile ================================================ PY?=python3 REPODIR:=$(dir $(firstword $(MAKEFILE_LIST))).. ARGS?= .PHONY: test test: deps cd $(REPODIR) && $(PY) -m unittest $(ARGS) .PHONY: deps deps: @-pwd @$(PY) -c "import sys; print('Python ' + sys.version.replace('\n',''))" @$(PY) -c "import platform; print(platform.platform())" @$(MAKE) --version @-git --version @echo SHELL=$(SHELL) test-fast: export SKIP_SLOW_TESTS=1 test-fast: test test-verbose: ARGS+=-v test-verbose: test ================================================ FILE: tests/README.md ================================================ # Some sanity checks for Makefile.venv Execute `python -m unittest` from the repo's top directory or run `make` from the `tests/` directory. Slow tests can be skipped by setting `SKIP_SLOW_TESTS` environment variable, e.g. `make SKIP_SLOW_TESTS=1` (shortcut: `make test-fast`) ================================================ FILE: tests/__init__.py ================================================ ''' Some sanity checks for Makefile.venv ''' ================================================ FILE: tests/common.py ================================================ ''' Common utilities for testing Makefile.venv ''' import os import sys from shutil import copyfile from subprocess import run, PIPE from tempfile import TemporaryDirectory from unittest import TestCase, skipIf slow_test = skipIf( os.environ.get('SKIP_SLOW_TESTS', False), 'slow test' ) class MakefileTestCase(TestCase): '''Base class for Makefile.venv tests''' MAKEFILE = 'Makefile.venv' MAKE = 'make' TIMEOUT = int(os.getenv('TEST_SUBPROCESS_TIMEOUT', 60)) # seconds TMPPREFIX = 'Makefile.venv_test_' def __init__(self, *a, **ka): self.env = os.environ.copy() self.env.update(dict( LANG='C', )) for variable in ( # PY is intentionally passed through because test runner may # be using non-standard interpreter command/path 'REQUIREMENTS_TXT', 'SETUP_CFG', 'SETUP_PY', 'VENV', 'VENVDEPENDS', 'VENVDIR', 'WORKDIR', ): if variable in self.env: # Clear environment variables for tests self.env.pop(variable) return super().__init__(*a, **ka) def make(self, *args, makefile=None, debug=False, dry_run=False, returncode=0): '''Execute Makefile.venv with GNU Make in temporary directory''' if makefile is None: makefile = self.MAKEFILE command = [self.MAKE, '-C', self.tmpdir.name, '-f', os.path.abspath(makefile)] if debug: command.append('-drR') if dry_run: command.append('-n') command.extend(args) process = run(command, stdout=PIPE, stderr=PIPE, timeout=self.TIMEOUT, text=True) if returncode is not None: self.check_returncode(process, returncode) return process def check_returncode(self, process, returncode=0): '''Check subprocess return code in automated tests''' self.assertEqual( process.returncode, returncode, msg='\n'.join( part for part in ( '{} exited with code {} (expected {})'.format(self.MAKE, process.returncode, returncode), '\nstdout:', process.stdout, 'stderr:', process.stderr, ) if part.strip() ) ) def copy(self, filename=None, content=None, makefile=False, data_dir='tests/data', dest_dir=None): '''Copy test data to temporary directory. Return full path to resulting file''' if not any([filename, content, makefile]): raise ValueError('At least one of parameters must be provided: filename, content, makefile') if content is None: src = os.path.join(data_dir, filename) with open(src) as source: content = source.read() if makefile and not filename: filename = 'Makefile' if dest_dir: dest_dir = os.path.join(self.tmpdir.name, dest_dir) if not os.path.exists(dest_dir): os.mkdir(dest_dir) dest = os.path.join(dest_dir, filename) else: dest = os.path.join(self.tmpdir.name, filename) if makefile: content = content.replace( '{{ Makefile.venv }}', os.path.abspath(self.MAKEFILE) ) with open(dest, 'w') as output: output.write(content) return dest def setUp(self): self.tmpdir = TemporaryDirectory(prefix=self.TMPPREFIX) os.environ.clear() os.environ.update(self.env) def tearDown(self): self.tmpdir.cleanup() del self.tmpdir os.environ.clear() os.environ.update(self.env) ================================================ FILE: tests/data/dependencies.mk ================================================ .DEFAULT: hello include {{ Makefile.venv }} hello: venv $(VENV)/python hello.py freeze: venv $(VENV)/pip freeze oneoff: $(VENV)/pyflakes $(VENV)/pyflakes --help ================================================ FILE: tests/data/hello.py ================================================ import pyfiglet print('hello') ================================================ FILE: tests/data/pip-compile.mk ================================================ all: venv $(VENV)/pyflakes --help clean: clean-venv -$(RM) Makefile.venv requirements.txt # You need to explicitly specify this value because by default it's ok for # requirements.txt to be missing. This statement MUST come before `include # Makefile.venv` REQUIREMENTS_TXT=requirements.txt requirements.txt # add trailing whitespace # Save pip-compile path to variable for brevity PIP_COMPILE=$(VENV)/pip-compile$(EXE) include {{ Makefile.venv }} # You need to inject pip-compile into virtual environment # before Makefile.venv finishes working on it, but after venv is created. # There already exists a target you can add as dependency for this case: $(PIP_COMPILE): | $(VENV) $(VENV)/pip install pip-tools $(call touch,$@) # Your requirements file directly depends upon *.in file and also requires # pip-compile to be available requirements.txt: requirements.in | $(PIP_COMPILE) $(PIP_COMPILE) --output-file $@ $< ================================================ FILE: tests/data/pyproject.toml ================================================ [project] name = "hello" version = "0.0.1" dependencies = [ "pyfiglet", ] ================================================ FILE: tests/data/recipe-override.mk ================================================ # Makefile for tests/test_recipe_override.py include {{ Makefile.venv }} CUSTOM_PACKAGE=noop==1.0 $(VENV): $(PY) -m venv $(VENVDIR) $(VENV)/python -m pip install --upgrade pip setuptools wheel $(CUSTOM_PACKAGE) freeze: venv $(VENV)/pip freeze ================================================ FILE: tests/data/requirements-extra.txt ================================================ console ================================================ FILE: tests/data/requirements.in ================================================ pyflakes ================================================ FILE: tests/data/requirements.txt ================================================ pyfiglet ================================================ FILE: tests/data/setup.py ================================================ from setuptools import setup setup( name='hello', py_modules=['hello'], entry_points={ 'console_scripts': [ 'hello = hello', ], }, install_requires=[ 'pyfiglet', ], ) ================================================ FILE: tests/test_dependencies.py ================================================ import os from pathlib import Path from time import sleep from tests.common import MakefileTestCase, slow_test def touch(filepath): Path(filepath).touch() class TestDependencies(MakefileTestCase): @slow_test def test_requirements_txt(self): '''Check that requirements.txt is being processed correctly''' self.common_dependency_checks('requirements.txt') @slow_test def test_setup_py(self): '''Check that setup.py is being processed correctly''' self.common_dependency_checks('setup.py') @slow_test def test_pyproject_toml(self): '''Check that pyproject.toml is being processed correctly''' self.common_dependency_checks('pyproject.toml') @slow_test def test_setup_cfg(self): '''Check that setup.cfg is being processed correctly''' # Repeat setup.py test makefile = self.common_dependency_checks('setup.py') # Creating setup.cfg must trigger venv rebuild setup_cfg = Path(makefile).parent / 'setup.cfg' touch(setup_cfg) make = self.make('hello', makefile=makefile) self.assertIn('/pip install -e', make.stdout) # But only one rebuild make = self.make('hello', makefile=makefile, dry_run=True) self.assertNotIn('/pip install -e', make.stdout) def common_dependency_checks(self, dependency_list): '''Generic unit test for setup.py and requirements.txt''' dependencies = self.copy(dependency_list) hello = self.copy('hello.py') makefile = self.copy('dependencies.mk', makefile=True) # Create virtual environment with specified dependencies make = self.make('hello', makefile=makefile) for line in make.stdout.split('\n'): if line.strip() == 'hello': break else: raise AssertionError("'hello' not found in make stdout:\n{}".format(make.stdout)) self.assertIn('Collecting pyfiglet', make.stdout) # Second invocation must not trigger dependencies installation second = self.make('hello', makefile=makefile) self.assertNotIn('Collecting pyfiglet', second.stdout) # When dependencies list was modified, venv has to be updated sleep(1) # Ensure that timestamps differ significantly touch(dependencies) third = self.make('hello', makefile=makefile, dry_run=True) self.assertTrue(any('pip install %s' % x in third.stdout for x in {'-r', '-e'})) return makefile @slow_test def test_requirements_txt_multiple(self): '''Check that multiple requirements.txt files are supported''' data = ['requirements.txt', 'requirements-extra.txt', 'hello.py'] for name in data: self.copy(name) makefile = self.copy('dependencies.mk', makefile=True) os.environ['REQUIREMENTS_TXT'] = 'requirements.txt requirements-extra.txt' self.make('hello', makefile=makefile) freeze = self.make('freeze', makefile=makefile) for package in ['pyfiglet', 'console']: with self.subTest(package=package): self.assertIn('%s==' % package, freeze.stdout) for word in ['Collecting', 'installing', 'installed', 'cache']: with self.subTest(word=word): self.assertNotIn(word.lower(), freeze.stdout.lower()) @slow_test def test_one_off(self): '''Check that one-off requirements are supported''' makefile = self.copy('dependencies.mk', makefile=True) make = self.make('oneoff', makefile=makefile) pyflakes_words = ['pyflakes', '--version', '--help'] venv_words = ['Collecting pyflakes', 'Installing', 'Successfully'] for word in pyflakes_words + venv_words: with self.subTest(word=word): self.assertIn(word, make.stdout) # Second invocation must not rebuild venv repeat = self.make('oneoff', makefile=makefile) for word in pyflakes_words: with self.subTest(word=word): self.assertIn(word, repeat.stdout) for word in venv_words: with self.subTest(word=word): self.assertNotIn(word, repeat.stdout) ================================================ FILE: tests/test_makefile.py ================================================ ''' Test Makefile.venv invocation ''' import os.path from tests.common import MakefileTestCase, slow_test class TestInvocation(MakefileTestCase): def test_gnu_make(self): '''Only GNU Make is supported''' make = self.make('--version') self.assertTrue('gnu make' in make.stdout.lower()) @slow_test def test_creating(self, *cli_args): '''Create empty virtual environment''' make = self.make('debug-venv', 'venv', *cli_args) self.assertTrue( os.path.isdir(os.path.join(self.tmpdir.name, '.venv')), msg='Failed to create virtual environment', ) version = self.make('show-venv', *cli_args) for line in ('python ', 'pip ', 'venv: '): with self.subTest(line=line): self.assertIn(line.lower(), version.stdout.lower()) self.make('clean-venv', *cli_args) self.assertFalse( os.path.isdir(os.path.join(self.tmpdir.name, '.venv')), msg='Failed to remove virtual environment', ) @slow_test def test_no_builtin_variables(self): ''' Check that "make -R" does not break anything Thanks to @martinthomson: https://github.com/sio/Makefile.venv/issues/19 https://github.com/sio/Makefile.venv/pull/20 ''' return self.test_creating('-R') ================================================ FILE: tests/test_pip_compile.py ================================================ ''' Tests for issue #8 https://github.com/sio/Makefile.venv/issues/8 Support for generated requirements.txt files ''' from tests.common import MakefileTestCase, slow_test class TestPipCompile(MakefileTestCase): @slow_test def test_issue8(self): '''Check that REQUIREMENTS_TXT can be generated with a Makefile recipe''' self.copy('requirements.in') makefile = self.copy('pip-compile.mk', makefile=True) make = self.make(makefile=makefile) for line in [ 'Collecting pyflakes', 'show this help message and exit', 'usage: pyflakes', ]: self.assertIn(line, make.stdout) # Second invokation must not need to rebuild venv repeat = self.make(makefile=makefile) self.assertIn('pyflakes --help', '\n'.join(repeat.stdout.splitlines()[:2])) ================================================ FILE: tests/test_py_autodetect.py ================================================ ''' Tests for Python interpreter auto detection ''' import os import shutil import sys from pathlib import Path from tests.common import MakefileTestCase, slow_test from textwrap import dedent from unittest import skipIf skip_py_launcher = skipIf( (os.getenv('WINDIR', '') / Path('py.exe')).exists(), r'C:\Windows\py.exe exists and will interfere with this test' # See GNU Make source code for more information (src/w32/subproc/sub_proc.c): # https://github.com/mirror/make/blob/e62f4cf9a2eaf71c0d0102c28280e52e3c169b99/src/w32/subproc/sub_proc.c#L499-L510 ) class TestPyAutoDetect(MakefileTestCase): MAKE = shutil.which('make') # use abspath because we mangle the PATH later UNIX_WRAPPER = '''\ #!/bin/sh [ "$1" = "-3" ] && shift "{executable}" "$@" ''' WINDOWS_WRAPPER = '''\ @echo off if "%1"=="-3" shift REM Unfortunately shift does not affect the value of %* REM so we are stuck with unclean %1..%9 REM which is more than enough for our usecase {executable} %1 %2 %3 %4 %5 %6 %7 %8 %9 ''' def __init__(self, *a, **ka): super().__init__(*a, **ka) if sys.platform == 'win32': script = self.WINDOWS_WRAPPER else: script = self.UNIX_WRAPPER self.script = dedent(script) def setUp(self): '''Tests in this module require PY to be unset''' super().setUp() self.PY = os.environ.pop('PY', None) def save_script(self, relative_path: str, executable=None): '''Save Python interpreter entry point to provided relative path''' if executable is None: executable = sys.executable if sys.platform == 'win32': relative_path += '.cmd' dest = Path(self.tmpdir.name) / relative_path dest.parent.mkdir(parents=True, exist_ok=True) with dest.open('w') as out: out.write(self.script.format(executable=executable)) dest.chmod(0o777) def test_autodetect_happy_path(self): '''Check that 'python3' is used by default''' self.save_script('bin/python3') self.save_script('bin/py') self.save_script('bin/python') os.environ['PATH'] = 'bin' make = self.make('debug-venv') self.assertIn('PY="python3"', make.stdout) @slow_test def test_autodetect_venv(self): '''Check that VENV interpreter is used if exists''' if self.PY: # restore PY to be able to create venv os.environ['PY'] = self.PY create = self.make('venv') os.environ.pop('PY', None) os.environ['PATH'] = 'nonexistent' make = self.make('debug-venv') for line in make.stdout.splitlines(): if line.startswith('PY='): self.assertIn('.venv', line) break else: self.assertTrue(False, 'Failed to parse stdout:\n%s' % make.stdout) def test_autodetect_py_3(self): '''Check that 'py -3' is used when appropriate''' self.save_script('bin/py') self.save_script('bin/python') os.environ['PATH'] = 'bin' make = self.make('debug-venv') self.assertIn('PY="py -3"', make.stdout) @skip_py_launcher def test_autodetect_worst_path(self): '''Check that 'python' is used if nothing else is there''' self.save_script('bin/python') os.environ['PATH'] = 'bin' make = self.make('debug-venv') self.assertIn('PY="python"', make.stdout) @skip_py_launcher def test_autodetect_failure(self): '''Check that autodetect failure is raised to the top''' os.environ['PATH'] = 'bin' make = self.make('debug-venv', returncode=2) self.assertIn('Could not detect Python interpreter', make.stderr) ================================================ FILE: tests/test_pyproject_toml.py ================================================ from time import sleep from tests.common import MakefileTestCase, slow_test from tests.test_dependencies import touch class TestPyprojectToml(MakefileTestCase): def test_empty(self): ''' Empty value for PYPROJECT_TOML should result in ignoring the file even when it exists ''' sample_makefile = '\n'.join(( 'PYPROJECT_TOML=', 'include {{ Makefile.venv }}', )) makefile = self.copy(content=sample_makefile, makefile=True) self.copy('pyproject.toml') make = self.make(makefile=makefile, dry_run=True) self.assertNotIn('/pip install', make.stdout) def test_nonexistent(self): ''' Nonexistent path for PYPROJECT_TOML should result in error because non-default values are treated as hard dependencies and are expected to be made via Makefile recipe ''' sample_makefile = '\n'.join(( 'PYPROJECT_TOML=nonexistent.toml', 'include {{ Makefile.venv }}', )) makefile = self.copy(content=sample_makefile, makefile=True) self.copy('pyproject.toml') make = self.make(makefile=makefile, dry_run=True, returncode=None) self.assertEqual(make.returncode, 2) self.assertIn('no rule to make target', make.stderr.lower()) self.assertNotIn('/pip install', make.stdout) @slow_test def test_multiple(self): ''' Check that multiple pyproject.toml files are supported ''' files = [ 'pyproject.toml', 'hello.py', ] for dest_dir in ['one', 'two']: for filename in files: self.copy(filename, dest_dir=dest_dir) translate = { 'name = "hello"': 'name = "hello2"', 'pyfiglet': 'console' } with open('tests/data/pyproject.toml') as f: setup_content = f.read() for old, new in translate.items(): setup_content = setup_content.replace(old, new) second_setup_py = self.copy( # avoid package name collision 'pyproject.toml', content=setup_content, dest_dir='two', ) makefile_content = '\n'.join(( 'PYPROJECT_TOML=one/pyproject.toml two/pyproject.toml', 'include {{ Makefile.venv }}', )) makefile = self.copy(content=makefile_content, makefile=True) # First invocation creates venv make = self.make('venv', makefile=makefile) self.assertIn('pip install -e', make.stdout) for package in ['pyfiglet', 'console']: self.assertIn('Collecting %s' % package, make.stdout) # Second should be a noop make = self.make('venv', makefile=makefile) self.assertNotIn('pip install', make.stdout) # Touching one of setup.cfg files should trigger venv update sleep(1) # macOS mtime seems to happen in whole seconds touch(second_setup_py) make = self.make('venv', makefile=makefile, dry_run=True) self.assertIn('pip install -e', make.stdout) ================================================ FILE: tests/test_recipe_override.py ================================================ ''' Check that default recipe for $(VENV) can be safely overwritten by user Some of our users rely upon this unofficial GNU Make feature Documentation: https://www.gnu.org/software/make/manual/html_node/Multiple-Rules.html Feature in use: https://github.com/sio/Makefile.venv/issues/13 https://github.com/sio/Makefile.venv/pull/10 https://github.com/sio/bash-complete-partial-path/blob/2be6ef1f1885d3cb1ec2547ae41d78aa66f4ab78/Makefile#L42-L48 ''' from tests.common import MakefileTestCase, slow_test CUSTOM_PACKAGE = 'noop==1.0' class TestMakefileRecipeOverride(MakefileTestCase): @slow_test def test_recipe_override(self): '''Check that default recipe for $(VENV) may be overwritten by user''' makefile = self.copy('recipe-override.mk', makefile=True) first = self.make('freeze', makefile=makefile) self.assertIn(CUSTOM_PACKAGE, first.stdout.splitlines()) second = self.make('freeze', makefile=makefile) self.assertNotIn('pip install', second.stdout) self.assertIn(CUSTOM_PACKAGE, second.stdout.splitlines()) ================================================ FILE: tests/test_release_checksum.py ================================================ import re from hashlib import sha256 from unittest import TestCase from .test_release_version import skip_dev, DEV_VERSION_EXPLAINED def calculate_checksum(filename): with open(filename) as f: checksum = sha256() for line in f: checksum.update(line.encode()) return checksum.hexdigest() class TestChecksum(TestCase): makefile = 'Makefile.venv' readme = 'README.md' pattern = re.compile(r'\b[0-9a-f]{64}\b', re.IGNORECASE) def read_checksum(self, filename): with open(filename) as f: match = self.pattern.search(f.read()) if not match: raise ValueError('checksum not found in {}'.format(filename)) return match.group().lower() @skip_dev def test_checksum(self): ''' Check that installation instructions contain valid checksum (versions ending with -dev suffix are skipped) ''' recorded = self.read_checksum(self.readme) calculated = calculate_checksum(self.makefile) self.assertEqual( recorded, calculated, '{} but versions in README and in Makefile.venv do not match'.format(DEV_VERSION_EXPLAINED) ) ================================================ FILE: tests/test_release_version.py ================================================ import re from unittest import TestCase, skipIf from subprocess import run, PIPE VERSION_PATTERN = re.compile(r'\b(v\d{4}\.\d{2}\.\d{2}(?:-\w+|))\b', re.IGNORECASE) DEV_VERSION_EXPLAINED = 'Proper release detected (version does not end with -dev suffix)' def get_version(filename, pattern=VERSION_PATTERN): with open(filename) as f: match = pattern.search(f.read()) if not match: raise ValueError('version pattern not found in {}'.format(filename)) return match.group(1) skip_dev = skipIf( get_version('Makefile.venv').endswith('-dev'), 'not a final release' ) class TestVersion(TestCase): pattern = VERSION_PATTERN readme = 'README.md' makefile = 'Makefile.venv' changelog = 'CHANGELOG.md' @skip_dev def test_version(self): '''Check that release versions in README and in Makefile.venv match''' self.assertEqual( get_version(self.readme), get_version(self.makefile), ) @skip_dev def test_git_tag(self): ''' Check that git tag contains valid release version (versions ending with -dev suffix are skipped) ''' cmd = 'git log -1 --format=%D --'.split() + [self.makefile] process = run(cmd, stdout=PIPE, stderr=PIPE) git_log = process.stdout.decode() match = self.pattern.search(git_log) if not match: debug = 'exit code: {rc}\nstdout:\n{stdout}\nstderr:\n{stderr}'.format( rc = process.returncode, stdout = git_log, stderr = process.stderr.decode(), ) raise AssertionError('{} but version pattern not found in {!r}:\n{}'.format( DEV_VERSION_EXPLAINED, ' '.join(cmd), debug) ) git_version = match.group(1) self.assertEqual( git_version, get_version(self.makefile), '{} but git tag does not match the version in Makefile.venv'.format(DEV_VERSION_EXPLAINED) ) @skip_dev def test_changelog(self): '''Check that changelog contains an entry for current version''' version = get_version(self.makefile) header = '## %s' % version with open(self.changelog) as changelog: for line in changelog: if line.startswith(header): break else: raise AssertionError('header not found in {changelog}: {header!r}'.format( header=header, changelog=self.changelog, )) ================================================ FILE: tests/test_requirements_txt.py ================================================ ''' Check edge cases for REQUIREMENTS_TXT value Mentioned in: https://github.com/sio/Makefile.venv/issues/14#issuecomment-982578931 ''' from tests.common import MakefileTestCase class TestRequirementsTxt(MakefileTestCase): def test_requirements_txt_empty(self): ''' Empty value for REQUIREMENTS_TXT should result in ignoring requirements.txt even when it exists ''' sample_makefile = '\n'.join(( 'REQUIREMENTS_TXT=', 'include {{ Makefile.venv }}', )) makefile = self.copy(content=sample_makefile, makefile=True) self.copy('requirements.txt') make = self.make(makefile=makefile, dry_run=True) self.assertNotIn('/pip install', make.stdout) def test_requirements_txt_nonexistent(self): ''' Nonexistent path for REQUIREMENTS_TXT should result in error because non-default values are treated as hard dependencies and are expected to be made via Makefile recipe ''' sample_makefile = '\n'.join(( 'REQUIREMENTS_TXT=nonexistent.txt', 'include {{ Makefile.venv }}', )) makefile = self.copy(content=sample_makefile, makefile=True) self.copy('requirements.txt') make = self.make(makefile=makefile, dry_run=True, returncode=None) self.assertEqual(make.returncode, 2) self.assertIn('no rule to make target', make.stderr.lower()) self.assertNotIn('/pip install', make.stdout) ================================================ FILE: tests/test_setup_py.py ================================================ from time import sleep from tests.common import MakefileTestCase, slow_test from tests.test_dependencies import touch class TestSetupPy(MakefileTestCase): def test_setup_py_empty(self): ''' Empty value for SETUP_PY should result in ignoring the file even when it exists ''' sample_makefile = '\n'.join(( 'SETUP_PY=', 'include {{ Makefile.venv }}', )) makefile = self.copy(content=sample_makefile, makefile=True) self.copy('setup.py') make = self.make(makefile=makefile, dry_run=True) self.assertNotIn('/pip install', make.stdout) def test_setup_py_nonexistent(self): ''' Nonexistent path for SETUP_PY should result in error because non-default values are treated as hard dependencies and are expected to be made via Makefile recipe ''' sample_makefile = '\n'.join(( 'SETUP_PY=nonexistent.py', 'include {{ Makefile.venv }}', )) makefile = self.copy(content=sample_makefile, makefile=True) self.copy('setup.py') make = self.make(makefile=makefile, dry_run=True, returncode=None) self.assertEqual(make.returncode, 2) self.assertIn('no rule to make target', make.stderr.lower()) self.assertNotIn('/pip install', make.stdout) @slow_test def test_setup_py_multiple(self): ''' Check that multiple setup.py files are supported ''' files = [ 'setup.py', 'hello.py', ] for dest_dir in ['one', 'two']: for filename in files: self.copy(filename, dest_dir=dest_dir) setup_content = '\n'.join(( 'from setuptools import setup', 'setup(name="hello2", install_requires=["console",])' )) second_setup_py = self.copy( # avoid package name collision 'setup.py', content=setup_content, dest_dir='two', ) makefile_content = '\n'.join(( 'SETUP_PY=one/setup.py two/setup.py', 'include {{ Makefile.venv }}', )) makefile = self.copy(content=makefile_content, makefile=True) # First invocation creates venv make = self.make('venv', makefile=makefile) self.assertIn('pip install -e', make.stdout) for package in ['pyfiglet', 'console']: self.assertIn('Collecting %s' % package, make.stdout) # Second should be a noop make = self.make('venv', makefile=makefile) self.assertNotIn('pip install', make.stdout) # Touching one of setup.cfg files should trigger venv update sleep(1) # macOS mtime seems to happen in whole seconds touch(second_setup_py) make = self.make('venv', makefile=makefile, dry_run=True) self.assertIn('pip install -e', make.stdout) def test_setup_cfg_multiple(self): '''Check that multiple setup.cfg files are picked up correctly''' for dirname in {'one', 'two'}: for filename in {'setup.py', 'setup.cfg'}: self.copy(filename, content='', dest_dir=dirname) makefile = self.copy(makefile=True, content='\n'.join(( 'SETUP_PY=one/setup.py two/setup.py', 'include {{ Makefile.venv }}', ))) make = self.make('debug-venv', makefile=makefile) count = 0 for line in make.stdout.splitlines(): if 'one/setup.cfg two/setup.cfg' not in line: continue if 'VENVDEPENDS=' in line: count += 1 if 'VENV_LOCAL_PACKAGE=' in line: count += 1 self.assertEqual(count, 2, "Unexpected number of setup.cfg lines in stdout:\n{}".format(make.stdout)) ================================================ FILE: tests/test_spaces.py ================================================ from tests.common import MakefileTestCase, slow_test class TestSpacesInPath(MakefileTestCase): TMPPREFIX = 'Makefile.venv test with spaces ' @slow_test def test_spaces(self): '''Check that spaces in project path do not break anything''' for filename in {'hello.py', 'requirements.txt'}: self.copy(filename) makefile = self.copy('dependencies.mk', makefile=True) make = self.make('hello', makefile=makefile) self.assertIn('Collecting pyfiglet', make.stdout)