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

[WIP] wheel support (and transitively PEP-518), plus pipe display failed commands #852

Merged
merged 8 commits into from
Jun 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions changelog/850.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PEP-518 support: provide a tox configuration flag ``build`` which can be either ``sdist`` or ``wheel``. For ``sdist`` (default) we build the package as before by using ``python setup.py sdist``. However, when ``wheel`` is enabled now we'll use ``pip wheel`` to build it, and we'll also install wheels in these case into the environments. Note: ``pip`` 10 supports specifying project dependencies (such as ``setuptools-scm``, or a given ``setuptools`` version) via ``pyproject.toml``. Once ``pip`` supports building ``sdist`` to we'll migrate over the ``sdist`` build too.`@gaborbernat <https://github.com/gaborbernat>`_
1 change: 1 addition & 0 deletions changelog/851.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
While running tox invokes various commands (such as building the package, pip installing dependencies and so on), these were printed in case they failed as Python arrays. Changed the representation to a shell command, allowing the users to quickly replicate/debug the failure on their own.`@gaborbernat <https://github.com/gaborbernat>`_
2 changes: 0 additions & 2 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,6 @@ def generate_draft_news():


def setup(app):
# from sphinx.ext.autodoc import cut_lines
# app.connect('autodoc-process-docstring', cut_lines(4, what=['module']))
app.add_object_type(
"confval",
"confval",
Expand Down
30 changes: 30 additions & 0 deletions doc/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,36 @@ and will first lookup global tox settings in this section:
is identified. In a future version of tox, this warning will become an
error.

.. confval:: build=sdist|wheel|none

.. versionadded:: 3.1.0

tox will try to build the project as a Python package and install it in a fresh virtual
environment before running your test commands. Here one can select the type of build
tox will do for the projects package:

- if it's ``none`` tox will not try to build the package (implies no install happens later),
- if it's ``sdist`` it will create a source distribution by using:

.. code-block:: ini

python setup.py sdist

- if it's ``wheel`` it will create a Python wheel by using ``pip``:

.. code-block:: ini

pip wheel . --no-dep

.. note::

wheel should only be used with pip ``10+`` and a ``pyproject.toml``, the build will
fail if either is missing


**Default:** ``sdist``



Virtualenv test environment settings
------------------------------------
Expand Down
10 changes: 6 additions & 4 deletions src/tox/_pytestplugin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import print_function, unicode_literals

import os
import re
import textwrap
import time
from fnmatch import fnmatch
Expand Down Expand Up @@ -61,15 +62,12 @@ def create_new_config_file_(args, source=None, plugins=()):


@pytest.fixture
def cmd(request, capfd, monkeypatch):
def cmd(request, capfd):
if request.config.option.no_network:
pytest.skip("--no-network was specified, test cannot run")
request.addfinalizer(py.path.local().chdir)

def run(*argv):
key = str(b"PYTHONPATH")
python_paths = (i for i in (str(os.getcwd()), os.getenv(key)) if i)
monkeypatch.setenv(key, os.pathsep.join(python_paths))
with RunResult(capfd, argv) as result:
try:
main([str(x) for x in argv])
Expand Down Expand Up @@ -111,6 +109,10 @@ def __repr__(self):
self.ret, " ".join(str(i) for i in self.args), self.out, self.err
)

@property
def python_hash_seed(self):
return next(re.finditer(r"PYTHONHASHSEED='([0-9]+)'", self.out)).group(1)


class ReportExpectMock:
def __init__(self, session):
Expand Down
1 change: 1 addition & 0 deletions src/tox/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@ def __init__(self, config, inipath):
for name in config.indexserver:
config.indexserver[name] = IndexServerConfig(name, override)

config.build = reader.getstring("build", default="sdist")
reader.addsubstitutions(toxworkdir=config.toxworkdir)
config.distdir = reader.getpath("distdir", "{toxworkdir}/dist")
reader.addsubstitutions(distdir=config.distdir)
Expand Down
86 changes: 63 additions & 23 deletions src/tox/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
from __future__ import print_function

import os
import pipes
import re
import shutil
import subprocess
import sys
import time

import py
from pkg_resources import get_distribution

