From ed36d627e46622a17300fab567fc03332f6583c9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 26 Jul 2016 21:29:07 -0300 Subject: [PATCH 1/2] Use PyInstaller for freeze test env cx_freeze doesn't seem to be very well supported in Python 3.5. Using pyinstaller instead and rename environment to "freeze" which is a more generic term for freezing python code into standalone executables. Fix #1769 --- .travis.yml | 2 +- _pytest/genscript.py | 3 +- appveyor.yml | 2 +- doc/en/example/simple.rst | 43 ++++++------- testing/cx_freeze/install_cx_freeze.py | 64 ------------------- testing/cx_freeze/runtests_setup.py | 15 ----- testing/freeze/.gitignore | 3 + testing/freeze/create_executable.py | 13 ++++ .../{cx_freeze => freeze}/runtests_script.py | 16 ++--- .../tests/test_doctest.txt | 0 .../tests/test_trivial.py | 10 +-- testing/{cx_freeze => freeze}/tox_run.py | 25 ++++---- tox.ini | 11 ++-- 13 files changed, 69 insertions(+), 138 deletions(-) delete mode 100644 testing/cx_freeze/install_cx_freeze.py delete mode 100644 testing/cx_freeze/runtests_setup.py create mode 100644 testing/freeze/.gitignore create mode 100644 testing/freeze/create_executable.py rename testing/{cx_freeze => freeze}/runtests_script.py (95%) rename testing/{cx_freeze => freeze}/tests/test_doctest.txt (100%) rename testing/{cx_freeze => freeze}/tests/test_trivial.py (95%) rename testing/{cx_freeze => freeze}/tox_run.py (56%) diff --git a/.travis.yml b/.travis.yml index 3a8f36e95..74483452c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,7 +25,7 @@ env: - TESTENV=py35-trial - TESTENV=py27-nobyte - TESTENV=doctesting - - TESTENV=py27-cxfreeze + - TESTENV=freeze script: tox --recreate -e $TESTENV diff --git a/_pytest/genscript.py b/_pytest/genscript.py index d2962d8fc..f070d8816 100755 --- a/_pytest/genscript.py +++ b/_pytest/genscript.py @@ -100,7 +100,8 @@ def pytest_namespace(): def freeze_includes(): """ Returns a list of module names used by py.test that should be - included by cx_freeze. + included by cx_freeze/pyinstaller to generate a standalone + pytest executable. """ result = list(_iter_all_modules(py)) result += list(_iter_all_modules(_pytest)) diff --git a/appveyor.yml b/appveyor.yml index 2bd72db45..9c28366a0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,7 +10,7 @@ environment: # builds timing out in AppVeyor - TOXENV: "linting,py26,py27,py33,py34,py35,pypy" - TOXENV: "py27-pexpect,py27-xdist,py27-trial,py35-pexpect,py35-xdist,py35-trial" - - TOXENV: "py27-nobyte,doctesting,py27-cxfreeze" + - TOXENV: "py27-nobyte,doctesting,freeze" install: - echo Installed Pythons diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 1007ee1e1..76ee97ab3 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -699,36 +699,33 @@ and run it:: You'll see that the fixture finalizers could use the precise reporting information. -Integrating pytest runner and cx_freeze ------------------------------------------------------------ +Integrating pytest runner and PyInstaller +----------------------------------------- If you freeze your application using a tool like -`cx_freeze `_ in order to distribute it -to your end-users, it is a good idea to also package your test runner and run -your tests using the frozen application. +`PyInstaller `_ +in order to distribute it to your end-users, it is a good idea to also package +your test runner and run your tests using the frozen application. This way packaging +errors such as dependencies not being included into the executable can be detected early +while also allowing you to send test files to users so they can run them in their +machines, which can be invaluable to obtain more information about a hard to reproduce bug. -This way packaging errors such as dependencies not being -included into the executable can be detected early while also allowing you to -send test files to users so they can run them in their machines, which can be -invaluable to obtain more information about a hard to reproduce bug. - -Unfortunately ``cx_freeze`` can't discover them +Unfortunately ``PyInstaller`` can't discover them automatically because of ``pytest``'s use of dynamic module loading, so you -must declare them explicitly by using ``pytest.freeze_includes()``:: +must declare them explicitly by using ``pytest.freeze_includes()`` and an +auxiliary script: - # contents of setup.py - from cx_Freeze import setup, Executable +.. code-block:: python + + # contents of create_executable.py import pytest + import subprocess - setup( - name="app_main", - executables=[Executable("app_main.py")], - options={"build_exe": - { - 'includes': pytest.freeze_includes()} - }, - # ... other options - ) + hidden = [] + for x in pytest.freeze_includes(): + hidden.extend(['--hidden-import', x]) + args = ['pyinstaller'] + hidden + ['runtests_script.py'] + subprocess.check_call(' '.join(args), shell=True) If you don't want to ship a different executable just in order to run your tests, you can make your program check for a certain flag and pass control diff --git a/testing/cx_freeze/install_cx_freeze.py b/testing/cx_freeze/install_cx_freeze.py deleted file mode 100644 index 83dce87aa..000000000 --- a/testing/cx_freeze/install_cx_freeze.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -Installs cx_freeze from source, but first patching -setup.py as described here: - -http://stackoverflow.com/questions/25107697/compiling-cx-freeze-under-ubuntu -""" -import glob -import tarfile -import os -import sys -import platform -import py - -if __name__ == '__main__': - if 'ubuntu' not in platform.version().lower(): - - print('Not Ubuntu, installing using pip. (platform.version() is %r)' % - platform.version()) - res = os.system('pip install cx_freeze') - if res != 0: - sys.exit(res) - sys.exit(0) - - rootdir = py.path.local.make_numbered_dir(prefix='cx_freeze') - - res = os.system('pip install --download %s --no-use-wheel ' - 'cx_freeze' % rootdir) - if res != 0: - sys.exit(res) - - packages = glob.glob('%s/*.tar.gz' % rootdir) - assert len(packages) == 1 - tar_filename = packages[0] - - tar_file = tarfile.open(tar_filename) - try: - tar_file.extractall(path=str(rootdir)) - finally: - tar_file.close() - - basename = os.path.basename(tar_filename).replace('.tar.gz', '') - setup_py_filename = '%s/%s/setup.py' % (rootdir, basename) - with open(setup_py_filename) as f: - lines = f.readlines() - - line_to_patch = 'if not vars.get("Py_ENABLE_SHARED", 0):' - for index, line in enumerate(lines): - if line_to_patch in line: - indent = line[:line.index(line_to_patch)] - lines[index] = indent + 'if True:\n' - print('Patched line %d' % (index + 1)) - break - else: - sys.exit('Could not find line in setup.py to patch!') - - with open(setup_py_filename, 'w') as f: - f.writelines(lines) - - os.chdir('%s/%s' % (rootdir, basename)) - res = os.system('python setup.py install') - if res != 0: - sys.exit(res) - - sys.exit(0) diff --git a/testing/cx_freeze/runtests_setup.py b/testing/cx_freeze/runtests_setup.py deleted file mode 100644 index 01e8a8a89..000000000 --- a/testing/cx_freeze/runtests_setup.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Sample setup.py script that generates an executable with pytest runner embedded. -""" -if __name__ == '__main__': - from cx_Freeze import setup, Executable - import pytest - - setup( - name="runtests", - version="0.1", - description="example of how embedding py.test into an executable using cx_freeze", - executables=[Executable("runtests_script.py")], - options={"build_exe": {'includes': pytest.freeze_includes()}}, - ) - diff --git a/testing/freeze/.gitignore b/testing/freeze/.gitignore new file mode 100644 index 000000000..490310b6c --- /dev/null +++ b/testing/freeze/.gitignore @@ -0,0 +1,3 @@ +build/ +dist/ +*.spec \ No newline at end of file diff --git a/testing/freeze/create_executable.py b/testing/freeze/create_executable.py new file mode 100644 index 000000000..8cf259c40 --- /dev/null +++ b/testing/freeze/create_executable.py @@ -0,0 +1,13 @@ +""" +Generates an executable with pytest runner embedded using PyInstaller. +""" +if __name__ == '__main__': + import pytest + import subprocess + + hidden = [] + for x in pytest.freeze_includes(): + hidden.extend(['--hidden-import', x]) + args = ['pyinstaller', '--noconfirm'] + hidden + ['runtests_script.py'] + subprocess.check_call(' '.join(args), shell=True) + diff --git a/testing/cx_freeze/runtests_script.py b/testing/freeze/runtests_script.py similarity index 95% rename from testing/cx_freeze/runtests_script.py rename to testing/freeze/runtests_script.py index f2b032d76..cb961fc6c 100644 --- a/testing/cx_freeze/runtests_script.py +++ b/testing/freeze/runtests_script.py @@ -1,9 +1,9 @@ -""" -This is the script that is actually frozen into an executable: simply executes -py.test main(). -""" - -if __name__ == '__main__': - import sys - import pytest +""" +This is the script that is actually frozen into an executable: simply executes +py.test main(). +""" + +if __name__ == '__main__': + import sys + import pytest sys.exit(pytest.main()) \ No newline at end of file diff --git a/testing/cx_freeze/tests/test_doctest.txt b/testing/freeze/tests/test_doctest.txt similarity index 100% rename from testing/cx_freeze/tests/test_doctest.txt rename to testing/freeze/tests/test_doctest.txt diff --git a/testing/cx_freeze/tests/test_trivial.py b/testing/freeze/tests/test_trivial.py similarity index 95% rename from testing/cx_freeze/tests/test_trivial.py rename to testing/freeze/tests/test_trivial.py index d8a572baa..6cf6b05ad 100644 --- a/testing/cx_freeze/tests/test_trivial.py +++ b/testing/freeze/tests/test_trivial.py @@ -1,6 +1,6 @@ - -def test_upper(): - assert 'foo'.upper() == 'FOO' - -def test_lower(): + +def test_upper(): + assert 'foo'.upper() == 'FOO' + +def test_lower(): assert 'FOO'.lower() == 'foo' \ No newline at end of file diff --git a/testing/cx_freeze/tox_run.py b/testing/freeze/tox_run.py similarity index 56% rename from testing/cx_freeze/tox_run.py rename to testing/freeze/tox_run.py index e8df2684b..5310ac1b7 100644 --- a/testing/cx_freeze/tox_run.py +++ b/testing/freeze/tox_run.py @@ -1,15 +1,12 @@ -""" -Called by tox.ini: uses the generated executable to run the tests in ./tests/ -directory. - -.. note:: somehow calling "build/runtests_script" directly from tox doesn't - seem to work (at least on Windows). -""" -if __name__ == '__main__': - import os - import sys - - executable = os.path.join(os.getcwd(), 'build', 'runtests_script') - if sys.platform.startswith('win'): - executable += '.exe' +""" +Called by tox.ini: uses the generated executable to run the tests in ./tests/ +directory. +""" +if __name__ == '__main__': + import os + import sys + + executable = os.path.join(os.getcwd(), 'dist', 'runtests_script', 'runtests_script') + if sys.platform.startswith('win'): + executable += '.exe' sys.exit(os.system('%s tests' % executable)) \ No newline at end of file diff --git a/tox.ini b/tox.ini index 5fa9ef21e..b16a06887 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ distshare={homedir}/.tox/distshare envlist= linting,py26,py27,py33,py34,py35,pypy, {py27,py35}-{pexpect,xdist,trial}, - py27-nobyte,doctesting,py27-cxfreeze + py27-nobyte,doctesting,freeze [testenv] commands= py.test --lsof -rfsxX {posargs:testing} @@ -124,12 +124,11 @@ changedir=testing commands= {envpython} {envbindir}/py.test-jython -rfsxX {posargs} -[testenv:py27-cxfreeze] -changedir=testing/cx_freeze -platform=linux|darwin +[testenv:freeze] +changedir=testing/freeze +deps=pyinstaller commands= - {envpython} install_cx_freeze.py - {envpython} runtests_setup.py build --build-exe build + {envpython} create_executable.py {envpython} tox_run.py From ae9d3bf886e25078bc399278f245e1740dbba986 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 27 Jul 2016 08:49:00 -0300 Subject: [PATCH 2/2] Freeze docs: PyInstaller hook and wording As discussed during the review, suggest in general to use PyInstaller and just mention pytest.freeze_includes() in less detail on how to actually use it, because it varies from tool to tool. --- doc/en/example/simple.rst | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 76ee97ab3..5275c52c8 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -699,8 +699,8 @@ and run it:: You'll see that the fixture finalizers could use the precise reporting information. -Integrating pytest runner and PyInstaller ------------------------------------------ +Freezing pytest +--------------- If you freeze your application using a tool like `PyInstaller `_ @@ -708,29 +708,21 @@ in order to distribute it to your end-users, it is a good idea to also package your test runner and run your tests using the frozen application. This way packaging errors such as dependencies not being included into the executable can be detected early while also allowing you to send test files to users so they can run them in their -machines, which can be invaluable to obtain more information about a hard to reproduce bug. +machines, which can be useful to obtain more information about a hard to reproduce bug. -Unfortunately ``PyInstaller`` can't discover them -automatically because of ``pytest``'s use of dynamic module loading, so you -must declare them explicitly by using ``pytest.freeze_includes()`` and an -auxiliary script: +Fortunately recent ``PyInstaller`` releases already have a custom hook +for pytest, but if you are using another tool to freeze executables +such as ``cx_freeze`` or ``py2exe``, you can use ``pytest.freeze_includes()`` +to obtain the full list of internal pytest modules. How to configure the tools +to find the internal modules varies from tool to tool, however. + +Instead of freezing the pytest runner as a separate executable, you can make +your frozen program work as the pytest runner by some clever +argument handling during program startup. This allows you to +have a single executable, which is usually more convenient. .. code-block:: python - # contents of create_executable.py - import pytest - import subprocess - - hidden = [] - for x in pytest.freeze_includes(): - hidden.extend(['--hidden-import', x]) - args = ['pyinstaller'] + hidden + ['runtests_script.py'] - subprocess.check_call(' '.join(args), shell=True) - -If you don't want to ship a different executable just in order to run your tests, -you can make your program check for a certain flag and pass control -over to ``pytest`` instead. For example:: - # contents of app_main.py import sys @@ -742,7 +734,7 @@ over to ``pytest`` instead. For example:: # by your argument-parsing library of choice as usual ... -This makes it convenient to execute your tests from within your frozen -application, using standard ``py.test`` command-line options:: +This allows you to execute tests using the frozen +application with standard ``py.test`` command-line options:: ./app_main --pytest --verbose --tb=long --junitxml=results.xml test-suite/