├── .gitignore ├── README.md ├── pyenvcomp ├── __init__.py └── main.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/python 2 | # Edit at https://www.gitignore.io/?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | */__pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | pip-wheel-metadata/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # pyenv 72 | .python-version 73 | 74 | # pipenv 75 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 76 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 77 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 78 | # install all needed dependencies. 79 | #Pipfile.lock 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | .spyproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | 94 | # Mr Developer 95 | .mr.developer.cfg 96 | .project 97 | .pydevproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | .dmypy.json 105 | dmypy.json 106 | 107 | # Pyre type checker 108 | .pyre/ 109 | 110 | venv/ 111 | env/ 112 | .venv/ 113 | .env/ 114 | 115 | .vscode/ 116 | 117 | # End of https://www.gitignore.io/api/python -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :snake: pyenvcomp 2 | 3 | The python virtual environment comparator tool. Compare two python virtualenvs and find out the differences. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | pip install pyenvcomp 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```bash 14 | compare path1 path2 --display [all|diff|separate] 15 | ``` 16 | 17 | **path1** - path to one virtual environment. 18 | 19 | **path2** - path to another virtual environment. 20 | 21 | **List of display types available:** 22 | 23 | 1. `all` - displays all the differences, similarities and extra modules in each virtual environments. This is the default display option. 24 | 2. `diff` - displays just the list of modules which are present in both the virtual environments but have different versions. 25 | 3. `separate` - displays two different tables of extra modules in each virtual environments. 26 | 27 | ```bash 28 | usage: compare [-h] [-d DISPLAY] env path1 env path2 29 | 30 | positional arguments: 31 | env path1 location of the first virtual environment 32 | env path2 location of the second virtual environment 33 | 34 | optional arguments: 35 | -h, --help show this help message and exit 36 | -d DISPLAY, --display DISPLAY 37 | Compare envs based on either of these available options [all|diff|separate] 38 | ``` 39 | 40 | ## :man_technologist: Example 41 | 42 | Input: 43 | 44 | ```bash 45 | compare /home/koustav/Documents/test_env_1 /home/koustav/Documents/test_env_2 --display all 46 | ``` 47 | 48 | where, `test_env_1` and `test_env_2` are two python virtual environments. 49 | 50 | Output: 51 | 52 | ```bash 53 | SAME MODULE VERSIONS 54 | ╔═════════════════╤═══════════════════════╤═══════════════════════╗ 55 | ║ Module │ test_env_1(python3.8) │ test_env_2(python3.8) ║ 56 | ╠═════════════════╪═══════════════════════╪═══════════════════════╣ 57 | ║ appdirs │ 1.4.3 │ 1.4.3 ║ 58 | ║ CacheControl │ 0.12.6 │ 0.12.6 ║ 59 | ║ certifi │ 2019.11.28 │ 2019.11.28 ║ 60 | ║ chardet │ 3.0.4 │ 3.0.4 ║ 61 | ║ colorama │ 0.4.3 │ 0.4.3 ║ 62 | ║ contextlib2 │ 0.6.0 │ 0.6.0 ║ 63 | ║ distlib │ 0.3.0 │ 0.3.0 ║ 64 | ║ distro │ 1.4.0 │ 1.4.0 ║ 65 | ║ html5lib │ 1.0.1 │ 1.0.1 ║ 66 | ║ idna │ 2.8 │ 2.8 ║ 67 | ║ ipaddr │ 2.2.0 │ 2.2.0 ║ 68 | ║ lockfile │ 0.12.2 │ 0.12.2 ║ 69 | ║ msgpack │ 0.6.2 │ 0.6.2 ║ 70 | ║ packaging │ 20.3 │ 20.3 ║ 71 | ║ pep517 │ 0.8.2 │ 0.8.2 ║ 72 | ║ pip │ 20.0.2 │ 20.0.2 ║ 73 | ║ pkg_resources │ 0.0.0 │ 0.0.0 ║ 74 | ║ progress │ 1.5 │ 1.5 ║ 75 | ║ pyparsing │ 2.4.6 │ 2.4.6 ║ 76 | ║ python_dateutil │ 2.8.1 │ 2.8.1 ║ 77 | ║ pytoml │ 0.1.21 │ 0.1.21 ║ 78 | ║ pytz │ 2020.1 │ 2020.1 ║ 79 | ║ requests │ 2.22.0 │ 2.22.0 ║ 80 | ║ retrying │ 1.3.3 │ 1.3.3 ║ 81 | ║ setuptools │ 44.0.0 │ 44.0.0 ║ 82 | ║ six │ 1.14.0 │ 1.14.0 ║ 83 | ║ urllib3 │ 1.25.8 │ 1.25.8 ║ 84 | ║ webencodings │ 0.5.1 │ 0.5.1 ║ 85 | ║ wheel │ 0.34.2 │ 0.34.2 ║ 86 | ╚═════════════════╧═══════════════════════╧═══════════════════════╝ 87 | 88 | DIFFERENT MODULE VERSIONS 89 | ╔════════╤═══════════════════════╤═══════════════════════╗ 90 | ║ Module │ test_env_1(python3.8) │ test_env_2(python3.8) ║ 91 | ╠════════╪═══════════════════════╪═══════════════════════╣ 92 | ║ numpy │ 1.19.2 │ 1.19.1 ║ 93 | ║ pandas │ 1.1.2 │ 1.1.3 ║ 94 | ╚════════╧═══════════════════════╧═══════════════════════╝ 95 | 96 | ONLY IN test_env_1 (python3.8) 97 | ╔═══════════════════════╤═════════╗ 98 | ║ test_env_1(python3.8) │ version ║ 99 | ╠═══════════════════════╪═════════╣ 100 | ║ wrapt │ 1.12.1 ║ 101 | ║ pikepdf │ 1.19.3 ║ 102 | ║ lazy_object_proxy │ 1.4.3 ║ 103 | ║ pylint │ 2.6.0 ║ 104 | ║ toml │ 0.10.1 ║ 105 | ║ Pillow │ 7.2.0 ║ 106 | ║ lxml │ 4.5.2 ║ 107 | ║ astroid │ 2.4.2 ║ 108 | ║ isort │ 5.6.4 ║ 109 | ║ mccabe │ 0.6.1 ║ 110 | ╚═══════════════════════╧═════════╝ 111 | 112 | ONLY IN test_env_2 (python3.8) 113 | ╔═══════════════════════╤═════════╗ 114 | ║ test_env_2(python3.8) │ version ║ 115 | ╠═══════════════════════╪═════════╣ 116 | ║ Werkzeug │ 1.0.1 ║ 117 | ║ MarkupSafe │ 1.1.1 ║ 118 | ║ tornado │ 6.0.4 ║ 119 | ║ itsdangerous │ 1.1.0 ║ 120 | ║ click │ 7.1.2 ║ 121 | ║ Flask │ 1.1.2 ║ 122 | ║ Jinja2 │ 2.11.2 ║ 123 | ╚═══════════════════════╧═════════╝ 124 | ``` 125 | 126 | Visuals in the terminal would look slightly different than the above output visual. 127 | 128 | ## Future versions 129 | 130 | In the upcoming versions following features will be added: 131 | 132 | - [ ] Compare directly from `requirements.txt` file 133 | - [ ] Warning messages will be provided if any deprecated version of any module is being used. 134 | 135 | Inspired by problems faced while doing R&D at my workplace! :nerd_face: 136 | -------------------------------------------------------------------------------- /pyenvcomp/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.0.3" 2 | -------------------------------------------------------------------------------- /pyenvcomp/main.py: -------------------------------------------------------------------------------- 1 | """PYENVCOMP 2 | """ 3 | 4 | import os 5 | import sys 6 | import argparse 7 | import subprocess 8 | import tableformatter as tf 9 | from typing import List 10 | 11 | 12 | class Colors: 13 | """Color listing class""" 14 | 15 | HEADER = "\033[95m" 16 | OKBLUE = "\033[94m" 17 | OKGREEN = "\033[92m" 18 | WARNING = "\033[93m" 19 | FAIL = "\033[91m" 20 | ENDC = "\033[0m" 21 | BOLD = "\033[1m" 22 | UNDERLINE = "\033[4m" 23 | END = "\033[0m" 24 | 25 | 26 | # remove all colors if the platform is not linux 27 | if sys.platform != 'linux': 28 | colors = [ 29 | var 30 | for var, _ in Colors.__dict__.items() 31 | if not var.startswith("__") 32 | ] 33 | for var in colors: 34 | setattr(Colors, var, '') 35 | 36 | class ArgParse: 37 | """ 38 | Argument parsing class 39 | """ 40 | 41 | def __init__(self): 42 | """init function""" 43 | pass 44 | 45 | def parse_args(self): 46 | """Parses the commandline argument""" 47 | 48 | parser = argparse.ArgumentParser() 49 | 50 | parser.add_argument( 51 | "path1", 52 | metavar="env path1", 53 | type=str, 54 | help="location of the first virtual environment", 55 | ) 56 | parser.add_argument( 57 | "path2", 58 | metavar="env path2", 59 | type=str, 60 | help="location of the second virtual environment", 61 | ) 62 | parser.add_argument( 63 | "-d", 64 | "--display", 65 | type=str, 66 | help="Compare envs based on either of these available options [all|diff|separate]", 67 | ) 68 | 69 | args = parser.parse_args() 70 | self.path1 = args.path1 71 | self.path2 = args.path2 72 | self.display = args.display 73 | 74 | 75 | def envs_display(env1_path, env2_path, heading, diff_or_similar_list, similar: bool): 76 | """Displays the table of similar or different module versions.""" 77 | color = Colors.OKGREEN if similar else Colors.WARNING 78 | title = "SAME MODULE VERSIONS " if similar else "DIFFERENT MODULE VERSIONS " 79 | 80 | print(color + title + Colors.END) 81 | print(f'{env1_path.split(os.sep)[-1]} - {env1_path}') 82 | print(f'{env2_path.split(os.sep)[-1]} - {env2_path}') 83 | print(tf.generate_table(diff_or_similar_list, heading)) 84 | 85 | 86 | def env_display(env_name, env_py_version, env_path, heading, modules: list): 87 | """Displays the table of a single environment having extra modules.""" 88 | print( 89 | "ONLY IN " 90 | + Colors.BOLD 91 | + Colors.OKGREEN 92 | + f"{env_name} ({env_py_version})" 93 | + Colors.END 94 | ) 95 | print(f'({env_path})') 96 | print(tf.generate_table(modules, heading)) 97 | 98 | 99 | def env_map(modules: List[str]) -> dict: 100 | """Creates a dict of module to version for an environment""" 101 | mod_to_ver = {} 102 | for module in modules: 103 | try: 104 | mod, version, *_ = module.split("-") 105 | mod_to_ver[mod] = version 106 | except Exception: 107 | pass 108 | return mod_to_ver 109 | 110 | 111 | def get_raw_modules(path, env_py_version=None): 112 | """Returns raw list of modules along with versions in an environment""" 113 | if sys.platform == 'linux': 114 | env_modules = subprocess.check_output( 115 | f"ls -d {str(path)}/lib/{env_py_version}/site-packages/*.dist-info | xargs -I% basename % | sed 's/\.dist-info//;' ", 116 | shell=True, 117 | ) 118 | return env_modules.decode("utf-8").split("\n") 119 | else: 120 | env_modules = os.listdir(path + os.sep + "Lib" + os.sep + "site-packages") 121 | return [ 122 | module[: module.rfind(".")] 123 | for module in env_modules 124 | if "dist-info" in module 125 | ] 126 | 127 | 128 | def get_python_version(path): 129 | """Returns the python version of the environment""" 130 | if sys.platform == 'linux': 131 | return os.listdir(path + os.sep + "/lib")[0] 132 | else: 133 | py_ver_config_path = path + os.sep + 'pyvenv.cfg' 134 | with open(py_ver_config_path) as f: 135 | version = None 136 | for line in f: 137 | if line.lower().startswith('version'): 138 | version = line.split('=')[-1].strip() 139 | break 140 | return version 141 | 142 | 143 | def main(): 144 | """ 145 | The main comparing function 146 | """ 147 | arg_parse = ArgParse() 148 | arg_parse.parse_args() 149 | args = vars(arg_parse) 150 | 151 | env1_path = args["path1"] 152 | env2_path = args["path2"] 153 | display = args["display"] 154 | 155 | env1_name = env1_path[env1_path.rfind("/") + 1 :] 156 | env2_name = env2_path[env2_path.rfind("/") + 1 :] 157 | 158 | env1_py_version = get_python_version(env1_path) 159 | env2_py_version = get_python_version(env2_path) 160 | env1_modules = get_raw_modules(env1_path, env1_py_version) 161 | env2_modules = get_raw_modules(env2_path, env2_py_version) 162 | 163 | env1_map = {} 164 | env2_map = {} 165 | 166 | env1_map = env_map(env1_modules) 167 | env2_map = env_map(env2_modules) 168 | 169 | env1_modules = set(env1_map.keys()) 170 | env2_modules = set(env2_map.keys()) 171 | 172 | common_modules = env1_modules & env2_modules 173 | only_in_env1 = env1_modules - env2_modules 174 | only_in_env2 = env2_modules - env1_modules 175 | 176 | similar_list = [] 177 | non_similar_list = [] 178 | for module in common_modules: 179 | if env1_map[module] == env2_map[module]: 180 | similar_list.append([module, env1_map[module], env2_map[module]]) 181 | else: 182 | non_similar_list.append([module, env1_map[module], env2_map[module]]) 183 | 184 | only_env_1_list = [] 185 | only_env_2_list = [] 186 | 187 | for module in only_in_env1: 188 | only_env_1_list.append([module, env1_map[module]]) 189 | 190 | for module in only_in_env2: 191 | only_env_2_list.append([module, env2_map[module]]) 192 | 193 | heading = [ 194 | "Module", 195 | f"{env1_name} ({env1_py_version})", 196 | f"{env2_name} ({env2_py_version})", 197 | ] 198 | env1_heading = [f"{env1_name}({env1_py_version})", "version"] 199 | env2_heading = [f"{env2_name}({env2_py_version})", "version"] 200 | print( 201 | ''' 202 | ______ _________ ___ ____________ __ _______ 203 | / __ \ \/ / ____/ | / / | / / ____/ __ \/ |/ / __ \\ 204 | / /_/ /\ / __/ / |/ /| | / / / / / / / /|_/ / /_/ / 205 | / ____/ / / /___/ /| / | |/ / /___/ /_/ / / / / ____/ 206 | /_/ /_/_____/_/ |_/ |___/\____/\____/_/ /_/_/ 207 | 208 | ''' 209 | ) 210 | try: 211 | if display in ["all", None]: 212 | envs_display(env1_path, env2_path, heading, similar_list, similar=True) 213 | envs_display(env1_path, env2_path, heading, non_similar_list, similar=False) 214 | env_display( 215 | env1_name, env1_py_version, env1_path, env1_heading, only_env_1_list 216 | ) 217 | env_display( 218 | env2_name, env2_py_version, env2_path, env2_heading, only_env_2_list 219 | ) 220 | elif display == "diff": 221 | envs_display(env1_path, env2_path, heading, non_similar_list, similar=False) 222 | elif display == "similar": 223 | envs_display(env1_path, env2_path, heading, similar_list, similar=True) 224 | elif display == "separate": 225 | env_display( 226 | env1_name, env1_py_version, env1_path, env1_heading, only_env_1_list 227 | ) 228 | env_display( 229 | env2_name, env2_py_version, env2_path, env2_heading, only_env_2_list 230 | ) 231 | except Exception as exception: 232 | print(exception) 233 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.3 2 | CacheControl==0.12.6 3 | certifi==2019.11.28 4 | chardet==3.0.4 5 | colorama==0.4.3 6 | contextlib2==0.6.0 7 | distlib==0.3.0 8 | distro==1.4.0 9 | html5lib==1.0.1 10 | idna==2.8 11 | ipaddr==2.2.0 12 | lockfile==0.12.2 13 | msgpack==0.6.2 14 | packaging==20.3 15 | pep517==0.8.2 16 | progress==1.5 17 | pyparsing==2.4.6 18 | pytoml==0.1.21 19 | requests==2.22.0 20 | retrying==1.3.3 21 | six==1.14.0 22 | tableformatter==0.1.5 23 | urllib3==1.25.8 24 | wcwidth==0.2.5 25 | webencodings==0.5.1 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from setuptools import setup 3 | from pyenvcomp import __version__ 4 | 5 | with open('README.md', encoding='utf-8') as readme_file: 6 | long_description = readme_file.read() 7 | 8 | # This call to setup() does all the work 9 | setup( 10 | name="pyenvcomp", 11 | version=__version__, 12 | description="Detailed display of the difference between two given python virtual environments.", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | url="https://github.com/KoustavCode/pyenvcomp.git", 16 | author="Koustav Chanda", 17 | author_email="koustavtocode@gmail.com", 18 | license="MIT", 19 | classifiers=[ 20 | "License :: OSI Approved :: MIT License", 21 | "Programming Language :: Python :: 3", 22 | "Programming Language :: Python :: 3.8", 23 | ], 24 | packages=["pyenvcomp"], 25 | include_package_data=True, 26 | install_requires=['tableformatter','colored','colorama'], 27 | entry_points={ 28 | "console_scripts": [ 29 | "compare=pyenvcomp.main:main", 30 | ] 31 | }, 32 | ) --------------------------------------------------------------------------------