├── tests ├── __init__.py ├── test_worker.py ├── test_cli.py └── test_utils.py ├── dep_license ├── VERSION ├── __main__.py ├── utils.py └── __init__.py ├── requirements-dev.txt ├── MANIFEST.in ├── setup.cfg ├── requirements.txt ├── Dockerfile ├── .github └── workflows │ ├── ci-pre-commit.yml │ └── ci.yml ├── .pre-commit-hooks.yaml ├── release.sh ├── .pre-commit-config.yaml ├── LICENSE ├── setup.py ├── .gitignore └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dep_license/VERSION: -------------------------------------------------------------------------------- 1 | v2.5.0 2 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include dep_license/VERSION 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 119 3 | 4 | [pep8] 5 | ignore = E265,E501,W504 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | gitdb>=4.0.4 2 | GitPython>=3.1.1 3 | smmap>=3.0.2 4 | tabulate>=0.8.7 5 | PyYAML>=5.3.1 6 | toml 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | RUN apk --update add git && \ 3 | pip install -U pip && \ 4 | pip install dep-license 5 | 6 | CMD ["deplic"] 7 | -------------------------------------------------------------------------------- /dep_license/__main__.py: -------------------------------------------------------------------------------- 1 | from dep_license import run 2 | 3 | 4 | def main(): 5 | return run() 6 | 7 | 8 | if __name__ == "__main__": 9 | exit(main()) 10 | -------------------------------------------------------------------------------- /.github/workflows/ci-pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | checks: 7 | name: "pre-commit hooks" 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-python@v2 12 | - uses: pre-commit/action@v2.0.0 13 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: deplic 2 | name: dep-license 3 | description: "dep-license: Report license information for dependencies in use by a Python project" 4 | entry: deplic 5 | language: python 6 | require_serial: true 7 | types: [text] 8 | files: (requirements.*\.txt$|setup.py$|Pipfile$|pyproject.toml$|conda.yaml$) 9 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | set -ex 2 | 3 | git pull origin main 4 | # bump version 5 | docker run --rm -v "$PWD":/app treeder/bump --filename dep_license/VERSION $1 6 | version=`cat dep_license/VERSION` 7 | echo "version: $version" 8 | 9 | # tag it 10 | git commit -a -m "version $version" 11 | git tag -a "$version" -m "version $version" 12 | git push 13 | git push --tags 14 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v2.4.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | 12 | - repo: https://github.com/asottile/reorder_python_imports 13 | rev: v2.3.0 14 | hooks: 15 | - id: reorder-python-imports 16 | 17 | - repo: https://gitlab.com/pycqa/flake8 18 | rev: 3.8.0 19 | hooks: 20 | - id: flake8 21 | 22 | - repo: https://github.com/psf/black 23 | rev: 22.3.0 24 | hooks: 25 | - id: black 26 | -------------------------------------------------------------------------------- /tests/test_worker.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from dep_license import start_concurrent 4 | from dep_license import worker 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "dependency,expected", 9 | [ 10 | ( 11 | "dep_license", 12 | { 13 | "Name": "dep_license", 14 | "Meta": "MIT", 15 | "Classifier": "OSI Approved::MIT License", 16 | }, 17 | ), 18 | ("SomethingThatDoesntExist", None), 19 | ], 20 | ) 21 | def test_worker(dependency, expected): 22 | assert worker(dependency) == expected 23 | 24 | 25 | def test_concurrent_workers(): 26 | results = start_concurrent(["dep_license", "SomethingElseThatDoesntExist"]) 27 | assert results == [ 28 | { 29 | "Name": "dep_license", 30 | "Meta": "MIT", 31 | "Classifier": "OSI Approved::MIT License", 32 | } 33 | ] 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Abdulelah Bin Mahfoodh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: CI 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | test: 10 | runs-on: ${{ matrix.os }} 11 | 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: ["ubuntu-latest", "windows-latest"] 16 | python-version: [3.7, 3.8, 3.9] 17 | 18 | env: 19 | PYTHON_VERSION: ${{ matrix.python-version }} 20 | PARALLEL: "true" 21 | COVERAGE: "false" 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v2 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | - name: Install dependencies 30 | shell: bash -l {0} 31 | run: | 32 | python -m pip install --upgrade pip 33 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 34 | if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi 35 | - name: Test with pytest 36 | shell: bash -l {0} 37 | run: | 38 | pytest 39 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | 5 | import pytest 6 | 7 | from dep_license import __version__ 8 | from dep_license import run 9 | 10 | project = os.path.dirname(os.path.dirname(__file__)) 11 | 12 | 13 | def test_version(capsys): 14 | with pytest.raises(SystemExit): 15 | run(["-v"]) 16 | out, _ = capsys.readouterr() 17 | assert out.strip() == __version__ 18 | 19 | 20 | def test_local_dir(capsys): 21 | ret = run([project]) 22 | out, _ = capsys.readouterr() 23 | assert ret == 0 24 | assert "Found" in out 25 | 26 | 27 | def test_local_file(capsys): 28 | ret = run([os.path.join(project, "requirements.txt")]) 29 | out, _ = capsys.readouterr() 30 | assert ret == 0 31 | assert "Found" in out 32 | 33 | 34 | def test_format(capsys): 35 | ret = run([project, "-f", "json"]) 36 | out, _ = capsys.readouterr() 37 | assert ret == 0 38 | output = json.loads("".join(out.splitlines()[2:])) 39 | assert isinstance(output, list) 40 | assert isinstance(output[0], dict) 41 | 42 | 43 | def test_output(tmpdir, capsys): 44 | x = tmpdir.join("output") 45 | ret = run([project, "-o", x.strpath, "-f", "json"]) 46 | out, _ = capsys.readouterr() 47 | assert ret == 0 48 | assert os.path.isfile(x.strpath) 49 | with open(x.strpath) as f: 50 | assert isinstance(json.load(f), list) 51 | 52 | 53 | def test_name_dependency_file(capsys): 54 | ret = run([project, "-n", "requirements.txt"]) 55 | out, _ = capsys.readouterr() 56 | assert ret == 0 57 | assert "Found" in out 58 | 59 | 60 | def test_check_banned(tmpdir, capsys): 61 | x = tmpdir.join("deplic.cfg") 62 | x.write( 63 | """ 64 | [deplic] 65 | banned = MIT 66 | """ 67 | ) 68 | ret = run([project, "-c", x.strpath]) 69 | out, _ = capsys.readouterr() 70 | assert ret == 1 71 | 72 | 73 | def test_check_env(capsys): 74 | ret = run([sys.executable, "-e"]) 75 | out, _ = capsys.readouterr() 76 | assert ret == 0 77 | assert "Found" in out 78 | 79 | 80 | def test_check_invalid_env(capsys): 81 | ret = run([os.path.dirname("/invalid/venv/path"), "-e"]) 82 | out, _ = capsys.readouterr() 83 | assert ret == 1 84 | 85 | 86 | def test_with_remote_git_repo(capsys): 87 | ret = run(["https://github.com/abduhbm/dep-license"]) 88 | out, _ = capsys.readouterr() 89 | assert ret == 0 90 | assert "Found" in out 91 | 92 | 93 | def test_invalid_project(capsys): 94 | ret = run(["SomethingDoesntExist"]) 95 | out, _ = capsys.readouterr() 96 | assert ret == 1 97 | assert out == "no dependencies found\n" 98 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import io 4 | import os 5 | import sys 6 | from shutil import rmtree 7 | 8 | from setuptools import Command 9 | from setuptools import find_packages 10 | from setuptools import setup 11 | 12 | NAME = "dep_license" 13 | DESCRIPTION = "Report licenses information for dependencies in use by a Python project" 14 | URL = "https://github.com/abduhbm/dep-license" 15 | AUTHOR = "Abdulelah Bin Mahfoodh" 16 | 17 | REQUIRED = ["tabulate", "GitPython", "toml", "PyYAML"] 18 | 19 | here = os.path.abspath(os.path.dirname(__file__)) 20 | 21 | with io.open(os.path.join(here, "README.md"), encoding="utf-8") as f: 22 | long_description = "\n" + f.read() 23 | 24 | with open(os.path.join(here, NAME, "VERSION")) as version_file: 25 | version = version_file.read().strip() 26 | 27 | 28 | class UploadCommand(Command): 29 | """Support setup.py upload.""" 30 | 31 | description = "Build and publish the package." 32 | user_options = [] 33 | 34 | @staticmethod 35 | def status(s): 36 | """Prints things in bold.""" 37 | print("\033[1m{0}\033[0m".format(s)) 38 | 39 | def initialize_options(self): 40 | pass 41 | 42 | def finalize_options(self): 43 | pass 44 | 45 | def run(self): 46 | try: 47 | self.status("Removing previous builds…") 48 | rmtree(os.path.join(here, "dist")) 49 | except OSError: 50 | pass 51 | 52 | self.status("Building Source and Wheel (universal) distribution…") 53 | os.system("{0} setup.py sdist bdist_wheel --universal".format(sys.executable)) 54 | 55 | self.status("Uploading the package to PyPi via Twine…") 56 | os.system("twine upload dist/*") 57 | 58 | sys.exit() 59 | 60 | 61 | setup( 62 | name=NAME, 63 | version=version, 64 | description=DESCRIPTION, 65 | long_description=long_description, 66 | long_description_content_type="text/markdown", 67 | author=AUTHOR, 68 | # author_email=EMAIL, 69 | url=URL, 70 | packages=find_packages(exclude=["tests"]), 71 | include_package_data=True, 72 | install_requires=REQUIRED, 73 | license="MIT", 74 | keywords="license check dependency package report", 75 | classifiers=[ 76 | "Development Status :: 3 - Alpha", 77 | "Intended Audience :: Developers", 78 | "License :: OSI Approved :: MIT License", 79 | "Programming Language :: Python", 80 | "Programming Language :: Python :: 3", 81 | "Programming Language :: Python :: 3.6", 82 | "Programming Language :: Python :: 3.7", 83 | "Programming Language :: Python :: 3.8", 84 | "Programming Language :: Python :: 3.9", 85 | "Topic :: Utilities", 86 | "Topic :: System :: System Shells", 87 | ], 88 | # $ setup.py publish support. 89 | cmdclass={"upload": UploadCommand}, 90 | entry_points={"console_scripts": ["deplic=dep_license.__main__:main"]}, 91 | ) 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | 127 | ### JetBrains template 128 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 129 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 130 | 131 | # User-specific stuff 132 | .idea/**/workspace.xml 133 | .idea/**/tasks.xml 134 | .idea/**/usage.statistics.xml 135 | .idea/**/dictionaries 136 | .idea/**/shelf 137 | 138 | # Generated files 139 | .idea/**/contentModel.xml 140 | 141 | # Sensitive or high-churn files 142 | .idea/**/dataSources/ 143 | .idea/**/dataSources.ids 144 | .idea/**/dataSources.local.xml 145 | .idea/**/sqlDataSources.xml 146 | .idea/**/dynamic.xml 147 | .idea/**/uiDesigner.xml 148 | .idea/**/dbnavigator.xml 149 | 150 | # Gradle 151 | .idea/**/gradle.xml 152 | .idea/**/libraries 153 | 154 | .idea 155 | .vscode 156 | .DS_Store 157 | 158 | # Gradle and Maven with auto-import 159 | # When using Gradle or Maven with auto-import, you should exclude module files, 160 | # since they will be recreated, and may cause churn. Uncomment if using 161 | # auto-import. 162 | # .idea/modules.xml 163 | # .idea/*.iml 164 | # .idea/modules 165 | # *.iml 166 | # *.ipr 167 | 168 | # CMake 169 | cmake-build-*/ 170 | 171 | # Mongo Explorer plugin 172 | .idea/**/mongoSettings.xml 173 | 174 | # File-based project format 175 | *.iws 176 | 177 | # IntelliJ 178 | out/ 179 | 180 | # mpeltonen/sbt-idea plugin 181 | .idea_modules/ 182 | 183 | # JIRA plugin 184 | atlassian-ide-plugin.xml 185 | 186 | # Cursive Clojure plugin 187 | .idea/replstate.xml 188 | 189 | # Crashlytics plugin (for Android Studio and IntelliJ) 190 | com_crashlytics_export_strings.xml 191 | crashlytics.properties 192 | crashlytics-build.properties 193 | fabric.properties 194 | 195 | # Editor-based Rest Client 196 | .idea/httpRequests 197 | 198 | # Android studio 3.1+ serialized cache file 199 | .idea/caches/build_file_checksums.ser 200 | 201 | ### VirtualEnv template 202 | # Virtualenv 203 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 204 | .Python 205 | [Bb]in 206 | [Ii]nclude 207 | [Ll]ib 208 | [Ll]ib64 209 | [Ll]ocal 210 | [Ss]cripts 211 | pyvenv.cfg 212 | .venv 213 | pip-selfcheck.json 214 | 215 | deplic.cfg 216 | -------------------------------------------------------------------------------- /dep_license/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import sys 5 | from collections import OrderedDict 6 | 7 | import pkg_resources 8 | import toml 9 | import yaml 10 | from distutils.core import run_setup 11 | 12 | logger = logging.getLogger("__name__") 13 | 14 | 15 | def parse_file(input_file, base_name, dev=False): 16 | try: 17 | if base_name == "Pipfile": 18 | return parse_pip_file(input_file, dev=dev) 19 | 20 | elif base_name == "Pipfile.lock": 21 | return parse_pip_lock_file(input_file, dev=dev) 22 | 23 | elif base_name == "setup.py": 24 | return parse_setup_file(input_file) 25 | 26 | elif base_name == "pyproject.toml": 27 | return parse_pyproject_file(input_file) + parse_pyproject_file_poetry( 28 | input_file 29 | ) 30 | 31 | elif base_name == "conda.yml": 32 | return parse_conda_yaml_file(input_file) 33 | 34 | elif base_name == "poetry.lock": 35 | return parse_poetry_lock_file(input_file) 36 | else: 37 | return parse_req_file(input_file) 38 | except Exception as e: 39 | logger.error(f"{base_name}: {e}") 40 | return [] 41 | 42 | 43 | def parse_req_file(input_file): 44 | output = [] 45 | with open(input_file) as f: 46 | lines = [x for x in f.readlines() if not x.startswith("-")] 47 | for line in lines: 48 | try: 49 | for r in pkg_resources.parse_requirements(line): 50 | output.append(r.name) 51 | except pkg_resources.RequirementParseError: 52 | pass 53 | 54 | return output 55 | 56 | 57 | def parse_pip_file(input_file, dev=False): 58 | output = [] 59 | cf = toml.load(input_file) 60 | output += list(cf.get("packages", {}).keys()) 61 | if dev: 62 | output += list(cf.get("dev-packages", {}).keys()) 63 | return output 64 | 65 | 66 | def parse_pip_lock_file(input_file, dev=False): 67 | output = [] 68 | r_type = ["default"] 69 | if dev: 70 | r_type.append("develop") 71 | with open(input_file, "r") as f: 72 | cf = json.load(f, object_pairs_hook=OrderedDict) 73 | if cf: 74 | for t in r_type: 75 | output.extend(list(cf[t].keys())) 76 | 77 | return output 78 | 79 | 80 | def parse_pyproject_file(input_file): 81 | output = [] 82 | cf = toml.load(input_file) 83 | reqs = cf.get("build-system", {}).get("requires", []) 84 | for i in pkg_resources.parse_requirements(reqs): 85 | output.append(i.project_name) 86 | return output 87 | 88 | 89 | def parse_pyproject_file_poetry(input_file): 90 | cf = toml.load(input_file) 91 | return [ 92 | k 93 | for k, v in cf.get("tool", {}).get("poetry", {}).get("dependencies", {}).items() 94 | if not (isinstance(v, dict) and "path" in v) and k not in ["python"] 95 | ] 96 | 97 | 98 | def parse_poetry_lock_file(input_file): 99 | output = [] 100 | cf = toml.load(input_file) 101 | output = [pkg["name"] for pkg in cf.get("package", []) if "name" in pkg] 102 | return output 103 | 104 | 105 | def parse_setup_file(input_file): 106 | output = [] 107 | cur_dir = os.getcwd() 108 | setup_dir = os.path.abspath(os.path.dirname(input_file)) 109 | sys.path.append(setup_dir) 110 | os.chdir(setup_dir) 111 | try: 112 | setup = run_setup(input_file, stop_after="config") 113 | except Exception as e: 114 | logger.error(f"run_setup: {e}") 115 | return [] 116 | 117 | reqs_var = ["install_requires", "setup_requires", "extras_require"] 118 | for v in reqs_var: 119 | reqs = getattr(setup, v) 120 | if isinstance(reqs, list): 121 | for i in pkg_resources.parse_requirements(reqs): 122 | output.append(i.project_name) 123 | 124 | elif isinstance(reqs, dict): 125 | for i in pkg_resources.parse_requirements( 126 | {v for req in reqs.values() for v in req} 127 | ): 128 | output.append(i.project_name) 129 | os.chdir(cur_dir) 130 | return output 131 | 132 | 133 | def parse_conda_yaml_file(input_file): 134 | output = [] 135 | with open(input_file, "r") as f: 136 | cf = yaml.safe_load(f) 137 | if cf and "dependencies" in cf and isinstance(cf["dependencies"], list): 138 | reqs = [] 139 | for r in cf["dependencies"]: 140 | if isinstance(r, dict) and "pip" in r: 141 | for i in r["pip"]: 142 | reqs.append(i) 143 | for i in pkg_resources.parse_requirements(reqs): 144 | output.append(i.project_name) 145 | 146 | return output 147 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from dep_license import utils 4 | 5 | 6 | def test_parsing_requirement_file(tmpdir): 7 | x = tmpdir.join("requirements.txt") 8 | x.write("dep_license==0.0.0\n" "pytest") 9 | assert utils.parse_req_file(x.strpath) == ["dep_license", "pytest"] 10 | 11 | 12 | def test_parsing_pip_file(tmpdir): 13 | x = tmpdir.join("Pipfile") 14 | x.write( 15 | """ 16 | [[source]] 17 | url = "https://pypi.org/simple" 18 | verify_ssl = true 19 | name = "pypi" 20 | [packages] 21 | flask = "*" 22 | dep_license = "*" 23 | [dev-packages] 24 | pytest = "*" 25 | [requires] 26 | python_version = "3.7" 27 | """ 28 | ) 29 | assert utils.parse_pip_file(x.strpath) == ["flask", "dep_license"] 30 | assert utils.parse_pip_file(x.strpath, dev=True) == [ 31 | "flask", 32 | "dep_license", 33 | "pytest", 34 | ] 35 | 36 | 37 | def test_parsing_pip_lock_file(tmpdir): 38 | x = tmpdir.join("Pipfile.lock") 39 | x.write( 40 | """ 41 | { 42 | "_meta": { 43 | "hash": { 44 | "sha256": "6e593b27afd61ba861fc65b67a1b68a2ff296053aa3f08a628a27174df727652" 45 | }, 46 | "pipfile-spec": 6, 47 | "requires": {}, 48 | "sources": [ 49 | { 50 | "name": "pypi", 51 | "url": "https://pypi.org/simple", 52 | "verify_ssl": true 53 | } 54 | ] 55 | }, 56 | "default": { 57 | "certifi": { 58 | "hashes": [ 59 | "sha256:54a07c09c586b0e4c619f02a5e94e36619da8e2b053e20f594348c0611803704", 60 | "sha256:40523d2efb60523e113b44602298f0960e900388cf3bb6043f645cf57ea9e3f5" 61 | ], 62 | "version": "==2017.7.27.1" 63 | } 64 | }, 65 | "develop": { 66 | "pytest": { 67 | "hashes": [ 68 | "sha256:b84f554f8ddc23add65c411bf112b2d88e2489fd45f753b1cae5936358bdf314", 69 | "sha256:f46e49e0340a532764991c498244a60e3a37d7424a532b3ff1a6a7653f1a403a" 70 | ], 71 | "version": "==3.2.2" 72 | } 73 | } 74 | } 75 | """ 76 | ) 77 | assert utils.parse_pip_lock_file(x.strpath) == ["certifi"] 78 | assert utils.parse_pip_lock_file(x.strpath, dev=True) == ["certifi", "pytest"] 79 | 80 | 81 | def test_parsing_setup_file(tmpdir): 82 | x = tmpdir.join("setup.py") 83 | x.write( 84 | "from setuptools import setup, find_packages\n" 85 | 'REQUIRED = ["tabulate", "numpy", "toml"]\n' 86 | 'setup(name="foo", version="1.0", install_requires=REQUIRED)' 87 | ) 88 | assert utils.parse_setup_file(x.strpath) == ["tabulate", "numpy", "toml"] 89 | 90 | 91 | def test_parsing_pyproject_file(tmpdir): 92 | x = tmpdir.join("pyproject.toml") 93 | x.write( 94 | """ 95 | [build-system] 96 | requires = ["setuptools", "wheel", "numpy"] 97 | """ 98 | ) 99 | assert utils.parse_pyproject_file(x.strpath) == ["setuptools", "wheel", "numpy"] 100 | 101 | 102 | def test_parsing_pyproject_file_poetry(tmpdir): 103 | x = tmpdir.join("pyproject.toml") 104 | x.write( 105 | """ 106 | [tool.poetry.dependencies] 107 | python = ">=3.8,<3.11" 108 | pandas = "^1.4.3" 109 | fastapi = "^0.79.0" 110 | numpy = "^1.23.1" 111 | 112 | mkdocs = { version = "^1.1.2", optional = true} 113 | jinja2 = { version = "^2.10.2", optional = true } 114 | MarkupSafe = { version = "2.0.1", optional = true } 115 | mkdocs-include-markdown-plugin = { version = "^1.0.0", optional = true} 116 | mkdocs-material = { version = "^6.1.7", optional = true} 117 | mkdocstrings = { version = "^0.17.0", optional = true} 118 | mkdocs-material-extensions = { version = "^1.0.1", optional = true} 119 | mkdocs-autorefs = {version = "^0.2.1", optional = true} 120 | pytkdocs = {extras = ["numpy-style"], version = "^0.15.0", optional = true} 121 | mkdocs-gen-files = {version = "^0.3.3", python = "^3.7", optional = true} 122 | mkdocs-literate-nav = {version = "^0.4.1", optional = true} 123 | bump2version = {version = "^1.0.1", optional = true} 124 | 125 | pytest = { version = "^6.2.4", optional = true} 126 | pytest-cov = { version = "^2.12.0", optional = true} 127 | 128 | types-pyyaml = [{optional = true, version = "*"}] 129 | typing_extensions = [{optional = true, version = "*"}] 130 | mypy = [{optional = true, version = "*"}] 131 | 132 | pandas-stubs = [{optional = true, version = "^1.4.3"}] 133 | """ 134 | ) 135 | assert utils.parse_pyproject_file_poetry(x.strpath) == [ 136 | "pandas", 137 | "fastapi", 138 | "numpy", 139 | "mkdocs", 140 | "jinja2", 141 | "MarkupSafe", 142 | "mkdocs-include-markdown-plugin", 143 | "mkdocs-material", 144 | "mkdocstrings", 145 | "mkdocs-material-extensions", 146 | "mkdocs-autorefs", 147 | "pytkdocs", 148 | "mkdocs-gen-files", 149 | "mkdocs-literate-nav", 150 | "bump2version", 151 | "pytest", 152 | "pytest-cov", 153 | "types-pyyaml", 154 | "typing_extensions", 155 | "mypy", 156 | "pandas-stubs", 157 | ] 158 | 159 | 160 | def test_parsing_conda_yaml_file(tmpdir): 161 | x = tmpdir.join("conda.yml") 162 | x.write( 163 | """ 164 | name: hyperparam_example 165 | channels: 166 | - defaults 167 | - anaconda 168 | - conda-forge 169 | dependencies: 170 | - python=3.6 171 | - pip: 172 | - numpy==1.14.3 173 | - pandas==0.22.0 174 | """ 175 | ) 176 | assert utils.parse_conda_yaml_file(x.strpath) == ["numpy", "pandas"] 177 | 178 | 179 | def test_parsing_poetry_lock_file(tmpdir): 180 | x = tmpdir.join("poetry.lock") 181 | x.write( 182 | """ 183 | [[package]] 184 | name = "anyio" 185 | version = "3.5.0" 186 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 187 | category = "dev" 188 | optional = false 189 | python-versions = ">=3.6.2" 190 | 191 | [[package]] 192 | name = "appnope" 193 | version = "0.1.2" 194 | description = "Disable App Nap on macOS >= 10.9" 195 | category = "dev" 196 | optional = false 197 | python-versions = "*" 198 | """ 199 | ) 200 | assert utils.parse_poetry_lock_file(x.strpath) == ["anyio", "appnope"] 201 | 202 | 203 | @pytest.mark.parametrize( 204 | "f", 205 | ( 206 | "requirements.txt", 207 | "pyproject.toml", 208 | "Pipfile", 209 | "Pipfile.lock", 210 | "conda.yml", 211 | "poetry.lock", 212 | ), 213 | ) 214 | def test_passing_dependency_file(f, tmpdir): 215 | x = tmpdir.join(f) 216 | x.write(" ") 217 | r = utils.parse_file(x.strpath, f) 218 | assert r == [] 219 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dep_license 2 | =========== 3 | 4 | [![CI](https://github.com/abduhbm/dep-license/workflows/CI/badge.svg?branch=master)](https://github.com/abduhbm/dep-license/actions?query=workflow%3A%22CI%22) 5 | [![PyPI version](https://img.shields.io/pypi/v/dep-license.svg)](https://pypi.python.org/pypi/dep-license) 6 | 7 | **dep_license (deplic)**: a simple utility to report licenses information for dependencies in use by a Python project. 8 | 9 | deplic supports reporting dependencies from local project directories, local / remote `git` repos, 10 | or selected virtual environment paths. 11 | 12 | Supported dependency files: 13 | * `setup.py` 14 | * `setup.cfg` 15 | * `requirements.txt` 16 | * `pyproject.toml` 17 | * `Pipfile` 18 | * `Pipfile.lock` 19 | * `conda.yaml` 20 | * `poetry.lock` 21 | 22 | ### Installation 23 | 24 | ``` 25 | $ pip install dep_license 26 | ``` 27 | 28 | ### Command-Line Options 29 | 30 | ``` 31 | usage: deplic [-h] [-w WORKERS] [-f FORMAT] [-o OUTPUT] [-d] [-n NAME] 32 | [-c CHECK] [-e] [-v] 33 | PROJECT [PROJECT ...] 34 | 35 | positional arguments: 36 | PROJECT path to project or its GIT repo 37 | 38 | optional arguments: 39 | -h, --help show this help message and exit 40 | -w WORKERS, --workers WORKERS 41 | number of workers to run in parallel (default: 5) 42 | -f FORMAT, --format FORMAT 43 | define how result is formatted (default: github) 44 | -o OUTPUT, --output OUTPUT 45 | path for output file (default: None) 46 | -d, --dev include dev packages from Pipfile (default: False) 47 | -n NAME, --name NAME name for dependency file (default: None) 48 | -c CHECK, --check CHECK 49 | path to a configuration file to check against banned 50 | licenses (default: None) 51 | -e, --env check against selected python executable (default: 52 | False) 53 | -v, --version show program's version number and exit 54 | ``` 55 | 56 | ### Usage 57 | 58 | Report a list of dependency licenses used in a local project: 59 | ``` 60 | $ deplic /path/to/python/project 61 | Found dependencies: 3 62 | 63 | | Name | Meta | Classifier | 64 | |------------|--------|--------------------------------------------------| 65 | | pandas | BSD | | 66 | | matplotlib | PSF | OSI Approved::Python Software Foundation License | 67 | | numpy | BSD | OSI Approved | 68 | ``` 69 | 70 | Specify the file to be parsed: 71 | 72 | ``` 73 | $ deplic /path/to/python/project/requirements.txt 74 | Found dependencies: 1 75 | 76 | | Name | Meta | Classifier | 77 | |--------|--------|--------------| 78 | | numpy | BSD | OSI Approved | 79 | 80 | ``` 81 | 82 | Support for Pipfile: 83 | ``` 84 | $ deplic /path/to/python/project/Pipfile 85 | Found dependencies: 3 86 | 87 | | Name | Meta | Classifier | 88 | |------------|--------|--------------------------------------------------| 89 | | numpy | BSD | OSI Approved | 90 | | pandas | BSD | | 91 | | matplotlib | PSF | OSI Approved::Python Software Foundation License | 92 | ``` 93 | 94 | Report from selected `virtualenv` path: 95 | ``` 96 | deplic $VIRTUAL_ENV/bin/python --env 97 | Found dependencies: 3 98 | 99 | | Name | Meta | Classifier | 100 | |--------------------|--------------------------------------|--------------------------------------------------| 101 | | smmap | BSD | OSI Approved::BSD License | 102 | | tabulate | MIT | OSI Approved::MIT License | 103 | | six | MIT | OSI Approved::MIT License | 104 | ``` 105 | 106 | Format and store output as JSON file: 107 | ``` 108 | deplic /path/to/python/project -f json -o dep-licenses.json 109 | Found dependencies: 3 110 | 111 | [ 112 | { 113 | "Name": "matplotlib", 114 | "Meta": "PSF", 115 | "Classifier": "OSI Approved::Python Software Foundation License" 116 | }, 117 | { 118 | "Name": "pandas", 119 | "Meta": "BSD", 120 | "Classifier": "" 121 | }, 122 | { 123 | "Name": "numpy", 124 | "Meta": "BSD", 125 | "Classifier": "OSI Approved" 126 | } 127 | ] 128 | ``` 129 | 130 | Get the list dev-packages from the project's GIT repo: 131 | ``` 132 | $ deplic https://github.com/kennethreitz/requests -p 16 -d -f md 133 | Found dependencies: 16 134 | Running with 16 processes... 135 | 136 | Name Meta Classifier 137 | --------------- ------------------------------------------------------------ ------------------------------------- 138 | pytest MIT license OSI Approved::MIT License 139 | codecov http://www.apache.org/licenses/LICENSE-2.0 OSI Approved::Apache Software License 140 | pytest-mock MIT OSI Approved::MIT License 141 | sphinx BSD OSI Approved::BSD License 142 | tox MIT OSI Approved::MIT License 143 | pytest-httpbin MIT OSI Approved::MIT License 144 | docutils public domain, Python, 2-Clause BSD, GPL 3 (see COPYING.txt) Public Domain 145 | pytest-cov MIT OSI Approved::BSD License 146 | pytest-xdist MIT OSI Approved::MIT License 147 | pysocks BSD 148 | httpbin MIT OSI Approved::MIT License 149 | alabaster OSI Approved::BSD License 150 | readme-renderer Apache License, Version 2.0 OSI Approved::Apache Software License 151 | detox MIT OSI Approved::MIT License 152 | 153 | ``` 154 | 155 | Specify which requirements file to parse: 156 | ``` 157 | $ deplic https://github.com/pandas-dev/pandas -n requirements-dev.txt -f csv -p 16 -o pandas_dev.csv 158 | ``` 159 | 160 | Run a check against banned licenses listed in a configuration file: 161 | ```bash 162 | $ more deplic.cfg 163 | ``` 164 | ```ini 165 | [deplic] 166 | banned = AGPL-3.0 167 | # or multi-lines 168 | # banned = 169 | # AGPL-3.0, 170 | # ... 171 | ``` 172 | 173 | ``` 174 | $ deplic --check ./deplic.cfg /path/to/working/project 175 | 176 | BANNED: edx-opaque-keys :: AGPL-3.0 - OSI Approved::GNU Affero General Public License v3 177 | BANNED: edx-rbac :: AGPL 3.0 - OSI Approved::GNU Affero General Public License v3 or later (AGPLv3+) 178 | BANNED: edx-django-utils :: AGPL 3.0 - OSI Approved::GNU Affero General Public License v3 or later (AGPLv3+) 179 | BANNED: django-config-models :: AGPL 3.0 - OSI Approved::GNU Affero General Public License v3 or later (AGPLv3+) 180 | ``` 181 | 182 | ### Using dep-license in Docker 183 | ```bash 184 | $ docker run -t -v $PWD:/stage abduh/dep-license deplic /stage 185 | Found dependencies: 1 186 | 187 | | Name | Meta | Classifier | 188 | |--------------|--------|---------------------------| 189 | | editdistance | | OSI Approved::MIT License | 190 | ``` 191 | 192 | ### Output Formats: 193 | 194 | Supported table formats are (thanks to python-tabulate package): 195 | 196 | - "plain" 197 | - "simple" 198 | - "github" 199 | - "grid" 200 | - "fancy_grid" 201 | - "pipe" 202 | - "orgtbl" 203 | - "jira" 204 | - "presto" 205 | - "psql" 206 | - "rst" 207 | - "mediawiki" 208 | - "moinmoin" 209 | - "youtrack" 210 | - "html" 211 | - "latex" 212 | - "latex_raw" 213 | - "latex_booktabs" 214 | - "textile" 215 | - "csv" 216 | -------------------------------------------------------------------------------- /dep_license/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import concurrent.futures 4 | import json 5 | import logging 6 | import os 7 | import stat 8 | import subprocess 9 | import sys 10 | import tempfile 11 | import warnings 12 | from shutil import rmtree 13 | from urllib.request import urlopen 14 | 15 | import git 16 | from tabulate import tabulate 17 | 18 | from dep_license.utils import parse_file 19 | 20 | logger = logging.getLogger("dep_license") 21 | 22 | __version__ = ( 23 | open(os.path.join(os.path.abspath(os.path.dirname(__file__)), "VERSION"), "r") 24 | .readline() 25 | .strip() 26 | ) 27 | 28 | SUPPORTED_FILES = [ 29 | "requirements.txt", 30 | "Pipfile", 31 | "Pipfile.lock", 32 | "pyproject.toml", 33 | "setup.py", 34 | "conda.yml", 35 | "poetry.lock", 36 | ] 37 | PYPYI_URL = "https://pypi.python.org/pypi" 38 | COLUMNS = ["Name", "Meta", "Classifier"] 39 | 40 | 41 | def is_valid_git_remote(project): 42 | import git 43 | 44 | g = git.cmd.Git() 45 | try: 46 | g.ls_remote(project).split("\n") 47 | return True 48 | except Exception: 49 | return False 50 | 51 | 52 | def readonly_handler(func, path, execinfo): 53 | """ 54 | Work-around for python problem with shutils tree remove functions on Windows. 55 | See: 56 | https://stackoverflow.com/questions/23924223 57 | """ 58 | os.chmod(path, stat.S_IWRITE) 59 | func(path) 60 | 61 | 62 | def get_params(argv=None): 63 | parser = argparse.ArgumentParser( 64 | formatter_class=argparse.ArgumentDefaultsHelpFormatter 65 | ) 66 | parser.add_argument("PROJECT", nargs="+", help="path to project or its GIT repo") 67 | parser.add_argument( 68 | "-w", "--workers", default=5, help="number of workers to run in parallel" 69 | ) 70 | parser.add_argument( 71 | "-f", "--format", default="github", help="define how result is formatted" 72 | ) 73 | parser.add_argument("-o", "--output", default=None, help="path for output file") 74 | parser.add_argument( 75 | "-d", 76 | "--dev", 77 | action="store_true", 78 | default=False, 79 | help="include dev packages from Pipfile", 80 | ) 81 | parser.add_argument("-n", "--name", default=None, help="name for dependency file") 82 | parser.add_argument( 83 | "-c", 84 | "--check", 85 | nargs="?", 86 | const="setup.cfg", 87 | default=None, 88 | help="path to a configuration file to check against banned licenses", 89 | ) 90 | parser.add_argument( 91 | "-e", 92 | "--env", 93 | action="store_true", 94 | default=False, 95 | help="check against selected python executable", 96 | ) 97 | parser.add_argument("-v", "--version", action="version", version=__version__) 98 | 99 | args = parser.parse_args(argv) 100 | project = args.PROJECT 101 | w = args.workers 102 | fmt = args.format 103 | output = args.output 104 | dev = args.dev 105 | name = args.name 106 | check = args.check 107 | env = args.env 108 | 109 | return project, w, fmt, output, dev, name, check, env 110 | 111 | 112 | def worker(d): 113 | d = d.replace('"', "") 114 | d = d.replace("'", "") 115 | record = [d] 116 | try: 117 | with urlopen("{}/{}/json".format(PYPYI_URL, d)) as conn: 118 | output = json.loads(conn.read().decode()).get("info") 119 | 120 | except Exception: 121 | logger.warning(f"{d}: error in fetching pypi metadata") 122 | return None 123 | 124 | meta = output.get("license", "") 125 | record.append(meta.strip() if meta is not None else "") 126 | 127 | license_class = set() 128 | classifier = output.get("classifiers", "") 129 | for c in classifier: 130 | if c.startswith("License"): 131 | license_class.add("::".join([x.strip() for x in c.split("::")[1:]])) 132 | 133 | license_class_str = ( 134 | license_class.pop() if len(license_class) == 1 else ", ".join(license_class) 135 | ) 136 | record.append(license_class_str) 137 | 138 | return dict(zip(COLUMNS, record)) 139 | 140 | 141 | def start_concurrent(dependencies, max_workers=5): 142 | results = [] 143 | with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: 144 | future_to_worker = {executor.submit(worker, x): x for x in dependencies} 145 | for future in concurrent.futures.as_completed(future_to_worker): 146 | dependency = future_to_worker[future] 147 | try: 148 | data = future.result() 149 | except Exception as e: # pragma: no cover 150 | logger.error(f"{dependency}: {e}") 151 | continue 152 | else: 153 | if data: 154 | results.append(data) 155 | 156 | return results 157 | 158 | 159 | def run(argv=None): 160 | warnings.simplefilter("ignore", UserWarning) 161 | 162 | projects, max_workers, fmt, output_file, dev, name, check, env = get_params(argv) 163 | return_val = 0 164 | 165 | if name: 166 | req_files = [name] 167 | else: 168 | req_files = SUPPORTED_FILES 169 | 170 | dependencies = [] 171 | 172 | for project in projects: 173 | if env: 174 | if not os.path.isfile(project): 175 | logger.error(f"{project} is invalid python executable.") 176 | continue 177 | try: 178 | out = subprocess.check_output([project, "-m", "pip", "freeze"]) 179 | if out: 180 | try: 181 | f = tempfile.NamedTemporaryFile(delete=False) 182 | f.write(out) 183 | f.close() 184 | dependencies += parse_file(f.name, "requirements.txt", dev=dev) 185 | finally: 186 | os.remove(f.name) 187 | except Exception: 188 | logger.error(f"{project}: error in freezing dependencies.") 189 | 190 | elif os.path.isdir(os.path.abspath(project)): 191 | project = os.path.abspath(project) 192 | for f in req_files: 193 | filename = os.path.join(project, f) 194 | if os.path.isfile(filename): 195 | dependencies += parse_file(filename, f, dev=dev) 196 | 197 | elif os.path.isfile(os.path.abspath(project)): 198 | project = os.path.abspath(project) 199 | filename = os.path.basename(project) 200 | if filename in req_files: 201 | dependencies += parse_file(project, filename, dev=dev) 202 | 203 | elif is_valid_git_remote(project): 204 | temp_dir = tempfile.TemporaryDirectory() 205 | git.Git(temp_dir.name).clone(project) 206 | dir_name = os.path.join( 207 | temp_dir.name, project.rsplit("/", 1)[-1].split(".")[0] 208 | ) 209 | for f in req_files: 210 | f_name = os.path.join(dir_name, f) 211 | if os.path.isfile(f_name): 212 | dependencies += parse_file(f_name, f, dev=dev) 213 | if sys.platform.startswith("win"): 214 | rmtree(temp_dir.name, onerror=readonly_handler) 215 | else: 216 | temp_dir.cleanup() 217 | 218 | else: 219 | logger.error(f"{project} is invalid project.") 220 | 221 | dependencies = list(set(dependencies)) 222 | if len(dependencies) == 0: 223 | print("no dependencies found") 224 | return 1 225 | 226 | print("Found dependencies: {}\n".format(len(dependencies))) 227 | logger.debug("Running with {} workers ...".format(max_workers)) 228 | 229 | results = start_concurrent(dependencies, max_workers=max_workers) 230 | if len(results) == 0: 231 | logger.error("no license information found") 232 | return 1 233 | 234 | output = "" 235 | fmt = fmt.lower() 236 | if fmt == "json": 237 | import json 238 | 239 | output = json.dumps(results, indent=4) 240 | 241 | else: 242 | rows = [] 243 | for r in results: 244 | rows.append(list(r.values())) 245 | if fmt == "csv": 246 | output += ",".join(COLUMNS) + "\n" 247 | for row in rows: 248 | output += ",".join(row) + "\n" 249 | else: 250 | output = tabulate(rows, COLUMNS, tablefmt=fmt) 251 | 252 | if not check: 253 | print(output, end="\n") 254 | 255 | if output_file: 256 | with open(output_file, "w") as f: 257 | f.write(output) 258 | f.close() 259 | print("output file is stored in {}".format(os.path.abspath(output_file))) 260 | 261 | if check: 262 | import configparser 263 | from difflib import get_close_matches 264 | 265 | if not os.path.isfile(check): 266 | logger.error("configuration file not found") 267 | return 1 268 | config = configparser.ConfigParser() 269 | config.read(check) 270 | try: 271 | banned_licenses = config.get("deplic", "banned") 272 | except Exception: 273 | banned_licenses = None 274 | 275 | if banned_licenses: 276 | banned_licenses = banned_licenses.split(",") 277 | banned_licenses = [x.lower().strip() for x in banned_licenses if x] 278 | banned_licenses = list(set(banned_licenses)) 279 | for r in results: 280 | name = r.get("Name") 281 | meta = r.get("Meta") 282 | classifier = r.get("Classifier") 283 | if get_close_matches( 284 | meta.lower().replace("license", "").strip(), banned_licenses 285 | ) or get_close_matches( 286 | classifier.lower() 287 | .replace("license", "") 288 | .replace("osi approved::", "") 289 | .strip(), 290 | banned_licenses, 291 | ): 292 | print( 293 | f"\x1b[1;31mBANNED\x1b[0m: " 294 | f"\x1b[1;33m{name}\x1b[0m " 295 | f":: \x1b[1;33m{meta} - {classifier}\x1b[0m", 296 | end="\n", 297 | ) 298 | return_val = 1 299 | 300 | return return_val 301 | --------------------------------------------------------------------------------