Skip to content

Commit bb0e111

Browse files
authored
Merge pull request #58 from nexB/setup-py
Add support for setup.py
2 parents 471eca1 + 1b796c3 commit bb0e111

File tree

13 files changed

+300
-43
lines changed

13 files changed

+300
-43
lines changed

src/_packagedcode/pypi.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,22 @@ class ResolvedPurl(NamedTuple):
659659
is_resolved: bool
660660

661661

662+
def create_dependency_for_python_requires(python_requires_specifier):
663+
"""
664+
Return a mock python DependentPackage created from a ``python_requires_specifier``.
665+
"""
666+
purl = PackageURL(type="generic", name="python")
667+
resolved_purl = get_resolved_purl(purl=purl, specifiers=SpecifierSet(python_requires_specifier))
668+
return models.DependentPackage(
669+
purl=str(resolved_purl.purl),
670+
scope="python",
671+
is_runtime=True,
672+
is_optional=False,
673+
is_resolved=resolved_purl.is_resolved,
674+
extracted_requirement=f"python_requires{python_requires_specifier}",
675+
)
676+
677+
662678
class BaseDependencyFileHandler(BasePypiHandler):
663679
"""
664680
Base class for a dependency files parsed with the same library
@@ -717,19 +733,8 @@ def parse(cls, location):
717733
dependent_packages.extend(cls.parse_reqs(reqs, scope))
718734
continue
719735
python_requires_specifier = section[sub_section]
720-
purl = PackageURL(
721-
type="generic",
722-
name="python",
723-
)
724-
resolved_purl = get_resolved_purl(purl=purl, specifiers=SpecifierSet(python_requires_specifier))
725-
dependent_packages.append(models.DependentPackage(
726-
purl=str(resolved_purl.purl),
727-
scope=scope,
728-
is_runtime=True,
729-
is_optional=False,
730-
is_resolved=resolved_purl.is_resolved,
731-
extracted_requirement=f"python_requires{python_requires_specifier}",
732-
))
736+
pd = create_dependency_for_python_requires(python_requires_specifier)
737+
dependent_packages.append(pd)
733738

734739
if section.name == "options.extras_require":
735740
for sub_section in section:

src/python_inspector/dependencies.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
from _packagedcode import models
1717
from _packagedcode.pypi import PipRequirementsFileHandler
18+
from _packagedcode.pypi import PythonSetupPyHandler
19+
from python_inspector.resolution import get_requirements_from_distribution
1820

1921
"""
2022
Utilities to resolve dependencies .
@@ -23,7 +25,7 @@
2325
TRACE = False
2426

2527

