Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

try to build wheels for directories/vcs #4714

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3517df5
Try to build wheels for directories/VCS
takluyver May 19, 2017
b241c22
Add news file
takluyver May 19, 2017
fa927a6
Fix list comp
takluyver May 19, 2017
48ee5d7
Allow temp files for another test
takluyver May 19, 2017
b229fd4
Fix cleanup on 'pip install' with no args
takluyver May 19, 2017
c5297e8
Cleanup wheel cache after 'pip freeze'
takluyver May 19, 2017
8fbd8f4
Expect temp files in another test
takluyver May 19, 2017
e58e50f
Update test of install output
takluyver May 19, 2017
2c67a91
Expect temp files on another test
takluyver May 19, 2017
ef1a3ae
Update another install output test
takluyver May 19, 2017
a7b0e4f
Update test to check for dist-info
takluyver May 19, 2017
58f6917
Remove string formatting operation
takluyver May 19, 2017
711c99a
Install 'wheel' for test that now requires it
takluyver May 20, 2017
25d5546
fix merge issues
xoviat Sep 7, 2017
882c8b9
flake8
alex Jun 26, 2017
ade439c
fix
alex Jul 6, 2017
396abfe
not a thing anymore
alex Jul 6, 2017
dd326a7
these are probably needed
alex Jul 6, 2017
951dfdd
flake8
alex Jul 6, 2017
e168cbb
cache: implement epherm cache directory and cleanup
xoviat Sep 7, 2017
dbcd1a7
cache: add epem parameter
xoviat Sep 7, 2017
c28d614
tests: install: fix merge errors
xoviat Sep 7, 2017
bd31280
:art:
xoviat Sep 7, 2017
b6377d4
:art:
xoviat Sep 7, 2017
edcc6de
tests: user: use common_wheels fixture
xoviat Sep 8, 2017
97c667b
wheel: rename ephen_cache flag
xoviat Sep 8, 2017
43eb3c6
:newspaper:
xoviat Sep 8, 2017
00281b3
cache: move ephem flag into two methods
xoviat Sep 8, 2017
02a5d58
req_set: revert 396abfe7fbcd1552dfca753e3b5d2b9f85bad89d.
xoviat Sep 9, 2017
fb09a60
cache: fix spelling error
xoviat Sep 25, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions news/4501.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Installing from a local directory or a VCS URL now builds a wheel to install,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you rephrase this to:

pip now builds a wheel when installing from a local directory or a VCS URL. Wheels from these sources are not cached.

?

rather than running ``setup.py install``. Wheels from these sources are not
cached.
16 changes: 14 additions & 2 deletions src/pip/_internal/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from pip._internal.compat import expanduser
from pip._internal.download import path_to_url
from pip._internal.wheel import InvalidWheelFilename, Wheel
from pip._internal.utils import temp_dir

logger = logging.getLogger(__name__)

Expand All @@ -32,6 +33,9 @@ def __init__(self, cache_dir, format_control, allowed_formats):
self.cache_dir = expanduser(cache_dir) if cache_dir else None
self.format_control = format_control
self.allowed_formats = allowed_formats
# Ephemeral cache: store wheels just for this run
self._ephem_cache_dir = temp_dir.TempDirectory(kind="ephem-cache")
self._ephem_cache_dir.create()

_valid_formats = {"source", "binary"}
assert self.allowed_formats.union(_valid_formats) == _valid_formats
Expand Down Expand Up @@ -102,6 +106,9 @@ def _link_for_candidate(self, link, candidate):

return index.Link(path_to_url(path))

