diff --git a/src/python_inspector/resolution.py b/src/python_inspector/resolution.py index 116c7e4d..6d275957 100644 --- a/src/python_inspector/resolution.py +++ b/src/python_inspector/resolution.py @@ -627,13 +627,52 @@ def format_resolution( return dependencies +def pdt_dfs(mapping, graph, src): + """ + Return a nested mapping of dependencies. + + This takes ``mapping`` and ``graph`` as input. And do a dfs + (aka. depth-first search see https://en.wikipedia.org/wiki/Depth-first_search) + on the ``graph`` to get the dependencies of the given ``src``. + And use the ``mapping`` to get the version of the given dependency. + """ + children = list(graph.iter_children(src)) + if not children: + return dict( + key=src, package_name=src, installed_version=str(mapping[src].version), dependencies=[] + ) + # recurse + dependencies = [pdt_dfs(mapping, graph, c) for c in children] + dependencies.sort(key=lambda d: d["key"]) + return dict( + key=src, + package_name=src, + installed_version=str(mapping[src].version), + dependencies=dependencies, + ) + + +def format_pdt_tree(results): + """ + Return a formatted tree of dependencies in the style of pipdeptree. + """ + mapping = results.mapping + graph = results.graph + dependencies = [] + for src in get_all_srcs(mapping=mapping, graph=graph): + dependencies.append(pdt_dfs(mapping=mapping, graph=graph, src=src)) + dependencies.sort(key=lambda d: d["key"]) + return dependencies + + def get_resolved_dependencies( requirements: List[Requirement], environment: Environment = None, repos: Sequence[PypiSimpleRepository] = tuple(), as_tree: bool = False, max_rounds: int = 200000, - debug: bool = False, + verbose: bool = False, + pdt_output: bool = False, ): """ Return resolved dependencies of a ``requirements`` list of Requirement for @@ -648,11 +687,14 @@ def get_resolved_dependencies( provider=PythonInputProvider(environment=environment, repos=repos), reporter=BaseReporter(), ) - results = resolver.resolve(requirements=requirements, max_rounds=max_rounds) - results = format_resolution(results, as_tree=as_tree, environment=environment, repos=repos) - return results + resolver_results = resolver.resolve(requirements=requirements, max_rounds=max_rounds) + if pdt_output: + return format_pdt_tree(resolver_results) + return format_resolution( + resolver_results, as_tree=as_tree, environment=environment, repos=repos + ) except Exception as e: - if debug: + if verbose: import click click.secho(f"{e!r}", err=True) diff --git a/src/python_inspector/resolve_cli.py b/src/python_inspector/resolve_cli.py index 8d0d9e2c..d30d27a0 100644 --- a/src/python_inspector/resolve_cli.py +++ b/src/python_inspector/resolve_cli.py @@ -10,7 +10,6 @@ # import json -import sys from typing import List import click @@ -34,6 +33,7 @@ @click.command() +@click.pass_context @click.option( "-r", "--requirement", @@ -99,11 +99,20 @@ "--json", "json_output", type=FileOptionType(mode="w", encoding="utf-8", lazy=True), - required=True, + required=False, metavar="FILE", help="Write output as pretty-printed JSON to FILE. " "Use the special '-' file name to print results on screen/stdout.", ) +@click.option( + "--json-pdt", + "pdt_output", + type=FileOptionType(mode="w", encoding="utf-8", lazy=True), + required=False, + metavar="FILE", + help="Write output as pretty-printed JSON to FILE as a tree in the style of pipdeptree. " + "Use the special '-' file name to print results on screen/stdout.", +) @click.option( "--max-rounds", "max_rounds", @@ -124,13 +133,14 @@ "--index-url are ignored when this option is active.", ) @click.option( - "--debug", + "--verbose", is_flag=True, hidden=True, help="Enable debug output.", ) @click.help_option("-h", "--help") def resolve_dependencies( + ctx, requirement_files, netrc_file, specifiers, @@ -138,10 +148,11 @@ def resolve_dependencies( operating_system, index_urls, json_output, + pdt_output, max_rounds, use_cached_index=False, use_pypi_json_api=False, - debug=TRACE, + verbose=TRACE, ): """ Resolve the dependencies of the packages listed in REQUIREMENT-FILE(s) file @@ -161,7 +172,15 @@ def resolve_dependencies( dad --spec "flask==2.1.2" --json - """ - if debug: + if not (json_output or pdt_output): + click.secho("No output file specified. Use --json or --json-pdt.", err=True) + ctx.exit(1) + + if json_output and pdt_output: + click.secho("Only one of --json or --json-pdt can be used.", err=True) + ctx.exit(1) + + if verbose: click.secho(f"Resolving dependencies...") netrc = None @@ -184,11 +203,10 @@ def resolve_dependencies( direct_dependencies.append(dep) if not direct_dependencies: - if debug: - click.secho("Error: no requirements requested.") - sys.exit(1) + click.secho("Error: no requirements requested.") + ctx.exit(1) - if debug: + if verbose: click.secho("direct_dependencies:") for dep in direct_dependencies: click.secho(f" {dep}") @@ -198,7 +216,7 @@ def resolve_dependencies( python_version=python_version, operating_system=operating_system ) - if debug: + if verbose: click.secho(f"environment: {environment}") repos = [] @@ -224,7 +242,7 @@ def resolve_dependencies( ) repos.append(repo) - if debug: + if verbose: click.secho("repos:") for repo in repos: click.secho(f" {repo}") @@ -236,7 +254,8 @@ def resolve_dependencies( repos=repos, as_tree=False, max_rounds=max_rounds, - debug=debug, + verbose=verbose, + pdt_output=pdt_output, ) cli_options = [f"--requirement {rf}" for rf in requirement_files] @@ -262,19 +281,35 @@ def resolve_dependencies( errors=[], ) - write_output( - headers=headers, - requirements=requirements, - resolved_dependencies=resolved_dependencies, - json_output=json_output, - ) - - if debug: + if json_output: + write_output( + headers=headers, + requirements=requirements, + resolved_dependencies=resolved_dependencies, + json_output=json_output, + ) + + else: + write_output( + headers=headers, + requirements=requirements, + resolved_dependencies=resolved_dependencies, + json_output=pdt_output, + pdt_output=True, + ) + + if verbose: click.secho("done!") def resolve( - direct_dependencies, environment, repos=tuple(), as_tree=False, max_rounds=200000, debug=False + direct_dependencies, + environment, + repos=tuple(), + as_tree=False, + max_rounds=200000, + verbose=False, + pdt_output=False, ): """ Resolve dependencies given a ``direct_dependencies`` list of @@ -292,7 +327,8 @@ def resolve( repos=repos, as_tree=as_tree, max_rounds=max_rounds, - debug=debug, + verbose=verbose, + pdt_output=pdt_output, ) initial_requirements = [d.to_dict() for d in direct_dependencies] @@ -312,16 +348,19 @@ def get_requirements_from_direct_dependencies( yield Requirement(requirement_string=dependency.extracted_requirement) -def write_output(headers, requirements, resolved_dependencies, json_output): +def write_output(headers, requirements, resolved_dependencies, json_output, pdt_output=False): """ Write headers, requirements and resolved_dependencies as JSON to ``json_output``. Return the output data. """ - output = dict( - headers=headers, - requirements=requirements, - resolved_dependencies=resolved_dependencies, - ) + if not pdt_output: + output = dict( + headers=headers, + requirements=requirements, + resolved_dependencies=resolved_dependencies, + ) + else: + output = resolved_dependencies json.dump(output, json_output, indent=2) return output diff --git a/tests/data/frozen-requirements-pdt-expected.json b/tests/data/frozen-requirements-pdt-expected.json new file mode 100644 index 00000000..641b4de2 --- /dev/null +++ b/tests/data/frozen-requirements-pdt-expected.json @@ -0,0 +1,723 @@ +[ + { + "key": "aboutcode-toolkit", + "package_name": "aboutcode-toolkit", + "installed_version": "7.0.2", + "dependencies": [ + { + "key": "attrs", + "package_name": "attrs", + "installed_version": "21.4.0", + "dependencies": [] + }, + { + "key": "boolean-py", + "package_name": "boolean-py", + "installed_version": "4.0", + "dependencies": [] + }, + { + "key": "certifi", + "package_name": "certifi", + "installed_version": "2022.5.18.1", + "dependencies": [] + }, + { + "key": "click", + "package_name": "click", + "installed_version": "8.0.4", + "dependencies": [] + }, + { + "key": "jinja2", + "package_name": "jinja2", + "installed_version": "3.0.3", + "dependencies": [ + { + "key": "markupsafe", + "package_name": "markupsafe", + "installed_version": "2.0.1", + "dependencies": [] + } + ] + }, + { + "key": "license-expression", + "package_name": "license-expression", + "installed_version": "30.0.0", + "dependencies": [ + { + "key": "boolean-py", + "package_name": "boolean-py", + "installed_version": "4.0", + "dependencies": [] + } + ] + }, + { + "key": "openpyxl", + "package_name": "openpyxl", + "installed_version": "3.0.10", + "dependencies": [ + { + "key": "et-xmlfile", + "package_name": "et-xmlfile", + "installed_version": "1.1.0", + "dependencies": [] + } + ] + }, + { + "key": "packageurl-python", + "package_name": "packageurl-python", + "installed_version": "0.9.9", + "dependencies": [] + }, + { + "key": "saneyaml", + "package_name": "saneyaml", + "installed_version": "0.5.2", + "dependencies": [ + { + "key": "pyyaml", + "package_name": "pyyaml", + "installed_version": "6.0", + "dependencies": [] + } + ] + } + ] + }, + { + "key": "black", + "package_name": "black", + "installed_version": "22.3.0", + "dependencies": [ + { + "key": "click", + "package_name": "click", + "installed_version": "8.0.4", + "dependencies": [] + }, + { + "key": "mypy-extensions", + "package_name": "mypy-extensions", + "installed_version": "0.4.3", + "dependencies": [] + }, + { + "key": "pathspec", + "package_name": "pathspec", + "installed_version": "0.9.0", + "dependencies": [] + }, + { + "key": "platformdirs", + "package_name": "platformdirs", + "installed_version": "2.4.0", + "dependencies": [] + }, + { + "key": "tomli", + "package_name": "tomli", + "installed_version": "1.2.3", + "dependencies": [] + }, + { + "key": "typing-extensions", + "package_name": "typing-extensions", + "installed_version": "4.1.1", + "dependencies": [] + } + ] + }, + { + "key": "commoncode", + "package_name": "commoncode", + "installed_version": "30.2.0", + "dependencies": [ + { + "key": "attrs", + "package_name": "attrs", + "installed_version": "21.4.0", + "dependencies": [] + }, + { + "key": "beautifulsoup4", + "package_name": "beautifulsoup4", + "installed_version": "4.11.1", + "dependencies": [ + { + "key": "soupsieve", + "package_name": "soupsieve", + "installed_version": "2.3.2.post1", + "dependencies": [] + } + ] + }, + { + "key": "click", + "package_name": "click", + "installed_version": "8.0.4", + "dependencies": [] + }, + { + "key": "intbitset", + "package_name": "intbitset", + "installed_version": "3.0.1", + "dependencies": [] + }, + { + "key": "requests", + "package_name": "requests", + "installed_version": "2.27.1", + "dependencies": [ + { + "key": "certifi", + "package_name": "certifi", + "installed_version": "2022.5.18.1", + "dependencies": [] + }, + { + "key": "charset-normalizer", + "package_name": "charset-normalizer", + "installed_version": "2.0.12", + "dependencies": [] + }, + { + "key": "idna", + "package_name": "idna", + "installed_version": "3.3", + "dependencies": [] + }, + { + "key": "urllib3", + "package_name": "urllib3", + "installed_version": "1.26.9", + "dependencies": [] + } + ] + }, + { + "key": "saneyaml", + "package_name": "saneyaml", + "installed_version": "0.5.2", + "dependencies": [ + { + "key": "pyyaml", + "package_name": "pyyaml", + "installed_version": "6.0", + "dependencies": [] + } + ] + }, + { + "key": "text-unidecode", + "package_name": "text-unidecode", + "installed_version": "1.3", + "dependencies": [] + } + ] + }, + { + "key": "dparse2", + "package_name": "dparse2", + "installed_version": "0.6.1", + "dependencies": [ + { + "key": "packaging", + "package_name": "packaging", + "installed_version": "21.3", + "dependencies": [ + { + "key": "pyparsing", + "package_name": "pyparsing", + "installed_version": "3.0.9", + "dependencies": [] + } + ] + }, + { + "key": "pyyaml", + "package_name": "pyyaml", + "installed_version": "6.0", + "dependencies": [] + }, + { + "key": "toml", + "package_name": "toml", + "installed_version": "0.10.2", + "dependencies": [] + } + ] + }, + { + "key": "flask", + "package_name": "flask", + "installed_version": "2.1.2", + "dependencies": [ + { + "key": "click", + "package_name": "click", + "installed_version": "8.0.4", + "dependencies": [] + }, + { + "key": "importlib-metadata", + "package_name": "importlib-metadata", + "installed_version": "4.8.3", + "dependencies": [ + { + "key": "zipp", + "package_name": "zipp", + "installed_version": "3.6.0", + "dependencies": [] + } + ] + }, + { + "key": "itsdangerous", + "package_name": "itsdangerous", + "installed_version": "2.1.2", + "dependencies": [] + }, + { + "key": "jinja2", + "package_name": "jinja2", + "installed_version": "3.0.3", + "dependencies": [ + { + "key": "markupsafe", + "package_name": "markupsafe", + "installed_version": "2.0.1", + "dependencies": [] + } + ] + }, + { + "key": "werkzeug", + "package_name": "werkzeug", + "installed_version": "2.1.2", + "dependencies": [] + } + ] + }, + { + "key": "isort", + "package_name": "isort", + "installed_version": "5.10.1", + "dependencies": [] + }, + { + "key": "pip-requirements-parser", + "package_name": "pip-requirements-parser", + "installed_version": "31.2.0", + "dependencies": [ + { + "key": "packaging", + "package_name": "packaging", + "installed_version": "21.3", + "dependencies": [ + { + "key": "pyparsing", + "package_name": "pyparsing", + "installed_version": "3.0.9", + "dependencies": [] + } + ] + } + ] + }, + { + "key": "pipdeptree", + "package_name": "pipdeptree", + "installed_version": "2.2.1", + "dependencies": [ + { + "key": "pip", + "package_name": "pip", + "installed_version": "22.2.1", + "dependencies": [] + } + ] + }, + { + "key": "pkginfo2", + "package_name": "pkginfo2", + "installed_version": "30.0.0", + "dependencies": [] + }, + { + "key": "pycodestyle", + "package_name": "pycodestyle", + "installed_version": "2.8.0", + "dependencies": [] + }, + { + "key": "pytest-xdist", + "package_name": "pytest-xdist", + "installed_version": "2.5.0", + "dependencies": [ + { + "key": "execnet", + "package_name": "execnet", + "installed_version": "1.9.0", + "dependencies": [] + }, + { + "key": "pytest", + "package_name": "pytest", + "installed_version": "7.0.1", + "dependencies": [ + { + "key": "attrs", + "package_name": "attrs", + "installed_version": "21.4.0", + "dependencies": [] + }, + { + "key": "iniconfig", + "package_name": "iniconfig", + "installed_version": "1.1.1", + "dependencies": [] + }, + { + "key": "packaging", + "package_name": "packaging", + "installed_version": "21.3", + "dependencies": [ + { + "key": "pyparsing", + "package_name": "pyparsing", + "installed_version": "3.0.9", + "dependencies": [] + } + ] + }, + { + "key": "pluggy", + "package_name": "pluggy", + "installed_version": "1.0.0", + "dependencies": [] + }, + { + "key": "py", + "package_name": "py", + "installed_version": "1.11.0", + "dependencies": [] + }, + { + "key": "tomli", + "package_name": "tomli", + "installed_version": "1.2.3", + "dependencies": [] + } + ] + }, + { + "key": "pytest-forked", + "package_name": "pytest-forked", + "installed_version": "1.4.0", + "dependencies": [ + { + "key": "py", + "package_name": "py", + "installed_version": "1.11.0", + "dependencies": [] + }, + { + "key": "pytest", + "package_name": "pytest", + "installed_version": "7.0.1", + "dependencies": [ + { + "key": "attrs", + "package_name": "attrs", + "installed_version": "21.4.0", + "dependencies": [] + }, + { + "key": "iniconfig", + "package_name": "iniconfig", + "installed_version": "1.1.1", + "dependencies": [] + }, + { + "key": "packaging", + "package_name": "packaging", + "installed_version": "21.3", + "dependencies": [ + { + "key": "pyparsing", + "package_name": "pyparsing", + "installed_version": "3.0.9", + "dependencies": [] + } + ] + }, + { + "key": "pluggy", + "package_name": "pluggy", + "installed_version": "1.0.0", + "dependencies": [] + }, + { + "key": "py", + "package_name": "py", + "installed_version": "1.11.0", + "dependencies": [] + }, + { + "key": "tomli", + "package_name": "tomli", + "installed_version": "1.2.3", + "dependencies": [] + } + ] + } + ] + } + ] + }, + { + "key": "resolvelib", + "package_name": "resolvelib", + "installed_version": "0.8.1", + "dependencies": [] + }, + { + "key": "tinynetrc", + "package_name": "tinynetrc", + "installed_version": "1.3.1", + "dependencies": [] + }, + { + "key": "twine", + "package_name": "twine", + "installed_version": "3.8.0", + "dependencies": [ + { + "key": "colorama", + "package_name": "colorama", + "installed_version": "0.4.4", + "dependencies": [] + }, + { + "key": "importlib-metadata", + "package_name": "importlib-metadata", + "installed_version": "4.8.3", + "dependencies": [ + { + "key": "zipp", + "package_name": "zipp", + "installed_version": "3.6.0", + "dependencies": [] + } + ] + }, + { + "key": "keyring", + "package_name": "keyring", + "installed_version": "23.4.1", + "dependencies": [ + { + "key": "importlib-metadata", + "package_name": "importlib-metadata", + "installed_version": "4.8.3", + "dependencies": [ + { + "key": "zipp", + "package_name": "zipp", + "installed_version": "3.6.0", + "dependencies": [] + } + ] + }, + { + "key": "jeepney", + "package_name": "jeepney", + "installed_version": "0.7.1", + "dependencies": [] + }, + { + "key": "secretstorage", + "package_name": "secretstorage", + "installed_version": "3.3.2", + "dependencies": [ + { + "key": "cryptography", + "package_name": "cryptography", + "installed_version": "37.0.2", + "dependencies": [ + { + "key": "cffi", + "package_name": "cffi", + "installed_version": "1.15.0", + "dependencies": [ + { + "key": "pycparser", + "package_name": "pycparser", + "installed_version": "2.21", + "dependencies": [] + } + ] + } + ] + }, + { + "key": "jeepney", + "package_name": "jeepney", + "installed_version": "0.7.1", + "dependencies": [] + } + ] + } + ] + }, + { + "key": "pkginfo", + "package_name": "pkginfo", + "installed_version": "1.8.3", + "dependencies": [] + }, + { + "key": "readme-renderer", + "package_name": "readme-renderer", + "installed_version": "34.0", + "dependencies": [ + { + "key": "bleach", + "package_name": "bleach", + "installed_version": "4.1.0", + "dependencies": [ + { + "key": "packaging", + "package_name": "packaging", + "installed_version": "21.3", + "dependencies": [ + { + "key": "pyparsing", + "package_name": "pyparsing", + "installed_version": "3.0.9", + "dependencies": [] + } + ] + }, + { + "key": "six", + "package_name": "six", + "installed_version": "1.16.0", + "dependencies": [] + }, + { + "key": "webencodings", + "package_name": "webencodings", + "installed_version": "0.5.1", + "dependencies": [] + } + ] + }, + { + "key": "docutils", + "package_name": "docutils", + "installed_version": "0.18.1", + "dependencies": [] + }, + { + "key": "pygments", + "package_name": "pygments", + "installed_version": "2.12.0", + "dependencies": [] + } + ] + }, + { + "key": "requests", + "package_name": "requests", + "installed_version": "2.27.1", + "dependencies": [ + { + "key": "certifi", + "package_name": "certifi", + "installed_version": "2022.5.18.1", + "dependencies": [] + }, + { + "key": "charset-normalizer", + "package_name": "charset-normalizer", + "installed_version": "2.0.12", + "dependencies": [] + }, + { + "key": "idna", + "package_name": "idna", + "installed_version": "3.3", + "dependencies": [] + }, + { + "key": "urllib3", + "package_name": "urllib3", + "installed_version": "1.26.9", + "dependencies": [] + } + ] + }, + { + "key": "requests-toolbelt", + "package_name": "requests-toolbelt", + "installed_version": "0.9.1", + "dependencies": [ + { + "key": "requests", + "package_name": "requests", + "installed_version": "2.27.1", + "dependencies": [ + { + "key": "certifi", + "package_name": "certifi", + "installed_version": "2022.5.18.1", + "dependencies": [] + }, + { + "key": "charset-normalizer", + "package_name": "charset-normalizer", + "installed_version": "2.0.12", + "dependencies": [] + }, + { + "key": "idna", + "package_name": "idna", + "installed_version": "3.3", + "dependencies": [] + }, + { + "key": "urllib3", + "package_name": "urllib3", + "installed_version": "1.26.9", + "dependencies": [] + } + ] + } + ] + }, + { + "key": "rfc3986", + "package_name": "rfc3986", + "installed_version": "1.5.0", + "dependencies": [] + }, + { + "key": "tqdm", + "package_name": "tqdm", + "installed_version": "4.64.0", + "dependencies": [] + }, + { + "key": "urllib3", + "package_name": "urllib3", + "installed_version": "1.26.9", + "dependencies": [] + } + ] + } +] \ No newline at end of file diff --git a/tests/data/frozen-requirements.txt b/tests/data/frozen-requirements.txt new file mode 100644 index 00000000..6e168295 --- /dev/null +++ b/tests/data/frozen-requirements.txt @@ -0,0 +1,69 @@ +aboutcode-toolkit==7.0.2 +attrs==21.4.0 +beautifulsoup4==4.11.1 +black==22.3.0 +bleach==4.1.0 +boolean.py==4.0 +certifi==2022.5.18.1 +cffi==1.15.0 +charset-normalizer==2.0.12 +click==8.0.4 +colorama==0.4.4 +commoncode==30.2.0 +cryptography==37.0.2 +docutils==0.18.1 +dparse2==0.6.1 +et-xmlfile==1.1.0 +execnet==1.9.0 +Flask==2.1.2 +idna==3.3 +importlib-metadata==4.8.3 +iniconfig==1.1.1 +intbitset==3.0.1 +isort==5.10.1 +itsdangerous==2.1.2 +jeepney==0.7.1 +Jinja2==3.0.3 +keyring==23.4.1 +license-expression==30.0.0 +MarkupSafe==2.0.1 +mypy-extensions==0.4.3 +openpyxl==3.0.10 +packageurl-python==0.9.9 +packaging==21.3 +pathspec==0.9.0 +pip-requirements-parser==31.2.0 +pipdeptree==2.2.1 +pkginfo==1.8.3 +pkginfo2==30.0.0 +platformdirs==2.4.0 +pluggy==1.0.0 +py==1.11.0 +pycodestyle==2.8.0 +pycparser==2.21 +Pygments==2.12.0 +pyparsing==3.0.9 +pytest==7.0.1 +pytest-forked==1.4.0 +pytest-xdist==2.5.0 +PyYAML==6.0 +readme-renderer==34.0 +requests==2.27.1 +requests-toolbelt==0.9.1 +resolvelib==0.8.1 +rfc3986==1.5.0 +saneyaml==0.5.2 +SecretStorage==3.3.2 +six==1.16.0 +soupsieve==2.3.2.post1 +text-unidecode==1.3 +tinynetrc==1.3.1 +toml==0.10.2 +tomli==1.2.3 +tqdm==4.64.0 +twine==3.8.0 +typing_extensions==4.1.1 +urllib3==1.26.9 +webencodings==0.5.1 +Werkzeug==2.1.2 +zipp==3.6.0 diff --git a/tests/data/pdt-expected.json b/tests/data/pdt-expected.json new file mode 100644 index 00000000..dd085dd6 --- /dev/null +++ b/tests/data/pdt-expected.json @@ -0,0 +1,40 @@ +[ + { + "key": "flask", + "package_name": "flask", + "installed_version": "1.0", + "dependencies": [ + { + "key": "click", + "package_name": "click", + "installed_version": "6.7", + "dependencies": [] + }, + { + "key": "itsdangerous", + "package_name": "itsdangerous", + "installed_version": "0.24", + "dependencies": [] + }, + { + "key": "jinja2", + "package_name": "jinja2", + "installed_version": "2.11.3", + "dependencies": [ + { + "key": "markupsafe", + "package_name": "markupsafe", + "installed_version": "1.0", + "dependencies": [] + } + ] + }, + { + "key": "werkzeug", + "package_name": "werkzeug", + "installed_version": "0.15.3", + "dependencies": [] + } + ] + } +] \ No newline at end of file diff --git a/tests/data/pdt.txt b/tests/data/pdt.txt new file mode 100644 index 00000000..85f5f8fb --- /dev/null +++ b/tests/data/pdt.txt @@ -0,0 +1,6 @@ +click==6.7 +Flask==1.0 +itsdangerous==0.24 +Jinja2==2.11.3 +MarkupSafe==1.0 +Werkzeug==0.15.3 \ No newline at end of file diff --git a/tests/data/pinned-requirements-pdt-expected.json b/tests/data/pinned-requirements-pdt-expected.json new file mode 100644 index 00000000..6ec917db --- /dev/null +++ b/tests/data/pinned-requirements-pdt-expected.json @@ -0,0 +1,274 @@ +[ + { + "key": "aboutcode-toolkit", + "package_name": "aboutcode-toolkit", + "installed_version": "7.0.2", + "dependencies": [ + { + "key": "attrs", + "package_name": "attrs", + "installed_version": "21.4.0", + "dependencies": [] + }, + { + "key": "boolean-py", + "package_name": "boolean-py", + "installed_version": "4.0", + "dependencies": [] + }, + { + "key": "certifi", + "package_name": "certifi", + "installed_version": "2022.5.18.1", + "dependencies": [] + }, + { + "key": "click", + "package_name": "click", + "installed_version": "8.0.4", + "dependencies": [] + }, + { + "key": "jinja2", + "package_name": "jinja2", + "installed_version": "3.1.2", + "dependencies": [ + { + "key": "markupsafe", + "package_name": "markupsafe", + "installed_version": "2.1.1", + "dependencies": [] + } + ] + }, + { + "key": "license-expression", + "package_name": "license-expression", + "installed_version": "30.0.0", + "dependencies": [ + { + "key": "boolean-py", + "package_name": "boolean-py", + "installed_version": "4.0", + "dependencies": [] + } + ] + }, + { + "key": "openpyxl", + "package_name": "openpyxl", + "installed_version": "3.0.10", + "dependencies": [ + { + "key": "et-xmlfile", + "package_name": "et-xmlfile", + "installed_version": "1.1.0", + "dependencies": [] + } + ] + }, + { + "key": "packageurl-python", + "package_name": "packageurl-python", + "installed_version": "0.9.9", + "dependencies": [] + }, + { + "key": "saneyaml", + "package_name": "saneyaml", + "installed_version": "0.5.2", + "dependencies": [ + { + "key": "pyyaml", + "package_name": "pyyaml", + "installed_version": "6.0", + "dependencies": [] + } + ] + } + ] + }, + { + "key": "colorama", + "package_name": "colorama", + "installed_version": "0.4.4", + "dependencies": [] + }, + { + "key": "commoncode", + "package_name": "commoncode", + "installed_version": "30.2.0", + "dependencies": [ + { + "key": "attrs", + "package_name": "attrs", + "installed_version": "21.4.0", + "dependencies": [] + }, + { + "key": "beautifulsoup4", + "package_name": "beautifulsoup4", + "installed_version": "4.11.1", + "dependencies": [ + { + "key": "soupsieve", + "package_name": "soupsieve", + "installed_version": "2.3.2.post1", + "dependencies": [] + } + ] + }, + { + "key": "click", + "package_name": "click", + "installed_version": "8.0.4", + "dependencies": [] + }, + { + "key": "intbitset", + "package_name": "intbitset", + "installed_version": "3.0.1", + "dependencies": [] + }, + { + "key": "requests", + "package_name": "requests", + "installed_version": "2.27.1", + "dependencies": [ + { + "key": "certifi", + "package_name": "certifi", + "installed_version": "2022.5.18.1", + "dependencies": [] + }, + { + "key": "charset-normalizer", + "package_name": "charset-normalizer", + "installed_version": "2.0.12", + "dependencies": [] + }, + { + "key": "idna", + "package_name": "idna", + "installed_version": "3.3", + "dependencies": [] + }, + { + "key": "urllib3", + "package_name": "urllib3", + "installed_version": "1.26.9", + "dependencies": [] + } + ] + }, + { + "key": "saneyaml", + "package_name": "saneyaml", + "installed_version": "0.5.2", + "dependencies": [ + { + "key": "pyyaml", + "package_name": "pyyaml", + "installed_version": "6.0", + "dependencies": [] + } + ] + }, + { + "key": "text-unidecode", + "package_name": "text-unidecode", + "installed_version": "1.3", + "dependencies": [] + } + ] + }, + { + "key": "dparse2", + "package_name": "dparse2", + "installed_version": "0.6.1", + "dependencies": [ + { + "key": "packaging", + "package_name": "packaging", + "installed_version": "21.3", + "dependencies": [ + { + "key": "pyparsing", + "package_name": "pyparsing", + "installed_version": "3.0.9", + "dependencies": [] + } + ] + }, + { + "key": "pyyaml", + "package_name": "pyyaml", + "installed_version": "6.0", + "dependencies": [] + }, + { + "key": "toml", + "package_name": "toml", + "installed_version": "0.10.2", + "dependencies": [] + } + ] + }, + { + "key": "importlib-metadata", + "package_name": "importlib-metadata", + "installed_version": "4.8.3", + "dependencies": [ + { + "key": "zipp", + "package_name": "zipp", + "installed_version": "3.6.0", + "dependencies": [] + } + ] + }, + { + "key": "pip-requirements-parser", + "package_name": "pip-requirements-parser", + "installed_version": "31.2.0", + "dependencies": [ + { + "key": "packaging", + "package_name": "packaging", + "installed_version": "21.3", + "dependencies": [ + { + "key": "pyparsing", + "package_name": "pyparsing", + "installed_version": "3.0.9", + "dependencies": [] + } + ] + } + ] + }, + { + "key": "pkginfo2", + "package_name": "pkginfo2", + "installed_version": "30.0.0", + "dependencies": [] + }, + { + "key": "resolvelib", + "package_name": "resolvelib", + "installed_version": "0.8.1", + "dependencies": [] + }, + { + "key": "typing", + "package_name": "typing", + "installed_version": "3.6.6", + "dependencies": [] + }, + { + "key": "typing-extensions", + "package_name": "typing-extensions", + "installed_version": "4.1.1", + "dependencies": [] + } +] \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py index 00ff1dc0..8f5f9077 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -42,6 +42,48 @@ def test_cli_with_default_urls(): ) +@pytest.mark.online +def test_pdt_output(): + requirements_file = test_env.get_test_loc("pdt.txt") + expected_file = test_env.get_test_loc("pdt-expected.json", must_exist=False) + extra_options = [] + check_requirements_resolution( + requirements_file=requirements_file, + expected_file=expected_file, + extra_options=extra_options, + pdt_output=True, + regen=REGEN_TEST_FIXTURES, + ) + + +@pytest.mark.online +def test_pdt_output_with_pinned_requirements(): + requirements_file = test_env.get_test_loc("pinned-requirements.txt") + expected_file = test_env.get_test_loc("pinned-requirements-pdt-expected.json", must_exist=False) + extra_options = [] + check_requirements_resolution( + requirements_file=requirements_file, + expected_file=expected_file, + extra_options=extra_options, + pdt_output=True, + regen=REGEN_TEST_FIXTURES, + ) + + +@pytest.mark.online +def test_pdt_output_with_frozen_requirements(): + requirements_file = test_env.get_test_loc("frozen-requirements.txt") + expected_file = test_env.get_test_loc("frozen-requirements-pdt-expected.json", must_exist=False) + extra_options = [] + check_requirements_resolution( + requirements_file=requirements_file, + expected_file=expected_file, + extra_options=extra_options, + pdt_output=True, + regen=REGEN_TEST_FIXTURES, + ) + + @pytest.mark.online def test_cli_with_single_index_url(): expected_file = test_env.get_test_loc("single-url-expected.json", must_exist=False) @@ -164,20 +206,33 @@ def check_specs_resolution( ) +def test_passing_of_json_pdt_and_json_flags(): + result_file = test_env.get_temp_file("json") + options = ["--specifier", "foo", "--json", result_file, "--json-pdt", result_file] + run_cli(options=options, expected_rc=1) + + +def test_passing_of_no_json_output_flag(): + options = ["--specifier", "foo"] + run_cli(options=options, expected_rc=1) + + def check_requirements_resolution( requirements_file, expected_file, extra_options=tuple(), regen=REGEN_TEST_FIXTURES, + pdt_output=False, ): result_file = test_env.get_temp_file("json") - options = ["--requirement", requirements_file, "--json", result_file] + if pdt_output: + options = ["--requirement", requirements_file, "--json-pdt", result_file] + else: + options = ["--requirement", requirements_file, "--json", result_file] options.extend(extra_options) run_cli(options=options) check_json_results( - result_file=result_file, - expected_file=expected_file, - regen=regen, + result_file=result_file, expected_file=expected_file, regen=regen, clean=not pdt_output )