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
<!--Template for new entries
## CURRENT
*
*
[Source code tree](https://github.com/sio/Makefile.venv/tree/CURRENT)
| [Tarball](https://github.com/sio/Makefile.venv/tarball/CURRENT)
| [Commit history](https://github.com/sio/Makefile.venv/compare/PREVIOUS...CURRENT)
-->
## 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
# <http://www.apache.org/licenses/LICENSE-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
<a href="https://asciinema.org/a/279646" target="_blank">
<img src="https://asciinema.org/a/279646.svg" title="Demo screencast"/>
</a>
## 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)
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
SYMBOL INDEX (61 symbols across 14 files)
FILE: pypi/src/Makefile_venv/__init__.py
function path (line 7) | def path():
function path (line 12) | def path():
function cli (line 16) | def cli():
FILE: pypi/tests/test_installable.py
function test_makefile_is_installed (line 9) | def test_makefile_is_installed():
function test_shell_entrypoint (line 18) | def test_shell_entrypoint():
FILE: tests/common.py
class MakefileTestCase (line 20) | class MakefileTestCase(TestCase):
method __init__ (line 28) | def __init__(self, *a, **ka):
method make (line 49) | def make(self, *args, makefile=None, debug=False, dry_run=False, retur...
method check_returncode (line 65) | def check_returncode(self, process, returncode=0):
method copy (line 82) | def copy(self, filename=None, content=None, makefile=False, data_dir='...
method setUp (line 108) | def setUp(self):
method tearDown (line 113) | def tearDown(self):
FILE: tests/test_dependencies.py
function touch (line 8) | def touch(filepath):
class TestDependencies (line 12) | class TestDependencies(MakefileTestCase):
method test_requirements_txt (line 15) | def test_requirements_txt(self):
method test_setup_py (line 20) | def test_setup_py(self):
method test_pyproject_toml (line 25) | def test_pyproject_toml(self):
method test_setup_cfg (line 30) | def test_setup_cfg(self):
method common_dependency_checks (line 45) | def common_dependency_checks(self, dependency_list):
method test_requirements_txt_multiple (line 72) | def test_requirements_txt_multiple(self):
method test_one_off (line 90) | def test_one_off(self):
FILE: tests/test_makefile.py
class TestInvocation (line 10) | class TestInvocation(MakefileTestCase):
method test_gnu_make (line 12) | def test_gnu_make(self):
method test_creating (line 18) | def test_creating(self, *cli_args):
method test_no_builtin_variables (line 38) | def test_no_builtin_variables(self):
FILE: tests/test_pip_compile.py
class TestPipCompile (line 12) | class TestPipCompile(MakefileTestCase):
method test_issue8 (line 15) | def test_issue8(self):
FILE: tests/test_py_autodetect.py
class TestPyAutoDetect (line 22) | class TestPyAutoDetect(MakefileTestCase):
method __init__ (line 40) | def __init__(self, *a, **ka):
method setUp (line 48) | def setUp(self):
method save_script (line 53) | def save_script(self, relative_path: str, executable=None):
method test_autodetect_happy_path (line 65) | def test_autodetect_happy_path(self):
method test_autodetect_venv (line 75) | def test_autodetect_venv(self):
method test_autodetect_py_3 (line 91) | def test_autodetect_py_3(self):
method test_autodetect_worst_path (line 100) | def test_autodetect_worst_path(self):
method test_autodetect_failure (line 108) | def test_autodetect_failure(self):
FILE: tests/test_pyproject_toml.py
class TestPyprojectToml (line 5) | class TestPyprojectToml(MakefileTestCase):
method test_empty (line 7) | def test_empty(self):
method test_nonexistent (line 21) | def test_nonexistent(self):
method test_multiple (line 39) | def test_multiple(self):
FILE: tests/test_recipe_override.py
class TestMakefileRecipeOverride (line 19) | class TestMakefileRecipeOverride(MakefileTestCase):
method test_recipe_override (line 22) | def test_recipe_override(self):
FILE: tests/test_release_checksum.py
function calculate_checksum (line 8) | def calculate_checksum(filename):
class TestChecksum (line 16) | class TestChecksum(TestCase):
method read_checksum (line 21) | def read_checksum(self, filename):
method test_checksum (line 29) | def test_checksum(self):
FILE: tests/test_release_version.py
function get_version (line 10) | def get_version(filename, pattern=VERSION_PATTERN):
class TestVersion (line 24) | class TestVersion(TestCase):
method test_version (line 32) | def test_version(self):
method test_git_tag (line 40) | def test_git_tag(self):
method test_changelog (line 68) | def test_changelog(self):
FILE: tests/test_requirements_txt.py
class TestRequirementsTxt (line 11) | class TestRequirementsTxt(MakefileTestCase):
method test_requirements_txt_empty (line 13) | def test_requirements_txt_empty(self):
method test_requirements_txt_nonexistent (line 27) | def test_requirements_txt_nonexistent(self):
FILE: tests/test_setup_py.py
class TestSetupPy (line 5) | class TestSetupPy(MakefileTestCase):
method test_setup_py_empty (line 7) | def test_setup_py_empty(self):
method test_setup_py_nonexistent (line 21) | def test_setup_py_nonexistent(self):
method test_setup_py_multiple (line 39) | def test_setup_py_multiple(self):
method test_setup_cfg_multiple (line 81) | def test_setup_cfg_multiple(self):
FILE: tests/test_spaces.py
class TestSpacesInPath (line 4) | class TestSpacesInPath(MakefileTestCase):
method test_spaces (line 9) | def test_spaces(self):
Condensed preview — 43 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (87K chars).
[
{
"path": ".gitattributes",
"chars": 34,
"preview": "*.venv linguist-language=Makefile\n"
},
{
"path": ".github/workflows/test.yml",
"chars": 3393,
"preview": "name: Run automated tests\non:\n push:\n paths-ignore:\n - '**.md'\n - '.git*'\n pull_request:\n schedule:\n "
},
{
"path": ".gitignore",
"chars": 228,
"preview": "# Used for testing \"include Makefile.venv\"\n/Makefile\n\n# Python tests\n*.pyc\n\n# Packaging\n*.egg-info\n.venv/\n.tox/\n/pypi/sr"
},
{
"path": "CHANGELOG.md",
"chars": 8623,
"preview": "# Changelog for Makefile.venv\n\n<!--Template for new entries\n\n\n## CURRENT\n\n*\n*\n\n[Source code tree](https://github.com/sio"
},
{
"path": "LICENSE",
"chars": 11357,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "Makefile.venv",
"chars": 7378,
"preview": "#\n# SEAMLESSLY MANAGE PYTHON VIRTUAL ENVIRONMENT WITH A MAKEFILE\n#\n# https://github.com/sio/Makefile.venv v2"
},
{
"path": "README.md",
"chars": 10579,
"preview": "# Seamlessly manage Python virtual environment with a Makefile\n\n*Makefile.venv* takes care of creating, updating and inv"
},
{
"path": "docs/README.md",
"chars": 526,
"preview": "# Documentation for Makefile.venv\n\n## Usage\n\nGeneral usage is described in [README.md](../README.md). If you feel that s"
},
{
"path": "docs/howto-custom-venv-recipe.md",
"chars": 3069,
"preview": "# Using custom Makefile recipe to create virtual environment\n\nSometimes assumptions made by *Makefile.venv* about how vi"
},
{
"path": "docs/howto-pip-compile.md",
"chars": 2836,
"preview": "# Using pip-compile with Makefile.venv\n\n---\n\n> **Note:** *Described workflow is possible only with v2020.08.04 and newer"
},
{
"path": "docs/howto-pipenv.md",
"chars": 224,
"preview": "# Integrating pipenv into Makefile.venv workflow\n\nIt's possible, but this article has not been written yet.\n\nSee [pip-co"
},
{
"path": "docs/howto-poetry.md",
"chars": 224,
"preview": "# Integrating poetry into Makefile.venv workflow\n\nIt's possible, but this article has not been written yet.\n\nSee [pip-co"
},
{
"path": "pypi/Makefile",
"chars": 643,
"preview": "PACKAGE_ROOT=src/Makefile_venv\nPACKAGE_DATA=\\\n\t$(PACKAGE_ROOT)/LICENSE \\\n\t$(PACKAGE_ROOT)/Makefile.venv \\\n\t$(PACKAGE_ROO"
},
{
"path": "pypi/README.md",
"chars": 533,
"preview": "# Packaging Makefile.venv for PyPI\n\nThis directory contains helper code and configuration files used to build a\nPython p"
},
{
"path": "pypi/pyproject.toml",
"chars": 168,
"preview": "[build-system]\nrequires = [\n \"setuptools>=45\",\n \"setuptools_scm>=6.2\",\n \"wheel\",\n]\nbuild-backend = \"setuptools."
},
{
"path": "pypi/setup.cfg",
"chars": 762,
"preview": "[metadata]\nname = Makefile.venv\nurl = https://github.com/sio/Makefile.venv\ndescription = Seamlessly manage Python virtua"
},
{
"path": "pypi/src/Makefile_venv/__init__.py",
"chars": 488,
"preview": "'''\nPython entrypoint that returns absolute path to Makefile.venv\n'''\n\ntry:\n from importlib.resources import as_file,"
},
{
"path": "pypi/tests/test_installable.py",
"chars": 569,
"preview": "import pytest\n\nfrom subprocess import run\nfrom pathlib import Path\n\nimport Makefile_venv\n\n\ndef test_makefile_is_installe"
},
{
"path": "pypi/tox.ini",
"chars": 126,
"preview": "[tox]\nenvlist =\n pypi-latest\nisolated_build = True\n\n\n[testenv]\ndeps =\n pytest\ncommands =\n pytest -rA --color=ye"
},
{
"path": "tests/Makefile",
"chars": 448,
"preview": "PY?=python3\nREPODIR:=$(dir $(firstword $(MAKEFILE_LIST)))..\nARGS?=\n\n.PHONY: test\ntest: deps\n\tcd $(REPODIR) && $(PY) -m u"
},
{
"path": "tests/README.md",
"chars": 279,
"preview": "# Some sanity checks for Makefile.venv\n\nExecute `python -m unittest` from the repo's top directory\nor run `make` from th"
},
{
"path": "tests/__init__.py",
"chars": 45,
"preview": "'''\nSome sanity checks for Makefile.venv\n'''\n"
},
{
"path": "tests/common.py",
"chars": 3871,
"preview": "'''\nCommon utilities for testing Makefile.venv\n'''\n\n\nimport os\nimport sys\nfrom shutil import copyfile\nfrom subprocess im"
},
{
"path": "tests/data/dependencies.mk",
"chars": 168,
"preview": ".DEFAULT: hello\n\ninclude {{ Makefile.venv }}\n\nhello: venv\n\t$(VENV)/python hello.py\n\nfreeze: venv\n\t$(VENV)/pip freeze\n\non"
},
{
"path": "tests/data/hello.py",
"chars": 31,
"preview": "import pyfiglet\nprint('hello')\n"
},
{
"path": "tests/data/pip-compile.mk",
"chars": 937,
"preview": "all: venv\n\t$(VENV)/pyflakes --help\n\n\nclean: clean-venv\n\t-$(RM) Makefile.venv requirements.txt\n\n\n# You need to explicitly"
},
{
"path": "tests/data/pyproject.toml",
"chars": 78,
"preview": "[project]\nname = \"hello\"\nversion = \"0.0.1\"\ndependencies = [\n \"pyfiglet\",\n]\n"
},
{
"path": "tests/data/recipe-override.mk",
"chars": 250,
"preview": "# Makefile for tests/test_recipe_override.py\n\ninclude {{ Makefile.venv }}\n\nCUSTOM_PACKAGE=noop==1.0\n\n$(VENV):\n\t$(PY) -m "
},
{
"path": "tests/data/requirements-extra.txt",
"chars": 8,
"preview": "console\n"
},
{
"path": "tests/data/requirements.in",
"chars": 9,
"preview": "pyflakes\n"
},
{
"path": "tests/data/requirements.txt",
"chars": 9,
"preview": "pyfiglet\n"
},
{
"path": "tests/data/setup.py",
"chars": 228,
"preview": "from setuptools import setup\n\nsetup(\n name='hello',\n py_modules=['hello'],\n entry_points={\n 'console_scr"
},
{
"path": "tests/test_dependencies.py",
"chars": 4240,
"preview": "import os\nfrom pathlib import Path\nfrom time import sleep\n\nfrom tests.common import MakefileTestCase, slow_test\n\n\ndef to"
},
{
"path": "tests/test_makefile.py",
"chars": 1381,
"preview": "'''\nTest Makefile.venv invocation\n'''\n\n\nimport os.path\nfrom tests.common import MakefileTestCase, slow_test\n\n\nclass Test"
},
{
"path": "tests/test_pip_compile.py",
"chars": 878,
"preview": "'''\nTests for issue #8\nhttps://github.com/sio/Makefile.venv/issues/8\n\nSupport for generated requirements.txt files\n'''\n\n"
},
{
"path": "tests/test_py_autodetect.py",
"chars": 3847,
"preview": "'''\nTests for Python interpreter auto detection\n'''\n\nimport os\nimport shutil\nimport sys\nfrom pathlib import Path\nfrom te"
},
{
"path": "tests/test_pyproject_toml.py",
"chars": 3130,
"preview": "from time import sleep\nfrom tests.common import MakefileTestCase, slow_test\nfrom tests.test_dependencies import touch\n\nc"
},
{
"path": "tests/test_recipe_override.py",
"chars": 1100,
"preview": "'''\nCheck that default recipe for $(VENV) can be safely overwritten by user\n\nSome of our users rely upon this unofficial"
},
{
"path": "tests/test_release_checksum.py",
"chars": 1216,
"preview": "import re\nfrom hashlib import sha256\nfrom unittest import TestCase\n\nfrom .test_release_version import skip_dev, DEV_VERS"
},
{
"path": "tests/test_release_version.py",
"chars": 2626,
"preview": "import re\nfrom unittest import TestCase, skipIf\nfrom subprocess import run, PIPE\n\n\nVERSION_PATTERN = re.compile(r'\\b(v\\d"
},
{
"path": "tests/test_requirements_txt.py",
"chars": 1512,
"preview": "'''\nCheck edge cases for REQUIREMENTS_TXT value\n\nMentioned in:\n https://github.com/sio/Makefile.venv/issues/14#issuec"
},
{
"path": "tests/test_setup_py.py",
"chars": 3823,
"preview": "from time import sleep\nfrom tests.common import MakefileTestCase, slow_test\nfrom tests.test_dependencies import touch\n\nc"
},
{
"path": "tests/test_spaces.py",
"chars": 526,
"preview": "from tests.common import MakefileTestCase, slow_test\n\n\nclass TestSpacesInPath(MakefileTestCase):\n\n TMPPREFIX = 'Makef"
}
]
About this extraction
This page contains the full source code of the sio/Makefile.venv GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 43 files (80.5 KB), approximately 21.9k tokens, and a symbol index with 61 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.