def cleanup(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be on WheelCache. Also, since it only clears the ephemeral cache, I'd suggest renaming this to cleanup_ephem.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After thinking about this some more, I don't agree. I think that first of all, cleanup methods have very little support in the Python ecosystem. Second of all, when there is a cleanup method, it should be defined on the base class so that it's always called after using the object and specific implementations should go on derived classes.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, specifically, I propose making this pass, and then moving the guts to WheelCache.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

"""Remove the ephermal caches created temporarily to build wheels"""
self._ephem_cache_dir.cleanup()

class WheelCache(Cache):
"""A cache of wheels for future installs.
Expand All @@ -110,7 +117,7 @@ class WheelCache(Cache):
def __init__(self, cache_dir, format_control):
super(WheelCache, self).__init__(cache_dir, format_control, {"binary"})

def get_path_for_link(self, link):
def get_path_for_link(self, link, ephem=False):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having a boolean in the signature, I'd suggest you implement a new method WheelCache.get_ephem_path_for_link.

Relevant Blog Post: https://martinfowler.com/bliki/FlagArgument.html

"""Return a directory to store cached wheels for link

Because there are M wheels for any one sdist, we provide a directory
Expand All @@ -127,9 +134,14 @@ def get_path_for_link(self, link):
"""
parts = self._get_cache_path_parts(link)

if not ephem:
cache_dir = self.cache_dir
else:
cache_dir = self._ephem_cache_dir.path

# Inside of the base location for cached wheels, expand our parts and
# join them all together.
return os.path.join(self.cache_dir, "wheels", *parts)
return os.path.join(cache_dir, "wheels", *parts)

def get(self, link, package_name):
candidates = []
Expand Down
7 changes: 5 additions & 2 deletions src/pip/_internal/commands/freeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,8 @@ def run(self, options, args):
skip=skip,
exclude_editable=options.exclude_editable)

for line in freeze(**freeze_kwargs):
sys.stdout.write(line + '\n')
try:
for line in freeze(**freeze_kwargs):
sys.stdout.write(line + '\n')
finally:
wheel_cache.cleanup()
24 changes: 13 additions & 11 deletions src/pip/_internal/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,18 +247,19 @@ def run(self, options, args):
use_user_site=options.use_user_site,
)

self.populate_requirement_set(
requirement_set, args, options, finder, session, self.name,
wheel_cache
)
preparer = RequirementPreparer(
build_dir=directory.path,
src_dir=options.src_dir,
download_dir=None,
wheel_download_dir=None,
progress_bar=options.progress_bar,
)
try:
self.populate_requirement_set(
requirement_set, args, options, finder, session,
self.name, wheel_cache
)
preparer = RequirementPreparer(
build_dir=directory.path,
src_dir=options.src_dir,
download_dir=None,
wheel_download_dir=None,
progress_bar=options.progress_bar,
)

resolver = Resolver(
preparer=preparer,
finder=finder,
Expand Down Expand Up @@ -350,6 +351,7 @@ def run(self, options, args):
# Clean up
if not options.no_clean:
requirement_set.cleanup_files()
wheel_cache.cleanup()

if options.target_dir:
self._handle_target_dir(
Expand Down
53 changes: 27 additions & 26 deletions src/pip/_internal/commands/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,35 +146,35 @@ def run(self, options, args):
require_hashes=options.require_hashes,
)

self.populate_requirement_set(
requirement_set, args, options, finder, session, self.name,
wheel_cache
)
try:
self.populate_requirement_set(
requirement_set, args, options, finder, session,
self.name, wheel_cache
)

preparer = RequirementPreparer(
build_dir=directory.path,
src_dir=options.src_dir,
download_dir=None,
wheel_download_dir=options.wheel_dir,
progress_bar=options.progress_bar,
)
preparer = RequirementPreparer(
build_dir=directory.path,
src_dir=options.src_dir,
download_dir=None,
wheel_download_dir=options.wheel_dir,
progress_bar=options.progress_bar,
)

resolver = Resolver(
preparer=preparer,
finder=finder,
session=session,
wheel_cache=wheel_cache,
use_user_site=False,
upgrade_strategy="to-satisfy-only",
force_reinstall=False,
ignore_dependencies=options.ignore_dependencies,
ignore_requires_python=options.ignore_requires_python,
ignore_installed=True,
isolated=options.isolated_mode,
)
resolver.resolve(requirement_set)
resolver = Resolver(
preparer=preparer,
finder=finder,
session=session,
wheel_cache=wheel_cache,
use_user_site=False,
upgrade_strategy="to-satisfy-only",
force_reinstall=False,
ignore_dependencies=options.ignore_dependencies,
ignore_requires_python=options.ignore_requires_python,
ignore_installed=True,
isolated=options.isolated_mode,
)
resolver.resolve(requirement_set)

try:
# build wheels
wb = WheelBuilder(
requirement_set,
Expand All @@ -196,3 +196,4 @@ def run(self, options, args):
finally:
if not options.no_clean:
requirement_set.cleanup_files()
wheel_cache.cleanup()
3 changes: 0 additions & 3 deletions src/pip/_internal/req/req_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ def __init__(self,
require_hashes=False, target_dir=None, use_user_site=False,
pycompile=True):
"""Create a RequirementSet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an unrelated (albeit obvious) change. It'd be nice if you revert this hunk and make a new PR for it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was 396abfe done by @alex. I would need to get his consent before removing it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uhhh, no clue, feel free to ignore whatever I did.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will remove this then.

:param wheel_cache: The pip wheel cache, for passing to
InstallRequirement.
"""

self.requirements = OrderedDict()
Expand Down
20 changes: 11 additions & 9 deletions src/pip/_internal/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,7 @@ def build(self, session, autobuilding=False):

buildset = []
for req in reqset:
ephem_cache = False
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bike-shedding: I'll suggest painting this as use_ephem_cache; I won't push if anyone disagrees.

if req.constraint:
continue
if req.is_wheel:
Expand All @@ -750,41 +751,42 @@ def build(self, session, autobuilding=False):
elif autobuilding and req.editable:
pass
elif autobuilding and req.link and not req.link.is_artifact:
pass
# VCS checkout. Build wheel just for this run.
ephem_cache = True
elif autobuilding and not req.source_dir:
pass
else:
if autobuilding:
link = req.link
base, ext = link.splitext()
if index.egg_info_matches(base, None, link) is None:
# Doesn't look like a package - don't autobuild a wheel
# because we'll have no way to lookup the result sanely
continue
if "binary" not in index.fmt_ctl_formats(
# E.g. local directory. Build wheel just for this run.
ephem_cache = True
elif "binary" not in index.fmt_ctl_formats(
self.finder.format_control,
canonicalize_name(req.name)):
logger.info(
"Skipping bdist_wheel for %s, due to binaries "
"being disabled for it.", req.name)
continue
buildset.append(req)
buildset.append((req, ephem_cache))

if not buildset:
return True

# Build the wheels.
logger.info(
'Building wheels for collected packages: %s',
', '.join([req.name for req in buildset]),
', '.join([req.name for (req, _) in buildset]),
)
with indent_log():
build_success, build_failure = [], []
for req in buildset:
for req, ephem in buildset:
python_tag = None
if autobuilding:
python_tag = pep425tags.implementation_tag
output_dir = self.wheel_cache.get_path_for_link(req.link)
output_dir = self.wheel_cache.get_path_for_link(req.link,
ephem=ephem)
try:
ensure_dir(output_dir)
except OSError as e:
Expand Down
21 changes: 11 additions & 10 deletions tests/functional/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -983,16 +983,15 @@ def test_install_builds_wheels(script, data, common_wheels):
# and built wheels for upper and wheelbroken
assert "Running setup.py bdist_wheel for upper" in str(res), str(res)
assert "Running setup.py bdist_wheel for wheelb" in str(res), str(res)
# But not requires_wheel... which is a local dir and thus uncachable.
assert "Running setup.py bdist_wheel for requir" not in str(res), str(res)
# Wheels are built for local directories, but not cached.
assert "Running setup.py bdist_wheel for requir" in str(res), str(res)
# wheelbroken has to run install
# into the cache
assert wheels != [], str(res)
# and installed from the wheel
assert "Running setup.py install for upper" not in str(res), str(res)
# the local tree can't build a wheel (because we can't assume that every
# build will have a suitable unique key to cache on).
assert "Running setup.py install for requires-wheel" in str(res), str(res)
# Wheels are built for local directories, but not cached.
assert "Running setup.py install for requir" not in str(res), str(res)
# wheelbroken has to run install
assert "Running setup.py install for wheelb" in str(res), str(res)
# We want to make sure we used the correct implementation tag
Expand All @@ -1016,13 +1015,15 @@ def test_install_no_binary_disables_building_wheels(
assert expected in str(res), str(res)
# and built wheels for wheelbroken only
assert "Running setup.py bdist_wheel for wheelb" in str(res), str(res)
# But not requires_wheel... which is a local dir and thus uncachable.
assert "Running setup.py bdist_wheel for requir" not in str(res), str(res)
# Wheels are built for local directories, but not cached
assert "Running setup.py bdist_wheel for requir" in str(res), str(res)
# Nor upper, which was blacklisted
assert "Running setup.py bdist_wheel for upper" not in str(res), str(res)
# the local tree can't build a wheel (because we can't assume that every
# build will have a suitable unique key to cache on).
assert "Running setup.py install for requires-wheel" in str(res), str(res)
# wheelbroken has to run install
# into the cache
assert wheels != [], str(res)
# Wheels are built for local directories, but not cached
assert "Running setup.py install for requir" not in str(res), str(res)
# And these two fell back to sdist based installed.
assert "Running setup.py install for wheelb" in str(res), str(res)
assert "Running setup.py install for upper" in str(res), str(res)
Expand Down
4 changes: 2 additions & 2 deletions tests/functional/test_install_cleanup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def test_no_clean_option_blocks_cleaning_after_install(script, data):
build = script.base_path / 'pip-build'
script.pip(
'install', '--no-clean', '--no-index', '--build', build,
'--find-links=%s' % data.find_links, 'simple',
'--find-links=%s' % data.find_links, 'simple', expect_temp=True,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cache should be cleaning up after itself even if this happens?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was controlled by the no_clean option but I have disabled that behavior.

)
assert exists(build)

Expand Down Expand Up @@ -132,7 +132,7 @@ def test_cleanup_prevented_upon_build_dir_exception(script, data):
result = script.pip(
'install', '-f', data.find_links, '--no-index', 'simple',
'--build', build,
expect_error=True,
expect_error=True, expect_temp=True,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cache should be cleaning up after itself even if this happens?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@takluyver Can you explain this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I can't remember the details. At some point some tests failed, and that seemed to be the right thing to make them work.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps the PR does not clean up the cache as advertised? What is the affect of this parameter?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT, this parameter means that the script run is expected to leave some temporary files, which is not what we want.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What we can do it turn the following line into a with statement and set the wheelcache as a context manager:

wheel_cache = WheelCache(options.cache_dir, options.format_control)

Is there any other location where the wheelcache is initialized?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xoviat -- a simple grep tells me there are 6 places.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pradyunsg So this test is named cleanup_prevented_on_build_dir_exception, and if you look at the control flow on install, it makes sense why the directory would not cleaned up. I attempted to fix that with the contextmanager, but your proposed solution necessitates that temporary files will be created here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. That means that this change in tests corresponds to a change in the expected behaviour. :)

PS: 41b83f8 was not the change I was suggesting. I thought there was no call to cleanup in finally.

)

assert result.returncode == PREVIOUS_BUILD_DIR_ERROR
Expand Down
8 changes: 5 additions & 3 deletions tests/functional/test_install_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,24 +68,26 @@ def test_install_subversion_usersite_editable_with_distribute(
)
result.assert_installed('INITools', use_user_site=True)

@pytest.mark.network
def test_install_curdir_usersite(self, script, virtualenv, data):
"""
Test installing current directory ('.') into usersite
"""
virtualenv.system_site_packages = True
script.pip("install", "wheel")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure this is actually needed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not validated this myself but from experience with related tests, I would strongly suspect that it is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: use the common_wheels fixture.

run_from = data.packages.join("FSPkg")
result = script.pip(
'install', '-vvv', '--user', curdir,
cwd=run_from,
expect_error=False,
)
fspkg_folder = script.user_site / 'fspkg'
egg_info_folder = (
script.user_site / 'FSPkg-0.1.dev0-py%s.egg-info' % pyversion
dist_info_folder = (
script.user_site / 'FSPkg-0.1.dev0.dist-info'
)
assert fspkg_folder in result.files_created, result.stdout

assert egg_info_folder in result.files_created
assert dist_info_folder in result.files_created

def test_install_user_venv_nositepkgs_fails(self, script, data):
"""
Expand Down
2 changes: 1 addition & 1 deletion tests/functional/test_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ def test_pip_wheel_fail_cause_of_previous_build_dir(
result = script.pip(
'wheel', '--no-index', '--find-links=%s' % data.find_links,
'--build', script.venv_path / 'build',
'simple==3.0', expect_error=True,
'simple==3.0', expect_error=True, expect_temp=True,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cache should be cleaning up after itself even if this happens?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto here for what I said below.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto here for what I said below. ;)

)

# Then I see that the error code is the right one
Expand Down