26-
def get_dependencies_from_requirements(requirements_file="requirements.txt", *args, **kwargs):
28+
def get_dependencies_from_requirements(requirements_file="requirements.txt"):
2729
"""
2830
Yield DependentPackage for each requirement in a `requirement`
2931
file.
@@ -38,7 +40,7 @@ def get_dependencies_from_requirements(requirements_file="requirements.txt", *ar
3840
yield dependent_package
3941

4042

41-
def get_extra_data_from_requirements(requirements_file="requirements.txt", *args, **kwargs):
43+
def get_extra_data_from_requirements(requirements_file="requirements.txt"):
4244
"""
4345
Yield extra_data for each requirement in a `requirement`
4446
file.
@@ -47,7 +49,7 @@ def get_extra_data_from_requirements(requirements_file="requirements.txt", *args
4749
yield package_data.extra_data
4850

4951

50-
def get_dependency(specifier, *args, **kwargs):
52+
def get_dependency(specifier):
5153
"""
5254
Return a DependentPackage given a requirement ``specifier`` string.
5355

src/python_inspector/resolution.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from typing import List
1616
from typing import NamedTuple
1717
from typing import Sequence
18+
from typing import Tuple
1819
from typing import Union
1920
from zipfile import ZipFile
2021

@@ -88,17 +89,20 @@ def get_response(url: str) -> Dict:
8889

8990

9091
def get_requirements_from_distribution(
91-
handler: BasePypiHandler, location: str
92+
handler: BasePypiHandler,
93+
location: str,
9294
) -> List[Requirement]:
9395
"""
9496
Return a list of requirements from a source distribution or wheel at
9597
``location`` using the provided ``handler`` DatafileHandler for parsing.
9698
"""
9799
if not os.path.exists(location):
98100
return []
99-
deps = list(handler.parse(location))
100-
assert len(deps) == 1
101-
return list(get_requirements_from_dependencies(dependencies=deps[0].dependencies))
101+
deps = []
102+
for package_data in handler.parse(location):
103+
dependencies = package_data.dependencies
104+
deps.extend(get_requirements_from_dependencies(dependencies=dependencies))
105+
return deps
102106

103107

104108
def get_environment_marker_from_environment(environment):
@@ -200,7 +204,9 @@ def fetch_and_extract_sdist(
200204
return os.path.join(utils_pypi.CACHE_THIRDPARTY_DIR, "extracted_sdists", sdist_file, sdist_file)
201205

202206

203-
def get_requirements_from_dependencies(dependencies: List[DependentPackage]) -> List[Requirement]:
207+
def get_requirements_from_dependencies(
208+
dependencies: List[DependentPackage], scopes: Tuple[str] = ("install",)
209+
) -> List[Requirement]:
204210
"""
205211
Generate parsed requirements for the given ``dependencies``.
206212
"""
@@ -209,7 +215,7 @@ def get_requirements_from_dependencies(dependencies: List[DependentPackage]) ->
209215
continue
210216

211217
# TODO: consider other scopes and using the is_runtime flag
212-
if dep.scope != "install":
218+
if dep.scope not in scopes:
213219
continue
214220

215221
# FIXME We are skipping editable requirements
@@ -284,12 +290,16 @@ def get_versions_for_package_from_repo(
284290
if wheels:
285291
valid_wheel_present = False
286292
for wheel in wheels:
287-
if utils_pypi.valid_distribution(wheel, python_version):
293+
if utils_pypi.valid_python_version(
294+
python_requires=wheel.python_requires, python_version=python_version
295+
):
288296
valid_wheel_present = True
289297
if valid_wheel_present:
290298
versions.append(version)
291299
if package.sdist:
292-
if utils_pypi.valid_distribution(package.sdist, python_version):
300+
if utils_pypi.valid_python_version(
301+
python_requires=package.sdist.python_requires, python_version=python_version
302+
):
293303
versions.append(version)
294304
return versions
295305

src/python_inspector/resolve_cli.py

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@
1919
from tinynetrc import Netrc
2020

2121
from _packagedcode.models import DependentPackage
22-
from _packagedcode.pypi import PipRequirementsFileHandler
22+
from _packagedcode.pypi import PythonSetupPyHandler
2323
from _packagedcode.pypi import can_process_dependent_package
2424
from python_inspector import dependencies
2525
from python_inspector import utils
2626
from python_inspector import utils_pypi
2727
from python_inspector.cli_utils import FileOptionType
2828
from python_inspector.resolution import get_environment_marker_from_environment
29+
from python_inspector.resolution import get_python_version_from_env_tag
2930
from python_inspector.resolution import get_resolved_dependencies
3031

3132
TRACE = False
@@ -50,13 +51,14 @@
5051
"This option can be used multiple times.",
5152
)
5253
@click.option(
53-
"-n",
54-
"--netrc",
55-
"netrc_file",
54+
"-s",
55+
"--setup-py",
56+
"setup_py_file",
5657
type=click.Path(exists=True, readable=True, path_type=str, dir_okay=False),
57-
metavar="NETRC-FILE",
58+
metavar="SETUP-PY-FILE",
5859
required=False,
59-
help="Netrc file to use for authentication. ",
60+
help="Path to setuptools setup.py file listing dependencies and metadata. "
61+
"This option can be used multiple times.",
6062
)
6163
@click.option(
6264
"--spec",
@@ -117,9 +119,20 @@
117119
help="Write output as pretty-printed JSON to FILE as a tree in the style of pipdeptree. "
118120
"Use the special '-' file name to print results on screen/stdout.",
119121
)
122+
@click.option(
123+
"-n",
124+
"--netrc",
125+
"netrc_file",
126+
type=click.Path(exists=True, readable=True, path_type=str, dir_okay=False),
127+
metavar="NETRC-FILE",
128+
hidden=True,
129+
required=False,
130+
help="Netrc file to use for authentication. ",
131+
)
120132
@click.option(
121133
"--max-rounds",
122134
"max_rounds",
135+
hidden=True,
123136
type=int,
124137
default=200000,
125138
help="Increase the max rounds whenever the resolution is too deep",
@@ -146,21 +159,23 @@
146159
def resolve_dependencies(
147160
ctx,
148161
requirement_files,
149-
netrc_file,
162+
setup_py_file,
150163
specifiers,
151164
python_version,
152165
operating_system,
153166
index_urls,
154167
json_output,
155168
pdt_output,
169+
netrc_file,
156170
max_rounds,
157171
use_cached_index=False,
158172
use_pypi_json_api=False,
159173
verbose=TRACE,
160174
):
161175
"""
162-
Resolve the dependencies of the packages listed in REQUIREMENT-FILE(s) file
163-
and SPECIFIER(s) and save the results as JSON to FILE.
176+
Resolve the dependencies for the package requirements listed in one or
177+
more REQUIREMENT-FILE file, one or more SPECIFIER and one setuptools
178+
SETUP-PY-FILE file and save the results as JSON to FILE.
164179
165180
Resolve the dependencies for the requested ``--python-version`` PYVER and
166181
``--operating_system`` OS combination defaulting Python version 3.8 and
@@ -221,6 +236,29 @@ def resolve_dependencies(
221236
dep = dependencies.get_dependency(specifier=specifier)
222237
direct_dependencies.append(dep)
223238

239+
if setup_py_file:
240+
package_data = list(PythonSetupPyHandler.parse(location=setup_py_file))
241+
assert len(package_data) == 1
242+
package_data = package_data[0]
243+
# validate if python require matches our current python version
244+
python_requires = package_data.extra_data.get("python_requires")
245+
if not utils_pypi.valid_python_version(
246+
python_version=get_python_version_from_env_tag(python_version),
247+
python_requires=python_requires,
248+
):
249+
click.secho(
250+
f"Python version {get_python_version_from_env_tag(python_version)} "
251+
f"is not compatible with setup.py {setup_py_file} "
252+
f"python_requires {python_requires}",
253+
err=True,
254+
)
255+
ctx.exit(1)
256+
257+
for dep in package_data.dependencies:
258+
# TODO : we need to handle to all the scopes
259+
if dep.scope == "install":
260+
direct_dependencies.append(dep)
261+
224262
if not direct_dependencies:
225263
click.secho("Error: no requirements requested.")
226264
ctx.exit(1)

src/python_inspector/utils_pypi.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,15 @@
9898
TRACE_ULTRA_DEEP = False
9999

100100
# Supported environments
101-
PYTHON_VERSIONS = "36", "37", "38", "39", "310"
101+
PYTHON_VERSIONS = "36", "37", "38", "39", "310", "27"
102102

103103
PYTHON_DOT_VERSIONS_BY_VER = {
104104
"36": "3.6",
105105
"37": "3.7",
106106
"38": "3.8",
107107
"39": "3.9",
108108
"310": "3.10",
109+
"27": "2.7",
109110
}
110111

111112

@@ -122,6 +123,7 @@ def get_python_dot_version(version):
122123
"38": ["cp38", "cp38m", "abi3"],
123124
"39": ["cp39", "cp39m", "abi3"],
124125
"310": ["cp310", "cp310m", "abi3"],
126+
"27": ["cp27", "cp27m"],
125127
}
126128

127129
PLATFORMS_BY_OS = {
@@ -256,7 +258,9 @@ def get_valid_sdist(repo, name, version, python_version=DEFAULT_PYTHON_VERSION):
256258
if TRACE_DEEP:
257259
print(f" get_valid_sdist: No sdist for {name}=={version}")
258260
return
259-
if not valid_distribution(sdist, python_version):
261+
if not valid_python_version(
262+
python_requires=sdist.python_requires, python_version=python_version
263+
):
260264
return
261265
if TRACE_DEEP:
262266
print(f" get_valid_sdist: Getting sdist from index (or cache): {sdist.download_url}")
@@ -285,7 +289,9 @@ def get_supported_and_valid_wheels(
285289
return []
286290
wheels = []
287291
for wheel in supported_wheels:
288-
if not valid_distribution(wheel, python_version):
292+
if not valid_python_version(
293+
python_requires=wheel.python_requires, python_version=python_version
294+
):
289295
continue
290296
if TRACE_DEEP:
291297
print(
@@ -296,13 +302,13 @@ def get_supported_and_valid_wheels(
296302
return wheels
297303

298304

299-
def valid_distribution(distribution, python_version):
305+
def valid_python_version(python_version, python_requires):
300306
"""
301-
Return True if distribution is a valid distribution for the given Python version.
307+
Return True if ``python_version`` is in the ``python_requires``.
302308
"""
303-
if not distribution.python_requires:
309+
if not python_requires:
304310
return True
305-
return python_version in SpecifierSet(distribution.python_requires)
311+
return python_version in SpecifierSet(python_requires)
306312

307313

308314
def download_sdist(

tests/data/setup/simple-setup.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright (C) 2017-2021 HERE Europe B.V.
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# SPDX-License-Identifier: Apache-2.0
16+
# License-Filename: LICENSE
17+
18+
from setuptools import find_packages
19+
from setuptools import setup
20+
21+
setup(
22+
name="Example-App",
23+
description="A synthetic test case for OSS Review Toolkit",
24+
version="2.4.0",
25+
url="https://example.org/app",
26+
license="MIT License",
27+
classifiers=["License :: OSI Approved :: MIT License", "Programming Language :: Python :: 2"],
28+
python_requires=">2, <=3",
29+
install_requires=["license-expression>=0.1, <1.2"],
30+
packages=find_packages(),
31+
)

0 commit comments

Comments
 (0)