import tox
from tox._verlib import IrrationalVersionError, NormalizedVersion
Expand Down Expand Up @@ -136,9 +138,16 @@ def _initlogpath(self, actionid):
def popen(self, args, cwd=None, env=None, redirect=True, returnout=False, ignore_ret=False):
stdout = outpath = None
resultjson = self.session.config.option.resultjson

cmd_args = [str(x) for x in args]
cmd_args_shell = " ".join(pipes.quote(i) for i in cmd_args)
if resultjson or redirect:
fout = self._initlogpath(self.id)
fout.write("actionid: {}\nmsg: {}\ncmdargs: {!r}\n\n".format(self.id, self.msg, args))
fout.write(
"actionid: {}\nmsg: {}\ncmdargs: {!r}\n\n".format(
self.id, self.msg, cmd_args_shell
)
)
fout.flush()
outpath = py.path.local(fout.name)
fin = outpath.open("rb")
Expand All @@ -153,11 +162,13 @@ def popen(self, args, cwd=None, env=None, redirect=True, returnout=False, ignore
popen = self._popen(args, cwd, env=env, stdout=stdout, stderr=subprocess.STDOUT)
except OSError as e:
self.report.error(
"invocation failed (errno {:d}), args: {}, cwd: {}".format(e.errno, args, cwd)
"invocation failed (errno {:d}), args: {}, cwd: {}".format(
e.errno, cmd_args_shell, cwd
)
)
raise
popen.outpath = outpath
popen.args = [str(x) for x in args]
popen.args = cmd_args
popen.cwd = cwd
popen.action = self
self._popenlist.append(popen)
Expand Down Expand Up @@ -431,7 +442,7 @@ def _copyfiles(self, srcdir, pathlist, destdir):
target.dirpath().ensure(dir=1)
src.copy(target)

def _makesdist(self):
def _get_package(self):
setup = self.config.setupdir.join("setup.py")
if not setup.check():
self.report.error(
Expand All @@ -446,19 +457,8 @@ def _makesdist(self):
)
raise SystemExit(1)
with self.newaction(None, "packaging") as action:
action.setactivity("sdist-make", setup)
self.make_emptydir(self.config.distdir)
action.popen(
[
sys.executable,
setup,
"sdist",
"--formats=zip",
"--dist-dir",
self.config.distdir,
],
cwd=self.config.setupdir,
)
action.setactivity("{}-make".format(self.config.build), setup)
self._make_package(action, setup)
try:
return self.config.distdir.listdir()[0]
except py.error.ENOENT:
Expand All @@ -478,6 +478,46 @@ def _makesdist(self):
)
raise SystemExit(1)

def _make_package(self, action, setup):
self.make_emptydir(self.config.distdir)
if self.config.build == "sdist":
action.popen(
[
sys.executable,
setup,
"sdist",
"--formats=zip",
"--dist-dir",
self.config.distdir,
],
cwd=self.config.setupdir,
)
elif self.config.build == "wheel":
if NormalizedVersion(get_distribution("pip").version).parts[0][0] > 10:
raise RuntimeError("wheel support requires pip 10 or later")
py_project_toml = self.config.setupdir.join("pyproject.toml")
if not py_project_toml.exists():
raise RuntimeError(
"wheel support requires creating and setting build-requires in {}".format(
py_project_toml
)
)
action.popen(
[
sys.executable,
"-m",
"pip",
"wheel",
Copy link
Contributor

Choose a reason for hiding this comment

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

we could also invoke setup.py bdist_wheel instead of involving pip -- though that drifts further from PEP518

Copy link
Member Author

Choose a reason for hiding this comment

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

We plan to come closer to PEP-517/518 not move away from it. Once pip has that support we'll be able to provide flint, poetry etc support natively, without magical tox plugins.

"-w",
self.config.distdir, # target folder
"--no-deps", # don't build wheels for package dependencies
self.config.setupdir, # what to build - ourselves
],
cwd=self.config.setupdir,
)
else:
raise RuntimeError("invalid build type {}".format(self.config.build))
Copy link
Contributor

Choose a reason for hiding this comment

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

there's an open issue to change these to instead use the virtualenv python (instead of tox's python) to run these builds -- maybe worth changing in this review since it's already being touched?

Copy link
Member Author

Choose a reason for hiding this comment

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

