Created
January 28, 2022 19:36
-
-
Save qstokkink/4e99ff4bc19266b27f7c942dfba3a58d to your computer and use it in GitHub Desktop.
Python Dependency Find Version
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Python Dependency Find Version (pydepfindver.py) | |
============================================= | |
Brute force all known versions of your pip dependencies to see which ones work for your project. | |
- Includes html report functionality. | |
- Stores (intermediate) results in files so you can resume or quickly update for new releases of dependencies. | |
- Uses venv. | |
An example for https://github.com/Tribler/py-ipv8: | |
# Run `run_all_tests.py -q -a -p 32` in the `E:\\py-ipv8\\` folder for every requirement in `requirements.txt` | |
run_all_from_requirements(['run_all_tests.py', '-q', '-a', '-p', '32'], "E:\\py-ipv8\\requirements.txt") | |
# After everything is done, make a nice html document. | |
results_to_html() | |
""" | |
import json | |
import os | |
from distutils.version import LooseVersion | |
import pkg_resources | |
import subprocess | |
import sys | |
import venv | |
from glob import glob | |
from typing import Optional, Dict, List | |
from urllib import request | |
class Environment(venv.EnvBuilder): | |
""" | |
Virtual environment to test different package versions in. | |
""" | |
def __init__(self, env_dir: str = "venv-pydepfindver"): | |
super().__init__(system_site_packages=False, | |
clear=False, | |
symlinks=True, | |
upgrade=True, | |
with_pip=True, | |
prompt="") | |
self.context = None | |
self.create(env_dir) | |
def ensure_directories(self, env_dir): | |
self.context = super().ensure_directories(env_dir) | |
return self.context | |
def install_pip_dependencies(self, requirements_path: str) -> None: | |
""" | |
Install all requirements specified in a ``requirements.txt``. | |
:param requirements_path: the path of the requirements.txt file | |
:returns: None | |
:raises subprocess.CalledProcessError: if installation failed | |
""" | |
cmd = [self.context.env_exe, '-m', 'pip', 'install', '--upgrade', '-r', requirements_path] | |
subprocess.check_output(cmd, stderr=subprocess.STDOUT) | |
def update_pip_dependency(self, dependency: str, version: str) -> None: | |
""" | |
Update a given dependency to a specific version. | |
:param dependency: the package name to set the version for | |
:param version: the version to set | |
:returns: None | |
:raises subprocess.CalledProcessError: if updating failed | |
""" | |
cmd = [self.context.env_exe, '-m', 'pip', 'install', '--upgrade', f"{dependency}=={version}"] | |
subprocess.check_output(cmd, stderr=subprocess.STDOUT) | |
def run_test(self, dependency: str, version: str, commands: List[str], directory: str = ".") -> bool: | |
""" | |
Update a pip dependency to a specific version and run the venv Python with given commands from some directory. | |
For example, one call to this method translated to bash: | |
.. code-block:: Python | |
run_test("mypackage", "1.2.3", ["arg1", 2], "/home/project") | |
.. code-block:: Bash | |
$ python -m venv venv-pydepfindver | |
$ pushd /home/project | |
$ venv-pydepfindver/Scripts/python -m pip install --upgrade mypackage==1.2.3 | |
$ venv-pydepfindver/Scripts/python "arg1" 2 | |
$ popd | |
:param dependency: the package name to set the version for | |
:param version: the version to set | |
:param commands: the (subprocess call style) args to give to the venv Python | |
:param directory: the working directory for the run | |
:return: whether the command succeeded | |
""" | |
print("> Fetching requested version") | |
self.update_pip_dependency(dependency, version) | |
cmd = [self.context.env_exe] + commands | |
print("> Running", repr(cmd)) | |
try: | |
subprocess.run(cmd, cwd=directory, capture_output=False, stderr=subprocess.STDOUT, check=True) | |
except subprocess.CalledProcessError: | |
return False | |
return True | |
def result_file_name(dependency: str) -> str: | |
""" | |
Create a file name from a dependency name based on the Python interpreter version. | |
:param dependency: the dependency to create a result file name for | |
:return: the result file name | |
""" | |
return f"{dependency}-{hex(sys.hexversion)}.dep" | |
def pip_versions(dependency: str) -> Dict[str, object]: | |
""" | |
All version descriptors of a certain pip package name. | |
:param dependency: the package name to fetch available versions for | |
:return: the mapping of version names to their descriptors | |
""" | |
return json.loads(request.urlopen(f"https://pypi.org/pypi/{dependency}/json").read())['releases'] | |
def run_tests(dependency: str, | |
commands: List[str], | |
directory: str = ".", | |
requirements_path: Optional[str] = None, | |
env_dir: str = "venv-pydepfindver") -> None: | |
""" | |
Run commands for all pip versions of a dependency. | |
Optionally, change the working directory, use a requirements file or customize the venv directory. | |
Results are cached in ".dep" files, to allow pausing and to avoid having to regenerate all results when | |
a dependency has a new version released. | |
:param dependency: the pip dependency to brute force check | |
:param commands: the (subprocess call style) args to give to the venv Python | |
:param directory: the working directory for the run | |
:param requirements_path: the path of the requirements.txt file | |
:param env_dir: the folder name for the created venv | |
:returns: None | |
""" | |
# Env creation | |
env = Environment(env_dir) | |
if requirements_path is not None: | |
env.install_pip_dependencies(requirements_path) | |
# Read existing results | |
result_path = result_file_name(dependency) | |
results = {} | |
if os.path.exists(result_path): | |
with open(result_path, 'r') as result_file: | |
results.update(json.load(result_file)) | |
# Filter already tested | |
to_test = set(pip_versions(dependency).keys()) - set(results.keys()) | |
if not to_test: | |
print("Skipping", dependency, "existing results leave no versions to test!") | |
# Run stuff | |
for version in to_test: | |
print("Running", dependency, "at version", version) | |
try: | |
success = env.run_test(dependency, version, commands, directory) | |
except subprocess.CalledProcessError: | |
# Didn't even pass setup | |
success = False | |
results[version] = success | |
print("SUCCESS!" if success else "FAILURE!") | |
# Write results | |
with open(result_path, 'w') as result_file: | |
json.dump(results, result_file) | |
def run_all_from_requirements(commands: List[str], requirements_path: str) -> None: | |
""" | |
Easy way to call `run_tests` for all dependencies of projects with a ``requirements.txt``. | |
:param commands: the (subprocess call style) args to give to the venv Python | |
:param requirements_path: the path of the requirements.txt file | |
:returns: None | |
""" | |
with open(requirements_path, "r") as requirements_file: | |
requirements = pkg_resources.parse_requirements(requirements_file.read()) | |
directory = os.path.dirname(requirements_path) | |
for requirement in requirements: | |
dependency = requirement.project_name | |
run_tests(dependency, commands, directory=directory, requirements_path=requirements_path) | |
def results_to_html(output: str = "results.html") -> None: | |
""" | |
Parse the results in the cwd and turn them into an html report. | |
:param output: the name of the html file to write | |
:returns: None | |
""" | |
result_paths = sorted(glob("*.dep")) | |
html_output = "" | |
for result_path in result_paths: | |
with open(result_path, 'r') as result_file: | |
results = json.load(result_file) | |
html_output += f"<p style=\"word-wrap: normal; line-height: 1.8;\"><b>{result_path[:-4]}:</b><br>\n" | |
for key in sorted(results.keys(), key=LooseVersion): | |
bgcol = "lightgreen" if results[key] else "red" | |
html_output += (f"<span style=\"background-color: {bgcol}; border: 2px solid black; margin: 1px;" | |
f"padding: 2px;\">{key}</span>\n") | |
html_output += "</p>\n" | |
with open(output, "w") as output_file: | |
output_file.write(html_output) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example for https://github.com/Tribler/py-ipv8, invoked using: