Skip to content

Commit 1ba7601

Browse files
committed
Add tests for live_eval
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 7da295a commit 1ba7601

12 files changed

Lines changed: 1438 additions & 208 deletions

File tree

requirements_builder.ABOUT

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ install_requires =
6868
saneyaml >= 0.5.2
6969
tinynetrc >= 1.3.1
7070
toml >= 0.10.0
71+
mock >= 3.0.5
7172

7273
[options.packages.find]
7374
where = src

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

436383
def get_requirements_for_package_from_pypi_json_api(
437384
self, purl: PackageURL
@@ -731,6 +678,70 @@ def get_package_list(results):
731678
return list(sorted(packages))
732679

733680

681+
def get_setup_dependencies(location, insecure=False, use_requirements=True):
682+
"""
683+
Yield dependencies from the given setup.py and setup.cfg location.
684+
"""
685+
686+
setup_py_location = os.path.join(
687+
location,
688+
"setup.py",
689+
)
690+
setup_cfg_location = os.path.join(
691+
location,
692+
"setup.cfg",
693+
)
694+
695+
if not os.path.exists(setup_py_location) and not os.path.exists(setup_cfg_location):
696+
raise Exception(f"No setup.py or setup.cfg found in pypi sdist {location}")
697+
698+
# Some commonon packages like flask may have some dependencies in setup.cfg
699+
# and some dependencies in setup.py. We are going to check both.
700+
location_by_sdist_parser = {
701+
PythonSetupPyHandler: setup_py_location,
702+
SetupCfgHandler: setup_cfg_location,
703+
}
704+
705+
# Set to True if we found any dependencies in setup.py or setup.cfg
706+
has_deps = False
707+
708+
for handler, location in location_by_sdist_parser.items():
709+
deps = get_requirements_from_distribution(
710+
handler=handler,
711+
location=location,
712+
)
713+
if deps:
714+
has_deps = True
715+
yield from deps
716+
717+
if (
718+
use_requirements
719+
and not has_deps
720+
and contain_string(string="requirements.txt", files=[setup_py_location, setup_cfg_location])
721+
):
722+
# Look in requirements file if and only if thy are refered in setup.py or setup.cfg
723+
# And no deps have been yielded by requirements file.
724+
requirement_location = os.path.join(
725+
location,
726+
"requirements.txt",
727+
)
728+
deps = get_requirements_from_distribution(
729+
handler=PipRequirementsFileHandler,
730+
location=requirement_location,
731+
)
732+
if deps:
733+
has_deps = True
734+
yield from deps
735+
736+
if not has_deps and contain_string(
737+
string="_require", files=[setup_py_location, setup_cfg_location]
738+
):
739+
if insecure:
740+
yield from parse_setup_py_insecurely(setup_py=setup_py_location)
741+
else:
742+
raise Exception("Unable to collect setup.py dependencies securely")
743+
744+
734745
def get_resolved_dependencies(
735746
requirements: List[Requirement],
736747
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)