Skip to content

Commit 4281d75

Browse files
committed
Add tests for live_eval
Signed-off-by: Tushar Goel <[email protected]>
1 parent d555132 commit 4281d75

File tree

12 files changed

+947
-416
lines changed

12 files changed

+947
-416
lines changed

src/python_inspector/resolution.py

Lines changed: 70 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
from packaging.version import LegacyVersion
2727
from packaging.version import Version
2828
from packaging.version import parse as parse_version
29-
from requirements_builder.requirements_builder import iter_requirements
3029
from resolvelib import AbstractProvider
3130
from resolvelib import Resolver
3231
from resolvelib.reporters import BaseReporter
@@ -40,6 +39,7 @@
4039
from _packagedcode.pypi import SetupCfgHandler
4140
from _packagedcode.pypi import can_process_dependent_package
4241
from python_inspector import utils_pypi
42+
from python_inspector.setup_py_live_eval import iter_requirements
4343
from python_inspector.utils_pypi import Environment
4444
from python_inspector.utils_pypi import PypiSimpleRepository
4545

@@ -97,6 +97,8 @@ def get_requirements_from_distribution(
9797
Return a list of requirements from a source distribution or wheel at
9898
``location`` using the provided ``handler`` DatafileHandler for parsing.
9999
"""
100+
if not location:
101+
return []
100102
if not os.path.exists(location):
101103
return []
102104
deps = []
@@ -137,9 +139,8 @@ def parse_setup_py_insecurely(setup_py):
137139
"""
138140
if not os.path.exists(setup_py):
139141
return []
140-
unparsed_requirements = iter_requirements(level="", extras=[], setup_file=setup_py)
141-
for requirement in unparsed_requirements:
142-
yield Requirement(requirement)
142+
for req in iter_requirements(level="", extras=[], setup_file=setup_py):
143+
yield Requirement(req)
143144

144145

145146
def is_valid_version(
@@ -373,61 +374,7 @@ def get_requirements_for_package_from_pypi_simple(
373374
)
374375
if not sdist_location:
375376
return
376-
setup_py_location = os.path.join(
377-
sdist_location,
378-
"setup.py",
379-
)
380-
setup_cfg_location = os.path.join(
381-
sdist_location,
382-
"setup.cfg",
383-
)
384-
385-
if not os.path.exists(setup_py_location) and not os.path.exists(setup_cfg_location):
386-
raise Exception(f"No setup.py or setup.cfg found in pypi sdist {sdist_location}")
387-
388-
# Some commonon packages like flask may have some dependencies in setup.cfg
389-
# and some dependencies in setup.py. We are going to check both.
390-
location_by_sdist_parser = {
391-
PythonSetupPyHandler: setup_py_location,
392-
SetupCfgHandler: setup_cfg_location,
393-
}
394-
395-
# Set to True if we found any dependencies in setup.py or setup.cfg
396-
has_deps = False
397-
398-
for handler, location in location_by_sdist_parser.items():
399-
deps = get_requirements_from_distribution(
400-
handler=handler,
401-
location=location,
402-
)
403-
if deps:
404-
has_deps = True
405-
yield from deps
406-
407-
if not has_deps and contain_string(
408-
string="requirements.txt", files=[setup_py_location, setup_cfg_location]
409-
):
410-
# Look in requirements file if and only if thy are refered in setup.py or setup.cfg
411-
# And no deps have been yielded by requirements file.
412-
requirement_location = os.path.join(
413-
sdist_location,
414-
"requirements.txt",
415-
)
416-
deps = get_requirements_from_distribution(
417-
handler=PipRequirementsFileHandler,
418-
location=requirement_location,
419-
)
420-
if deps:
421-
has_deps = True
422-
yield from deps
423-
424-
if not has_deps and contain_string(
425-
string="_require", files=[setup_py_location, setup_cfg_location]
426-
):
427-
if self.insecure:
428-
yield from parse_setup_py_insecurely(setup_py=setup_py_location)
429-
else:
430-
raise Exception("Unable to collect setup.py dependencies securely")
377+
yield from get_setup_dependencies(location=sdist_location, insecure=self.insecure)
431378

432379
def get_requirements_for_package_from_pypi_json_api(
433380
self, purl: PackageURL
@@ -704,6 +651,70 @@ def format_pdt_tree(results):
704651
return dependencies
705652

706653

654+
def get_setup_dependencies(location, insecure=False, use_requirements=True):
655+
"""
656+
Yield dependencies from the given setup.py and setup.cfg location.
657+
"""
658+
659+
setup_py_location = os.path.join(
660+
location,
661+
"setup.py",
662+
)
663+
setup_cfg_location = os.path.join(
664+
location,
665+
"setup.cfg",
666+
)
667+
668+
if not os.path.exists(setup_py_location) and not os.path.exists(setup_cfg_location):
669+
raise Exception(f"No setup.py or setup.cfg found in pypi sdist {location}")
670+
671+
# Some commonon packages like flask may have some dependencies in setup.cfg
672+
# and some dependencies in setup.py. We are going to check both.
673+
location_by_sdist_parser = {
674+
PythonSetupPyHandler: setup_py_location,
675+
SetupCfgHandler: setup_cfg_location,
676+
}
677+
678+
# Set to True if we found any dependencies in setup.py or setup.cfg
679+
has_deps = False
680+
681+
for handler, location in location_by_sdist_parser.items():
682+
deps = get_requirements_from_distribution(
683+
handler=handler,
684+
location=location,
685+
)
686+
if deps:
687+
has_deps = True
688+
yield from deps
689+
690+
if (
691+
use_requirements
692+
and not has_deps
693+
and contain_string(string="requirements.txt", files=[setup_py_location, setup_cfg_location])
694+
):
695+
# Look in requirements file if and only if thy are refered in setup.py or setup.cfg
696+
# And no deps have been yielded by requirements file.
697+
requirement_location = os.path.join(
698+
location,
699+
"requirements.txt",
700+
)
701+
deps = get_requirements_from_distribution(
702+
handler=PipRequirementsFileHandler,
703+
location=requirement_location,
704+
)
705+
if deps:
706+
has_deps = True
707+
yield from deps
708+
709+
if not has_deps and contain_string(
710+
string="_require", files=[setup_py_location, setup_cfg_location]
711+
):
712+
if insecure:
713+
yield from parse_setup_py_insecurely(setup_py=setup_py_location)
714+
else:
715+
raise Exception("Unable to collect setup.py dependencies securely")
716+
717+
707718
def get_resolved_dependencies(
708719
requirements: List[Requirement],
709720
environment: Environment = None,

src/python_inspector/setup_py_live_eval.py

Lines changed: 52 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
#
1010
"""Generate requirements from `setup.py` and `requirements-devel.txt`."""
1111

12-
from __future__ import absolute_import, print_function
12+
from __future__ import absolute_import
13+
from __future__ import print_function
1314

1415
import os
1516
import re
@@ -37,18 +38,16 @@ def parse_set(string):
3738
def minver_error(pkg_name):
3839
"""Report error about missing minimum version constraint and exit."""
3940
print(
40-
'ERROR: specify minimal version of "{0}" using '
41-
'">=" or "=="'.format(pkg_name),
42-
file=sys.stderr
41+
'ERROR: specify minimal version of "{0}" using ' '">=" or "=="'.format(pkg_name),
42+
file=sys.stderr,
4343
)
4444
sys.exit(1)
4545

4646

4747
def build_pkg_name(pkg):
4848
"""Build package name, including extras if present."""
4949
if pkg.extras:
50-
return '{0}[{1}]'.format(
51-
pkg.project_name, ','.join(sorted(pkg.extras)))
50+
return "{0}[{1}]".format(pkg.project_name, ",".join(sorted(pkg.extras)))
5251
return pkg.project_name
5352

5453

@@ -66,96 +65,77 @@ def parse_pip_file(path):
6665
line = line.strip()
6766

6867
# see https://pip.readthedocs.io/en/1.1/requirements.html
69-
if line.startswith('-e'):
68+
if line.startswith("-e"):
7069
# devel requirement
71-
splitted = line.split('#egg=')
70+
splitted = line.split("#egg=")
7271
rdev[splitted[1].lower()] = line
7372

74-
elif line.startswith('-r'):
73+
elif line.startswith("-r"):
7574
# recursive file command
76-
splitted = re.split('-r\\s+', line)
75+
splitted = re.split("-r\\s+", line)
7776
subrdev, subrnormal, substuff = parse_pip_file(
7877
os.path.join(os.path.dirname(path), splitted[1])
7978
)
8079
for k, v in subrdev.items():
8180
if k not in rdev:
8281
rdev[k] = v
8382
rnormal.extend(subrnormal)
84-
elif line.startswith('-'):
83+
elif line.startswith("-"):
8584
# another special command we don't recognize
8685
stuff.append(line)
8786
else:
8887
# ordinary requirement, similarly to them used in setup.py
8988
rnormal.append(line)
9089
except IOError:
91-
print(
92-
'Warning: could not parse requirements file "{0}"!'.format(path),
93-
file=sys.stderr
94-
)
90+
print('Warning: could not parse requirements file "{0}"!'.format(path), file=sys.stderr)
9591

9692
return rdev, rnormal, stuff
9793

9894

99-
def iter_requirements(level, extras, pip_file, setup_fp, setup_cfg_fp=None):
95+
def iter_requirements(level, extras, setup_file):
10096
"""Iterate over requirements."""
97+
from pathlib import Path
98+
99+
setup_file = str(Path(setup_file).absolute())
101100
result = dict()
102101
requires = []
103102
stuff = []
104-
if level == 'dev' or setup_fp is None:
105-
result, requires, stuff = parse_pip_file(pip_file)
106-
103+
cd = os.getcwd()
104+
os.chdir(os.path.dirname(setup_file))
107105
install_requires = []
108106
requires_extras = {}
109-
if setup_fp is not None:
110-
with mock.patch.object(setuptools, 'setup') as mock_setup:
111-
sys.path.append(os.path.dirname(setup_fp.name))
112-
g = {'__file__': setup_fp.name, '__name__': '__main__'}
113-
exec(setup_fp.read(), g)
114-
sys.path.pop()
115-
assert g['setup'] # silence warning about unused imports
116-
117-
# called arguments are in `mock_setup.call_args`
118-
mock_args, mock_kwargs = mock_setup.call_args
119-
install_requires = mock_kwargs.get(
120-
'install_requires', install_requires
121-
)
122-
requires_extras = mock_kwargs.get('extras_require', requires_extras)
123-
124-
if setup_cfg_fp is not None:
125-
parser = configparser.ConfigParser()
126-
parser.read_file(setup_cfg_fp)
127-
128-
if parser.has_section("options"):
129-
value = parser.get("options", "install_requires",
130-
fallback="").strip()
131-
132-
if value:
133-
install_requires = [s.strip() for s in value.splitlines()]
134-
135-
if parser.has_section("options.extras_require"):
136-
for name, value in parser.items("options.extras_require"):
137-
requires_extras[name] = [s.strip()
138-
for s in value.strip().splitlines()]
139-
140-
install_requires.extend(requires)
107+
# change directory to setup.py path
108+
with mock.patch.object(setuptools, "setup") as mock_setup:
109+
sys.path.append(os.path.dirname(setup_file))
110+
g = {"__file__": setup_file, "__name__": "__main__"}
111+
with open(setup_file) as sf:
112+
exec(sf.read(), g)
113+
sys.path.pop()
114+
assert g["setup"] # silence warning about unused imports
115+
# called arguments are in `mock_setup.call_args`
116+
os.chdir(cd)
117+
mock_args, mock_kwargs = mock_setup.call_args
118+
install_requires = mock_kwargs.get("install_requires", install_requires)
119+
120+
requires_extras = mock_kwargs.get("extras_require", requires_extras)
141121

142122
for e, reqs in requires_extras.items():
143123
# Handle conditions on extras. See pkginfo_to_metadata function
144124
# in Wheel for details.
145-
condition = ''
146-
if ':' in e:
147-
e, condition = e.split(':', 1)
125+
condition = ""
126+
if ":" in e:
127+
e, condition = e.split(":", 1)
148128
if not e or e in extras:
149129
if condition:
150-
reqs = ['{0}; {1}'.format(r, condition) for r in reqs]
130+
reqs = ["{0}; {1}".format(r, condition) for r in reqs]
151131
install_requires.extend(reqs)
152132

153133
for pkg in pkg_resources.parse_requirements(install_requires):
154134
# skip things we already know
155135
# FIXME be smarter about merging things
156136

157137
# Evaluate environment markers skip if not applicable
158-
if hasattr(pkg, 'marker') and pkg.marker is not None:
138+
if hasattr(pkg, "marker") and pkg.marker is not None:
159139
if not pkg.marker.evaluate():
160140
continue
161141
else:
@@ -166,44 +146,37 @@ def iter_requirements(level, extras, pip_file, setup_fp, setup_cfg_fp=None):
166146
continue
167147

168148
specs = dict(pkg.specs)
169-
if (('>=' in specs) and ('>' in specs)) \
170-
or (('<=' in specs) and ('<' in specs)):
149+
if ((">=" in specs) and (">" in specs)) or (("<=" in specs) and ("<" in specs)):
171150
print(
172-
'ERROR: Do not specify such weird constraints! '
173-
'("{0}")'.format(pkg),
174-
file=sys.stderr
151+
"ERROR: Do not specify such weird constraints! " '("{0}")'.format(pkg),
152+
file=sys.stderr,
175153
)
176154
sys.exit(1)
177155

178-
if '==' in specs:
179-
result[pkg.key] = '{0}=={1}'.format(
180-
build_pkg_name(pkg), specs['=='])
156+
if "==" in specs:
157+
result[pkg.key] = "{0}=={1}".format(build_pkg_name(pkg), specs["=="])
181158

182-
elif '>=' in specs:
183-
if level == 'min':
184-
result[pkg.key] = '{0}=={1}'.format(
185-
build_pkg_name(pkg), specs['>=']
186-
)
159+
elif ">=" in specs:
160+
if level == "min":
161+
result[pkg.key] = "{0}=={1}".format(build_pkg_name(pkg), specs[">="])
187162
else:
188163
result[pkg.key] = pkg
189164

190-
elif '>' in specs:
191-
if level == 'min':
165+
elif ">" in specs:
166+
if level == "min":
192167
minver_error(build_pkg_name(pkg))
193168
else:
194169
result[pkg.key] = pkg
195170

196-
elif '~=' in specs:
197-
if level == 'min':
198-
result[pkg.key] = '{0}=={1}'.format(
199-
build_pkg_name(pkg), specs['~='])
171+
elif "~=" in specs:
172+
if level == "min":
173+
result[pkg.key] = "{0}=={1}".format(build_pkg_name(pkg), specs["~="])
200174
else:
201-
ver, _ = os.path.splitext(specs['~='])
202-
result[pkg.key] = '{0}>={1},=={2}.*'.format(
203-
build_pkg_name(pkg), specs['~='], ver)
175+
ver, _ = os.path.splitext(specs["~="])
176+
result[pkg.key] = "{0}>={1},=={2}.*".format(build_pkg_name(pkg), specs["~="], ver)
204177

205178
else:
206-
if level == 'min':
179+
if level == "min":
207180
minver_error(build_pkg_name(pkg))
208181
else:
209182
result[pkg.key] = build_pkg_name(pkg)

0 commit comments

Comments
 (0)