Again the creation of a build venv is the responsibility of pip, and in case of the wheel it already does that. For doing the same for sdist the solution will be to move to pip build sdist and we'll support implementing PEP-517 inside pip. tox will not implement what you're describing as it's outside the scope of the tool.

Copy link

Choose a reason for hiding this comment

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

I'm sorry for breaking into your conversation but I have one argument to invoke pip wheel with virtualenv's python - packages with C extensions. They depend on Python version with which they are built and afaiu pip will build wheels only for interpeter it's being run by, so such wheels built by tox's interpreter will not suit environment with another one.

Choose a reason for hiding this comment

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

That’s a valid point. Maybe tox should run pip wheel multiple times for each python version, when it isn’t a universal pure python wheel.

Copy link
Member Author

Choose a reason for hiding this comment

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

For the first phase we decided to focus on universal wheel. Hence the way this works now. it's still a wip though and likely will wait for pip 517 to merge to adopt this because of some sanity checks not applied by pip at the moment.

Choose a reason for hiding this comment

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

Makes sense, thanks for the clarification


def make_emptydir(self, path):
if path.check():
self.report.info(" removing {}".format(path))
Expand Down Expand Up @@ -576,23 +616,23 @@ def get_installpkg_path(self):
if not path:
path = self.config.sdistsrc
path = self._resolve_pkg(path)
self.report.info("using package {!r}, skipping 'sdist' activity ".format(str(path)))
self.report.info(
"using package {!r}, skipping '{}' activity ".format(str(path), self.config.build)
)
else:
try:
path = self._makesdist()
path = self._get_package()
except tox.exception.InvocationError:
v = sys.exc_info()[1]
self.report.error("FAIL could not package project - v = {!r}".format(v))
return
sdistfile = self.config.distshare.join(path.basename)
if sdistfile != path:
self.report.info("copying new sdistfile to {!r}".format(str(sdistfile)))
self.report.info("copying new package to {!r}".format(str(sdistfile)))
try:
sdistfile.dirpath().ensure(dir=1)
except py.error.Error:
self.report.warning(
"could not copy distfile to {}".format(sdistfile.dirpath())
)
self.report.warning("could not copy package to {}".format(sdistfile.dirpath()))
else:
path.copy(sdistfile)
return path
Expand Down
17 changes: 15 additions & 2 deletions src/tox/venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,15 @@ def test(self, redirect=False):
raise

def _pcall(
self, args, cwd, venv=True, testcommand=False, action=None, redirect=True, ignore_ret=False
self,
args,
cwd,
venv=True,
testcommand=False,
action=None,
redirect=True,
ignore_ret=False,
no_python_path=False,
):
os.environ.pop("VIRTUALENV_PYTHON", None)

Expand All @@ -414,6 +422,8 @@ def _pcall(
env = self._getenv(testcommand=testcommand)
bindir = str(self.envconfig.envbindir)
env["PATH"] = p = os.pathsep.join([bindir, os.environ["PATH"]])
if no_python_path:
env.pop("PYTHONPATH", None)
self.session.report.verbosity2("setting PATH={}".format(p))
return action.popen(args, cwd=cwd, env=env, redirect=redirect, ignore_ret=ignore_ret)

Expand Down Expand Up @@ -487,7 +497,10 @@ def tox_runtest(venv, redirect):
def tox_runenvreport(venv, action):
# write out version dependency information
args = venv.envconfig.list_dependencies_command
output = venv._pcall(args, cwd=venv.envconfig.config.toxinidir, action=action)
# we clear the PYTHONPATH to allow reporting packages outside of this environment
output = venv._pcall(
args, cwd=venv.envconfig.config.toxinidir, action=action, no_python_path=True
)
Copy link
Contributor

Choose a reason for hiding this comment

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

is this related to this change?

Copy link
Member Author

Choose a reason for hiding this comment

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

It is. During testing this change I've encountered the situation of a faulty PYTHONPATH can report packages outside the venv as installed during the build. This defends against such.

Copy link
Contributor

Choose a reason for hiding this comment

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

hmmm, maybe split this into a separate PR so it's more clear why this changed?

Copy link
Member Author

Choose a reason for hiding this comment

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

Felt linked against this pr to close as without it the tests would fail, so thought not worth the extra effort.

# the output contains a mime-header, skip it
output = output.split("\n\n")[-1]
packages = output.strip().split("\n")
Expand Down
Loading