From 1231a4e097e55c5ac793ddaedad23bfd610591e6 Mon Sep 17 00:00:00 2001 From: Tarek Ziade Date: Thu, 19 May 2011 13:07:25 +0200 Subject: [PATCH] initial import of the packaging package in the standard library --- Lib/packaging/__init__.py | 17 + Lib/packaging/_trove.py | 552 +++++++ Lib/packaging/command/__init__.py | 56 + Lib/packaging/command/bdist.py | 141 ++ Lib/packaging/command/bdist_dumb.py | 137 ++ Lib/packaging/command/bdist_msi.py | 740 +++++++++ Lib/packaging/command/bdist_wininst.py | 342 ++++ Lib/packaging/command/build.py | 151 ++ Lib/packaging/command/build_clib.py | 198 +++ Lib/packaging/command/build_ext.py | 666 ++++++++ Lib/packaging/command/build_py.py | 410 +++++ Lib/packaging/command/build_scripts.py | 132 ++ Lib/packaging/command/check.py | 88 + Lib/packaging/command/clean.py | 76 + Lib/packaging/command/cmd.py | 440 +++++ Lib/packaging/command/command_template | 35 + Lib/packaging/command/config.py | 351 ++++ Lib/packaging/command/install_data.py | 79 + Lib/packaging/command/install_dist.py | 625 +++++++ Lib/packaging/command/install_distinfo.py | 175 ++ Lib/packaging/command/install_headers.py | 43 + Lib/packaging/command/install_lib.py | 222 +++ Lib/packaging/command/install_scripts.py | 59 + Lib/packaging/command/register.py | 282 ++++ Lib/packaging/command/sdist.py | 375 +++++ Lib/packaging/command/test.py | 81 + Lib/packaging/command/upload.py | 201 +++ Lib/packaging/command/upload_docs.py | 173 ++ Lib/packaging/command/wininst-10.0-amd64.exe | Bin 0 -> 222208 bytes Lib/packaging/command/wininst-10.0.exe | Bin 0 -> 190464 bytes Lib/packaging/command/wininst-6.0.exe | Bin 0 -> 61440 bytes Lib/packaging/command/wininst-7.1.exe | Bin 0 -> 65536 bytes Lib/packaging/command/wininst-8.0.exe | Bin 0 -> 61440 bytes Lib/packaging/command/wininst-9.0-amd64.exe | Bin 0 -> 223744 bytes Lib/packaging/command/wininst-9.0.exe | Bin 0 -> 196096 bytes Lib/packaging/compat.py | 57 + Lib/packaging/compiler/__init__.py | 282 ++++ Lib/packaging/compiler/bcppcompiler.py | 356 ++++ Lib/packaging/compiler/ccompiler.py | 868 ++++++++++ Lib/packaging/compiler/cygwinccompiler.py | 355 ++++ Lib/packaging/compiler/extension.py | 121 ++ Lib/packaging/compiler/msvc9compiler.py | 720 ++++++++ Lib/packaging/compiler/msvccompiler.py | 636 ++++++++ Lib/packaging/compiler/unixccompiler.py | 339 ++++ Lib/packaging/config.py | 357 ++++ Lib/packaging/create.py | 693 ++++++++ Lib/packaging/database.py | 627 +++++++ Lib/packaging/depgraph.py | 270 +++ Lib/packaging/dist.py | 819 ++++++++++ Lib/packaging/errors.py | 142 ++ Lib/packaging/fancy_getopt.py | 451 +++++ Lib/packaging/install.py | 483 ++++++ Lib/packaging/manifest.py | 372 +++++ Lib/packaging/markers.py | 187 +++ Lib/packaging/metadata.py | 552 +++++++ Lib/packaging/pypi/__init__.py | 9 + Lib/packaging/pypi/base.py | 48 + Lib/packaging/pypi/dist.py | 547 +++++++ Lib/packaging/pypi/errors.py | 39 + Lib/packaging/pypi/mirrors.py | 52 + Lib/packaging/pypi/simple.py | 452 +++++ Lib/packaging/pypi/wrapper.py | 99 ++ Lib/packaging/pypi/xmlrpc.py | 200 +++ Lib/packaging/resources.py | 25 + Lib/packaging/run.py | 645 ++++++++ Lib/packaging/tests/LONG_DESC.txt | 44 + Lib/packaging/tests/PKG-INFO | 57 + Lib/packaging/tests/SETUPTOOLS-PKG-INFO | 182 +++ Lib/packaging/tests/SETUPTOOLS-PKG-INFO2 | 183 +++ Lib/packaging/tests/__init__.py | 133 ++ Lib/packaging/tests/__main__.py | 20 + .../fake_dists/babar-0.1.dist-info/INSTALLER | 0 .../fake_dists/babar-0.1.dist-info/METADATA | 4 + .../fake_dists/babar-0.1.dist-info/RECORD | 0 .../fake_dists/babar-0.1.dist-info/REQUESTED | 0 .../fake_dists/babar-0.1.dist-info/RESOURCES | 2 + Lib/packaging/tests/fake_dists/babar.cfg | 1 + Lib/packaging/tests/fake_dists/babar.png | 0 .../fake_dists/bacon-0.1.egg-info/PKG-INFO | 6 + .../banana-0.4.egg/EGG-INFO/PKG-INFO | 18 + .../banana-0.4.egg/EGG-INFO/SOURCES.txt | 0 .../EGG-INFO/dependency_links.txt | 1 + .../banana-0.4.egg/EGG-INFO/entry_points.txt | 3 + .../banana-0.4.egg/EGG-INFO/not-zip-safe | 1 + .../banana-0.4.egg/EGG-INFO/requires.txt | 6 + .../banana-0.4.egg/EGG-INFO/top_level.txt | 0 .../tests/fake_dists/cheese-2.0.2.egg-info | 5 + .../choxie-2.0.0.9.dist-info/INSTALLER | 0 .../choxie-2.0.0.9.dist-info/METADATA | 9 + .../choxie-2.0.0.9.dist-info/RECORD | 0 .../choxie-2.0.0.9.dist-info/REQUESTED | 0 .../choxie-2.0.0.9/choxie/__init__.py | 1 + .../choxie-2.0.0.9/choxie/chocolate.py | 10 + .../fake_dists/choxie-2.0.0.9/truffles.py | 5 + .../coconuts-aster-10.3.egg-info/PKG-INFO | 5 + .../grammar-1.0a4.dist-info/INSTALLER | 0 .../grammar-1.0a4.dist-info/METADATA | 5 + .../fake_dists/grammar-1.0a4.dist-info/RECORD | 0 .../grammar-1.0a4.dist-info/REQUESTED | 0 .../grammar-1.0a4/grammar/__init__.py | 1 + .../fake_dists/grammar-1.0a4/grammar/utils.py | 8 + .../fake_dists/nut-funkyversion.egg-info | 3 + .../tests/fake_dists/strawberry-0.6.egg | Bin 0 -> 1402 bytes .../towel_stuff-0.1.dist-info/INSTALLER | 0 .../towel_stuff-0.1.dist-info/METADATA | 7 + .../towel_stuff-0.1.dist-info/RECORD | 0 .../towel_stuff-0.1.dist-info/REQUESTED | 0 .../towel_stuff-0.1/towel_stuff/__init__.py | 18 + .../tests/fake_dists/truffles-5.0.egg-info | 3 + Lib/packaging/tests/fixer/__init__.py | 0 Lib/packaging/tests/fixer/fix_idioms.py | 134 ++ Lib/packaging/tests/pypi_server.py | 444 +++++ Lib/packaging/tests/pypi_test_server.py | 59 + .../source/f/foobar/foobar-0.1.tar.gz | Bin 0 -> 110 bytes .../simple/badmd5/badmd5-0.1.tar.gz | 0 .../simple/badmd5/index.html | 3 + .../simple/foobar/index.html | 3 + .../downloads_with_md5/simple/index.html | 2 + .../foo_bar_baz/simple/bar/index.html | 6 + .../foo_bar_baz/simple/baz/index.html | 6 + .../foo_bar_baz/simple/foo/index.html | 6 + .../pypiserver/foo_bar_baz/simple/index.html | 3 + .../pypiserver/project_list/simple/index.html | 5 + .../test_found_links/simple/foobar/index.html | 6 + .../test_found_links/simple/index.html | 1 + .../test_pypi_server/external/index.html | 1 + .../test_pypi_server/simple/index.html | 1 + .../with_externals/external/external.html | 3 + .../with_externals/simple/foobar/index.html | 4 + .../with_externals/simple/index.html | 1 + .../with_norel_links/external/homepage.html | 7 + .../with_norel_links/external/nonrel.html | 1 + .../with_norel_links/simple/foobar/index.html | 6 + .../with_norel_links/simple/index.html | 1 + .../simple/foobar/index.html | 4 + .../with_real_externals/simple/index.html | 1 + Lib/packaging/tests/support.py | 259 +++ Lib/packaging/tests/test_ccompiler.py | 15 + Lib/packaging/tests/test_command_bdist.py | 77 + .../tests/test_command_bdist_dumb.py | 103 ++ Lib/packaging/tests/test_command_bdist_msi.py | 25 + .../tests/test_command_bdist_wininst.py | 32 + Lib/packaging/tests/test_command_build.py | 55 + .../tests/test_command_build_clib.py | 141 ++ Lib/packaging/tests/test_command_build_ext.py | 353 ++++ Lib/packaging/tests/test_command_build_py.py | 124 ++ .../tests/test_command_build_scripts.py | 112 ++ Lib/packaging/tests/test_command_check.py | 131 ++ Lib/packaging/tests/test_command_clean.py | 48 + Lib/packaging/tests/test_command_cmd.py | 101 ++ Lib/packaging/tests/test_command_config.py | 76 + .../tests/test_command_install_data.py | 80 + .../tests/test_command_install_dist.py | 210 +++ .../tests/test_command_install_distinfo.py | 192 +++ .../tests/test_command_install_headers.py | 38 + .../tests/test_command_install_lib.py | 111 ++ .../tests/test_command_install_scripts.py | 78 + Lib/packaging/tests/test_command_register.py | 259 +++ Lib/packaging/tests/test_command_sdist.py | 407 +++++ Lib/packaging/tests/test_command_test.py | 225 +++ Lib/packaging/tests/test_command_upload.py | 157 ++ .../tests/test_command_upload_docs.py | 205 +++ Lib/packaging/tests/test_compiler.py | 66 + Lib/packaging/tests/test_config.py | 424 +++++ Lib/packaging/tests/test_create.py | 235 +++ Lib/packaging/tests/test_cygwinccompiler.py | 88 + Lib/packaging/tests/test_database.py | 506 ++++++ Lib/packaging/tests/test_depgraph.py | 301 ++++ Lib/packaging/tests/test_dist.py | 445 +++++ Lib/packaging/tests/test_extension.py | 15 + Lib/packaging/tests/test_install.py | 353 ++++ Lib/packaging/tests/test_manifest.py | 72 + Lib/packaging/tests/test_markers.py | 71 + Lib/packaging/tests/test_metadata.py | 279 ++++ Lib/packaging/tests/test_mixin2to3.py | 75 + Lib/packaging/tests/test_msvc9compiler.py | 140 ++ Lib/packaging/tests/test_pypi_dist.py | 277 ++++ Lib/packaging/tests/test_pypi_server.py | 81 + Lib/packaging/tests/test_pypi_simple.py | 326 ++++ Lib/packaging/tests/test_pypi_xmlrpc.py | 93 ++ Lib/packaging/tests/test_resources.py | 168 ++ Lib/packaging/tests/test_run.py | 62 + Lib/packaging/tests/test_uninstall.py | 99 ++ Lib/packaging/tests/test_unixccompiler.py | 132 ++ Lib/packaging/tests/test_util.py | 928 +++++++++++ Lib/packaging/tests/test_version.py | 252 +++ Lib/packaging/util.py | 1451 +++++++++++++++++ Lib/packaging/version.py | 449 +++++ Lib/sysconfig.cfg | 111 ++ Lib/sysconfig.py | 281 ++-- Lib/test/test_packaging.py | 5 + Lib/test/test_sysconfig.py | 27 +- Tools/scripts/pysetup3 | 4 + 193 files changed, 30378 insertions(+), 151 deletions(-) create mode 100644 Lib/packaging/__init__.py create mode 100644 Lib/packaging/_trove.py create mode 100644 Lib/packaging/command/__init__.py create mode 100644 Lib/packaging/command/bdist.py create mode 100644 Lib/packaging/command/bdist_dumb.py create mode 100644 Lib/packaging/command/bdist_msi.py create mode 100644 Lib/packaging/command/bdist_wininst.py create mode 100644 Lib/packaging/command/build.py create mode 100644 Lib/packaging/command/build_clib.py create mode 100644 Lib/packaging/command/build_ext.py create mode 100644 Lib/packaging/command/build_py.py create mode 100644 Lib/packaging/command/build_scripts.py create mode 100644 Lib/packaging/command/check.py create mode 100644 Lib/packaging/command/clean.py create mode 100644 Lib/packaging/command/cmd.py create mode 100644 Lib/packaging/command/command_template create mode 100644 Lib/packaging/command/config.py create mode 100644 Lib/packaging/command/install_data.py create mode 100644 Lib/packaging/command/install_dist.py create mode 100644 Lib/packaging/command/install_distinfo.py create mode 100644 Lib/packaging/command/install_headers.py create mode 100644 Lib/packaging/command/install_lib.py create mode 100644 Lib/packaging/command/install_scripts.py create mode 100644 Lib/packaging/command/register.py create mode 100644 Lib/packaging/command/sdist.py create mode 100644 Lib/packaging/command/test.py create mode 100644 Lib/packaging/command/upload.py create mode 100644 Lib/packaging/command/upload_docs.py create mode 100644 Lib/packaging/command/wininst-10.0-amd64.exe create mode 100644 Lib/packaging/command/wininst-10.0.exe create mode 100644 Lib/packaging/command/wininst-6.0.exe create mode 100644 Lib/packaging/command/wininst-7.1.exe create mode 100644 Lib/packaging/command/wininst-8.0.exe create mode 100644 Lib/packaging/command/wininst-9.0-amd64.exe create mode 100644 Lib/packaging/command/wininst-9.0.exe create mode 100644 Lib/packaging/compat.py create mode 100644 Lib/packaging/compiler/__init__.py create mode 100644 Lib/packaging/compiler/bcppcompiler.py create mode 100644 Lib/packaging/compiler/ccompiler.py create mode 100644 Lib/packaging/compiler/cygwinccompiler.py create mode 100644 Lib/packaging/compiler/extension.py create mode 100644 Lib/packaging/compiler/msvc9compiler.py create mode 100644 Lib/packaging/compiler/msvccompiler.py create mode 100644 Lib/packaging/compiler/unixccompiler.py create mode 100644 Lib/packaging/config.py create mode 100644 Lib/packaging/create.py create mode 100644 Lib/packaging/database.py create mode 100644 Lib/packaging/depgraph.py create mode 100644 Lib/packaging/dist.py create mode 100644 Lib/packaging/errors.py create mode 100644 Lib/packaging/fancy_getopt.py create mode 100644 Lib/packaging/install.py create mode 100644 Lib/packaging/manifest.py create mode 100644 Lib/packaging/markers.py create mode 100644 Lib/packaging/metadata.py create mode 100644 Lib/packaging/pypi/__init__.py create mode 100644 Lib/packaging/pypi/base.py create mode 100644 Lib/packaging/pypi/dist.py create mode 100644 Lib/packaging/pypi/errors.py create mode 100644 Lib/packaging/pypi/mirrors.py create mode 100644 Lib/packaging/pypi/simple.py create mode 100644 Lib/packaging/pypi/wrapper.py create mode 100644 Lib/packaging/pypi/xmlrpc.py create mode 100644 Lib/packaging/resources.py create mode 100644 Lib/packaging/run.py create mode 100644 Lib/packaging/tests/LONG_DESC.txt create mode 100644 Lib/packaging/tests/PKG-INFO create mode 100644 Lib/packaging/tests/SETUPTOOLS-PKG-INFO create mode 100644 Lib/packaging/tests/SETUPTOOLS-PKG-INFO2 create mode 100644 Lib/packaging/tests/__init__.py create mode 100644 Lib/packaging/tests/__main__.py create mode 100644 Lib/packaging/tests/fake_dists/babar-0.1.dist-info/INSTALLER create mode 100644 Lib/packaging/tests/fake_dists/babar-0.1.dist-info/METADATA create mode 100644 Lib/packaging/tests/fake_dists/babar-0.1.dist-info/RECORD create mode 100644 Lib/packaging/tests/fake_dists/babar-0.1.dist-info/REQUESTED create mode 100644 Lib/packaging/tests/fake_dists/babar-0.1.dist-info/RESOURCES create mode 100644 Lib/packaging/tests/fake_dists/babar.cfg create mode 100644 Lib/packaging/tests/fake_dists/babar.png create mode 100644 Lib/packaging/tests/fake_dists/bacon-0.1.egg-info/PKG-INFO create mode 100644 Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/PKG-INFO create mode 100644 Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/SOURCES.txt create mode 100644 Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/dependency_links.txt create mode 100644 Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/entry_points.txt create mode 100644 Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/not-zip-safe create mode 100644 Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/requires.txt create mode 100644 Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/top_level.txt create mode 100644 Lib/packaging/tests/fake_dists/cheese-2.0.2.egg-info create mode 100644 Lib/packaging/tests/fake_dists/choxie-2.0.0.9.dist-info/INSTALLER create mode 100644 Lib/packaging/tests/fake_dists/choxie-2.0.0.9.dist-info/METADATA create mode 100644 Lib/packaging/tests/fake_dists/choxie-2.0.0.9.dist-info/RECORD create mode 100644 Lib/packaging/tests/fake_dists/choxie-2.0.0.9.dist-info/REQUESTED create mode 100644 Lib/packaging/tests/fake_dists/choxie-2.0.0.9/choxie/__init__.py create mode 100644 Lib/packaging/tests/fake_dists/choxie-2.0.0.9/choxie/chocolate.py create mode 100644 Lib/packaging/tests/fake_dists/choxie-2.0.0.9/truffles.py create mode 100644 Lib/packaging/tests/fake_dists/coconuts-aster-10.3.egg-info/PKG-INFO create mode 100644 Lib/packaging/tests/fake_dists/grammar-1.0a4.dist-info/INSTALLER create mode 100644 Lib/packaging/tests/fake_dists/grammar-1.0a4.dist-info/METADATA create mode 100644 Lib/packaging/tests/fake_dists/grammar-1.0a4.dist-info/RECORD create mode 100644 Lib/packaging/tests/fake_dists/grammar-1.0a4.dist-info/REQUESTED create mode 100644 Lib/packaging/tests/fake_dists/grammar-1.0a4/grammar/__init__.py create mode 100644 Lib/packaging/tests/fake_dists/grammar-1.0a4/grammar/utils.py create mode 100644 Lib/packaging/tests/fake_dists/nut-funkyversion.egg-info create mode 100644 Lib/packaging/tests/fake_dists/strawberry-0.6.egg create mode 100644 Lib/packaging/tests/fake_dists/towel_stuff-0.1.dist-info/INSTALLER create mode 100644 Lib/packaging/tests/fake_dists/towel_stuff-0.1.dist-info/METADATA create mode 100644 Lib/packaging/tests/fake_dists/towel_stuff-0.1.dist-info/RECORD create mode 100644 Lib/packaging/tests/fake_dists/towel_stuff-0.1.dist-info/REQUESTED create mode 100644 Lib/packaging/tests/fake_dists/towel_stuff-0.1/towel_stuff/__init__.py create mode 100644 Lib/packaging/tests/fake_dists/truffles-5.0.egg-info create mode 100644 Lib/packaging/tests/fixer/__init__.py create mode 100644 Lib/packaging/tests/fixer/fix_idioms.py create mode 100644 Lib/packaging/tests/pypi_server.py create mode 100644 Lib/packaging/tests/pypi_test_server.py create mode 100644 Lib/packaging/tests/pypiserver/downloads_with_md5/packages/source/f/foobar/foobar-0.1.tar.gz create mode 100644 Lib/packaging/tests/pypiserver/downloads_with_md5/simple/badmd5/badmd5-0.1.tar.gz create mode 100644 Lib/packaging/tests/pypiserver/downloads_with_md5/simple/badmd5/index.html create mode 100644 Lib/packaging/tests/pypiserver/downloads_with_md5/simple/foobar/index.html create mode 100644 Lib/packaging/tests/pypiserver/downloads_with_md5/simple/index.html create mode 100644 Lib/packaging/tests/pypiserver/foo_bar_baz/simple/bar/index.html create mode 100644 Lib/packaging/tests/pypiserver/foo_bar_baz/simple/baz/index.html create mode 100644 Lib/packaging/tests/pypiserver/foo_bar_baz/simple/foo/index.html create mode 100644 Lib/packaging/tests/pypiserver/foo_bar_baz/simple/index.html create mode 100644 Lib/packaging/tests/pypiserver/project_list/simple/index.html create mode 100644 Lib/packaging/tests/pypiserver/test_found_links/simple/foobar/index.html create mode 100644 Lib/packaging/tests/pypiserver/test_found_links/simple/index.html create mode 100644 Lib/packaging/tests/pypiserver/test_pypi_server/external/index.html create mode 100644 Lib/packaging/tests/pypiserver/test_pypi_server/simple/index.html create mode 100644 Lib/packaging/tests/pypiserver/with_externals/external/external.html create mode 100644 Lib/packaging/tests/pypiserver/with_externals/simple/foobar/index.html create mode 100644 Lib/packaging/tests/pypiserver/with_externals/simple/index.html create mode 100644 Lib/packaging/tests/pypiserver/with_norel_links/external/homepage.html create mode 100644 Lib/packaging/tests/pypiserver/with_norel_links/external/nonrel.html create mode 100644 Lib/packaging/tests/pypiserver/with_norel_links/simple/foobar/index.html create mode 100644 Lib/packaging/tests/pypiserver/with_norel_links/simple/index.html create mode 100644 Lib/packaging/tests/pypiserver/with_real_externals/simple/foobar/index.html create mode 100644 Lib/packaging/tests/pypiserver/with_real_externals/simple/index.html create mode 100644 Lib/packaging/tests/support.py create mode 100644 Lib/packaging/tests/test_ccompiler.py create mode 100644 Lib/packaging/tests/test_command_bdist.py create mode 100644 Lib/packaging/tests/test_command_bdist_dumb.py create mode 100644 Lib/packaging/tests/test_command_bdist_msi.py create mode 100644 Lib/packaging/tests/test_command_bdist_wininst.py create mode 100644 Lib/packaging/tests/test_command_build.py create mode 100644 Lib/packaging/tests/test_command_build_clib.py create mode 100644 Lib/packaging/tests/test_command_build_ext.py create mode 100644 Lib/packaging/tests/test_command_build_py.py create mode 100644 Lib/packaging/tests/test_command_build_scripts.py create mode 100644 Lib/packaging/tests/test_command_check.py create mode 100644 Lib/packaging/tests/test_command_clean.py create mode 100644 Lib/packaging/tests/test_command_cmd.py create mode 100644 Lib/packaging/tests/test_command_config.py create mode 100644 Lib/packaging/tests/test_command_install_data.py create mode 100644 Lib/packaging/tests/test_command_install_dist.py create mode 100644 Lib/packaging/tests/test_command_install_distinfo.py create mode 100644 Lib/packaging/tests/test_command_install_headers.py create mode 100644 Lib/packaging/tests/test_command_install_lib.py create mode 100644 Lib/packaging/tests/test_command_install_scripts.py create mode 100644 Lib/packaging/tests/test_command_register.py create mode 100644 Lib/packaging/tests/test_command_sdist.py create mode 100644 Lib/packaging/tests/test_command_test.py create mode 100644 Lib/packaging/tests/test_command_upload.py create mode 100644 Lib/packaging/tests/test_command_upload_docs.py create mode 100644 Lib/packaging/tests/test_compiler.py create mode 100644 Lib/packaging/tests/test_config.py create mode 100644 Lib/packaging/tests/test_create.py create mode 100644 Lib/packaging/tests/test_cygwinccompiler.py create mode 100644 Lib/packaging/tests/test_database.py create mode 100644 Lib/packaging/tests/test_depgraph.py create mode 100644 Lib/packaging/tests/test_dist.py create mode 100644 Lib/packaging/tests/test_extension.py create mode 100644 Lib/packaging/tests/test_install.py create mode 100644 Lib/packaging/tests/test_manifest.py create mode 100644 Lib/packaging/tests/test_markers.py create mode 100644 Lib/packaging/tests/test_metadata.py create mode 100644 Lib/packaging/tests/test_mixin2to3.py create mode 100644 Lib/packaging/tests/test_msvc9compiler.py create mode 100644 Lib/packaging/tests/test_pypi_dist.py create mode 100644 Lib/packaging/tests/test_pypi_server.py create mode 100644 Lib/packaging/tests/test_pypi_simple.py create mode 100644 Lib/packaging/tests/test_pypi_xmlrpc.py create mode 100644 Lib/packaging/tests/test_resources.py create mode 100644 Lib/packaging/tests/test_run.py create mode 100644 Lib/packaging/tests/test_uninstall.py create mode 100644 Lib/packaging/tests/test_unixccompiler.py create mode 100644 Lib/packaging/tests/test_util.py create mode 100644 Lib/packaging/tests/test_version.py create mode 100644 Lib/packaging/util.py create mode 100644 Lib/packaging/version.py create mode 100644 Lib/sysconfig.cfg create mode 100644 Lib/test/test_packaging.py create mode 100755 Tools/scripts/pysetup3 diff --git a/Lib/packaging/__init__.py b/Lib/packaging/__init__.py new file mode 100644 index 000000000000..93b611765cc5 --- /dev/null +++ b/Lib/packaging/__init__.py @@ -0,0 +1,17 @@ +"""Support for packaging, distribution and installation of Python projects. + +Third-party tools can use parts of packaging as building blocks +without causing the other modules to be imported: + + import packaging.version + import packaging.metadata + import packaging.pypi.simple + import packaging.tests.pypi_server +""" + +from logging import getLogger + +__all__ = ['__version__', 'logger'] + +__version__ = "1.0a3" +logger = getLogger('packaging') diff --git a/Lib/packaging/_trove.py b/Lib/packaging/_trove.py new file mode 100644 index 000000000000..9a8719ce68cd --- /dev/null +++ b/Lib/packaging/_trove.py @@ -0,0 +1,552 @@ +"""Temporary helper for create.""" + +# XXX get the list from PyPI and cache it instead of hardcoding + +# XXX see if it would be more useful to store it as another structure +# than a list of strings + +all_classifiers = [ +'Development Status :: 1 - Planning', +'Development Status :: 2 - Pre-Alpha', +'Development Status :: 3 - Alpha', +'Development Status :: 4 - Beta', +'Development Status :: 5 - Production/Stable', +'Development Status :: 6 - Mature', +'Development Status :: 7 - Inactive', +'Environment :: Console', +'Environment :: Console :: Curses', +'Environment :: Console :: Framebuffer', +'Environment :: Console :: Newt', +'Environment :: Console :: svgalib', +"Environment :: Handhelds/PDA's", +'Environment :: MacOS X', +'Environment :: MacOS X :: Aqua', +'Environment :: MacOS X :: Carbon', +'Environment :: MacOS X :: Cocoa', +'Environment :: No Input/Output (Daemon)', +'Environment :: Other Environment', +'Environment :: Plugins', +'Environment :: Web Environment', +'Environment :: Web Environment :: Buffet', +'Environment :: Web Environment :: Mozilla', +'Environment :: Web Environment :: ToscaWidgets', +'Environment :: Win32 (MS Windows)', +'Environment :: X11 Applications', +'Environment :: X11 Applications :: Gnome', +'Environment :: X11 Applications :: GTK', +'Environment :: X11 Applications :: KDE', +'Environment :: X11 Applications :: Qt', +'Framework :: BFG', +'Framework :: Buildout', +'Framework :: Chandler', +'Framework :: CubicWeb', +'Framework :: Django', +'Framework :: IDLE', +'Framework :: Paste', +'Framework :: Plone', +'Framework :: Pylons', +'Framework :: Setuptools Plugin', +'Framework :: Trac', +'Framework :: TurboGears', +'Framework :: TurboGears :: Applications', +'Framework :: TurboGears :: Widgets', +'Framework :: Twisted', +'Framework :: ZODB', +'Framework :: Zope2', +'Framework :: Zope3', +'Intended Audience :: Customer Service', +'Intended Audience :: Developers', +'Intended Audience :: Education', +'Intended Audience :: End Users/Desktop', +'Intended Audience :: Financial and Insurance Industry', +'Intended Audience :: Healthcare Industry', +'Intended Audience :: Information Technology', +'Intended Audience :: Legal Industry', +'Intended Audience :: Manufacturing', +'Intended Audience :: Other Audience', +'Intended Audience :: Religion', +'Intended Audience :: Science/Research', +'Intended Audience :: System Administrators', +'Intended Audience :: Telecommunications Industry', +'License :: Aladdin Free Public License (AFPL)', +'License :: DFSG approved', +'License :: Eiffel Forum License (EFL)', +'License :: Free For Educational Use', +'License :: Free For Home Use', +'License :: Free for non-commercial use', +'License :: Freely Distributable', +'License :: Free To Use But Restricted', +'License :: Freeware', +'License :: Netscape Public License (NPL)', +'License :: Nokia Open Source License (NOKOS)', +'License :: OSI Approved', +'License :: OSI Approved :: Academic Free License (AFL)', +'License :: OSI Approved :: Apache Software License', +'License :: OSI Approved :: Apple Public Source License', +'License :: OSI Approved :: Artistic License', +'License :: OSI Approved :: Attribution Assurance License', +'License :: OSI Approved :: BSD License', +'License :: OSI Approved :: Common Public License', +'License :: OSI Approved :: Eiffel Forum License', +'License :: OSI Approved :: European Union Public Licence 1.0 (EUPL 1.0)', +'License :: OSI Approved :: European Union Public Licence 1.1 (EUPL 1.1)', +'License :: OSI Approved :: GNU Affero General Public License v3', +'License :: OSI Approved :: GNU Free Documentation License (FDL)', +'License :: OSI Approved :: GNU General Public License (GPL)', +'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)', +'License :: OSI Approved :: IBM Public License', +'License :: OSI Approved :: Intel Open Source License', +'License :: OSI Approved :: ISC License (ISCL)', +'License :: OSI Approved :: Jabber Open Source License', +'License :: OSI Approved :: MIT License', +'License :: OSI Approved :: MITRE Collaborative Virtual Workspace License (CVW)', +'License :: OSI Approved :: Motosoto License', +'License :: OSI Approved :: Mozilla Public License 1.0 (MPL)', +'License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1)', +'License :: OSI Approved :: Nethack General Public License', +'License :: OSI Approved :: Nokia Open Source License', +'License :: OSI Approved :: Open Group Test Suite License', +'License :: OSI Approved :: Python License (CNRI Python License)', +'License :: OSI Approved :: Python Software Foundation License', +'License :: OSI Approved :: Qt Public License (QPL)', +'License :: OSI Approved :: Ricoh Source Code Public License', +'License :: OSI Approved :: Sleepycat License', +'License :: OSI Approved :: Sun Industry Standards Source License (SISSL)', +'License :: OSI Approved :: Sun Public License', +'License :: OSI Approved :: University of Illinois/NCSA Open Source License', +'License :: OSI Approved :: Vovida Software License 1.0', +'License :: OSI Approved :: W3C License', +'License :: OSI Approved :: X.Net License', +'License :: OSI Approved :: zlib/libpng License', +'License :: OSI Approved :: Zope Public License', +'License :: Other/Proprietary License', +'License :: Public Domain', +'License :: Repoze Public License', +'Natural Language :: Afrikaans', +'Natural Language :: Arabic', +'Natural Language :: Bengali', +'Natural Language :: Bosnian', +'Natural Language :: Bulgarian', +'Natural Language :: Catalan', +'Natural Language :: Chinese (Simplified)', +'Natural Language :: Chinese (Traditional)', +'Natural Language :: Croatian', +'Natural Language :: Czech', +'Natural Language :: Danish', +'Natural Language :: Dutch', +'Natural Language :: English', +'Natural Language :: Esperanto', +'Natural Language :: Finnish', +'Natural Language :: French', +'Natural Language :: German', +'Natural Language :: Greek', +'Natural Language :: Hebrew', +'Natural Language :: Hindi', +'Natural Language :: Hungarian', +'Natural Language :: Icelandic', +'Natural Language :: Indonesian', +'Natural Language :: Italian', +'Natural Language :: Japanese', +'Natural Language :: Javanese', +'Natural Language :: Korean', +'Natural Language :: Latin', +'Natural Language :: Latvian', +'Natural Language :: Macedonian', +'Natural Language :: Malay', +'Natural Language :: Marathi', +'Natural Language :: Norwegian', +'Natural Language :: Panjabi', +'Natural Language :: Persian', +'Natural Language :: Polish', +'Natural Language :: Portuguese', +'Natural Language :: Portuguese (Brazilian)', +'Natural Language :: Romanian', +'Natural Language :: Russian', +'Natural Language :: Serbian', +'Natural Language :: Slovak', +'Natural Language :: Slovenian', +'Natural Language :: Spanish', +'Natural Language :: Swedish', +'Natural Language :: Tamil', +'Natural Language :: Telugu', +'Natural Language :: Thai', +'Natural Language :: Turkish', +'Natural Language :: Ukranian', +'Natural Language :: Urdu', +'Natural Language :: Vietnamese', +'Operating System :: BeOS', +'Operating System :: MacOS', +'Operating System :: MacOS :: MacOS 9', +'Operating System :: MacOS :: MacOS X', +'Operating System :: Microsoft', +'Operating System :: Microsoft :: MS-DOS', +'Operating System :: Microsoft :: Windows', +'Operating System :: Microsoft :: Windows :: Windows 3.1 or Earlier', +'Operating System :: Microsoft :: Windows :: Windows 95/98/2000', +'Operating System :: Microsoft :: Windows :: Windows CE', +'Operating System :: Microsoft :: Windows :: Windows NT/2000', +'Operating System :: OS/2', +'Operating System :: OS Independent', +'Operating System :: Other OS', +'Operating System :: PalmOS', +'Operating System :: PDA Systems', +'Operating System :: POSIX', +'Operating System :: POSIX :: AIX', +'Operating System :: POSIX :: BSD', +'Operating System :: POSIX :: BSD :: BSD/OS', +'Operating System :: POSIX :: BSD :: FreeBSD', +'Operating System :: POSIX :: BSD :: NetBSD', +'Operating System :: POSIX :: BSD :: OpenBSD', +'Operating System :: POSIX :: GNU Hurd', +'Operating System :: POSIX :: HP-UX', +'Operating System :: POSIX :: IRIX', +'Operating System :: POSIX :: Linux', +'Operating System :: POSIX :: Other', +'Operating System :: POSIX :: SCO', +'Operating System :: POSIX :: SunOS/Solaris', +'Operating System :: Unix', +'Programming Language :: Ada', +'Programming Language :: APL', +'Programming Language :: ASP', +'Programming Language :: Assembly', +'Programming Language :: Awk', +'Programming Language :: Basic', +'Programming Language :: C', +'Programming Language :: C#', +'Programming Language :: C++', +'Programming Language :: Cold Fusion', +'Programming Language :: Cython', +'Programming Language :: Delphi/Kylix', +'Programming Language :: Dylan', +'Programming Language :: Eiffel', +'Programming Language :: Emacs-Lisp', +'Programming Language :: Erlang', +'Programming Language :: Euler', +'Programming Language :: Euphoria', +'Programming Language :: Forth', +'Programming Language :: Fortran', +'Programming Language :: Haskell', +'Programming Language :: Java', +'Programming Language :: JavaScript', +'Programming Language :: Lisp', +'Programming Language :: Logo', +'Programming Language :: ML', +'Programming Language :: Modula', +'Programming Language :: Objective C', +'Programming Language :: Object Pascal', +'Programming Language :: OCaml', +'Programming Language :: Other', +'Programming Language :: Other Scripting Engines', +'Programming Language :: Pascal', +'Programming Language :: Perl', +'Programming Language :: PHP', +'Programming Language :: Pike', +'Programming Language :: Pliant', +'Programming Language :: PL/SQL', +'Programming Language :: PROGRESS', +'Programming Language :: Prolog', +'Programming Language :: Python', +'Programming Language :: Python :: 2', +'Programming Language :: Python :: 2.3', +'Programming Language :: Python :: 2.4', +'Programming Language :: Python :: 2.5', +'Programming Language :: Python :: 2.6', +'Programming Language :: Python :: 2.7', +'Programming Language :: Python :: 3', +'Programming Language :: Python :: 3.0', +'Programming Language :: Python :: 3.1', +'Programming Language :: Python :: 3.2', +'Programming Language :: REBOL', +'Programming Language :: Rexx', +'Programming Language :: Ruby', +'Programming Language :: Scheme', +'Programming Language :: Simula', +'Programming Language :: Smalltalk', +'Programming Language :: SQL', +'Programming Language :: Tcl', +'Programming Language :: Unix Shell', +'Programming Language :: Visual Basic', +'Programming Language :: XBasic', +'Programming Language :: YACC', +'Programming Language :: Zope', +'Topic :: Adaptive Technologies', +'Topic :: Artistic Software', +'Topic :: Communications', +'Topic :: Communications :: BBS', +'Topic :: Communications :: Chat', +'Topic :: Communications :: Chat :: AOL Instant Messenger', +'Topic :: Communications :: Chat :: ICQ', +'Topic :: Communications :: Chat :: Internet Relay Chat', +'Topic :: Communications :: Chat :: Unix Talk', +'Topic :: Communications :: Conferencing', +'Topic :: Communications :: Email', +'Topic :: Communications :: Email :: Address Book', +'Topic :: Communications :: Email :: Email Clients (MUA)', +'Topic :: Communications :: Email :: Filters', +'Topic :: Communications :: Email :: Mailing List Servers', +'Topic :: Communications :: Email :: Mail Transport Agents', +'Topic :: Communications :: Email :: Post-Office', +'Topic :: Communications :: Email :: Post-Office :: IMAP', +'Topic :: Communications :: Email :: Post-Office :: POP3', +'Topic :: Communications :: Fax', +'Topic :: Communications :: FIDO', +'Topic :: Communications :: File Sharing', +'Topic :: Communications :: File Sharing :: Gnutella', +'Topic :: Communications :: File Sharing :: Napster', +'Topic :: Communications :: Ham Radio', +'Topic :: Communications :: Internet Phone', +'Topic :: Communications :: Telephony', +'Topic :: Communications :: Usenet News', +'Topic :: Database', +'Topic :: Database :: Database Engines/Servers', +'Topic :: Database :: Front-Ends', +'Topic :: Desktop Environment', +'Topic :: Desktop Environment :: File Managers', +'Topic :: Desktop Environment :: Gnome', +'Topic :: Desktop Environment :: GNUstep', +'Topic :: Desktop Environment :: K Desktop Environment (KDE)', +'Topic :: Desktop Environment :: K Desktop Environment (KDE) :: Themes', +'Topic :: Desktop Environment :: PicoGUI', +'Topic :: Desktop Environment :: PicoGUI :: Applications', +'Topic :: Desktop Environment :: PicoGUI :: Themes', +'Topic :: Desktop Environment :: Screen Savers', +'Topic :: Desktop Environment :: Window Managers', +'Topic :: Desktop Environment :: Window Managers :: Afterstep', +'Topic :: Desktop Environment :: Window Managers :: Afterstep :: Themes', +'Topic :: Desktop Environment :: Window Managers :: Applets', +'Topic :: Desktop Environment :: Window Managers :: Blackbox', +'Topic :: Desktop Environment :: Window Managers :: Blackbox :: Themes', +'Topic :: Desktop Environment :: Window Managers :: CTWM', +'Topic :: Desktop Environment :: Window Managers :: CTWM :: Themes', +'Topic :: Desktop Environment :: Window Managers :: Enlightenment', +'Topic :: Desktop Environment :: Window Managers :: Enlightenment :: Epplets', +'Topic :: Desktop Environment :: Window Managers :: Enlightenment :: Themes DR15', +'Topic :: Desktop Environment :: Window Managers :: Enlightenment :: Themes DR16', +'Topic :: Desktop Environment :: Window Managers :: Enlightenment :: Themes DR17', +'Topic :: Desktop Environment :: Window Managers :: Fluxbox', +'Topic :: Desktop Environment :: Window Managers :: Fluxbox :: Themes', +'Topic :: Desktop Environment :: Window Managers :: FVWM', +'Topic :: Desktop Environment :: Window Managers :: FVWM :: Themes', +'Topic :: Desktop Environment :: Window Managers :: IceWM', +'Topic :: Desktop Environment :: Window Managers :: IceWM :: Themes', +'Topic :: Desktop Environment :: Window Managers :: MetaCity', +'Topic :: Desktop Environment :: Window Managers :: MetaCity :: Themes', +'Topic :: Desktop Environment :: Window Managers :: Oroborus', +'Topic :: Desktop Environment :: Window Managers :: Oroborus :: Themes', +'Topic :: Desktop Environment :: Window Managers :: Sawfish', +'Topic :: Desktop Environment :: Window Managers :: Sawfish :: Themes 0.30', +'Topic :: Desktop Environment :: Window Managers :: Sawfish :: Themes pre-0.30', +'Topic :: Desktop Environment :: Window Managers :: Waimea', +'Topic :: Desktop Environment :: Window Managers :: Waimea :: Themes', +'Topic :: Desktop Environment :: Window Managers :: Window Maker', +'Topic :: Desktop Environment :: Window Managers :: Window Maker :: Applets', +'Topic :: Desktop Environment :: Window Managers :: Window Maker :: Themes', +'Topic :: Desktop Environment :: Window Managers :: XFCE', +'Topic :: Desktop Environment :: Window Managers :: XFCE :: Themes', +'Topic :: Documentation', +'Topic :: Education', +'Topic :: Education :: Computer Aided Instruction (CAI)', +'Topic :: Education :: Testing', +'Topic :: Games/Entertainment', +'Topic :: Games/Entertainment :: Arcade', +'Topic :: Games/Entertainment :: Board Games', +'Topic :: Games/Entertainment :: First Person Shooters', +'Topic :: Games/Entertainment :: Fortune Cookies', +'Topic :: Games/Entertainment :: Multi-User Dungeons (MUD)', +'Topic :: Games/Entertainment :: Puzzle Games', +'Topic :: Games/Entertainment :: Real Time Strategy', +'Topic :: Games/Entertainment :: Role-Playing', +'Topic :: Games/Entertainment :: Side-Scrolling/Arcade Games', +'Topic :: Games/Entertainment :: Simulation', +'Topic :: Games/Entertainment :: Turn Based Strategy', +'Topic :: Home Automation', +'Topic :: Internet', +'Topic :: Internet :: File Transfer Protocol (FTP)', +'Topic :: Internet :: Finger', +'Topic :: Internet :: Log Analysis', +'Topic :: Internet :: Name Service (DNS)', +'Topic :: Internet :: Proxy Servers', +'Topic :: Internet :: WAP', +'Topic :: Internet :: WWW/HTTP', +'Topic :: Internet :: WWW/HTTP :: Browsers', +'Topic :: Internet :: WWW/HTTP :: Dynamic Content', +'Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries', +'Topic :: Internet :: WWW/HTTP :: Dynamic Content :: Message Boards', +'Topic :: Internet :: WWW/HTTP :: Dynamic Content :: News/Diary', +'Topic :: Internet :: WWW/HTTP :: Dynamic Content :: Page Counters', +'Topic :: Internet :: WWW/HTTP :: HTTP Servers', +'Topic :: Internet :: WWW/HTTP :: Indexing/Search', +'Topic :: Internet :: WWW/HTTP :: Site Management', +'Topic :: Internet :: WWW/HTTP :: Site Management :: Link Checking', +'Topic :: Internet :: WWW/HTTP :: WSGI', +'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', +'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware', +'Topic :: Internet :: WWW/HTTP :: WSGI :: Server', +'Topic :: Internet :: Z39.50', +'Topic :: Multimedia', +'Topic :: Multimedia :: Graphics', +'Topic :: Multimedia :: Graphics :: 3D Modeling', +'Topic :: Multimedia :: Graphics :: 3D Rendering', +'Topic :: Multimedia :: Graphics :: Capture', +'Topic :: Multimedia :: Graphics :: Capture :: Digital Camera', +'Topic :: Multimedia :: Graphics :: Capture :: Scanners', +'Topic :: Multimedia :: Graphics :: Capture :: Screen Capture', +'Topic :: Multimedia :: Graphics :: Editors', +'Topic :: Multimedia :: Graphics :: Editors :: Raster-Based', +'Topic :: Multimedia :: Graphics :: Editors :: Vector-Based', +'Topic :: Multimedia :: Graphics :: Graphics Conversion', +'Topic :: Multimedia :: Graphics :: Presentation', +'Topic :: Multimedia :: Graphics :: Viewers', +'Topic :: Multimedia :: Sound/Audio', +'Topic :: Multimedia :: Sound/Audio :: Analysis', +'Topic :: Multimedia :: Sound/Audio :: Capture/Recording', +'Topic :: Multimedia :: Sound/Audio :: CD Audio', +'Topic :: Multimedia :: Sound/Audio :: CD Audio :: CD Playing', +'Topic :: Multimedia :: Sound/Audio :: CD Audio :: CD Ripping', +'Topic :: Multimedia :: Sound/Audio :: CD Audio :: CD Writing', +'Topic :: Multimedia :: Sound/Audio :: Conversion', +'Topic :: Multimedia :: Sound/Audio :: Editors', +'Topic :: Multimedia :: Sound/Audio :: MIDI', +'Topic :: Multimedia :: Sound/Audio :: Mixers', +'Topic :: Multimedia :: Sound/Audio :: Players', +'Topic :: Multimedia :: Sound/Audio :: Players :: MP3', +'Topic :: Multimedia :: Sound/Audio :: Sound Synthesis', +'Topic :: Multimedia :: Sound/Audio :: Speech', +'Topic :: Multimedia :: Video', +'Topic :: Multimedia :: Video :: Capture', +'Topic :: Multimedia :: Video :: Conversion', +'Topic :: Multimedia :: Video :: Display', +'Topic :: Multimedia :: Video :: Non-Linear Editor', +'Topic :: Office/Business', +'Topic :: Office/Business :: Financial', +'Topic :: Office/Business :: Financial :: Accounting', +'Topic :: Office/Business :: Financial :: Investment', +'Topic :: Office/Business :: Financial :: Point-Of-Sale', +'Topic :: Office/Business :: Financial :: Spreadsheet', +'Topic :: Office/Business :: Groupware', +'Topic :: Office/Business :: News/Diary', +'Topic :: Office/Business :: Office Suites', +'Topic :: Office/Business :: Scheduling', +'Topic :: Other/Nonlisted Topic', +'Topic :: Printing', +'Topic :: Religion', +'Topic :: Scientific/Engineering', +'Topic :: Scientific/Engineering :: Artificial Intelligence', +'Topic :: Scientific/Engineering :: Astronomy', +'Topic :: Scientific/Engineering :: Atmospheric Science', +'Topic :: Scientific/Engineering :: Bio-Informatics', +'Topic :: Scientific/Engineering :: Chemistry', +'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)', +'Topic :: Scientific/Engineering :: GIS', +'Topic :: Scientific/Engineering :: Human Machine Interfaces', +'Topic :: Scientific/Engineering :: Image Recognition', +'Topic :: Scientific/Engineering :: Information Analysis', +'Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator', +'Topic :: Scientific/Engineering :: Mathematics', +'Topic :: Scientific/Engineering :: Medical Science Apps.', +'Topic :: Scientific/Engineering :: Physics', +'Topic :: Scientific/Engineering :: Visualization', +'Topic :: Security', +'Topic :: Security :: Cryptography', +'Topic :: Sociology', +'Topic :: Sociology :: Genealogy', +'Topic :: Sociology :: History', +'Topic :: Software Development', +'Topic :: Software Development :: Assemblers', +'Topic :: Software Development :: Bug Tracking', +'Topic :: Software Development :: Build Tools', +'Topic :: Software Development :: Code Generators', +'Topic :: Software Development :: Compilers', +'Topic :: Software Development :: Debuggers', +'Topic :: Software Development :: Disassemblers', +'Topic :: Software Development :: Documentation', +'Topic :: Software Development :: Embedded Systems', +'Topic :: Software Development :: Internationalization', +'Topic :: Software Development :: Interpreters', +'Topic :: Software Development :: Libraries', +'Topic :: Software Development :: Libraries :: Application Frameworks', +'Topic :: Software Development :: Libraries :: Java Libraries', +'Topic :: Software Development :: Libraries :: Perl Modules', +'Topic :: Software Development :: Libraries :: PHP Classes', +'Topic :: Software Development :: Libraries :: Pike Modules', +'Topic :: Software Development :: Libraries :: pygame', +'Topic :: Software Development :: Libraries :: Python Modules', +'Topic :: Software Development :: Libraries :: Ruby Modules', +'Topic :: Software Development :: Libraries :: Tcl Extensions', +'Topic :: Software Development :: Localization', +'Topic :: Software Development :: Object Brokering', +'Topic :: Software Development :: Object Brokering :: CORBA', +'Topic :: Software Development :: Pre-processors', +'Topic :: Software Development :: Quality Assurance', +'Topic :: Software Development :: Testing', +'Topic :: Software Development :: Testing :: Traffic Generation', +'Topic :: Software Development :: User Interfaces', +'Topic :: Software Development :: Version Control', +'Topic :: Software Development :: Version Control :: CVS', +'Topic :: Software Development :: Version Control :: RCS', +'Topic :: Software Development :: Version Control :: SCCS', +'Topic :: Software Development :: Widget Sets', +'Topic :: System', +'Topic :: System :: Archiving', +'Topic :: System :: Archiving :: Backup', +'Topic :: System :: Archiving :: Compression', +'Topic :: System :: Archiving :: Mirroring', +'Topic :: System :: Archiving :: Packaging', +'Topic :: System :: Benchmark', +'Topic :: System :: Boot', +'Topic :: System :: Boot :: Init', +'Topic :: System :: Clustering', +'Topic :: System :: Console Fonts', +'Topic :: System :: Distributed Computing', +'Topic :: System :: Emulators', +'Topic :: System :: Filesystems', +'Topic :: System :: Hardware', +'Topic :: System :: Hardware :: Hardware Drivers', +'Topic :: System :: Hardware :: Mainframes', +'Topic :: System :: Hardware :: Symmetric Multi-processing', +'Topic :: System :: Installation/Setup', +'Topic :: System :: Logging', +'Topic :: System :: Monitoring', +'Topic :: System :: Networking', +'Topic :: System :: Networking :: Firewalls', +'Topic :: System :: Networking :: Monitoring', +'Topic :: System :: Networking :: Monitoring :: Hardware Watchdog', +'Topic :: System :: Networking :: Time Synchronization', +'Topic :: System :: Operating System', +'Topic :: System :: Operating System Kernels', +'Topic :: System :: Operating System Kernels :: BSD', +'Topic :: System :: Operating System Kernels :: GNU Hurd', +'Topic :: System :: Operating System Kernels :: Linux', +'Topic :: System :: Power (UPS)', +'Topic :: System :: Recovery Tools', +'Topic :: System :: Shells', +'Topic :: System :: Software Distribution', +'Topic :: System :: Systems Administration', +'Topic :: System :: Systems Administration :: Authentication/Directory', +'Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP', +'Topic :: System :: Systems Administration :: Authentication/Directory :: NIS', +'Topic :: System :: System Shells', +'Topic :: Terminals', +'Topic :: Terminals :: Serial', +'Topic :: Terminals :: Telnet', +'Topic :: Terminals :: Terminal Emulators/X Terminals', +'Topic :: Text Editors', +'Topic :: Text Editors :: Documentation', +'Topic :: Text Editors :: Emacs', +'Topic :: Text Editors :: Integrated Development Environments (IDE)', +'Topic :: Text Editors :: Text Processing', +'Topic :: Text Editors :: Word Processors', +'Topic :: Text Processing', +'Topic :: Text Processing :: Filters', +'Topic :: Text Processing :: Fonts', +'Topic :: Text Processing :: General', +'Topic :: Text Processing :: Indexing', +'Topic :: Text Processing :: Linguistic', +'Topic :: Text Processing :: Markup', +'Topic :: Text Processing :: Markup :: HTML', +'Topic :: Text Processing :: Markup :: LaTeX', +'Topic :: Text Processing :: Markup :: SGML', +'Topic :: Text Processing :: Markup :: VRML', +'Topic :: Text Processing :: Markup :: XML', +'Topic :: Utilities', +] diff --git a/Lib/packaging/command/__init__.py b/Lib/packaging/command/__init__.py new file mode 100644 index 000000000000..6a3785061b9d --- /dev/null +++ b/Lib/packaging/command/__init__.py @@ -0,0 +1,56 @@ +"""Subpackage containing all standard commands.""" + +from packaging.errors import PackagingModuleError +from packaging.util import resolve_name + +__all__ = ['get_command_names', 'set_command', 'get_command_class', + 'STANDARD_COMMANDS'] + +_COMMANDS = { + 'check': 'packaging.command.check.check', + 'test': 'packaging.command.test.test', + 'build': 'packaging.command.build.build', + 'build_py': 'packaging.command.build_py.build_py', + 'build_ext': 'packaging.command.build_ext.build_ext', + 'build_clib': 'packaging.command.build_clib.build_clib', + 'build_scripts': 'packaging.command.build_scripts.build_scripts', + 'clean': 'packaging.command.clean.clean', + 'install_dist': 'packaging.command.install_dist.install_dist', + 'install_lib': 'packaging.command.install_lib.install_lib', + 'install_headers': 'packaging.command.install_headers.install_headers', + 'install_scripts': 'packaging.command.install_scripts.install_scripts', + 'install_data': 'packaging.command.install_data.install_data', + 'install_distinfo': + 'packaging.command.install_distinfo.install_distinfo', + 'sdist': 'packaging.command.sdist.sdist', + 'bdist': 'packaging.command.bdist.bdist', + 'bdist_dumb': 'packaging.command.bdist_dumb.bdist_dumb', + 'bdist_wininst': 'packaging.command.bdist_wininst.bdist_wininst', + 'register': 'packaging.command.register.register', + 'upload': 'packaging.command.upload.upload', + 'upload_docs': 'packaging.command.upload_docs.upload_docs'} + +STANDARD_COMMANDS = set(_COMMANDS) + + +def get_command_names(): + """Return registered commands""" + return sorted(_COMMANDS) + + +def set_command(location): + cls = resolve_name(location) + # XXX we want to do the duck-type checking here + _COMMANDS[cls.get_command_name()] = cls + + +def get_command_class(name): + """Return the registered command""" + try: + cls = _COMMANDS[name] + if isinstance(cls, str): + cls = resolve_name(cls) + _COMMANDS[name] = cls + return cls + except KeyError: + raise PackagingModuleError("Invalid command %s" % name) diff --git a/Lib/packaging/command/bdist.py b/Lib/packaging/command/bdist.py new file mode 100644 index 000000000000..4338a970e82b --- /dev/null +++ b/Lib/packaging/command/bdist.py @@ -0,0 +1,141 @@ +"""Create a built (binary) distribution. + +If a --formats option was given on the command line, this command will +call the corresponding bdist_* commands; if the option was absent, a +bdist_* command depending on the current platform will be called. +""" + +import os + +from packaging import util +from packaging.command.cmd import Command +from packaging.errors import PackagingPlatformError, PackagingOptionError + + +def show_formats(): + """Print list of available formats (arguments to "--format" option). + """ + from packaging.fancy_getopt import FancyGetopt + formats = [] + for format in bdist.format_commands: + formats.append(("formats=" + format, None, + bdist.format_command[format][1])) + pretty_printer = FancyGetopt(formats) + pretty_printer.print_help("List of available distribution formats:") + + +class bdist(Command): + + description = "create a built (binary) distribution" + + user_options = [('bdist-base=', 'b', + "temporary directory for creating built distributions"), + ('plat-name=', 'p', + "platform name to embed in generated filenames " + "(default: %s)" % util.get_platform()), + ('formats=', None, + "formats for distribution (comma-separated list)"), + ('dist-dir=', 'd', + "directory to put final built distributions in " + "[default: dist]"), + ('skip-build', None, + "skip rebuilding everything (for testing/debugging)"), + ('owner=', 'u', + "Owner name used when creating a tar file" + " [default: current user]"), + ('group=', 'g', + "Group name used when creating a tar file" + " [default: current group]"), + ] + + boolean_options = ['skip-build'] + + help_options = [ + ('help-formats', None, + "lists available distribution formats", show_formats), + ] + + # This is of course very simplistic. The various UNIX family operating + # systems have their specific formats, but they are out of scope for us; + # bdist_dumb is, well, dumb; it's more a building block for other + # packaging tools than a real end-user binary format. + default_format = {'posix': 'gztar', + 'nt': 'zip', + 'os2': 'zip'} + + # Establish the preferred order (for the --help-formats option). + format_commands = ['gztar', 'bztar', 'ztar', 'tar', + 'wininst', 'zip', 'msi'] + + # And the real information. + format_command = {'gztar': ('bdist_dumb', "gzip'ed tar file"), + 'bztar': ('bdist_dumb', "bzip2'ed tar file"), + 'ztar': ('bdist_dumb', "compressed tar file"), + 'tar': ('bdist_dumb', "tar file"), + 'wininst': ('bdist_wininst', + "Windows executable installer"), + 'zip': ('bdist_dumb', "ZIP file"), + 'msi': ('bdist_msi', "Microsoft Installer") + } + + + def initialize_options(self): + self.bdist_base = None + self.plat_name = None + self.formats = None + self.dist_dir = None + self.skip_build = False + self.group = None + self.owner = None + + def finalize_options(self): + # have to finalize 'plat_name' before 'bdist_base' + if self.plat_name is None: + if self.skip_build: + self.plat_name = util.get_platform() + else: + self.plat_name = self.get_finalized_command('build').plat_name + + # 'bdist_base' -- parent of per-built-distribution-format + # temporary directories (eg. we'll probably have + # "build/bdist./dumb", etc.) + if self.bdist_base is None: + build_base = self.get_finalized_command('build').build_base + self.bdist_base = os.path.join(build_base, + 'bdist.' + self.plat_name) + + self.ensure_string_list('formats') + if self.formats is None: + try: + self.formats = [self.default_format[os.name]] + except KeyError: + raise PackagingPlatformError("don't know how to create built distributions " + \ + "on platform %s" % os.name) + + if self.dist_dir is None: + self.dist_dir = "dist" + + def run(self): + # Figure out which sub-commands we need to run. + commands = [] + for format in self.formats: + try: + commands.append(self.format_command[format][0]) + except KeyError: + raise PackagingOptionError("invalid format '%s'" % format) + + # Reinitialize and run each command. + for i in range(len(self.formats)): + cmd_name = commands[i] + sub_cmd = self.get_reinitialized_command(cmd_name) + + # passing the owner and group names for tar archiving + if cmd_name == 'bdist_dumb': + sub_cmd.owner = self.owner + sub_cmd.group = self.group + + # If we're going to need to run this command again, tell it to + # keep its temporary files around so subsequent runs go faster. + if cmd_name in commands[i+1:]: + sub_cmd.keep_temp = True + self.run_command(cmd_name) diff --git a/Lib/packaging/command/bdist_dumb.py b/Lib/packaging/command/bdist_dumb.py new file mode 100644 index 000000000000..f74b72092546 --- /dev/null +++ b/Lib/packaging/command/bdist_dumb.py @@ -0,0 +1,137 @@ +"""Create a "dumb" built distribution. + +A dumb distribution is just an archive meant to be unpacked under +sys.prefix or sys.exec_prefix. +""" + +import os + +from shutil import rmtree +from sysconfig import get_python_version +from packaging.util import get_platform +from packaging.command.cmd import Command +from packaging.errors import PackagingPlatformError +from packaging import logger + +class bdist_dumb(Command): + + description = 'create a "dumb" built distribution' + + user_options = [('bdist-dir=', 'd', + "temporary directory for creating the distribution"), + ('plat-name=', 'p', + "platform name to embed in generated filenames " + "(default: %s)" % get_platform()), + ('format=', 'f', + "archive format to create (tar, ztar, gztar, zip)"), + ('keep-temp', 'k', + "keep the pseudo-installation tree around after " + + "creating the distribution archive"), + ('dist-dir=', 'd', + "directory to put final built distributions in"), + ('skip-build', None, + "skip rebuilding everything (for testing/debugging)"), + ('relative', None, + "build the archive using relative paths" + "(default: false)"), + ('owner=', 'u', + "Owner name used when creating a tar file" + " [default: current user]"), + ('group=', 'g', + "Group name used when creating a tar file" + " [default: current group]"), + ] + + boolean_options = ['keep-temp', 'skip-build', 'relative'] + + default_format = { 'posix': 'gztar', + 'nt': 'zip', + 'os2': 'zip' } + + + def initialize_options(self): + self.bdist_dir = None + self.plat_name = None + self.format = None + self.keep_temp = False + self.dist_dir = None + self.skip_build = False + self.relative = False + self.owner = None + self.group = None + + def finalize_options(self): + if self.bdist_dir is None: + bdist_base = self.get_finalized_command('bdist').bdist_base + self.bdist_dir = os.path.join(bdist_base, 'dumb') + + if self.format is None: + try: + self.format = self.default_format[os.name] + except KeyError: + raise PackagingPlatformError(("don't know how to create dumb built distributions " + + "on platform %s") % os.name) + + self.set_undefined_options('bdist', 'dist_dir', 'plat_name') + + def run(self): + if not self.skip_build: + self.run_command('build') + + install = self.get_reinitialized_command('install_dist', + reinit_subcommands=True) + install.root = self.bdist_dir + install.skip_build = self.skip_build + install.warn_dir = False + + logger.info("installing to %s", self.bdist_dir) + self.run_command('install_dist') + + # And make an archive relative to the root of the + # pseudo-installation tree. + archive_basename = "%s.%s" % (self.distribution.get_fullname(), + self.plat_name) + + # OS/2 objects to any ":" characters in a filename (such as when + # a timestamp is used in a version) so change them to hyphens. + if os.name == "os2": + archive_basename = archive_basename.replace(":", "-") + + pseudoinstall_root = os.path.join(self.dist_dir, archive_basename) + if not self.relative: + archive_root = self.bdist_dir + else: + if (self.distribution.has_ext_modules() and + (install.install_base != install.install_platbase)): + raise PackagingPlatformError( + "can't make a dumb built distribution where base and " + "platbase are different (%r, %r)" % + (install.install_base, install.install_platbase)) + else: + archive_root = os.path.join( + self.bdist_dir, + self._ensure_relative(install.install_base)) + + # Make the archive + filename = self.make_archive(pseudoinstall_root, + self.format, root_dir=archive_root, + owner=self.owner, group=self.group) + if self.distribution.has_ext_modules(): + pyversion = get_python_version() + else: + pyversion = 'any' + self.distribution.dist_files.append(('bdist_dumb', pyversion, + filename)) + + if not self.keep_temp: + if self.dry_run: + logger.info('removing %s', self.bdist_dir) + else: + rmtree(self.bdist_dir) + + def _ensure_relative(self, path): + # copied from dir_util, deleted + drive, path = os.path.splitdrive(path) + if path[0:1] == os.sep: + path = drive + path[1:] + return path diff --git a/Lib/packaging/command/bdist_msi.py b/Lib/packaging/command/bdist_msi.py new file mode 100644 index 000000000000..493f8b34e346 --- /dev/null +++ b/Lib/packaging/command/bdist_msi.py @@ -0,0 +1,740 @@ +"""Create a Microsoft Installer (.msi) binary distribution.""" + +# Copyright (C) 2005, 2006 Martin von Löwis +# Licensed to PSF under a Contributor Agreement. + +import sys +import os +import msilib + + +from sysconfig import get_python_version +from shutil import rmtree +from packaging.command.cmd import Command +from packaging.version import NormalizedVersion +from packaging.errors import PackagingOptionError +from packaging import logger as log +from packaging.util import get_platform +from msilib import schema, sequence, text +from msilib import Directory, Feature, Dialog, add_data + +class MSIVersion(NormalizedVersion): + """ + MSI ProductVersion must be strictly numeric. + MSIVersion disallows prerelease and postrelease versions. + """ + def __init__(self, *args, **kwargs): + super(MSIVersion, self).__init__(*args, **kwargs) + if not self.is_final: + raise ValueError("ProductVersion must be strictly numeric") + +class PyDialog(Dialog): + """Dialog class with a fixed layout: controls at the top, then a ruler, + then a list of buttons: back, next, cancel. Optionally a bitmap at the + left.""" + def __init__(self, *args, **kw): + """Dialog(database, name, x, y, w, h, attributes, title, first, + default, cancel, bitmap=true)""" + Dialog.__init__(self, *args) + ruler = self.h - 36 + #if kw.get("bitmap", True): + # self.bitmap("Bitmap", 0, 0, bmwidth, ruler, "PythonWin") + self.line("BottomLine", 0, ruler, self.w, 0) + + def title(self, title): + "Set the title text of the dialog at the top." + # name, x, y, w, h, flags=Visible|Enabled|Transparent|NoPrefix, + # text, in VerdanaBold10 + self.text("Title", 15, 10, 320, 60, 0x30003, + r"{\VerdanaBold10}%s" % title) + + def back(self, title, next, name = "Back", active = 1): + """Add a back button with a given title, the tab-next button, + its name in the Control table, possibly initially disabled. + + Return the button, so that events can be associated""" + if active: + flags = 3 # Visible|Enabled + else: + flags = 1 # Visible + return self.pushbutton(name, 180, self.h-27 , 56, 17, flags, title, next) + + def cancel(self, title, next, name = "Cancel", active = 1): + """Add a cancel button with a given title, the tab-next button, + its name in the Control table, possibly initially disabled. + + Return the button, so that events can be associated""" + if active: + flags = 3 # Visible|Enabled + else: + flags = 1 # Visible + return self.pushbutton(name, 304, self.h-27, 56, 17, flags, title, next) + + def next(self, title, next, name = "Next", active = 1): + """Add a Next button with a given title, the tab-next button, + its name in the Control table, possibly initially disabled. + + Return the button, so that events can be associated""" + if active: + flags = 3 # Visible|Enabled + else: + flags = 1 # Visible + return self.pushbutton(name, 236, self.h-27, 56, 17, flags, title, next) + + def xbutton(self, name, title, next, xpos): + """Add a button with a given title, the tab-next button, + its name in the Control table, giving its x position; the + y-position is aligned with the other buttons. + + Return the button, so that events can be associated""" + return self.pushbutton(name, int(self.w*xpos - 28), self.h-27, 56, 17, 3, title, next) + +class bdist_msi(Command): + + description = "create a Microsoft Installer (.msi) binary distribution" + + user_options = [('bdist-dir=', None, + "temporary directory for creating the distribution"), + ('plat-name=', 'p', + "platform name to embed in generated filenames " + "(default: %s)" % get_platform()), + ('keep-temp', 'k', + "keep the pseudo-installation tree around after " + + "creating the distribution archive"), + ('target-version=', None, + "require a specific python version" + + " on the target system"), + ('no-target-compile', 'c', + "do not compile .py to .pyc on the target system"), + ('no-target-optimize', 'o', + "do not compile .py to .pyo (optimized)" + "on the target system"), + ('dist-dir=', 'd', + "directory to put final built distributions in"), + ('skip-build', None, + "skip rebuilding everything (for testing/debugging)"), + ('install-script=', None, + "basename of installation script to be run after" + "installation or before deinstallation"), + ('pre-install-script=', None, + "Fully qualified filename of a script to be run before " + "any files are installed. This script need not be in the " + "distribution"), + ] + + boolean_options = ['keep-temp', 'no-target-compile', 'no-target-optimize', + 'skip-build'] + + all_versions = ['2.0', '2.1', '2.2', '2.3', '2.4', + '2.5', '2.6', '2.7', '2.8', '2.9', + '3.0', '3.1', '3.2', '3.3', '3.4', + '3.5', '3.6', '3.7', '3.8', '3.9'] + other_version = 'X' + + def initialize_options(self): + self.bdist_dir = None + self.plat_name = None + self.keep_temp = False + self.no_target_compile = False + self.no_target_optimize = False + self.target_version = None + self.dist_dir = None + self.skip_build = False + self.install_script = None + self.pre_install_script = None + self.versions = None + + def finalize_options(self): + if self.bdist_dir is None: + bdist_base = self.get_finalized_command('bdist').bdist_base + self.bdist_dir = os.path.join(bdist_base, 'msi') + short_version = get_python_version() + if (not self.target_version) and self.distribution.has_ext_modules(): + self.target_version = short_version + if self.target_version: + self.versions = [self.target_version] + if not self.skip_build and self.distribution.has_ext_modules()\ + and self.target_version != short_version: + raise PackagingOptionError("target version can only be %s, or the '--skip-build'" \ + " option must be specified" % (short_version,)) + else: + self.versions = list(self.all_versions) + + self.set_undefined_options('bdist', 'dist_dir', 'plat_name') + + if self.pre_install_script: + raise PackagingOptionError("the pre-install-script feature is not yet implemented") + + if self.install_script: + for script in self.distribution.scripts: + if self.install_script == os.path.basename(script): + break + else: + raise PackagingOptionError("install_script '%s' not found in scripts" % \ + self.install_script) + self.install_script_key = None + + + def run(self): + if not self.skip_build: + self.run_command('build') + + install = self.get_reinitialized_command('install_dist', + reinit_subcommands=True) + install.prefix = self.bdist_dir + install.skip_build = self.skip_build + install.warn_dir = False + + install_lib = self.get_reinitialized_command('install_lib') + # we do not want to include pyc or pyo files + install_lib.compile = False + install_lib.optimize = 0 + + if self.distribution.has_ext_modules(): + # If we are building an installer for a Python version other + # than the one we are currently running, then we need to ensure + # our build_lib reflects the other Python version rather than ours. + # Note that for target_version!=sys.version, we must have skipped the + # build step, so there is no issue with enforcing the build of this + # version. + target_version = self.target_version + if not target_version: + assert self.skip_build, "Should have already checked this" + target_version = sys.version[0:3] + plat_specifier = ".%s-%s" % (self.plat_name, target_version) + build = self.get_finalized_command('build') + build.build_lib = os.path.join(build.build_base, + 'lib' + plat_specifier) + + log.info("installing to %s", self.bdist_dir) + install.ensure_finalized() + + # avoid warning of 'install_lib' about installing + # into a directory not in sys.path + sys.path.insert(0, os.path.join(self.bdist_dir, 'PURELIB')) + + install.run() + + del sys.path[0] + + self.mkpath(self.dist_dir) + fullname = self.distribution.get_fullname() + installer_name = self.get_installer_filename(fullname) + installer_name = os.path.abspath(installer_name) + if os.path.exists(installer_name): os.unlink(installer_name) + + metadata = self.distribution.metadata + author = metadata.author + if not author: + author = metadata.maintainer + if not author: + author = "UNKNOWN" + version = MSIVersion(metadata.get_version()) + # Prefix ProductName with Python x.y, so that + # it sorts together with the other Python packages + # in Add-Remove-Programs (APR) + fullname = self.distribution.get_fullname() + if self.target_version: + product_name = "Python %s %s" % (self.target_version, fullname) + else: + product_name = "Python %s" % (fullname) + self.db = msilib.init_database(installer_name, schema, + product_name, msilib.gen_uuid(), + str(version), author) + msilib.add_tables(self.db, sequence) + props = [('DistVersion', version)] + email = metadata.author_email or metadata.maintainer_email + if email: + props.append(("ARPCONTACT", email)) + if metadata.url: + props.append(("ARPURLINFOABOUT", metadata.url)) + if props: + add_data(self.db, 'Property', props) + + self.add_find_python() + self.add_files() + self.add_scripts() + self.add_ui() + self.db.Commit() + + if hasattr(self.distribution, 'dist_files'): + tup = 'bdist_msi', self.target_version or 'any', fullname + self.distribution.dist_files.append(tup) + + if not self.keep_temp: + log.info("removing temporary build directory %s", self.bdist_dir) + if not self.dry_run: + rmtree(self.bdist_dir) + + def add_files(self): + db = self.db + cab = msilib.CAB("distfiles") + rootdir = os.path.abspath(self.bdist_dir) + + root = Directory(db, cab, None, rootdir, "TARGETDIR", "SourceDir") + f = Feature(db, "Python", "Python", "Everything", + 0, 1, directory="TARGETDIR") + + items = [(f, root, '')] + for version in self.versions + [self.other_version]: + target = "TARGETDIR" + version + name = default = "Python" + version + desc = "Everything" + if version is self.other_version: + title = "Python from another location" + level = 2 + else: + title = "Python %s from registry" % version + level = 1 + f = Feature(db, name, title, desc, 1, level, directory=target) + dir = Directory(db, cab, root, rootdir, target, default) + items.append((f, dir, version)) + db.Commit() + + seen = {} + for feature, dir, version in items: + todo = [dir] + while todo: + dir = todo.pop() + for file in os.listdir(dir.absolute): + afile = os.path.join(dir.absolute, file) + if os.path.isdir(afile): + short = "%s|%s" % (dir.make_short(file), file) + default = file + version + newdir = Directory(db, cab, dir, file, default, short) + todo.append(newdir) + else: + if not dir.component: + dir.start_component(dir.logical, feature, 0) + if afile not in seen: + key = seen[afile] = dir.add_file(file) + if file==self.install_script: + if self.install_script_key: + raise PackagingOptionError( + "Multiple files with name %s" % file) + self.install_script_key = '[#%s]' % key + else: + key = seen[afile] + add_data(self.db, "DuplicateFile", + [(key + version, dir.component, key, None, dir.logical)]) + db.Commit() + cab.commit(db) + + def add_find_python(self): + """Adds code to the installer to compute the location of Python. + + Properties PYTHON.MACHINE.X.Y and PYTHON.USER.X.Y will be set from the + registry for each version of Python. + + Properties TARGETDIRX.Y will be set from PYTHON.USER.X.Y if defined, + else from PYTHON.MACHINE.X.Y. + + Properties PYTHONX.Y will be set to TARGETDIRX.Y\\python.exe""" + + start = 402 + for ver in self.versions: + install_path = r"SOFTWARE\Python\PythonCore\%s\InstallPath" % ver + machine_reg = "python.machine." + ver + user_reg = "python.user." + ver + machine_prop = "PYTHON.MACHINE." + ver + user_prop = "PYTHON.USER." + ver + machine_action = "PythonFromMachine" + ver + user_action = "PythonFromUser" + ver + exe_action = "PythonExe" + ver + target_dir_prop = "TARGETDIR" + ver + exe_prop = "PYTHON" + ver + if msilib.Win64: + # type: msidbLocatorTypeRawValue + msidbLocatorType64bit + Type = 2+16 + else: + Type = 2 + add_data(self.db, "RegLocator", + [(machine_reg, 2, install_path, None, Type), + (user_reg, 1, install_path, None, Type)]) + add_data(self.db, "AppSearch", + [(machine_prop, machine_reg), + (user_prop, user_reg)]) + add_data(self.db, "CustomAction", + [(machine_action, 51+256, target_dir_prop, "[" + machine_prop + "]"), + (user_action, 51+256, target_dir_prop, "[" + user_prop + "]"), + (exe_action, 51+256, exe_prop, "[" + target_dir_prop + "]\\python.exe"), + ]) + add_data(self.db, "InstallExecuteSequence", + [(machine_action, machine_prop, start), + (user_action, user_prop, start + 1), + (exe_action, None, start + 2), + ]) + add_data(self.db, "InstallUISequence", + [(machine_action, machine_prop, start), + (user_action, user_prop, start + 1), + (exe_action, None, start + 2), + ]) + add_data(self.db, "Condition", + [("Python" + ver, 0, "NOT TARGETDIR" + ver)]) + start += 4 + assert start < 500 + + def add_scripts(self): + if self.install_script: + start = 6800 + for ver in self.versions + [self.other_version]: + install_action = "install_script." + ver + exe_prop = "PYTHON" + ver + add_data(self.db, "CustomAction", + [(install_action, 50, exe_prop, self.install_script_key)]) + add_data(self.db, "InstallExecuteSequence", + [(install_action, "&Python%s=3" % ver, start)]) + start += 1 + # XXX pre-install scripts are currently refused in finalize_options() + # but if this feature is completed, it will also need to add + # entries for each version as the above code does + if self.pre_install_script: + scriptfn = os.path.join(self.bdist_dir, "preinstall.bat") + with open(scriptfn, "w") as f: + # The batch file will be executed with [PYTHON], so that %1 + # is the path to the Python interpreter; %0 will be the path + # of the batch file. + # rem =""" + # %1 %0 + # exit + # """ + # + f.write('rem ="""\n%1 %0\nexit\n"""\n') + with open(self.pre_install_script) as fp: + f.write(fp.read()) + add_data(self.db, "Binary", + [("PreInstall", msilib.Binary(scriptfn)), + ]) + add_data(self.db, "CustomAction", + [("PreInstall", 2, "PreInstall", None), + ]) + add_data(self.db, "InstallExecuteSequence", + [("PreInstall", "NOT Installed", 450), + ]) + + def add_ui(self): + db = self.db + x = y = 50 + w = 370 + h = 300 + title = "[ProductName] Setup" + + # see "Dialog Style Bits" + modal = 3 # visible | modal + modeless = 1 # visible + + # UI customization properties + add_data(db, "Property", + # See "DefaultUIFont Property" + [("DefaultUIFont", "DlgFont8"), + # See "ErrorDialog Style Bit" + ("ErrorDialog", "ErrorDlg"), + ("Progress1", "Install"), # modified in maintenance type dlg + ("Progress2", "installs"), + ("MaintenanceForm_Action", "Repair"), + # possible values: ALL, JUSTME + ("WhichUsers", "ALL") + ]) + + # Fonts, see "TextStyle Table" + add_data(db, "TextStyle", + [("DlgFont8", "Tahoma", 9, None, 0), + ("DlgFontBold8", "Tahoma", 8, None, 1), #bold + ("VerdanaBold10", "Verdana", 10, None, 1), + ("VerdanaRed9", "Verdana", 9, 255, 0), + ]) + + # UI Sequences, see "InstallUISequence Table", "Using a Sequence Table" + # Numbers indicate sequence; see sequence.py for how these action integrate + add_data(db, "InstallUISequence", + [("PrepareDlg", "Not Privileged or Windows9x or Installed", 140), + ("WhichUsersDlg", "Privileged and not Windows9x and not Installed", 141), + # In the user interface, assume all-users installation if privileged. + ("SelectFeaturesDlg", "Not Installed", 1230), + # XXX no support for resume installations yet + #("ResumeDlg", "Installed AND (RESUME OR Preselected)", 1240), + ("MaintenanceTypeDlg", "Installed AND NOT RESUME AND NOT Preselected", 1250), + ("ProgressDlg", None, 1280)]) + + add_data(db, 'ActionText', text.ActionText) + add_data(db, 'UIText', text.UIText) + ##################################################################### + # Standard dialogs: FatalError, UserExit, ExitDialog + fatal=PyDialog(db, "FatalError", x, y, w, h, modal, title, + "Finish", "Finish", "Finish") + fatal.title("[ProductName] Installer ended prematurely") + fatal.back("< Back", "Finish", active = 0) + fatal.cancel("Cancel", "Back", active = 0) + fatal.text("Description1", 15, 70, 320, 80, 0x30003, + "[ProductName] setup ended prematurely because of an error. Your system has not been modified. To install this program at a later time, please run the installation again.") + fatal.text("Description2", 15, 155, 320, 20, 0x30003, + "Click the Finish button to exit the Installer.") + c=fatal.next("Finish", "Cancel", name="Finish") + c.event("EndDialog", "Exit") + + user_exit=PyDialog(db, "UserExit", x, y, w, h, modal, title, + "Finish", "Finish", "Finish") + user_exit.title("[ProductName] Installer was interrupted") + user_exit.back("< Back", "Finish", active = 0) + user_exit.cancel("Cancel", "Back", active = 0) + user_exit.text("Description1", 15, 70, 320, 80, 0x30003, + "[ProductName] setup was interrupted. Your system has not been modified. " + "To install this program at a later time, please run the installation again.") + user_exit.text("Description2", 15, 155, 320, 20, 0x30003, + "Click the Finish button to exit the Installer.") + c = user_exit.next("Finish", "Cancel", name="Finish") + c.event("EndDialog", "Exit") + + exit_dialog = PyDialog(db, "ExitDialog", x, y, w, h, modal, title, + "Finish", "Finish", "Finish") + exit_dialog.title("Completing the [ProductName] Installer") + exit_dialog.back("< Back", "Finish", active = 0) + exit_dialog.cancel("Cancel", "Back", active = 0) + exit_dialog.text("Description", 15, 235, 320, 20, 0x30003, + "Click the Finish button to exit the Installer.") + c = exit_dialog.next("Finish", "Cancel", name="Finish") + c.event("EndDialog", "Return") + + ##################################################################### + # Required dialog: FilesInUse, ErrorDlg + inuse = PyDialog(db, "FilesInUse", + x, y, w, h, + 19, # KeepModeless|Modal|Visible + title, + "Retry", "Retry", "Retry", bitmap=False) + inuse.text("Title", 15, 6, 200, 15, 0x30003, + r"{\DlgFontBold8}Files in Use") + inuse.text("Description", 20, 23, 280, 20, 0x30003, + "Some files that need to be updated are currently in use.") + inuse.text("Text", 20, 55, 330, 50, 3, + "The following applications are using files that need to be updated by this setup. Close these applications and then click Retry to continue the installation or Cancel to exit it.") + inuse.control("List", "ListBox", 20, 107, 330, 130, 7, "FileInUseProcess", + None, None, None) + c=inuse.back("Exit", "Ignore", name="Exit") + c.event("EndDialog", "Exit") + c=inuse.next("Ignore", "Retry", name="Ignore") + c.event("EndDialog", "Ignore") + c=inuse.cancel("Retry", "Exit", name="Retry") + c.event("EndDialog","Retry") + + # See "Error Dialog". See "ICE20" for the required names of the controls. + error = Dialog(db, "ErrorDlg", + 50, 10, 330, 101, + 65543, # Error|Minimize|Modal|Visible + title, + "ErrorText", None, None) + error.text("ErrorText", 50,9,280,48,3, "") + #error.control("ErrorIcon", "Icon", 15, 9, 24, 24, 5242881, None, "py.ico", None, None) + error.pushbutton("N",120,72,81,21,3,"No",None).event("EndDialog","ErrorNo") + error.pushbutton("Y",240,72,81,21,3,"Yes",None).event("EndDialog","ErrorYes") + error.pushbutton("A",0,72,81,21,3,"Abort",None).event("EndDialog","ErrorAbort") + error.pushbutton("C",42,72,81,21,3,"Cancel",None).event("EndDialog","ErrorCancel") + error.pushbutton("I",81,72,81,21,3,"Ignore",None).event("EndDialog","ErrorIgnore") + error.pushbutton("O",159,72,81,21,3,"Ok",None).event("EndDialog","ErrorOk") + error.pushbutton("R",198,72,81,21,3,"Retry",None).event("EndDialog","ErrorRetry") + + ##################################################################### + # Global "Query Cancel" dialog + cancel = Dialog(db, "CancelDlg", 50, 10, 260, 85, 3, title, + "No", "No", "No") + cancel.text("Text", 48, 15, 194, 30, 3, + "Are you sure you want to cancel [ProductName] installation?") + #cancel.control("Icon", "Icon", 15, 15, 24, 24, 5242881, None, + # "py.ico", None, None) + c=cancel.pushbutton("Yes", 72, 57, 56, 17, 3, "Yes", "No") + c.event("EndDialog", "Exit") + + c=cancel.pushbutton("No", 132, 57, 56, 17, 3, "No", "Yes") + c.event("EndDialog", "Return") + + ##################################################################### + # Global "Wait for costing" dialog + costing = Dialog(db, "WaitForCostingDlg", 50, 10, 260, 85, modal, title, + "Return", "Return", "Return") + costing.text("Text", 48, 15, 194, 30, 3, + "Please wait while the installer finishes determining your disk space requirements.") + c = costing.pushbutton("Return", 102, 57, 56, 17, 3, "Return", None) + c.event("EndDialog", "Exit") + + ##################################################################### + # Preparation dialog: no user input except cancellation + prep = PyDialog(db, "PrepareDlg", x, y, w, h, modeless, title, + "Cancel", "Cancel", "Cancel") + prep.text("Description", 15, 70, 320, 40, 0x30003, + "Please wait while the Installer prepares to guide you through the installation.") + prep.title("Welcome to the [ProductName] Installer") + c=prep.text("ActionText", 15, 110, 320, 20, 0x30003, "Pondering...") + c.mapping("ActionText", "Text") + c=prep.text("ActionData", 15, 135, 320, 30, 0x30003, None) + c.mapping("ActionData", "Text") + prep.back("Back", None, active=0) + prep.next("Next", None, active=0) + c=prep.cancel("Cancel", None) + c.event("SpawnDialog", "CancelDlg") + + ##################################################################### + # Feature (Python directory) selection + seldlg = PyDialog(db, "SelectFeaturesDlg", x, y, w, h, modal, title, + "Next", "Next", "Cancel") + seldlg.title("Select Python Installations") + + seldlg.text("Hint", 15, 30, 300, 20, 3, + "Select the Python locations where %s should be installed." + % self.distribution.get_fullname()) + + seldlg.back("< Back", None, active=0) + c = seldlg.next("Next >", "Cancel") + order = 1 + c.event("[TARGETDIR]", "[SourceDir]", ordering=order) + for version in self.versions + [self.other_version]: + order += 1 + c.event("[TARGETDIR]", "[TARGETDIR%s]" % version, + "FEATURE_SELECTED AND &Python%s=3" % version, + ordering=order) + c.event("SpawnWaitDialog", "WaitForCostingDlg", ordering=order + 1) + c.event("EndDialog", "Return", ordering=order + 2) + c = seldlg.cancel("Cancel", "Features") + c.event("SpawnDialog", "CancelDlg") + + c = seldlg.control("Features", "SelectionTree", 15, 60, 300, 120, 3, + "FEATURE", None, "PathEdit", None) + c.event("[FEATURE_SELECTED]", "1") + ver = self.other_version + install_other_cond = "FEATURE_SELECTED AND &Python%s=3" % ver + dont_install_other_cond = "FEATURE_SELECTED AND &Python%s<>3" % ver + + c = seldlg.text("Other", 15, 200, 300, 15, 3, + "Provide an alternate Python location") + c.condition("Enable", install_other_cond) + c.condition("Show", install_other_cond) + c.condition("Disable", dont_install_other_cond) + c.condition("Hide", dont_install_other_cond) + + c = seldlg.control("PathEdit", "PathEdit", 15, 215, 300, 16, 1, + "TARGETDIR" + ver, None, "Next", None) + c.condition("Enable", install_other_cond) + c.condition("Show", install_other_cond) + c.condition("Disable", dont_install_other_cond) + c.condition("Hide", dont_install_other_cond) + + ##################################################################### + # Disk cost + cost = PyDialog(db, "DiskCostDlg", x, y, w, h, modal, title, + "OK", "OK", "OK", bitmap=False) + cost.text("Title", 15, 6, 200, 15, 0x30003, + "{\DlgFontBold8}Disk Space Requirements") + cost.text("Description", 20, 20, 280, 20, 0x30003, + "The disk space required for the installation of the selected features.") + cost.text("Text", 20, 53, 330, 60, 3, + "The highlighted volumes (if any) do not have enough disk space " + "available for the currently selected features. You can either " + "remove some files from the highlighted volumes, or choose to " + "install less features onto local drive(s), or select different " + "destination drive(s).") + cost.control("VolumeList", "VolumeCostList", 20, 100, 330, 150, 393223, + None, "{120}{70}{70}{70}{70}", None, None) + cost.xbutton("OK", "Ok", None, 0.5).event("EndDialog", "Return") + + ##################################################################### + # WhichUsers Dialog. Only available on NT, and for privileged users. + # This must be run before FindRelatedProducts, because that will + # take into account whether the previous installation was per-user + # or per-machine. We currently don't support going back to this + # dialog after "Next" was selected; to support this, we would need to + # find how to reset the ALLUSERS property, and how to re-run + # FindRelatedProducts. + # On Windows9x, the ALLUSERS property is ignored on the command line + # and in the Property table, but installer fails according to the documentation + # if a dialog attempts to set ALLUSERS. + whichusers = PyDialog(db, "WhichUsersDlg", x, y, w, h, modal, title, + "AdminInstall", "Next", "Cancel") + whichusers.title("Select whether to install [ProductName] for all users of this computer.") + # A radio group with two options: allusers, justme + g = whichusers.radiogroup("AdminInstall", 15, 60, 260, 50, 3, + "WhichUsers", "", "Next") + g.add("ALL", 0, 5, 150, 20, "Install for all users") + g.add("JUSTME", 0, 25, 150, 20, "Install just for me") + + whichusers.back("Back", None, active=0) + + c = whichusers.next("Next >", "Cancel") + c.event("[ALLUSERS]", "1", 'WhichUsers="ALL"', 1) + c.event("EndDialog", "Return", ordering = 2) + + c = whichusers.cancel("Cancel", "AdminInstall") + c.event("SpawnDialog", "CancelDlg") + + ##################################################################### + # Installation Progress dialog (modeless) + progress = PyDialog(db, "ProgressDlg", x, y, w, h, modeless, title, + "Cancel", "Cancel", "Cancel", bitmap=False) + progress.text("Title", 20, 15, 200, 15, 0x30003, + "{\DlgFontBold8}[Progress1] [ProductName]") + progress.text("Text", 35, 65, 300, 30, 3, + "Please wait while the Installer [Progress2] [ProductName]. " + "This may take several minutes.") + progress.text("StatusLabel", 35, 100, 35, 20, 3, "Status:") + + c=progress.text("ActionText", 70, 100, w-70, 20, 3, "Pondering...") + c.mapping("ActionText", "Text") + + #c=progress.text("ActionData", 35, 140, 300, 20, 3, None) + #c.mapping("ActionData", "Text") + + c=progress.control("ProgressBar", "ProgressBar", 35, 120, 300, 10, 65537, + None, "Progress done", None, None) + c.mapping("SetProgress", "Progress") + + progress.back("< Back", "Next", active=False) + progress.next("Next >", "Cancel", active=False) + progress.cancel("Cancel", "Back").event("SpawnDialog", "CancelDlg") + + ################################################################### + # Maintenance type: repair/uninstall + maint = PyDialog(db, "MaintenanceTypeDlg", x, y, w, h, modal, title, + "Next", "Next", "Cancel") + maint.title("Welcome to the [ProductName] Setup Wizard") + maint.text("BodyText", 15, 63, 330, 42, 3, + "Select whether you want to repair or remove [ProductName].") + g=maint.radiogroup("RepairRadioGroup", 15, 108, 330, 60, 3, + "MaintenanceForm_Action", "", "Next") + #g.add("Change", 0, 0, 200, 17, "&Change [ProductName]") + g.add("Repair", 0, 18, 200, 17, "&Repair [ProductName]") + g.add("Remove", 0, 36, 200, 17, "Re&move [ProductName]") + + maint.back("< Back", None, active=False) + c=maint.next("Finish", "Cancel") + # Change installation: Change progress dialog to "Change", then ask + # for feature selection + #c.event("[Progress1]", "Change", 'MaintenanceForm_Action="Change"', 1) + #c.event("[Progress2]", "changes", 'MaintenanceForm_Action="Change"', 2) + + # Reinstall: Change progress dialog to "Repair", then invoke reinstall + # Also set list of reinstalled features to "ALL" + c.event("[REINSTALL]", "ALL", 'MaintenanceForm_Action="Repair"', 5) + c.event("[Progress1]", "Repairing", 'MaintenanceForm_Action="Repair"', 6) + c.event("[Progress2]", "repairs", 'MaintenanceForm_Action="Repair"', 7) + c.event("Reinstall", "ALL", 'MaintenanceForm_Action="Repair"', 8) + + # Uninstall: Change progress to "Remove", then invoke uninstall + # Also set list of removed features to "ALL" + c.event("[REMOVE]", "ALL", 'MaintenanceForm_Action="Remove"', 11) + c.event("[Progress1]", "Removing", 'MaintenanceForm_Action="Remove"', 12) + c.event("[Progress2]", "removes", 'MaintenanceForm_Action="Remove"', 13) + c.event("Remove", "ALL", 'MaintenanceForm_Action="Remove"', 14) + + # Close dialog when maintenance action scheduled + c.event("EndDialog", "Return", 'MaintenanceForm_Action<>"Change"', 20) + #c.event("NewDialog", "SelectFeaturesDlg", 'MaintenanceForm_Action="Change"', 21) + + maint.cancel("Cancel", "RepairRadioGroup").event("SpawnDialog", "CancelDlg") + + def get_installer_filename(self, fullname): + # Factored out to allow overriding in subclasses + if self.target_version: + base_name = "%s.%s-py%s.msi" % (fullname, self.plat_name, + self.target_version) + else: + base_name = "%s.%s.msi" % (fullname, self.plat_name) + installer_name = os.path.join(self.dist_dir, base_name) + return installer_name diff --git a/Lib/packaging/command/bdist_wininst.py b/Lib/packaging/command/bdist_wininst.py new file mode 100644 index 000000000000..dbb74eaeadb5 --- /dev/null +++ b/Lib/packaging/command/bdist_wininst.py @@ -0,0 +1,342 @@ +"""Create an executable installer for Windows.""" + +# FIXME synchronize bytes/str use with same file in distutils + +import sys +import os + +from shutil import rmtree +from sysconfig import get_python_version +from packaging.command.cmd import Command +from packaging.errors import PackagingOptionError, PackagingPlatformError +from packaging import logger +from packaging.util import get_platform + + +class bdist_wininst(Command): + + description = "create an executable installer for Windows" + + user_options = [('bdist-dir=', None, + "temporary directory for creating the distribution"), + ('plat-name=', 'p', + "platform name to embed in generated filenames " + "(default: %s)" % get_platform()), + ('keep-temp', 'k', + "keep the pseudo-installation tree around after " + + "creating the distribution archive"), + ('target-version=', None, + "require a specific python version" + + " on the target system"), + ('no-target-compile', 'c', + "do not compile .py to .pyc on the target system"), + ('no-target-optimize', 'o', + "do not compile .py to .pyo (optimized)" + "on the target system"), + ('dist-dir=', 'd', + "directory to put final built distributions in"), + ('bitmap=', 'b', + "bitmap to use for the installer instead of python-powered logo"), + ('title=', 't', + "title to display on the installer background instead of default"), + ('skip-build', None, + "skip rebuilding everything (for testing/debugging)"), + ('install-script=', None, + "basename of installation script to be run after" + "installation or before deinstallation"), + ('pre-install-script=', None, + "Fully qualified filename of a script to be run before " + "any files are installed. This script need not be in the " + "distribution"), + ('user-access-control=', None, + "specify Vista's UAC handling - 'none'/default=no " + "handling, 'auto'=use UAC if target Python installed for " + "all users, 'force'=always use UAC"), + ] + + boolean_options = ['keep-temp', 'no-target-compile', 'no-target-optimize', + 'skip-build'] + + def initialize_options(self): + self.bdist_dir = None + self.plat_name = None + self.keep_temp = False + self.no_target_compile = False + self.no_target_optimize = False + self.target_version = None + self.dist_dir = None + self.bitmap = None + self.title = None + self.skip_build = False + self.install_script = None + self.pre_install_script = None + self.user_access_control = None + + + def finalize_options(self): + if self.bdist_dir is None: + if self.skip_build and self.plat_name: + # If build is skipped and plat_name is overridden, bdist will + # not see the correct 'plat_name' - so set that up manually. + bdist = self.distribution.get_command_obj('bdist') + bdist.plat_name = self.plat_name + # next the command will be initialized using that name + bdist_base = self.get_finalized_command('bdist').bdist_base + self.bdist_dir = os.path.join(bdist_base, 'wininst') + if not self.target_version: + self.target_version = "" + if not self.skip_build and self.distribution.has_ext_modules(): + short_version = get_python_version() + if self.target_version and self.target_version != short_version: + raise PackagingOptionError("target version can only be %s, or the '--skip-build'" \ + " option must be specified" % (short_version,)) + self.target_version = short_version + + self.set_undefined_options('bdist', 'dist_dir', 'plat_name') + + if self.install_script: + for script in self.distribution.scripts: + if self.install_script == os.path.basename(script): + break + else: + raise PackagingOptionError("install_script '%s' not found in scripts" % \ + self.install_script) + + def run(self): + if (sys.platform != "win32" and + (self.distribution.has_ext_modules() or + self.distribution.has_c_libraries())): + raise PackagingPlatformError \ + ("distribution contains extensions and/or C libraries; " + "must be compiled on a Windows 32 platform") + + if not self.skip_build: + self.run_command('build') + + install = self.get_reinitialized_command('install', + reinit_subcommands=True) + install.root = self.bdist_dir + install.skip_build = self.skip_build + install.warn_dir = False + install.plat_name = self.plat_name + + install_lib = self.get_reinitialized_command('install_lib') + # we do not want to include pyc or pyo files + install_lib.compile = False + install_lib.optimize = 0 + + if self.distribution.has_ext_modules(): + # If we are building an installer for a Python version other + # than the one we are currently running, then we need to ensure + # our build_lib reflects the other Python version rather than ours. + # Note that for target_version!=sys.version, we must have skipped the + # build step, so there is no issue with enforcing the build of this + # version. + target_version = self.target_version + if not target_version: + assert self.skip_build, "Should have already checked this" + target_version = sys.version[0:3] + plat_specifier = ".%s-%s" % (self.plat_name, target_version) + build = self.get_finalized_command('build') + build.build_lib = os.path.join(build.build_base, + 'lib' + plat_specifier) + + # Use a custom scheme for the zip-file, because we have to decide + # at installation time which scheme to use. + for key in ('purelib', 'platlib', 'headers', 'scripts', 'data'): + value = key.upper() + if key == 'headers': + value = value + '/Include/$dist_name' + setattr(install, + 'install_' + key, + value) + + logger.info("installing to %s", self.bdist_dir) + install.ensure_finalized() + + # avoid warning of 'install_lib' about installing + # into a directory not in sys.path + sys.path.insert(0, os.path.join(self.bdist_dir, 'PURELIB')) + + install.run() + + del sys.path[0] + + # And make an archive relative to the root of the + # pseudo-installation tree. + from tempfile import NamedTemporaryFile + archive_basename = NamedTemporaryFile().name + fullname = self.distribution.get_fullname() + arcname = self.make_archive(archive_basename, "zip", + root_dir=self.bdist_dir) + # create an exe containing the zip-file + self.create_exe(arcname, fullname, self.bitmap) + if self.distribution.has_ext_modules(): + pyversion = get_python_version() + else: + pyversion = 'any' + self.distribution.dist_files.append(('bdist_wininst', pyversion, + self.get_installer_filename(fullname))) + # remove the zip-file again + logger.debug("removing temporary file '%s'", arcname) + os.remove(arcname) + + if not self.keep_temp: + if self.dry_run: + logger.info('removing %s', self.bdist_dir) + else: + rmtree(self.bdist_dir) + + def get_inidata(self): + # Return data describing the installation. + + lines = [] + metadata = self.distribution.metadata + + # Write the [metadata] section. + lines.append("[metadata]") + + # 'info' will be displayed in the installer's dialog box, + # describing the items to be installed. + info = (metadata.long_description or '') + '\n' + + # Escape newline characters + def escape(s): + return s.replace("\n", "\\n") + + for name in ["author", "author_email", "description", "maintainer", + "maintainer_email", "name", "url", "version"]: + data = getattr(metadata, name, "") + if data: + info = info + ("\n %s: %s" % \ + (name.capitalize(), escape(data))) + lines.append("%s=%s" % (name, escape(data))) + + # The [setup] section contains entries controlling + # the installer runtime. + lines.append("\n[Setup]") + if self.install_script: + lines.append("install_script=%s" % self.install_script) + lines.append("info=%s" % escape(info)) + lines.append("target_compile=%d" % (not self.no_target_compile)) + lines.append("target_optimize=%d" % (not self.no_target_optimize)) + if self.target_version: + lines.append("target_version=%s" % self.target_version) + if self.user_access_control: + lines.append("user_access_control=%s" % self.user_access_control) + + title = self.title or self.distribution.get_fullname() + lines.append("title=%s" % escape(title)) + import time + import packaging + build_info = "Built %s with packaging-%s" % \ + (time.ctime(time.time()), packaging.__version__) + lines.append("build_info=%s" % build_info) + return "\n".join(lines) + + def create_exe(self, arcname, fullname, bitmap=None): + import struct + + self.mkpath(self.dist_dir) + + cfgdata = self.get_inidata() + + installer_name = self.get_installer_filename(fullname) + logger.info("creating %s", installer_name) + + if bitmap: + with open(bitmap, "rb") as fp: + bitmapdata = fp.read() + bitmaplen = len(bitmapdata) + else: + bitmaplen = 0 + + with open(installer_name, "wb") as file: + file.write(self.get_exe_bytes()) + if bitmap: + file.write(bitmapdata) + + # Convert cfgdata from unicode to ascii, mbcs encoded + if isinstance(cfgdata, str): + cfgdata = cfgdata.encode("mbcs") + + # Append the pre-install script + cfgdata = cfgdata + "\0" + if self.pre_install_script: + with open(self.pre_install_script) as fp: + script_data = fp.read() + cfgdata = cfgdata + script_data + "\n\0" + else: + # empty pre-install script + cfgdata = cfgdata + "\0" + file.write(cfgdata) + + # The 'magic number' 0x1234567B is used to make sure that the + # binary layout of 'cfgdata' is what the wininst.exe binary + # expects. If the layout changes, increment that number, make + # the corresponding changes to the wininst.exe sources, and + # recompile them. + header = struct.pack(" cur_version: + bv = get_build_version() + else: + if self.target_version < "2.4": + bv = 6.0 + else: + bv = 7.1 + else: + # for current version - use authoritative check. + bv = get_build_version() + + # wininst-x.y.exe is in the same directory as this file + directory = os.path.dirname(__file__) + # we must use a wininst-x.y.exe built with the same C compiler + # used for python. XXX What about mingw, borland, and so on? + + # if plat_name starts with "win" but is not "win32" + # we want to strip "win" and leave the rest (e.g. -amd64) + # for all other cases, we don't want any suffix + if self.plat_name != 'win32' and self.plat_name[:3] == 'win': + sfix = self.plat_name[3:] + else: + sfix = '' + + filename = os.path.join(directory, "wininst-%.1f%s.exe" % (bv, sfix)) + with open(filename, "rb") as fp: + return fp.read() diff --git a/Lib/packaging/command/build.py b/Lib/packaging/command/build.py new file mode 100644 index 000000000000..6580fd128644 --- /dev/null +++ b/Lib/packaging/command/build.py @@ -0,0 +1,151 @@ +"""Main build command, which calls the other build_* commands.""" + +import sys +import os + +from packaging.util import get_platform +from packaging.command.cmd import Command +from packaging.errors import PackagingOptionError +from packaging.compiler import show_compilers + + +class build(Command): + + description = "build everything needed to install" + + user_options = [ + ('build-base=', 'b', + "base directory for build library"), + ('build-purelib=', None, + "build directory for platform-neutral distributions"), + ('build-platlib=', None, + "build directory for platform-specific distributions"), + ('build-lib=', None, + "build directory for all distribution (defaults to either " + + "build-purelib or build-platlib"), + ('build-scripts=', None, + "build directory for scripts"), + ('build-temp=', 't', + "temporary build directory"), + ('plat-name=', 'p', + "platform name to build for, if supported " + "(default: %s)" % get_platform()), + ('compiler=', 'c', + "specify the compiler type"), + ('debug', 'g', + "compile extensions and libraries with debugging information"), + ('force', 'f', + "forcibly build everything (ignore file timestamps)"), + ('executable=', 'e', + "specify final destination interpreter path (build.py)"), + ('use-2to3', None, + "use 2to3 to make source python 3.x compatible"), + ('convert-2to3-doctests', None, + "use 2to3 to convert doctests in seperate text files"), + ('use-2to3-fixers', None, + "list additional fixers opted for during 2to3 conversion"), + ] + + boolean_options = ['debug', 'force'] + + help_options = [ + ('help-compiler', None, + "list available compilers", show_compilers), + ] + + def initialize_options(self): + self.build_base = 'build' + # these are decided only after 'build_base' has its final value + # (unless overridden by the user or client) + self.build_purelib = None + self.build_platlib = None + self.build_lib = None + self.build_temp = None + self.build_scripts = None + self.compiler = None + self.plat_name = None + self.debug = None + self.force = False + self.executable = None + self.use_2to3 = False + self.convert_2to3_doctests = None + self.use_2to3_fixers = None + + def finalize_options(self): + if self.plat_name is None: + self.plat_name = get_platform() + else: + # plat-name only supported for windows (other platforms are + # supported via ./configure flags, if at all). Avoid misleading + # other platforms. + if os.name != 'nt': + raise PackagingOptionError( + "--plat-name only supported on Windows (try " + "using './configure --help' on your platform)") + + plat_specifier = ".%s-%s" % (self.plat_name, sys.version[0:3]) + + # Make it so Python 2.x and Python 2.x with --with-pydebug don't + # share the same build directories. Doing so confuses the build + # process for C modules + if hasattr(sys, 'gettotalrefcount'): + plat_specifier += '-pydebug' + + # 'build_purelib' and 'build_platlib' just default to 'lib' and + # 'lib.' under the base build directory. We only use one of + # them for a given distribution, though -- + if self.build_purelib is None: + self.build_purelib = os.path.join(self.build_base, 'lib') + if self.build_platlib is None: + self.build_platlib = os.path.join(self.build_base, + 'lib' + plat_specifier) + + # 'build_lib' is the actual directory that we will use for this + # particular module distribution -- if user didn't supply it, pick + # one of 'build_purelib' or 'build_platlib'. + if self.build_lib is None: + if self.distribution.ext_modules: + self.build_lib = self.build_platlib + else: + self.build_lib = self.build_purelib + + # 'build_temp' -- temporary directory for compiler turds, + # "build/temp." + if self.build_temp is None: + self.build_temp = os.path.join(self.build_base, + 'temp' + plat_specifier) + if self.build_scripts is None: + self.build_scripts = os.path.join(self.build_base, + 'scripts-' + sys.version[0:3]) + + if self.executable is None: + self.executable = os.path.normpath(sys.executable) + + def run(self): + # Run all relevant sub-commands. This will be some subset of: + # - build_py - pure Python modules + # - build_clib - standalone C libraries + # - build_ext - Python extension modules + # - build_scripts - Python scripts + for cmd_name in self.get_sub_commands(): + self.run_command(cmd_name) + + # -- Predicates for the sub-command list --------------------------- + + def has_pure_modules(self): + return self.distribution.has_pure_modules() + + def has_c_libraries(self): + return self.distribution.has_c_libraries() + + def has_ext_modules(self): + return self.distribution.has_ext_modules() + + def has_scripts(self): + return self.distribution.has_scripts() + + sub_commands = [('build_py', has_pure_modules), + ('build_clib', has_c_libraries), + ('build_ext', has_ext_modules), + ('build_scripts', has_scripts), + ] diff --git a/Lib/packaging/command/build_clib.py b/Lib/packaging/command/build_clib.py new file mode 100644 index 000000000000..4a249964c5a5 --- /dev/null +++ b/Lib/packaging/command/build_clib.py @@ -0,0 +1,198 @@ +"""Build C/C++ libraries. + +This command is useful to build libraries that are included in the +distribution and needed by extension modules. +""" + +# XXX this module has *lots* of code ripped-off quite transparently from +# build_ext.py -- not surprisingly really, as the work required to build +# a static library from a collection of C source files is not really all +# that different from what's required to build a shared object file from +# a collection of C source files. Nevertheless, I haven't done the +# necessary refactoring to account for the overlap in code between the +# two modules, mainly because a number of subtle details changed in the +# cut 'n paste. Sigh. + +import os +from packaging.command.cmd import Command +from packaging.errors import PackagingSetupError +from packaging.compiler import customize_compiler +from packaging import logger + + +def show_compilers(): + from packaging.compiler import show_compilers + show_compilers() + + +class build_clib(Command): + + description = "build C/C++ libraries used by extension modules" + + user_options = [ + ('build-clib=', 'b', + "directory to build C/C++ libraries to"), + ('build-temp=', 't', + "directory to put temporary build by-products"), + ('debug', 'g', + "compile with debugging information"), + ('force', 'f', + "forcibly build everything (ignore file timestamps)"), + ('compiler=', 'c', + "specify the compiler type"), + ] + + boolean_options = ['debug', 'force'] + + help_options = [ + ('help-compiler', None, + "list available compilers", show_compilers), + ] + + def initialize_options(self): + self.build_clib = None + self.build_temp = None + + # List of libraries to build + self.libraries = None + + # Compilation options for all libraries + self.include_dirs = None + self.define = None + self.undef = None + self.debug = None + self.force = False + self.compiler = None + + + def finalize_options(self): + # This might be confusing: both build-clib and build-temp default + # to build-temp as defined by the "build" command. This is because + # I think that C libraries are really just temporary build + # by-products, at least from the point of view of building Python + # extensions -- but I want to keep my options open. + self.set_undefined_options('build', + ('build_temp', 'build_clib'), + ('build_temp', 'build_temp'), + 'compiler', 'debug', 'force') + + self.libraries = self.distribution.libraries + if self.libraries: + self.check_library_list(self.libraries) + + if self.include_dirs is None: + self.include_dirs = self.distribution.include_dirs or [] + if isinstance(self.include_dirs, str): + self.include_dirs = self.include_dirs.split(os.pathsep) + + # XXX same as for build_ext -- what about 'self.define' and + # 'self.undef' ? + + def run(self): + if not self.libraries: + return + + # Yech -- this is cut 'n pasted from build_ext.py! + from packaging.compiler import new_compiler + self.compiler = new_compiler(compiler=self.compiler, + dry_run=self.dry_run, + force=self.force) + customize_compiler(self.compiler) + + if self.include_dirs is not None: + self.compiler.set_include_dirs(self.include_dirs) + if self.define is not None: + # 'define' option is a list of (name,value) tuples + for name, value in self.define: + self.compiler.define_macro(name, value) + if self.undef is not None: + for macro in self.undef: + self.compiler.undefine_macro(macro) + + self.build_libraries(self.libraries) + + + def check_library_list(self, libraries): + """Ensure that the list of libraries is valid. + + `library` is presumably provided as a command option 'libraries'. + This method checks that it is a list of 2-tuples, where the tuples + are (library_name, build_info_dict). + + Raise PackagingSetupError if the structure is invalid anywhere; + just returns otherwise. + """ + if not isinstance(libraries, list): + raise PackagingSetupError("'libraries' option must be a list of tuples") + + for lib in libraries: + if not isinstance(lib, tuple) and len(lib) != 2: + raise PackagingSetupError("each element of 'libraries' must a 2-tuple") + + name, build_info = lib + + if not isinstance(name, str): + raise PackagingSetupError("first element of each tuple in 'libraries' " + \ + "must be a string (the library name)") + if '/' in name or (os.sep != '/' and os.sep in name): + raise PackagingSetupError(("bad library name '%s': " + + "may not contain directory separators") % \ + lib[0]) + + if not isinstance(build_info, dict): + raise PackagingSetupError("second element of each tuple in 'libraries' " + \ + "must be a dictionary (build info)") + + def get_library_names(self): + # Assume the library list is valid -- 'check_library_list()' is + # called from 'finalize_options()', so it should be! + if not self.libraries: + return None + + lib_names = [] + for lib_name, build_info in self.libraries: + lib_names.append(lib_name) + return lib_names + + + def get_source_files(self): + self.check_library_list(self.libraries) + filenames = [] + for lib_name, build_info in self.libraries: + sources = build_info.get('sources') + if sources is None or not isinstance(sources, (list, tuple)): + raise PackagingSetupError(("in 'libraries' option (library '%s'), " + "'sources' must be present and must be " + "a list of source filenames") % lib_name) + + filenames.extend(sources) + return filenames + + def build_libraries(self, libraries): + for lib_name, build_info in libraries: + sources = build_info.get('sources') + if sources is None or not isinstance(sources, (list, tuple)): + raise PackagingSetupError(("in 'libraries' option (library '%s'), " + + "'sources' must be present and must be " + + "a list of source filenames") % lib_name) + sources = list(sources) + + logger.info("building '%s' library", lib_name) + + # First, compile the source code to object files in the library + # directory. (This should probably change to putting object + # files in a temporary build directory.) + macros = build_info.get('macros') + include_dirs = build_info.get('include_dirs') + objects = self.compiler.compile(sources, + output_dir=self.build_temp, + macros=macros, + include_dirs=include_dirs, + debug=self.debug) + + # Now "link" the object files together into a static library. + # (On Unix at least, this isn't really linking -- it just + # builds an archive. Whatever.) + self.compiler.create_static_lib(objects, lib_name, + output_dir=self.build_clib, + debug=self.debug) diff --git a/Lib/packaging/command/build_ext.py b/Lib/packaging/command/build_ext.py new file mode 100644 index 000000000000..9b710411ca59 --- /dev/null +++ b/Lib/packaging/command/build_ext.py @@ -0,0 +1,666 @@ +"""Build extension modules.""" + +# FIXME Is this module limited to C extensions or do C++ extensions work too? +# The docstring of this module said that C++ was not supported, but other +# comments contradict that. + +import os +import re +import sys +import logging +import sysconfig + +from packaging.util import get_platform +from packaging.command.cmd import Command +from packaging.errors import (CCompilerError, CompileError, PackagingError, + PackagingPlatformError, PackagingSetupError) +from packaging.compiler import customize_compiler, show_compilers +from packaging.util import newer_group +from packaging.compiler.extension import Extension +from packaging import logger + +import site +HAS_USER_SITE = True + +if os.name == 'nt': + from packaging.compiler.msvccompiler import get_build_version + MSVC_VERSION = int(get_build_version()) + +# An extension name is just a dot-separated list of Python NAMEs (ie. +# the same as a fully-qualified module name). +extension_name_re = re.compile \ + (r'^[a-zA-Z_][a-zA-Z_0-9]*(\.[a-zA-Z_][a-zA-Z_0-9]*)*$') + + +class build_ext(Command): + + description = "build C/C++ extension modules (compile/link to build directory)" + + # XXX thoughts on how to deal with complex command-line options like + # these, i.e. how to make it so fancy_getopt can suck them off the + # command line and make it look like setup.py defined the appropriate + # lists of tuples of what-have-you. + # - each command needs a callback to process its command-line options + # - Command.__init__() needs access to its share of the whole + # command line (must ultimately come from + # Distribution.parse_command_line()) + # - it then calls the current command class' option-parsing + # callback to deal with weird options like -D, which have to + # parse the option text and churn out some custom data + # structure + # - that data structure (in this case, a list of 2-tuples) + # will then be present in the command object by the time + # we get to finalize_options() (i.e. the constructor + # takes care of both command-line and client options + # in between initialize_options() and finalize_options()) + + sep_by = " (separated by '%s')" % os.pathsep + user_options = [ + ('build-lib=', 'b', + "directory for compiled extension modules"), + ('build-temp=', 't', + "directory for temporary files (build by-products)"), + ('plat-name=', 'p', + "platform name to cross-compile for, if supported " + "(default: %s)" % get_platform()), + ('inplace', 'i', + "ignore build-lib and put compiled extensions into the source " + + "directory alongside your pure Python modules"), + ('include-dirs=', 'I', + "list of directories to search for header files" + sep_by), + ('define=', 'D', + "C preprocessor macros to define"), + ('undef=', 'U', + "C preprocessor macros to undefine"), + ('libraries=', 'l', + "external C libraries to link with"), + ('library-dirs=', 'L', + "directories to search for external C libraries" + sep_by), + ('rpath=', 'R', + "directories to search for shared C libraries at runtime"), + ('link-objects=', 'O', + "extra explicit link objects to include in the link"), + ('debug', 'g', + "compile/link with debugging information"), + ('force', 'f', + "forcibly build everything (ignore file timestamps)"), + ('compiler=', 'c', + "specify the compiler type"), + ('swig-opts=', None, + "list of SWIG command-line options"), + ('swig=', None, + "path to the SWIG executable"), + ] + + boolean_options = ['inplace', 'debug', 'force'] + + if HAS_USER_SITE: + user_options.append(('user', None, + "add user include, library and rpath")) + boolean_options.append('user') + + help_options = [ + ('help-compiler', None, + "list available compilers", show_compilers), + ] + + def initialize_options(self): + self.extensions = None + self.build_lib = None + self.plat_name = None + self.build_temp = None + self.inplace = False + self.package = None + + self.include_dirs = None + self.define = None + self.undef = None + self.libraries = None + self.library_dirs = None + self.rpath = None + self.link_objects = None + self.debug = None + self.force = None + self.compiler = None + self.swig = None + self.swig_opts = None + if HAS_USER_SITE: + self.user = None + + def finalize_options(self): + self.set_undefined_options('build', + 'build_lib', 'build_temp', 'compiler', + 'debug', 'force', 'plat_name') + + if self.package is None: + self.package = self.distribution.ext_package + + # Ensure that the list of extensions is valid, i.e. it is a list of + # Extension objects. + self.extensions = self.distribution.ext_modules + if self.extensions: + if not isinstance(self.extensions, (list, tuple)): + type_name = (self.extensions is None and 'None' + or type(self.extensions).__name__) + raise PackagingSetupError( + "'ext_modules' must be a sequence of Extension instances," + " not %s" % (type_name,)) + for i, ext in enumerate(self.extensions): + if isinstance(ext, Extension): + continue # OK! (assume type-checking done + # by Extension constructor) + type_name = (ext is None and 'None' or type(ext).__name__) + raise PackagingSetupError( + "'ext_modules' item %d must be an Extension instance," + " not %s" % (i, type_name)) + + # Make sure Python's include directories (for Python.h, pyconfig.h, + # etc.) are in the include search path. + py_include = sysconfig.get_path('include') + plat_py_include = sysconfig.get_path('platinclude') + if self.include_dirs is None: + self.include_dirs = self.distribution.include_dirs or [] + if isinstance(self.include_dirs, str): + self.include_dirs = self.include_dirs.split(os.pathsep) + + # Put the Python "system" include dir at the end, so that + # any local include dirs take precedence. + self.include_dirs.append(py_include) + if plat_py_include != py_include: + self.include_dirs.append(plat_py_include) + + if isinstance(self.libraries, str): + self.libraries = [self.libraries] + + # Life is easier if we're not forever checking for None, so + # simplify these options to empty lists if unset + if self.libraries is None: + self.libraries = [] + if self.library_dirs is None: + self.library_dirs = [] + elif isinstance(self.library_dirs, str): + self.library_dirs = self.library_dirs.split(os.pathsep) + + if self.rpath is None: + self.rpath = [] + elif isinstance(self.rpath, str): + self.rpath = self.rpath.split(os.pathsep) + + # for extensions under windows use different directories + # for Release and Debug builds. + # also Python's library directory must be appended to library_dirs + if os.name == 'nt': + # the 'libs' directory is for binary installs - we assume that + # must be the *native* platform. But we don't really support + # cross-compiling via a binary install anyway, so we let it go. + self.library_dirs.append(os.path.join(sys.exec_prefix, 'libs')) + if self.debug: + self.build_temp = os.path.join(self.build_temp, "Debug") + else: + self.build_temp = os.path.join(self.build_temp, "Release") + + # Append the source distribution include and library directories, + # this allows distutils on windows to work in the source tree + self.include_dirs.append(os.path.join(sys.exec_prefix, 'PC')) + if MSVC_VERSION == 9: + # Use the .lib files for the correct architecture + if self.plat_name == 'win32': + suffix = '' + else: + # win-amd64 or win-ia64 + suffix = self.plat_name[4:] + new_lib = os.path.join(sys.exec_prefix, 'PCbuild') + if suffix: + new_lib = os.path.join(new_lib, suffix) + self.library_dirs.append(new_lib) + + elif MSVC_VERSION == 8: + self.library_dirs.append(os.path.join(sys.exec_prefix, + 'PC', 'VS8.0')) + elif MSVC_VERSION == 7: + self.library_dirs.append(os.path.join(sys.exec_prefix, + 'PC', 'VS7.1')) + else: + self.library_dirs.append(os.path.join(sys.exec_prefix, + 'PC', 'VC6')) + + # OS/2 (EMX) doesn't support Debug vs Release builds, but has the + # import libraries in its "Config" subdirectory + if os.name == 'os2': + self.library_dirs.append(os.path.join(sys.exec_prefix, 'Config')) + + # for extensions under Cygwin and AtheOS Python's library directory must be + # appended to library_dirs + if sys.platform[:6] == 'cygwin' or sys.platform[:6] == 'atheos': + if sys.executable.startswith(os.path.join(sys.exec_prefix, "bin")): + # building third party extensions + self.library_dirs.append(os.path.join(sys.prefix, "lib", + "python" + sysconfig.get_python_version(), + "config")) + else: + # building python standard extensions + self.library_dirs.append(os.curdir) + + # for extensions under Linux or Solaris with a shared Python library, + # Python's library directory must be appended to library_dirs + sysconfig.get_config_var('Py_ENABLE_SHARED') + if ((sys.platform.startswith('linux') or sys.platform.startswith('gnu') + or sys.platform.startswith('sunos')) + and sysconfig.get_config_var('Py_ENABLE_SHARED')): + if sys.executable.startswith(os.path.join(sys.exec_prefix, "bin")): + # building third party extensions + self.library_dirs.append(sysconfig.get_config_var('LIBDIR')) + else: + # building python standard extensions + self.library_dirs.append(os.curdir) + + # The argument parsing will result in self.define being a string, but + # it has to be a list of 2-tuples. All the preprocessor symbols + # specified by the 'define' option will be set to '1'. Multiple + # symbols can be separated with commas. + + if self.define: + defines = self.define.split(',') + self.define = [(symbol, '1') for symbol in defines] + + # The option for macros to undefine is also a string from the + # option parsing, but has to be a list. Multiple symbols can also + # be separated with commas here. + if self.undef: + self.undef = self.undef.split(',') + + if self.swig_opts is None: + self.swig_opts = [] + else: + self.swig_opts = self.swig_opts.split(' ') + + # Finally add the user include and library directories if requested + if HAS_USER_SITE and self.user: + user_include = os.path.join(site.USER_BASE, "include") + user_lib = os.path.join(site.USER_BASE, "lib") + if os.path.isdir(user_include): + self.include_dirs.append(user_include) + if os.path.isdir(user_lib): + self.library_dirs.append(user_lib) + self.rpath.append(user_lib) + + def run(self): + from packaging.compiler import new_compiler + + # 'self.extensions', as supplied by setup.py, is a list of + # Extension instances. See the documentation for Extension (in + # distutils.extension) for details. + if not self.extensions: + return + + # If we were asked to build any C/C++ libraries, make sure that the + # directory where we put them is in the library search path for + # linking extensions. + if self.distribution.has_c_libraries(): + build_clib = self.get_finalized_command('build_clib') + self.libraries.extend(build_clib.get_library_names() or []) + self.library_dirs.append(build_clib.build_clib) + + # Temporary kludge until we remove the verbose arguments and use + # logging everywhere + verbose = logger.getEffectiveLevel() >= logging.DEBUG + + # Setup the CCompiler object that we'll use to do all the + # compiling and linking + self.compiler_obj = new_compiler(compiler=self.compiler, + verbose=verbose, + dry_run=self.dry_run, + force=self.force) + + customize_compiler(self.compiler_obj) + # If we are cross-compiling, init the compiler now (if we are not + # cross-compiling, init would not hurt, but people may rely on + # late initialization of compiler even if they shouldn't...) + if os.name == 'nt' and self.plat_name != get_platform(): + self.compiler_obj.initialize(self.plat_name) + + # And make sure that any compile/link-related options (which might + # come from the command line or from the setup script) are set in + # that CCompiler object -- that way, they automatically apply to + # all compiling and linking done here. + if self.include_dirs is not None: + self.compiler_obj.set_include_dirs(self.include_dirs) + if self.define is not None: + # 'define' option is a list of (name,value) tuples + for name, value in self.define: + self.compiler_obj.define_macro(name, value) + if self.undef is not None: + for macro in self.undef: + self.compiler_obj.undefine_macro(macro) + if self.libraries is not None: + self.compiler_obj.set_libraries(self.libraries) + if self.library_dirs is not None: + self.compiler_obj.set_library_dirs(self.library_dirs) + if self.rpath is not None: + self.compiler_obj.set_runtime_library_dirs(self.rpath) + if self.link_objects is not None: + self.compiler_obj.set_link_objects(self.link_objects) + + # Now actually compile and link everything. + self.build_extensions() + + def get_source_files(self): + filenames = [] + + # Wouldn't it be neat if we knew the names of header files too... + for ext in self.extensions: + filenames.extend(ext.sources) + + return filenames + + def get_outputs(self): + # And build the list of output (built) filenames. Note that this + # ignores the 'inplace' flag, and assumes everything goes in the + # "build" tree. + outputs = [] + for ext in self.extensions: + outputs.append(self.get_ext_fullpath(ext.name)) + return outputs + + def build_extensions(self): + for ext in self.extensions: + try: + self.build_extension(ext) + except (CCompilerError, PackagingError, CompileError) as e: + if not ext.optional: + raise + logger.warning('%s: building extension %r failed: %s', + self.get_command_name(), ext.name, e) + + def build_extension(self, ext): + sources = ext.sources + if sources is None or not isinstance(sources, (list, tuple)): + raise PackagingSetupError(("in 'ext_modules' option (extension '%s'), " + + "'sources' must be present and must be " + + "a list of source filenames") % ext.name) + sources = list(sources) + + ext_path = self.get_ext_fullpath(ext.name) + depends = sources + ext.depends + if not (self.force or newer_group(depends, ext_path, 'newer')): + logger.debug("skipping '%s' extension (up-to-date)", ext.name) + return + else: + logger.info("building '%s' extension", ext.name) + + # First, scan the sources for SWIG definition files (.i), run + # SWIG on 'em to create .c files, and modify the sources list + # accordingly. + sources = self.swig_sources(sources, ext) + + # Next, compile the source code to object files. + + # XXX not honouring 'define_macros' or 'undef_macros' -- the + # CCompiler API needs to change to accommodate this, and I + # want to do one thing at a time! + + # Two possible sources for extra compiler arguments: + # - 'extra_compile_args' in Extension object + # - CFLAGS environment variable (not particularly + # elegant, but people seem to expect it and I + # guess it's useful) + # The environment variable should take precedence, and + # any sensible compiler will give precedence to later + # command-line args. Hence we combine them in order: + extra_args = ext.extra_compile_args or [] + + macros = ext.define_macros[:] + for undef in ext.undef_macros: + macros.append((undef,)) + + objects = self.compiler_obj.compile(sources, + output_dir=self.build_temp, + macros=macros, + include_dirs=ext.include_dirs, + debug=self.debug, + extra_postargs=extra_args, + depends=ext.depends) + + # XXX -- this is a Vile HACK! + # + # The setup.py script for Python on Unix needs to be able to + # get this list so it can perform all the clean up needed to + # avoid keeping object files around when cleaning out a failed + # build of an extension module. Since Packaging does not + # track dependencies, we have to get rid of intermediates to + # ensure all the intermediates will be properly re-built. + # + self._built_objects = objects[:] + + # Now link the object files together into a "shared object" -- + # of course, first we have to figure out all the other things + # that go into the mix. + if ext.extra_objects: + objects.extend(ext.extra_objects) + extra_args = ext.extra_link_args or [] + + # Detect target language, if not provided + language = ext.language or self.compiler_obj.detect_language(sources) + + self.compiler_obj.link_shared_object( + objects, ext_path, + libraries=self.get_libraries(ext), + library_dirs=ext.library_dirs, + runtime_library_dirs=ext.runtime_library_dirs, + extra_postargs=extra_args, + export_symbols=self.get_export_symbols(ext), + debug=self.debug, + build_temp=self.build_temp, + target_lang=language) + + + def swig_sources(self, sources, extension): + """Walk the list of source files in 'sources', looking for SWIG + interface (.i) files. Run SWIG on all that are found, and + return a modified 'sources' list with SWIG source files replaced + by the generated C (or C++) files. + """ + new_sources = [] + swig_sources = [] + swig_targets = {} + + # XXX this drops generated C/C++ files into the source tree, which + # is fine for developers who want to distribute the generated + # source -- but there should be an option to put SWIG output in + # the temp dir. + + if ('-c++' in self.swig_opts or '-c++' in extension.swig_opts): + target_ext = '.cpp' + else: + target_ext = '.c' + + for source in sources: + base, ext = os.path.splitext(source) + if ext == ".i": # SWIG interface file + new_sources.append(base + '_wrap' + target_ext) + swig_sources.append(source) + swig_targets[source] = new_sources[-1] + else: + new_sources.append(source) + + if not swig_sources: + return new_sources + + swig = self.swig or self.find_swig() + swig_cmd = [swig, "-python"] + swig_cmd.extend(self.swig_opts) + + # Do not override commandline arguments + if not self.swig_opts: + for o in extension.swig_opts: + swig_cmd.append(o) + + for source in swig_sources: + target = swig_targets[source] + logger.info("swigging %s to %s", source, target) + self.spawn(swig_cmd + ["-o", target, source]) + + return new_sources + + def find_swig(self): + """Return the name of the SWIG executable. On Unix, this is + just "swig" -- it should be in the PATH. Tries a bit harder on + Windows. + """ + + if os.name == "posix": + return "swig" + elif os.name == "nt": + + # Look for SWIG in its standard installation directory on + # Windows (or so I presume!). If we find it there, great; + # if not, act like Unix and assume it's in the PATH. + for vers in ("1.3", "1.2", "1.1"): + fn = os.path.join("c:\\swig%s" % vers, "swig.exe") + if os.path.isfile(fn): + return fn + else: + return "swig.exe" + + elif os.name == "os2": + # assume swig available in the PATH. + return "swig.exe" + + else: + raise PackagingPlatformError(("I don't know how to find (much less run) SWIG " + "on platform '%s'") % os.name) + + # -- Name generators ----------------------------------------------- + # (extension names, filenames, whatever) + def get_ext_fullpath(self, ext_name): + """Returns the path of the filename for a given extension. + + The file is located in `build_lib` or directly in the package + (inplace option). + """ + fullname = self.get_ext_fullname(ext_name) + modpath = fullname.split('.') + filename = self.get_ext_filename(modpath[-1]) + + if not self.inplace: + # no further work needed + # returning : + # build_dir/package/path/filename + filename = os.path.join(*modpath[:-1]+[filename]) + return os.path.join(self.build_lib, filename) + + # the inplace option requires to find the package directory + # using the build_py command for that + package = '.'.join(modpath[0:-1]) + build_py = self.get_finalized_command('build_py') + package_dir = os.path.abspath(build_py.get_package_dir(package)) + + # returning + # package_dir/filename + return os.path.join(package_dir, filename) + + def get_ext_fullname(self, ext_name): + """Returns the fullname of a given extension name. + + Adds the `package.` prefix""" + if self.package is None: + return ext_name + else: + return self.package + '.' + ext_name + + def get_ext_filename(self, ext_name): + r"""Convert the name of an extension (eg. "foo.bar") into the name + of the file from which it will be loaded (eg. "foo/bar.so", or + "foo\bar.pyd"). + """ + ext_path = ext_name.split('.') + # OS/2 has an 8 character module (extension) limit :-( + if os.name == "os2": + ext_path[len(ext_path) - 1] = ext_path[len(ext_path) - 1][:8] + # extensions in debug_mode are named 'module_d.pyd' under windows + so_ext = sysconfig.get_config_var('SO') + if os.name == 'nt' and self.debug: + return os.path.join(*ext_path) + '_d' + so_ext + return os.path.join(*ext_path) + so_ext + + def get_export_symbols(self, ext): + """Return the list of symbols that a shared extension has to + export. This either uses 'ext.export_symbols' or, if it's not + provided, "init" + module_name. Only relevant on Windows, where + the .pyd file (DLL) must export the module "init" function. + """ + initfunc_name = "init" + ext.name.split('.')[-1] + if initfunc_name not in ext.export_symbols: + ext.export_symbols.append(initfunc_name) + return ext.export_symbols + + def get_libraries(self, ext): + """Return the list of libraries to link against when building a + shared extension. On most platforms, this is just 'ext.libraries'; + on Windows and OS/2, we add the Python library (eg. python20.dll). + """ + # The python library is always needed on Windows. For MSVC, this + # is redundant, since the library is mentioned in a pragma in + # pyconfig.h that MSVC groks. The other Windows compilers all seem + # to need it mentioned explicitly, though, so that's what we do. + # Append '_d' to the python import library on debug builds. + if sys.platform == "win32": + from packaging.compiler.msvccompiler import MSVCCompiler + if not isinstance(self.compiler_obj, MSVCCompiler): + template = "python%d%d" + if self.debug: + template = template + '_d' + pythonlib = (template % + (sys.hexversion >> 24, (sys.hexversion >> 16) & 0xff)) + # don't extend ext.libraries, it may be shared with other + # extensions, it is a reference to the original list + return ext.libraries + [pythonlib] + else: + return ext.libraries + elif sys.platform == "os2emx": + # EMX/GCC requires the python library explicitly, and I + # believe VACPP does as well (though not confirmed) - AIM Apr01 + template = "python%d%d" + # debug versions of the main DLL aren't supported, at least + # not at this time - AIM Apr01 + #if self.debug: + # template = template + '_d' + pythonlib = (template % + (sys.hexversion >> 24, (sys.hexversion >> 16) & 0xff)) + # don't extend ext.libraries, it may be shared with other + # extensions, it is a reference to the original list + return ext.libraries + [pythonlib] + elif sys.platform[:6] == "cygwin": + template = "python%d.%d" + pythonlib = (template % + (sys.hexversion >> 24, (sys.hexversion >> 16) & 0xff)) + # don't extend ext.libraries, it may be shared with other + # extensions, it is a reference to the original list + return ext.libraries + [pythonlib] + elif sys.platform[:6] == "atheos": + template = "python%d.%d" + pythonlib = (template % + (sys.hexversion >> 24, (sys.hexversion >> 16) & 0xff)) + # Get SHLIBS from Makefile + extra = [] + for lib in sysconfig.get_config_var('SHLIBS').split(): + if lib.startswith('-l'): + extra.append(lib[2:]) + else: + extra.append(lib) + # don't extend ext.libraries, it may be shared with other + # extensions, it is a reference to the original list + return ext.libraries + [pythonlib, "m"] + extra + + elif sys.platform == 'darwin': + # Don't use the default code below + return ext.libraries + + else: + if sysconfig.get_config_var('Py_ENABLE_SHARED'): + template = "python%d.%d" + pythonlib = (template % + (sys.hexversion >> 24, (sys.hexversion >> 16) & 0xff)) + return ext.libraries + [pythonlib] + else: + return ext.libraries diff --git a/Lib/packaging/command/build_py.py b/Lib/packaging/command/build_py.py new file mode 100644 index 000000000000..360f4c96ad46 --- /dev/null +++ b/Lib/packaging/command/build_py.py @@ -0,0 +1,410 @@ +"""Build pure Python modules (just copy to build directory).""" + +import os +import sys +from glob import glob + +from packaging import logger +from packaging.command.cmd import Command +from packaging.errors import PackagingOptionError, PackagingFileError +from packaging.util import convert_path +from packaging.compat import Mixin2to3 + +# marking public APIs +__all__ = ['build_py'] + +class build_py(Command, Mixin2to3): + + description = "build pure Python modules (copy to build directory)" + + user_options = [ + ('build-lib=', 'd', "directory to build (copy) to"), + ('compile', 'c', "compile .py to .pyc"), + ('no-compile', None, "don't compile .py files [default]"), + ('optimize=', 'O', + "also compile with optimization: -O1 for \"python -O\", " + "-O2 for \"python -OO\", and -O0 to disable [default: -O0]"), + ('force', 'f', "forcibly build everything (ignore file timestamps)"), + ('use-2to3', None, + "use 2to3 to make source python 3.x compatible"), + ('convert-2to3-doctests', None, + "use 2to3 to convert doctests in seperate text files"), + ('use-2to3-fixers', None, + "list additional fixers opted for during 2to3 conversion"), + ] + + boolean_options = ['compile', 'force'] + negative_opt = {'no-compile' : 'compile'} + + def initialize_options(self): + self.build_lib = None + self.py_modules = None + self.package = None + self.package_data = None + self.package_dir = None + self.compile = False + self.optimize = 0 + self.force = None + self._updated_files = [] + self._doctests_2to3 = [] + self.use_2to3 = False + self.convert_2to3_doctests = None + self.use_2to3_fixers = None + + def finalize_options(self): + self.set_undefined_options('build', + 'use_2to3', 'use_2to3_fixers', + 'convert_2to3_doctests', 'build_lib', + 'force') + + # Get the distribution options that are aliases for build_py + # options -- list of packages and list of modules. + self.packages = self.distribution.packages + self.py_modules = self.distribution.py_modules + self.package_data = self.distribution.package_data + self.package_dir = None + if self.distribution.package_dir is not None: + self.package_dir = convert_path(self.distribution.package_dir) + self.data_files = self.get_data_files() + + # Ick, copied straight from install_lib.py (fancy_getopt needs a + # type system! Hell, *everything* needs a type system!!!) + if not isinstance(self.optimize, int): + try: + self.optimize = int(self.optimize) + assert 0 <= self.optimize <= 2 + except (ValueError, AssertionError): + raise PackagingOptionError("optimize must be 0, 1, or 2") + + def run(self): + # XXX copy_file by default preserves atime and mtime. IMHO this is + # the right thing to do, but perhaps it should be an option -- in + # particular, a site administrator might want installed files to + # reflect the time of installation rather than the last + # modification time before the installed release. + + # XXX copy_file by default preserves mode, which appears to be the + # wrong thing to do: if a file is read-only in the working + # directory, we want it to be installed read/write so that the next + # installation of the same module distribution can overwrite it + # without problems. (This might be a Unix-specific issue.) Thus + # we turn off 'preserve_mode' when copying to the build directory, + # since the build directory is supposed to be exactly what the + # installation will look like (ie. we preserve mode when + # installing). + + # Two options control which modules will be installed: 'packages' + # and 'py_modules'. The former lets us work with whole packages, not + # specifying individual modules at all; the latter is for + # specifying modules one-at-a-time. + + if self.py_modules: + self.build_modules() + if self.packages: + self.build_packages() + self.build_package_data() + + if self.use_2to3 and self._updated_files: + self.run_2to3(self._updated_files, self._doctests_2to3, + self.use_2to3_fixers) + + self.byte_compile(self.get_outputs(include_bytecode=False)) + + # -- Top-level worker functions ------------------------------------ + + def get_data_files(self): + """Generate list of '(package,src_dir,build_dir,filenames)' tuples. + + Helper function for `finalize_options()`. + """ + data = [] + if not self.packages: + return data + for package in self.packages: + # Locate package source directory + src_dir = self.get_package_dir(package) + + # Compute package build directory + build_dir = os.path.join(*([self.build_lib] + package.split('.'))) + + # Length of path to strip from found files + plen = 0 + if src_dir: + plen = len(src_dir)+1 + + # Strip directory from globbed filenames + filenames = [ + file[plen:] for file in self.find_data_files(package, src_dir) + ] + data.append((package, src_dir, build_dir, filenames)) + return data + + def find_data_files(self, package, src_dir): + """Return filenames for package's data files in 'src_dir'. + + Helper function for `get_data_files()`. + """ + globs = (self.package_data.get('', []) + + self.package_data.get(package, [])) + files = [] + for pattern in globs: + # Each pattern has to be converted to a platform-specific path + filelist = glob(os.path.join(src_dir, convert_path(pattern))) + # Files that match more than one pattern are only added once + files.extend(fn for fn in filelist if fn not in files) + return files + + def build_package_data(self): + """Copy data files into build directory. + + Helper function for `run()`. + """ + # FIXME add tests for this method + for package, src_dir, build_dir, filenames in self.data_files: + for filename in filenames: + target = os.path.join(build_dir, filename) + srcfile = os.path.join(src_dir, filename) + self.mkpath(os.path.dirname(target)) + outf, copied = self.copy_file(srcfile, + target, preserve_mode=False) + if copied and srcfile in self.distribution.convert_2to3.doctests: + self._doctests_2to3.append(outf) + + # XXX - this should be moved to the Distribution class as it is not + # only needed for build_py. It also has no dependencies on this class. + def get_package_dir(self, package): + """Return the directory, relative to the top of the source + distribution, where package 'package' should be found + (at least according to the 'package_dir' option, if any).""" + + path = package.split('.') + if self.package_dir is not None: + path.insert(0, self.package_dir) + + if len(path) > 0: + return os.path.join(*path) + + return '' + + def check_package(self, package, package_dir): + """Helper function for `find_package_modules()` and `find_modules()'. + """ + # Empty dir name means current directory, which we can probably + # assume exists. Also, os.path.exists and isdir don't know about + # my "empty string means current dir" convention, so we have to + # circumvent them. + if package_dir != "": + if not os.path.exists(package_dir): + raise PackagingFileError( + "package directory '%s' does not exist" % package_dir) + if not os.path.isdir(package_dir): + raise PackagingFileError( + "supposed package directory '%s' exists, " + "but is not a directory" % package_dir) + + # Require __init__.py for all but the "root package" + if package: + init_py = os.path.join(package_dir, "__init__.py") + if os.path.isfile(init_py): + return init_py + else: + logger.warning(("package init file '%s' not found " + + "(or not a regular file)"), init_py) + + # Either not in a package at all (__init__.py not expected), or + # __init__.py doesn't exist -- so don't return the filename. + return None + + def check_module(self, module, module_file): + if not os.path.isfile(module_file): + logger.warning("file %s (for module %s) not found", + module_file, module) + return False + else: + return True + + def find_package_modules(self, package, package_dir): + self.check_package(package, package_dir) + module_files = glob(os.path.join(package_dir, "*.py")) + modules = [] + if self.distribution.script_name is not None: + setup_script = os.path.abspath(self.distribution.script_name) + else: + setup_script = None + + for f in module_files: + abs_f = os.path.abspath(f) + if abs_f != setup_script: + module = os.path.splitext(os.path.basename(f))[0] + modules.append((package, module, f)) + else: + logger.debug("excluding %s", setup_script) + return modules + + def find_modules(self): + """Finds individually-specified Python modules, ie. those listed by + module name in 'self.py_modules'. Returns a list of tuples (package, + module_base, filename): 'package' is a tuple of the path through + package-space to the module; 'module_base' is the bare (no + packages, no dots) module name, and 'filename' is the path to the + ".py" file (relative to the distribution root) that implements the + module. + """ + # Map package names to tuples of useful info about the package: + # (package_dir, checked) + # package_dir - the directory where we'll find source files for + # this package + # checked - true if we have checked that the package directory + # is valid (exists, contains __init__.py, ... ?) + packages = {} + + # List of (package, module, filename) tuples to return + modules = [] + + # We treat modules-in-packages almost the same as toplevel modules, + # just the "package" for a toplevel is empty (either an empty + # string or empty list, depending on context). Differences: + # - don't check for __init__.py in directory for empty package + for module in self.py_modules: + path = module.split('.') + package = '.'.join(path[0:-1]) + module_base = path[-1] + + try: + package_dir, checked = packages[package] + except KeyError: + package_dir = self.get_package_dir(package) + checked = False + + if not checked: + init_py = self.check_package(package, package_dir) + packages[package] = (package_dir, 1) + if init_py: + modules.append((package, "__init__", init_py)) + + # XXX perhaps we should also check for just .pyc files + # (so greedy closed-source bastards can distribute Python + # modules too) + module_file = os.path.join(package_dir, module_base + ".py") + if not self.check_module(module, module_file): + continue + + modules.append((package, module_base, module_file)) + + return modules + + def find_all_modules(self): + """Compute the list of all modules that will be built, whether + they are specified one-module-at-a-time ('self.py_modules') or + by whole packages ('self.packages'). Return a list of tuples + (package, module, module_file), just like 'find_modules()' and + 'find_package_modules()' do.""" + modules = [] + if self.py_modules: + modules.extend(self.find_modules()) + if self.packages: + for package in self.packages: + package_dir = self.get_package_dir(package) + m = self.find_package_modules(package, package_dir) + modules.extend(m) + return modules + + def get_source_files(self): + sources = [module[-1] for module in self.find_all_modules()] + sources += [ + os.path.join(src_dir, filename) + for package, src_dir, build_dir, filenames in self.data_files + for filename in filenames] + return sources + + def get_module_outfile(self, build_dir, package, module): + outfile_path = [build_dir] + list(package) + [module + ".py"] + return os.path.join(*outfile_path) + + def get_outputs(self, include_bytecode=True): + modules = self.find_all_modules() + outputs = [] + for package, module, module_file in modules: + package = package.split('.') + filename = self.get_module_outfile(self.build_lib, package, module) + outputs.append(filename) + if include_bytecode: + if self.compile: + outputs.append(filename + "c") + if self.optimize > 0: + outputs.append(filename + "o") + + outputs += [ + os.path.join(build_dir, filename) + for package, src_dir, build_dir, filenames in self.data_files + for filename in filenames] + + return outputs + + def build_module(self, module, module_file, package): + if isinstance(package, str): + package = package.split('.') + elif not isinstance(package, (list, tuple)): + raise TypeError( + "'package' must be a string (dot-separated), list, or tuple") + + # Now put the module source file into the "build" area -- this is + # easy, we just copy it somewhere under self.build_lib (the build + # directory for Python source). + outfile = self.get_module_outfile(self.build_lib, package, module) + dir = os.path.dirname(outfile) + self.mkpath(dir) + return self.copy_file(module_file, outfile, preserve_mode=False) + + def build_modules(self): + modules = self.find_modules() + for package, module, module_file in modules: + + # Now "build" the module -- ie. copy the source file to + # self.build_lib (the build directory for Python source). + # (Actually, it gets copied to the directory for this package + # under self.build_lib.) + self.build_module(module, module_file, package) + + def build_packages(self): + for package in self.packages: + + # Get list of (package, module, module_file) tuples based on + # scanning the package directory. 'package' is only included + # in the tuple so that 'find_modules()' and + # 'find_package_tuples()' have a consistent interface; it's + # ignored here (apart from a sanity check). Also, 'module' is + # the *unqualified* module name (ie. no dots, no package -- we + # already know its package!), and 'module_file' is the path to + # the .py file, relative to the current directory + # (ie. including 'package_dir'). + package_dir = self.get_package_dir(package) + modules = self.find_package_modules(package, package_dir) + + # Now loop over the modules we found, "building" each one (just + # copy it to self.build_lib). + for package_, module, module_file in modules: + assert package == package_ + self.build_module(module, module_file, package) + + def byte_compile(self, files): + if hasattr(sys, 'dont_write_bytecode') and sys.dont_write_bytecode: + logger.warning('%s: byte-compiling is disabled, skipping.', + self.get_command_name()) + return + + from packaging.util import byte_compile + prefix = self.build_lib + if prefix[-1] != os.sep: + prefix = prefix + os.sep + + # XXX this code is essentially the same as the 'byte_compile() + # method of the "install_lib" command, except for the determination + # of the 'prefix' string. Hmmm. + + if self.compile: + byte_compile(files, optimize=0, + force=self.force, prefix=prefix, dry_run=self.dry_run) + if self.optimize > 0: + byte_compile(files, optimize=self.optimize, + force=self.force, prefix=prefix, dry_run=self.dry_run) diff --git a/Lib/packaging/command/build_scripts.py b/Lib/packaging/command/build_scripts.py new file mode 100644 index 000000000000..7fba0e51cdee --- /dev/null +++ b/Lib/packaging/command/build_scripts.py @@ -0,0 +1,132 @@ +"""Build scripts (copy to build dir and fix up shebang line).""" + +import os +import re +import sysconfig + +from packaging.command.cmd import Command +from packaging.util import convert_path, newer +from packaging import logger +from packaging.compat import Mixin2to3 + + +# check if Python is called on the first line with this expression +first_line_re = re.compile('^#!.*python[0-9.]*([ \t].*)?$') + +class build_scripts(Command, Mixin2to3): + + description = "build scripts (copy and fix up shebang line)" + + user_options = [ + ('build-dir=', 'd', "directory to build (copy) to"), + ('force', 'f', "forcibly build everything (ignore file timestamps"), + ('executable=', 'e', "specify final destination interpreter path"), + ] + + boolean_options = ['force'] + + + def initialize_options(self): + self.build_dir = None + self.scripts = None + self.force = None + self.executable = None + self.outfiles = None + self.use_2to3 = False + self.convert_2to3_doctests = None + self.use_2to3_fixers = None + + def finalize_options(self): + self.set_undefined_options('build', + ('build_scripts', 'build_dir'), + 'use_2to3', 'use_2to3_fixers', + 'convert_2to3_doctests', 'force', + 'executable') + self.scripts = self.distribution.scripts + + def get_source_files(self): + return self.scripts + + def run(self): + if not self.scripts: + return + copied_files = self.copy_scripts() + if self.use_2to3 and copied_files: + self._run_2to3(copied_files, fixers=self.use_2to3_fixers) + + def copy_scripts(self): + """Copy each script listed in 'self.scripts'; if it's marked as a + Python script in the Unix way (first line matches 'first_line_re', + ie. starts with "\#!" and contains "python"), then adjust the first + line to refer to the current Python interpreter as we copy. + """ + self.mkpath(self.build_dir) + outfiles = [] + for script in self.scripts: + adjust = False + script = convert_path(script) + outfile = os.path.join(self.build_dir, os.path.basename(script)) + outfiles.append(outfile) + + if not self.force and not newer(script, outfile): + logger.debug("not copying %s (up-to-date)", script) + continue + + # Always open the file, but ignore failures in dry-run mode -- + # that way, we'll get accurate feedback if we can read the + # script. + try: + f = open(script, "r") + except IOError: + if not self.dry_run: + raise + f = None + else: + first_line = f.readline() + if not first_line: + logger.warning('%s: %s is an empty file (skipping)', + self.get_command_name(), script) + continue + + match = first_line_re.match(first_line) + if match: + adjust = True + post_interp = match.group(1) or '' + + if adjust: + logger.info("copying and adjusting %s -> %s", script, + self.build_dir) + if not self.dry_run: + outf = open(outfile, "w") + if not sysconfig.is_python_build(): + outf.write("#!%s%s\n" % + (self.executable, + post_interp)) + else: + outf.write("#!%s%s\n" % + (os.path.join( + sysconfig.get_config_var("BINDIR"), + "python%s%s" % (sysconfig.get_config_var("VERSION"), + sysconfig.get_config_var("EXE"))), + post_interp)) + outf.writelines(f.readlines()) + outf.close() + if f: + f.close() + else: + if f: + f.close() + self.copy_file(script, outfile) + + if os.name == 'posix': + for file in outfiles: + if self.dry_run: + logger.info("changing mode of %s", file) + else: + oldmode = os.stat(file).st_mode & 0o7777 + newmode = (oldmode | 0o555) & 0o7777 + if newmode != oldmode: + logger.info("changing mode of %s from %o to %o", + file, oldmode, newmode) + os.chmod(file, newmode) + return outfiles diff --git a/Lib/packaging/command/check.py b/Lib/packaging/command/check.py new file mode 100644 index 000000000000..94c4a97c15cb --- /dev/null +++ b/Lib/packaging/command/check.py @@ -0,0 +1,88 @@ +"""Check PEP compliance of metadata.""" + +from packaging import logger +from packaging.command.cmd import Command +from packaging.errors import PackagingSetupError +from packaging.util import resolve_name + +class check(Command): + + description = "check PEP compliance of metadata" + + user_options = [('metadata', 'm', 'Verify metadata'), + ('all', 'a', + ('runs extended set of checks')), + ('strict', 's', + 'Will exit with an error if a check fails')] + + boolean_options = ['metadata', 'all', 'strict'] + + def initialize_options(self): + """Sets default values for options.""" + self.all = False + self.metadata = True + self.strict = False + self._warnings = [] + + def finalize_options(self): + pass + + def warn(self, msg, *args): + """Wrapper around logging that also remembers messages.""" + # XXX we could use a special handler for this, but would need to test + # if it works even if the logger has a too high level + self._warnings.append((msg, args)) + return logger.warning(self.get_command_name() + msg, *args) + + def run(self): + """Runs the command.""" + # perform the various tests + if self.metadata: + self.check_metadata() + if self.all: + self.check_restructuredtext() + self.check_hooks_resolvable() + + # let's raise an error in strict mode, if we have at least + # one warning + if self.strict and len(self._warnings) > 0: + msg = '\n'.join(msg % args for msg, args in self._warnings) + raise PackagingSetupError(msg) + + def check_metadata(self): + """Ensures that all required elements of metadata are supplied. + + name, version, URL, author + + Warns if any are missing. + """ + missing, warnings = self.distribution.metadata.check(strict=True) + if missing != []: + self.warn('missing required metadata: %s', ', '.join(missing)) + for warning in warnings: + self.warn(warning) + + def check_restructuredtext(self): + """Checks if the long string fields are reST-compliant.""" + missing, warnings = self.distribution.metadata.check(restructuredtext=True) + if self.distribution.metadata.docutils_support: + for warning in warnings: + line = warning[-1].get('line') + if line is None: + warning = warning[1] + else: + warning = '%s (line %s)' % (warning[1], line) + self.warn(warning) + elif self.strict: + raise PackagingSetupError('The docutils package is needed.') + + def check_hooks_resolvable(self): + for options in self.distribution.command_options.values(): + for hook_kind in ("pre_hook", "post_hook"): + if hook_kind not in options: + break + for hook_name in options[hook_kind][1].values(): + try: + resolve_name(hook_name) + except ImportError: + self.warn('name %r cannot be resolved', hook_name) diff --git a/Lib/packaging/command/clean.py b/Lib/packaging/command/clean.py new file mode 100644 index 000000000000..4f60f4ea9359 --- /dev/null +++ b/Lib/packaging/command/clean.py @@ -0,0 +1,76 @@ +"""Clean up temporary files created by the build command.""" + +# Contributed by Bastian Kleineidam + +import os +from shutil import rmtree +from packaging.command.cmd import Command +from packaging import logger + +class clean(Command): + + description = "clean up temporary files from 'build' command" + user_options = [ + ('build-base=', 'b', + "base build directory (default: 'build.build-base')"), + ('build-lib=', None, + "build directory for all modules (default: 'build.build-lib')"), + ('build-temp=', 't', + "temporary build directory (default: 'build.build-temp')"), + ('build-scripts=', None, + "build directory for scripts (default: 'build.build-scripts')"), + ('bdist-base=', None, + "temporary directory for built distributions"), + ('all', 'a', + "remove all build output, not just temporary by-products") + ] + + boolean_options = ['all'] + + def initialize_options(self): + self.build_base = None + self.build_lib = None + self.build_temp = None + self.build_scripts = None + self.bdist_base = None + self.all = None + + def finalize_options(self): + self.set_undefined_options('build', 'build_base', 'build_lib', + 'build_scripts', 'build_temp') + self.set_undefined_options('bdist', 'bdist_base') + + def run(self): + # remove the build/temp. directory (unless it's already + # gone) + if os.path.exists(self.build_temp): + if self.dry_run: + logger.info('removing %s', self.build_temp) + else: + rmtree(self.build_temp) + else: + logger.debug("'%s' does not exist -- can't clean it", + self.build_temp) + + if self.all: + # remove build directories + for directory in (self.build_lib, + self.bdist_base, + self.build_scripts): + if os.path.exists(directory): + if self.dry_run: + logger.info('removing %s', directory) + else: + rmtree(directory) + else: + logger.warning("'%s' does not exist -- can't clean it", + directory) + + # just for the heck of it, try to remove the base build directory: + # we might have emptied it right now, but if not we don't care + if not self.dry_run: + try: + os.rmdir(self.build_base) + logger.info("removing '%s'", self.build_base) + except OSError: + pass diff --git a/Lib/packaging/command/cmd.py b/Lib/packaging/command/cmd.py new file mode 100644 index 000000000000..fa56aa63f645 --- /dev/null +++ b/Lib/packaging/command/cmd.py @@ -0,0 +1,440 @@ +"""Base class for commands.""" + +import os +import re +from shutil import copyfile, move, make_archive +from packaging import util +from packaging import logger +from packaging.errors import PackagingOptionError + + +class Command: + """Abstract base class for defining command classes, the "worker bees" + of the Packaging. A useful analogy for command classes is to think of + them as subroutines with local variables called "options". The options + are "declared" in 'initialize_options()' and "defined" (given their + final values, aka "finalized") in 'finalize_options()', both of which + must be defined by every command class. The distinction between the + two is necessary because option values might come from the outside + world (command line, config file, ...), and any options dependent on + other options must be computed *after* these outside influences have + been processed -- hence 'finalize_options()'. The "body" of the + subroutine, where it does all its work based on the values of its + options, is the 'run()' method, which must also be implemented by every + command class. + """ + + # 'sub_commands' formalizes the notion of a "family" of commands, + # eg. "install_dist" as the parent with sub-commands "install_lib", + # "install_headers", etc. The parent of a family of commands + # defines 'sub_commands' as a class attribute; it's a list of + # (command_name : string, predicate : unbound_method | string | None) + # tuples, where 'predicate' is a method of the parent command that + # determines whether the corresponding command is applicable in the + # current situation. (Eg. we "install_headers" is only applicable if + # we have any C header files to install.) If 'predicate' is None, + # that command is always applicable. + # + # 'sub_commands' is usually defined at the *end* of a class, because + # predicates can be unbound methods, so they must already have been + # defined. The canonical example is the "install_dist" command. + sub_commands = [] + + # Pre and post command hooks are run just before or just after the command + # itself. They are simple functions that receive the command instance. They + # are specified as callable objects or dotted strings (for lazy loading). + pre_hook = None + post_hook = None + + # -- Creation/initialization methods ------------------------------- + + def __init__(self, dist): + """Create and initialize a new Command object. Most importantly, + invokes the 'initialize_options()' method, which is the real + initializer and depends on the actual command being instantiated. + """ + # late import because of mutual dependence between these classes + from packaging.dist import Distribution + + if not isinstance(dist, Distribution): + raise TypeError("dist must be a Distribution instance") + if self.__class__ is Command: + raise RuntimeError("Command is an abstract class") + + self.distribution = dist + self.initialize_options() + + # Per-command versions of the global flags, so that the user can + # customize Packaging' behaviour command-by-command and let some + # commands fall back on the Distribution's behaviour. None means + # "not defined, check self.distribution's copy", while 0 or 1 mean + # false and true (duh). Note that this means figuring out the real + # value of each flag is a touch complicated -- hence "self._dry_run" + # will be handled by a property, below. + # XXX This needs to be fixed. [I changed it to a property--does that + # "fix" it?] + self._dry_run = None + + # Some commands define a 'self.force' option to ignore file + # timestamps, but methods defined *here* assume that + # 'self.force' exists for all commands. So define it here + # just to be safe. + self.force = None + + # The 'help' flag is just used for command line parsing, so + # none of that complicated bureaucracy is needed. + self.help = False + + # 'finalized' records whether or not 'finalize_options()' has been + # called. 'finalize_options()' itself should not pay attention to + # this flag: it is the business of 'ensure_finalized()', which + # always calls 'finalize_options()', to respect/update it. + self.finalized = False + + # XXX A more explicit way to customize dry_run would be better. + @property + def dry_run(self): + if self._dry_run is None: + return getattr(self.distribution, 'dry_run') + else: + return self._dry_run + + def ensure_finalized(self): + if not self.finalized: + self.finalize_options() + self.finalized = True + + # Subclasses must define: + # initialize_options() + # provide default values for all options; may be customized by + # setup script, by options from config file(s), or by command-line + # options + # finalize_options() + # decide on the final values for all options; this is called + # after all possible intervention from the outside world + # (command line, option file, etc.) has been processed + # run() + # run the command: do whatever it is we're here to do, + # controlled by the command's various option values + + def initialize_options(self): + """Set default values for all the options that this command + supports. Note that these defaults may be overridden by other + commands, by the setup script, by config files, or by the + command line. Thus, this is not the place to code dependencies + between options; generally, 'initialize_options()' implementations + are just a bunch of "self.foo = None" assignments. + + This method must be implemented by all command classes. + """ + raise RuntimeError( + "abstract method -- subclass %s must override" % self.__class__) + + def finalize_options(self): + """Set final values for all the options that this command supports. + This is always called as late as possible, ie. after any option + assignments from the command line or from other commands have been + done. Thus, this is the place to code option dependencies: if + 'foo' depends on 'bar', then it is safe to set 'foo' from 'bar' as + long as 'foo' still has the same value it was assigned in + 'initialize_options()'. + + This method must be implemented by all command classes. + """ + raise RuntimeError( + "abstract method -- subclass %s must override" % self.__class__) + + def dump_options(self, header=None, indent=""): + if header is None: + header = "command options for '%s':" % self.get_command_name() + logger.info(indent + header) + indent = indent + " " + negative_opt = getattr(self, 'negative_opt', ()) + for option, _, _ in self.user_options: + if option in negative_opt: + continue + option = option.replace('-', '_') + if option[-1] == "=": + option = option[:-1] + value = getattr(self, option) + logger.info(indent + "%s = %s", option, value) + + def run(self): + """A command's raison d'etre: carry out the action it exists to + perform, controlled by the options initialized in + 'initialize_options()', customized by other commands, the setup + script, the command line and config files, and finalized in + 'finalize_options()'. All terminal output and filesystem + interaction should be done by 'run()'. + + This method must be implemented by all command classes. + """ + raise RuntimeError( + "abstract method -- subclass %s must override" % self.__class__) + + # -- External interface -------------------------------------------- + # (called by outsiders) + + def get_source_files(self): + """Return the list of files that are used as inputs to this command, + i.e. the files used to generate the output files. The result is used + by the `sdist` command in determining the set of default files. + + Command classes should implement this method if they operate on files + from the source tree. + """ + return [] + + def get_outputs(self): + """Return the list of files that would be produced if this command + were actually run. Not affected by the "dry-run" flag or whether + any other commands have been run. + + Command classes should implement this method if they produce any + output files that get consumed by another command. e.g., `build_ext` + returns the list of built extension modules, but not any temporary + files used in the compilation process. + """ + return [] + + # -- Option validation methods ------------------------------------- + # (these are very handy in writing the 'finalize_options()' method) + # + # NB. the general philosophy here is to ensure that a particular option + # value meets certain type and value constraints. If not, we try to + # force it into conformance (eg. if we expect a list but have a string, + # split the string on comma and/or whitespace). If we can't force the + # option into conformance, raise PackagingOptionError. Thus, command + # classes need do nothing more than (eg.) + # self.ensure_string_list('foo') + # and they can be guaranteed that thereafter, self.foo will be + # a list of strings. + + def _ensure_stringlike(self, option, what, default=None): + val = getattr(self, option) + if val is None: + setattr(self, option, default) + return default + elif not isinstance(val, str): + raise PackagingOptionError("'%s' must be a %s (got `%s`)" % + (option, what, val)) + return val + + def ensure_string(self, option, default=None): + """Ensure that 'option' is a string; if not defined, set it to + 'default'. + """ + self._ensure_stringlike(option, "string", default) + + def ensure_string_list(self, option): + r"""Ensure that 'option' is a list of strings. If 'option' is + currently a string, we split it either on /,\s*/ or /\s+/, so + "foo bar baz", "foo,bar,baz", and "foo, bar baz" all become + ["foo", "bar", "baz"]. + """ + val = getattr(self, option) + if val is None: + return + elif isinstance(val, str): + setattr(self, option, re.split(r',\s*|\s+', val)) + else: + if isinstance(val, list): + # checks if all elements are str + ok = True + for element in val: + if not isinstance(element, str): + ok = False + break + else: + ok = False + + if not ok: + raise PackagingOptionError( + "'%s' must be a list of strings (got %r)" % (option, val)) + + def _ensure_tested_string(self, option, tester, + what, error_fmt, default=None): + val = self._ensure_stringlike(option, what, default) + if val is not None and not tester(val): + raise PackagingOptionError( + ("error in '%s' option: " + error_fmt) % (option, val)) + + def ensure_filename(self, option): + """Ensure that 'option' is the name of an existing file.""" + self._ensure_tested_string(option, os.path.isfile, + "filename", + "'%s' does not exist or is not a file") + + def ensure_dirname(self, option): + self._ensure_tested_string(option, os.path.isdir, + "directory name", + "'%s' does not exist or is not a directory") + + # -- Convenience methods for commands ------------------------------ + + @classmethod + def get_command_name(cls): + if hasattr(cls, 'command_name'): + return cls.command_name + else: + return cls.__name__ + + def set_undefined_options(self, src_cmd, *options): + """Set values of undefined options from another command. + + Undefined options are options set to None, which is the convention + used to indicate that an option has not been changed between + 'initialize_options()' and 'finalize_options()'. This method is + usually called from 'finalize_options()' for options that depend on + some other command rather than another option of the same command, + typically subcommands. + + The 'src_cmd' argument is the other command from which option values + will be taken (a command object will be created for it if necessary); + the remaining positional arguments are strings that give the name of + the option to set. If the name is different on the source and target + command, you can pass a tuple with '(name_on_source, name_on_dest)' so + that 'self.name_on_dest' will be set from 'src_cmd.name_on_source'. + """ + src_cmd_obj = self.distribution.get_command_obj(src_cmd) + src_cmd_obj.ensure_finalized() + for obj in options: + if isinstance(obj, tuple): + src_option, dst_option = obj + else: + src_option, dst_option = obj, obj + if getattr(self, dst_option) is None: + setattr(self, dst_option, + getattr(src_cmd_obj, src_option)) + + def get_finalized_command(self, command, create=True): + """Wrapper around Distribution's 'get_command_obj()' method: find + (create if necessary and 'create' is true) the command object for + 'command', call its 'ensure_finalized()' method, and return the + finalized command object. + """ + cmd_obj = self.distribution.get_command_obj(command, create) + cmd_obj.ensure_finalized() + return cmd_obj + + def get_reinitialized_command(self, command, reinit_subcommands=False): + return self.distribution.get_reinitialized_command( + command, reinit_subcommands) + + def run_command(self, command): + """Run some other command: uses the 'run_command()' method of + Distribution, which creates and finalizes the command object if + necessary and then invokes its 'run()' method. + """ + self.distribution.run_command(command) + + def get_sub_commands(self): + """Determine the sub-commands that are relevant in the current + distribution (ie., that need to be run). This is based on the + 'sub_commands' class attribute: each tuple in that list may include + a method that we call to determine if the subcommand needs to be + run for the current distribution. Return a list of command names. + """ + commands = [] + for sub_command in self.sub_commands: + if len(sub_command) == 2: + cmd_name, method = sub_command + if method is None or method(self): + commands.append(cmd_name) + else: + commands.append(sub_command) + return commands + + # -- External world manipulation ----------------------------------- + + def execute(self, func, args, msg=None, level=1): + util.execute(func, args, msg, dry_run=self.dry_run) + + def mkpath(self, name, mode=0o777, dry_run=None, verbose=0): + if dry_run is None: + dry_run = self.dry_run + name = os.path.normpath(name) + if os.path.isdir(name) or name == '': + return + if dry_run: + head = '' + for part in name.split(os.sep): + logger.info("created directory %s%s", head, part) + head += part + os.sep + return + os.makedirs(name, mode) + + def copy_file(self, infile, outfile, + preserve_mode=True, preserve_times=True, link=None, level=1): + """Copy a file respecting verbose, dry-run and force flags. (The + former two default to whatever is in the Distribution object, and + the latter defaults to false for commands that don't define it.)""" + if self.dry_run: + # XXX add a comment + return + if os.path.isdir(outfile): + outfile = os.path.join(outfile, os.path.split(infile)[-1]) + copyfile(infile, outfile) + return outfile, None # XXX + + def copy_tree(self, infile, outfile, preserve_mode=True, + preserve_times=True, preserve_symlinks=False, level=1): + """Copy an entire directory tree respecting verbose, dry-run, + and force flags. + """ + if self.dry_run: + return # see if we want to display something + + + return util.copy_tree(infile, outfile, preserve_mode, preserve_times, + preserve_symlinks, not self.force, dry_run=self.dry_run) + + def move_file(self, src, dst, level=1): + """Move a file respecting the dry-run flag.""" + if self.dry_run: + return # XXX log ? + return move(src, dst) + + def spawn(self, cmd, search_path=True, level=1): + """Spawn an external command respecting dry-run flag.""" + from packaging.util import spawn + spawn(cmd, search_path, dry_run=self.dry_run) + + def make_archive(self, base_name, format, root_dir=None, base_dir=None, + owner=None, group=None): + return make_archive(base_name, format, root_dir, + base_dir, dry_run=self.dry_run, + owner=owner, group=group) + + def make_file(self, infiles, outfile, func, args, + exec_msg=None, skip_msg=None, level=1): + """Special case of 'execute()' for operations that process one or + more input files and generate one output file. Works just like + 'execute()', except the operation is skipped and a different + message printed if 'outfile' already exists and is newer than all + files listed in 'infiles'. If the command defined 'self.force', + and it is true, then the command is unconditionally run -- does no + timestamp checks. + """ + if skip_msg is None: + skip_msg = "skipping %s (inputs unchanged)" % outfile + + # Allow 'infiles' to be a single string + if isinstance(infiles, str): + infiles = (infiles,) + elif not isinstance(infiles, (list, tuple)): + raise TypeError( + "'infiles' must be a string, or a list or tuple of strings") + + if exec_msg is None: + exec_msg = "generating %s from %s" % (outfile, ', '.join(infiles)) + + # If 'outfile' must be regenerated (either because it doesn't + # exist, is out-of-date, or the 'force' flag is true) then + # perform the action that presumably regenerates it + if self.force or util.newer_group(infiles, outfile): + self.execute(func, args, exec_msg, level) + + # Otherwise, print the "skip" message + else: + logger.debug(skip_msg) diff --git a/Lib/packaging/command/command_template b/Lib/packaging/command/command_template new file mode 100644 index 000000000000..a12d32bfb33c --- /dev/null +++ b/Lib/packaging/command/command_template @@ -0,0 +1,35 @@ +"""Do X and Y.""" + +from packaging import logger +from packaging.command.cmd import Command + + +class x(Command): + + # Brief (40-50 characters) description of the command + description = "" + + # List of option tuples: long name, short name (None if no short + # name), and help string. + user_options = [ + ('', '', # long option, short option (one letter) or None + ""), # help text + ] + + def initialize_options(self): + self. = None + self. = None + self. = None + + def finalize_options(self): + if self.x is None: + self.x = ... + + def run(self): + ... + logger.info(...) + + if not self.dry_run: + ... + + self.execute(..., dry_run=self.dry_run) diff --git a/Lib/packaging/command/config.py b/Lib/packaging/command/config.py new file mode 100644 index 000000000000..a5feb9139a7e --- /dev/null +++ b/Lib/packaging/command/config.py @@ -0,0 +1,351 @@ +"""Prepare the build. + +This module provides config, a (mostly) empty command class +that exists mainly to be sub-classed by specific module distributions and +applications. The idea is that while every "config" command is different, +at least they're all named the same, and users always see "config" in the +list of standard commands. Also, this is a good place to put common +configure-like tasks: "try to compile this C code", or "figure out where +this header file lives". +""" + +import os +import re + +from packaging.command.cmd import Command +from packaging.errors import PackagingExecError +from packaging.compiler import customize_compiler +from packaging import logger + +LANG_EXT = {'c': '.c', 'c++': '.cxx'} + +class config(Command): + + description = "prepare the build" + + user_options = [ + ('compiler=', None, + "specify the compiler type"), + ('cc=', None, + "specify the compiler executable"), + ('include-dirs=', 'I', + "list of directories to search for header files"), + ('define=', 'D', + "C preprocessor macros to define"), + ('undef=', 'U', + "C preprocessor macros to undefine"), + ('libraries=', 'l', + "external C libraries to link with"), + ('library-dirs=', 'L', + "directories to search for external C libraries"), + + ('noisy', None, + "show every action (compile, link, run, ...) taken"), + ('dump-source', None, + "dump generated source files before attempting to compile them"), + ] + + + # The three standard command methods: since the "config" command + # does nothing by default, these are empty. + + def initialize_options(self): + self.compiler = None + self.cc = None + self.include_dirs = None + self.libraries = None + self.library_dirs = None + + # maximal output for now + self.noisy = True + self.dump_source = True + + # list of temporary files generated along-the-way that we have + # to clean at some point + self.temp_files = [] + + def finalize_options(self): + if self.include_dirs is None: + self.include_dirs = self.distribution.include_dirs or [] + elif isinstance(self.include_dirs, str): + self.include_dirs = self.include_dirs.split(os.pathsep) + + if self.libraries is None: + self.libraries = [] + elif isinstance(self.libraries, str): + self.libraries = [self.libraries] + + if self.library_dirs is None: + self.library_dirs = [] + elif isinstance(self.library_dirs, str): + self.library_dirs = self.library_dirs.split(os.pathsep) + + def run(self): + pass + + + # Utility methods for actual "config" commands. The interfaces are + # loosely based on Autoconf macros of similar names. Sub-classes + # may use these freely. + + def _check_compiler(self): + """Check that 'self.compiler' really is a CCompiler object; + if not, make it one. + """ + # We do this late, and only on-demand, because this is an expensive + # import. + from packaging.compiler.ccompiler import CCompiler + from packaging.compiler import new_compiler + if not isinstance(self.compiler, CCompiler): + self.compiler = new_compiler(compiler=self.compiler, + dry_run=self.dry_run, force=True) + customize_compiler(self.compiler) + if self.include_dirs: + self.compiler.set_include_dirs(self.include_dirs) + if self.libraries: + self.compiler.set_libraries(self.libraries) + if self.library_dirs: + self.compiler.set_library_dirs(self.library_dirs) + + + def _gen_temp_sourcefile(self, body, headers, lang): + filename = "_configtest" + LANG_EXT[lang] + file = open(filename, "w") + if headers: + for header in headers: + file.write("#include <%s>\n" % header) + file.write("\n") + file.write(body) + if body[-1] != "\n": + file.write("\n") + file.close() + return filename + + def _preprocess(self, body, headers, include_dirs, lang): + src = self._gen_temp_sourcefile(body, headers, lang) + out = "_configtest.i" + self.temp_files.extend((src, out)) + self.compiler.preprocess(src, out, include_dirs=include_dirs) + return src, out + + def _compile(self, body, headers, include_dirs, lang): + src = self._gen_temp_sourcefile(body, headers, lang) + if self.dump_source: + dump_file(src, "compiling '%s':" % src) + obj = self.compiler.object_filenames([src])[0] + self.temp_files.extend((src, obj)) + self.compiler.compile([src], include_dirs=include_dirs) + return src, obj + + def _link(self, body, headers, include_dirs, libraries, library_dirs, + lang): + src, obj = self._compile(body, headers, include_dirs, lang) + prog = os.path.splitext(os.path.basename(src))[0] + self.compiler.link_executable([obj], prog, + libraries=libraries, + library_dirs=library_dirs, + target_lang=lang) + + if self.compiler.exe_extension is not None: + prog = prog + self.compiler.exe_extension + self.temp_files.append(prog) + + return src, obj, prog + + def _clean(self, *filenames): + if not filenames: + filenames = self.temp_files + self.temp_files = [] + logger.info("removing: %s", ' '.join(filenames)) + for filename in filenames: + try: + os.remove(filename) + except OSError: + pass + + + # XXX these ignore the dry-run flag: what to do, what to do? even if + # you want a dry-run build, you still need some sort of configuration + # info. My inclination is to make it up to the real config command to + # consult 'dry_run', and assume a default (minimal) configuration if + # true. The problem with trying to do it here is that you'd have to + # return either true or false from all the 'try' methods, neither of + # which is correct. + + # XXX need access to the header search path and maybe default macros. + + def try_cpp(self, body=None, headers=None, include_dirs=None, lang="c"): + """Construct a source file from 'body' (a string containing lines + of C/C++ code) and 'headers' (a list of header files to include) + and run it through the preprocessor. Return true if the + preprocessor succeeded, false if there were any errors. + ('body' probably isn't of much use, but what the heck.) + """ + from packaging.compiler.ccompiler import CompileError + self._check_compiler() + ok = True + try: + self._preprocess(body, headers, include_dirs, lang) + except CompileError: + ok = False + + self._clean() + return ok + + def search_cpp(self, pattern, body=None, headers=None, include_dirs=None, + lang="c"): + """Construct a source file (just like 'try_cpp()'), run it through + the preprocessor, and return true if any line of the output matches + 'pattern'. 'pattern' should either be a compiled regex object or a + string containing a regex. If both 'body' and 'headers' are None, + preprocesses an empty file -- which can be useful to determine the + symbols the preprocessor and compiler set by default. + """ + self._check_compiler() + src, out = self._preprocess(body, headers, include_dirs, lang) + + if isinstance(pattern, str): + pattern = re.compile(pattern) + + file = open(out) + match = False + while True: + line = file.readline() + if line == '': + break + if pattern.search(line): + match = True + break + + file.close() + self._clean() + return match + + def try_compile(self, body, headers=None, include_dirs=None, lang="c"): + """Try to compile a source file built from 'body' and 'headers'. + Return true on success, false otherwise. + """ + from packaging.compiler.ccompiler import CompileError + self._check_compiler() + try: + self._compile(body, headers, include_dirs, lang) + ok = True + except CompileError: + ok = False + + logger.info(ok and "success!" or "failure.") + self._clean() + return ok + + def try_link(self, body, headers=None, include_dirs=None, libraries=None, + library_dirs=None, lang="c"): + """Try to compile and link a source file, built from 'body' and + 'headers', to executable form. Return true on success, false + otherwise. + """ + from packaging.compiler.ccompiler import CompileError, LinkError + self._check_compiler() + try: + self._link(body, headers, include_dirs, + libraries, library_dirs, lang) + ok = True + except (CompileError, LinkError): + ok = False + + logger.info(ok and "success!" or "failure.") + self._clean() + return ok + + def try_run(self, body, headers=None, include_dirs=None, libraries=None, + library_dirs=None, lang="c"): + """Try to compile, link to an executable, and run a program + built from 'body' and 'headers'. Return true on success, false + otherwise. + """ + from packaging.compiler.ccompiler import CompileError, LinkError + self._check_compiler() + try: + src, obj, exe = self._link(body, headers, include_dirs, + libraries, library_dirs, lang) + self.spawn([exe]) + ok = True + except (CompileError, LinkError, PackagingExecError): + ok = False + + logger.info(ok and "success!" or "failure.") + self._clean() + return ok + + + # -- High-level methods -------------------------------------------- + # (these are the ones that are actually likely to be useful + # when implementing a real-world config command!) + + def check_func(self, func, headers=None, include_dirs=None, + libraries=None, library_dirs=None, decl=False, call=False): + + """Determine if function 'func' is available by constructing a + source file that refers to 'func', and compiles and links it. + If everything succeeds, returns true; otherwise returns false. + + The constructed source file starts out by including the header + files listed in 'headers'. If 'decl' is true, it then declares + 'func' (as "int func()"); you probably shouldn't supply 'headers' + and set 'decl' true in the same call, or you might get errors about + a conflicting declarations for 'func'. Finally, the constructed + 'main()' function either references 'func' or (if 'call' is true) + calls it. 'libraries' and 'library_dirs' are used when + linking. + """ + + self._check_compiler() + body = [] + if decl: + body.append("int %s ();" % func) + body.append("int main () {") + if call: + body.append(" %s();" % func) + else: + body.append(" %s;" % func) + body.append("}") + body = "\n".join(body) + "\n" + + return self.try_link(body, headers, include_dirs, + libraries, library_dirs) + + def check_lib(self, library, library_dirs=None, headers=None, + include_dirs=None, other_libraries=[]): + """Determine if 'library' is available to be linked against, + without actually checking that any particular symbols are provided + by it. 'headers' will be used in constructing the source file to + be compiled, but the only effect of this is to check if all the + header files listed are available. Any libraries listed in + 'other_libraries' will be included in the link, in case 'library' + has symbols that depend on other libraries. + """ + self._check_compiler() + return self.try_link("int main (void) { }", + headers, include_dirs, + [library]+other_libraries, library_dirs) + + def check_header(self, header, include_dirs=None, library_dirs=None, + lang="c"): + """Determine if the system header file named by 'header_file' + exists and can be found by the preprocessor; return true if so, + false otherwise. + """ + return self.try_cpp(body="/* No body */", headers=[header], + include_dirs=include_dirs) + + +def dump_file(filename, head=None): + """Dumps a file content into log.info. + + If head is not None, will be dumped before the file content. + """ + if head is None: + logger.info(filename) + else: + logger.info(head) + with open(filename) as file: + logger.info(file.read()) diff --git a/Lib/packaging/command/install_data.py b/Lib/packaging/command/install_data.py new file mode 100644 index 000000000000..9ca6279533e8 --- /dev/null +++ b/Lib/packaging/command/install_data.py @@ -0,0 +1,79 @@ +"""Install platform-independent data files.""" + +# Contributed by Bastian Kleineidam + +import os +from shutil import Error +from sysconfig import get_paths, format_value +from packaging import logger +from packaging.util import convert_path +from packaging.command.cmd import Command + + +class install_data(Command): + + description = "install platform-independent data files" + + user_options = [ + ('install-dir=', 'd', + "base directory for installing data files " + "(default: installation base dir)"), + ('root=', None, + "install everything relative to this alternate root directory"), + ('force', 'f', "force installation (overwrite existing files)"), + ] + + boolean_options = ['force'] + + def initialize_options(self): + self.install_dir = None + self.outfiles = [] + self.data_files_out = [] + self.root = None + self.force = False + self.data_files = self.distribution.data_files + self.warn_dir = True + + def finalize_options(self): + self.set_undefined_options('install_dist', + ('install_data', 'install_dir'), + 'root', 'force') + + def run(self): + self.mkpath(self.install_dir) + for _file in self.data_files.items(): + destination = convert_path(self.expand_categories(_file[1])) + dir_dest = os.path.abspath(os.path.dirname(destination)) + + self.mkpath(dir_dest) + try: + out = self.copy_file(_file[0], dir_dest)[0] + except Error as e: + logger.warning('%s: %s', self.get_command_name(), e) + out = destination + + self.outfiles.append(out) + self.data_files_out.append((_file[0], destination)) + + def expand_categories(self, path_with_categories): + local_vars = get_paths() + local_vars['distribution.name'] = self.distribution.metadata['Name'] + expanded_path = format_value(path_with_categories, local_vars) + expanded_path = format_value(expanded_path, local_vars) + if '{' in expanded_path and '}' in expanded_path: + logger.warning( + '%s: unable to expand %s, some categories may be missing', + self.get_command_name(), path_with_categories) + return expanded_path + + def get_source_files(self): + return list(self.data_files) + + def get_inputs(self): + return list(self.data_files) + + def get_outputs(self): + return self.outfiles + + def get_resources_out(self): + return self.data_files_out diff --git a/Lib/packaging/command/install_dist.py b/Lib/packaging/command/install_dist.py new file mode 100644 index 000000000000..dfe6df2d5a38 --- /dev/null +++ b/Lib/packaging/command/install_dist.py @@ -0,0 +1,625 @@ +"""Main install command, which calls the other install_* commands.""" + +import sys +import os + +import sysconfig +from sysconfig import get_config_vars, get_paths, get_path, get_config_var + +from packaging import logger +from packaging.command.cmd import Command +from packaging.errors import PackagingPlatformError +from packaging.util import write_file +from packaging.util import convert_path, change_root, get_platform +from packaging.errors import PackagingOptionError + + +HAS_USER_SITE = True + + +class install_dist(Command): + + description = "install everything from build directory" + + user_options = [ + # Select installation scheme and set base director(y|ies) + ('prefix=', None, + "installation prefix"), + ('exec-prefix=', None, + "(Unix only) prefix for platform-specific files"), + ('home=', None, + "(Unix only) home directory to install under"), + + # Or just set the base director(y|ies) + ('install-base=', None, + "base installation directory (instead of --prefix or --home)"), + ('install-platbase=', None, + "base installation directory for platform-specific files " + + "(instead of --exec-prefix or --home)"), + ('root=', None, + "install everything relative to this alternate root directory"), + + # Or explicitly set the installation scheme + ('install-purelib=', None, + "installation directory for pure Python module distributions"), + ('install-platlib=', None, + "installation directory for non-pure module distributions"), + ('install-lib=', None, + "installation directory for all module distributions " + + "(overrides --install-purelib and --install-platlib)"), + + ('install-headers=', None, + "installation directory for C/C++ headers"), + ('install-scripts=', None, + "installation directory for Python scripts"), + ('install-data=', None, + "installation directory for data files"), + + # Byte-compilation options -- see install_lib.py for details, as + # these are duplicated from there (but only install_lib does + # anything with them). + ('compile', 'c', "compile .py to .pyc [default]"), + ('no-compile', None, "don't compile .py files"), + ('optimize=', 'O', + 'also compile with optimization: -O1 for "python -O", ' + '-O2 for "python -OO", and -O0 to disable [default: -O0]'), + + # Miscellaneous control options + ('force', 'f', + "force installation (overwrite any existing files)"), + ('skip-build', None, + "skip rebuilding everything (for testing/debugging)"), + + # Where to install documentation (eventually!) + #('doc-format=', None, "format of documentation to generate"), + #('install-man=', None, "directory for Unix man pages"), + #('install-html=', None, "directory for HTML documentation"), + #('install-info=', None, "directory for GNU info files"), + + # XXX use a name that makes clear this is the old format + ('record=', None, + "filename in which to record a list of installed files " + "(not PEP 376-compliant)"), + ('resources=', None, + "data files mapping"), + + # .dist-info related arguments, read by install_dist_info + ('no-distinfo', None, + "do not create a .dist-info directory"), + ('installer=', None, + "the name of the installer"), + ('requested', None, + "generate a REQUESTED file (i.e."), + ('no-requested', None, + "do not generate a REQUESTED file"), + ('no-record', None, + "do not generate a RECORD file"), + ] + + boolean_options = ['compile', 'force', 'skip-build', 'no-distinfo', + 'requested', 'no-record'] + + if HAS_USER_SITE: + user_options.append( + ('user', None, + "install in user site-packages directory [%s]" % + get_path('purelib', '%s_user' % os.name))) + + boolean_options.append('user') + + negative_opt = {'no-compile': 'compile', 'no-requested': 'requested'} + + def initialize_options(self): + # High-level options: these select both an installation base + # and scheme. + self.prefix = None + self.exec_prefix = None + self.home = None + if HAS_USER_SITE: + self.user = False + + # These select only the installation base; it's up to the user to + # specify the installation scheme (currently, that means supplying + # the --install-{platlib,purelib,scripts,data} options). + self.install_base = None + self.install_platbase = None + self.root = None + + # These options are the actual installation directories; if not + # supplied by the user, they are filled in using the installation + # scheme implied by prefix/exec-prefix/home and the contents of + # that installation scheme. + self.install_purelib = None # for pure module distributions + self.install_platlib = None # non-pure (dists w/ extensions) + self.install_headers = None # for C/C++ headers + self.install_lib = None # set to either purelib or platlib + self.install_scripts = None + self.install_data = None + if HAS_USER_SITE: + self.install_userbase = get_config_var('userbase') + self.install_usersite = get_path('purelib', '%s_user' % os.name) + + self.compile = None + self.optimize = None + + # These two are for putting non-packagized distributions into their + # own directory and creating a .pth file if it makes sense. + # 'extra_path' comes from the setup file; 'install_path_file' can + # be turned off if it makes no sense to install a .pth file. (But + # better to install it uselessly than to guess wrong and not + # install it when it's necessary and would be used!) Currently, + # 'install_path_file' is always true unless some outsider meddles + # with it. + self.extra_path = None + self.install_path_file = True + + # 'force' forces installation, even if target files are not + # out-of-date. 'skip_build' skips running the "build" command, + # handy if you know it's not necessary. 'warn_dir' (which is *not* + # a user option, it's just there so the bdist_* commands can turn + # it off) determines whether we warn about installing to a + # directory not in sys.path. + self.force = False + self.skip_build = False + self.warn_dir = True + + # These are only here as a conduit from the 'build' command to the + # 'install_*' commands that do the real work. ('build_base' isn't + # actually used anywhere, but it might be useful in future.) They + # are not user options, because if the user told the install + # command where the build directory is, that wouldn't affect the + # build command. + self.build_base = None + self.build_lib = None + + # Not defined yet because we don't know anything about + # documentation yet. + #self.install_man = None + #self.install_html = None + #self.install_info = None + + self.record = None + self.resources = None + + # .dist-info related options + self.no_distinfo = None + self.installer = None + self.requested = None + self.no_record = None + self.no_resources = None + + # -- Option finalizing methods ------------------------------------- + # (This is rather more involved than for most commands, + # because this is where the policy for installing third- + # party Python modules on various platforms given a wide + # array of user input is decided. Yes, it's quite complex!) + + def finalize_options(self): + # This method (and its pliant slaves, like 'finalize_unix()', + # 'finalize_other()', and 'select_scheme()') is where the default + # installation directories for modules, extension modules, and + # anything else we care to install from a Python module + # distribution. Thus, this code makes a pretty important policy + # statement about how third-party stuff is added to a Python + # installation! Note that the actual work of installation is done + # by the relatively simple 'install_*' commands; they just take + # their orders from the installation directory options determined + # here. + + # Check for errors/inconsistencies in the options; first, stuff + # that's wrong on any platform. + + if ((self.prefix or self.exec_prefix or self.home) and + (self.install_base or self.install_platbase)): + raise PackagingOptionError( + "must supply either prefix/exec-prefix/home or " + "install-base/install-platbase -- not both") + + if self.home and (self.prefix or self.exec_prefix): + raise PackagingOptionError( + "must supply either home or prefix/exec-prefix -- not both") + + if HAS_USER_SITE and self.user and ( + self.prefix or self.exec_prefix or self.home or + self.install_base or self.install_platbase): + raise PackagingOptionError( + "can't combine user with prefix/exec_prefix/home or " + "install_base/install_platbase") + + # Next, stuff that's wrong (or dubious) only on certain platforms. + if os.name != "posix": + if self.exec_prefix: + logger.warning( + '%s: exec-prefix option ignored on this platform', + self.get_command_name()) + self.exec_prefix = None + + # Now the interesting logic -- so interesting that we farm it out + # to other methods. The goal of these methods is to set the final + # values for the install_{lib,scripts,data,...} options, using as + # input a heady brew of prefix, exec_prefix, home, install_base, + # install_platbase, user-supplied versions of + # install_{purelib,platlib,lib,scripts,data,...}, and the + # INSTALL_SCHEME dictionary above. Phew! + + self.dump_dirs("pre-finalize_{unix,other}") + + if os.name == 'posix': + self.finalize_unix() + else: + self.finalize_other() + + self.dump_dirs("post-finalize_{unix,other}()") + + # Expand configuration variables, tilde, etc. in self.install_base + # and self.install_platbase -- that way, we can use $base or + # $platbase in the other installation directories and not worry + # about needing recursive variable expansion (shudder). + + py_version = sys.version.split()[0] + prefix, exec_prefix, srcdir, projectbase = get_config_vars( + 'prefix', 'exec_prefix', 'srcdir', 'projectbase') + + metadata = self.distribution.metadata + self.config_vars = { + 'dist_name': metadata['Name'], + 'dist_version': metadata['Version'], + 'dist_fullname': metadata.get_fullname(), + 'py_version': py_version, + 'py_version_short': py_version[:3], + 'py_version_nodot': py_version[:3:2], + 'sys_prefix': prefix, + 'prefix': prefix, + 'sys_exec_prefix': exec_prefix, + 'exec_prefix': exec_prefix, + 'srcdir': srcdir, + 'projectbase': projectbase, + } + + if HAS_USER_SITE: + self.config_vars['userbase'] = self.install_userbase + self.config_vars['usersite'] = self.install_usersite + + self.expand_basedirs() + + self.dump_dirs("post-expand_basedirs()") + + # Now define config vars for the base directories so we can expand + # everything else. + self.config_vars['base'] = self.install_base + self.config_vars['platbase'] = self.install_platbase + + # Expand "~" and configuration variables in the installation + # directories. + self.expand_dirs() + + self.dump_dirs("post-expand_dirs()") + + # Create directories in the home dir: + if HAS_USER_SITE and self.user: + self.create_home_path() + + # Pick the actual directory to install all modules to: either + # install_purelib or install_platlib, depending on whether this + # module distribution is pure or not. Of course, if the user + # already specified install_lib, use their selection. + if self.install_lib is None: + if self.distribution.ext_modules: # has extensions: non-pure + self.install_lib = self.install_platlib + else: + self.install_lib = self.install_purelib + + # Convert directories from Unix /-separated syntax to the local + # convention. + self.convert_paths('lib', 'purelib', 'platlib', + 'scripts', 'data', 'headers') + if HAS_USER_SITE: + self.convert_paths('userbase', 'usersite') + + # Well, we're not actually fully completely finalized yet: we still + # have to deal with 'extra_path', which is the hack for allowing + # non-packagized module distributions (hello, Numerical Python!) to + # get their own directories. + self.handle_extra_path() + self.install_libbase = self.install_lib # needed for .pth file + self.install_lib = os.path.join(self.install_lib, self.extra_dirs) + + # If a new root directory was supplied, make all the installation + # dirs relative to it. + if self.root is not None: + self.change_roots('libbase', 'lib', 'purelib', 'platlib', + 'scripts', 'data', 'headers') + + self.dump_dirs("after prepending root") + + # Find out the build directories, ie. where to install from. + self.set_undefined_options('build', 'build_base', 'build_lib') + + # Punt on doc directories for now -- after all, we're punting on + # documentation completely! + + if self.no_distinfo is None: + self.no_distinfo = False + + def finalize_unix(self): + """Finalize options for posix platforms.""" + if self.install_base is not None or self.install_platbase is not None: + if ((self.install_lib is None and + self.install_purelib is None and + self.install_platlib is None) or + self.install_headers is None or + self.install_scripts is None or + self.install_data is None): + raise PackagingOptionError( + "install-base or install-platbase supplied, but " + "installation scheme is incomplete") + return + + if HAS_USER_SITE and self.user: + if self.install_userbase is None: + raise PackagingPlatformError( + "user base directory is not specified") + self.install_base = self.install_platbase = self.install_userbase + self.select_scheme("posix_user") + elif self.home is not None: + self.install_base = self.install_platbase = self.home + self.select_scheme("posix_home") + else: + if self.prefix is None: + if self.exec_prefix is not None: + raise PackagingOptionError( + "must not supply exec-prefix without prefix") + + self.prefix = os.path.normpath(sys.prefix) + self.exec_prefix = os.path.normpath(sys.exec_prefix) + + else: + if self.exec_prefix is None: + self.exec_prefix = self.prefix + + self.install_base = self.prefix + self.install_platbase = self.exec_prefix + self.select_scheme("posix_prefix") + + def finalize_other(self): + """Finalize options for non-posix platforms""" + if HAS_USER_SITE and self.user: + if self.install_userbase is None: + raise PackagingPlatformError( + "user base directory is not specified") + self.install_base = self.install_platbase = self.install_userbase + self.select_scheme(os.name + "_user") + elif self.home is not None: + self.install_base = self.install_platbase = self.home + self.select_scheme("posix_home") + else: + if self.prefix is None: + self.prefix = os.path.normpath(sys.prefix) + + self.install_base = self.install_platbase = self.prefix + try: + self.select_scheme(os.name) + except KeyError: + raise PackagingPlatformError( + "no support for installation on '%s'" % os.name) + + def dump_dirs(self, msg): + """Dump the list of user options.""" + logger.debug(msg + ":") + for opt in self.user_options: + opt_name = opt[0] + if opt_name[-1] == "=": + opt_name = opt_name[0:-1] + if opt_name in self.negative_opt: + opt_name = self.negative_opt[opt_name] + opt_name = opt_name.replace('-', '_') + val = not getattr(self, opt_name) + else: + opt_name = opt_name.replace('-', '_') + val = getattr(self, opt_name) + logger.debug(" %s: %s", opt_name, val) + + def select_scheme(self, name): + """Set the install directories by applying the install schemes.""" + # it's the caller's problem if they supply a bad name! + scheme = get_paths(name, expand=False) + for key, value in scheme.items(): + if key == 'platinclude': + key = 'headers' + value = os.path.join(value, self.distribution.metadata['Name']) + attrname = 'install_' + key + if hasattr(self, attrname): + if getattr(self, attrname) is None: + setattr(self, attrname, value) + + def _expand_attrs(self, attrs): + for attr in attrs: + val = getattr(self, attr) + if val is not None: + if os.name == 'posix' or os.name == 'nt': + val = os.path.expanduser(val) + # see if we want to push this work in sysconfig XXX + val = sysconfig._subst_vars(val, self.config_vars) + setattr(self, attr, val) + + def expand_basedirs(self): + """Call `os.path.expanduser` on install_{base,platbase} and root.""" + self._expand_attrs(['install_base', 'install_platbase', 'root']) + + def expand_dirs(self): + """Call `os.path.expanduser` on install dirs.""" + self._expand_attrs(['install_purelib', 'install_platlib', + 'install_lib', 'install_headers', + 'install_scripts', 'install_data']) + + def convert_paths(self, *names): + """Call `convert_path` over `names`.""" + for name in names: + attr = "install_" + name + setattr(self, attr, convert_path(getattr(self, attr))) + + def handle_extra_path(self): + """Set `path_file` and `extra_dirs` using `extra_path`.""" + if self.extra_path is None: + self.extra_path = self.distribution.extra_path + + if self.extra_path is not None: + if isinstance(self.extra_path, str): + self.extra_path = self.extra_path.split(',') + + if len(self.extra_path) == 1: + path_file = extra_dirs = self.extra_path[0] + elif len(self.extra_path) == 2: + path_file, extra_dirs = self.extra_path + else: + raise PackagingOptionError( + "'extra_path' option must be a list, tuple, or " + "comma-separated string with 1 or 2 elements") + + # convert to local form in case Unix notation used (as it + # should be in setup scripts) + extra_dirs = convert_path(extra_dirs) + else: + path_file = None + extra_dirs = '' + + # XXX should we warn if path_file and not extra_dirs? (in which + # case the path file would be harmless but pointless) + self.path_file = path_file + self.extra_dirs = extra_dirs + + def change_roots(self, *names): + """Change the install direcories pointed by name using root.""" + for name in names: + attr = "install_" + name + setattr(self, attr, change_root(self.root, getattr(self, attr))) + + def create_home_path(self): + """Create directories under ~.""" + if HAS_USER_SITE and not self.user: + return + home = convert_path(os.path.expanduser("~")) + for name, path in self.config_vars.items(): + if path.startswith(home) and not os.path.isdir(path): + os.makedirs(path, 0o700) + + # -- Command execution methods ------------------------------------- + + def run(self): + """Runs the command.""" + # Obviously have to build before we can install + if not self.skip_build: + self.run_command('build') + # If we built for any other platform, we can't install. + build_plat = self.distribution.get_command_obj('build').plat_name + # check warn_dir - it is a clue that the 'install_dist' is happening + # internally, and not to sys.path, so we don't check the platform + # matches what we are running. + if self.warn_dir and build_plat != get_platform(): + raise PackagingPlatformError("Can't install when " + "cross-compiling") + + # Run all sub-commands (at least those that need to be run) + for cmd_name in self.get_sub_commands(): + self.run_command(cmd_name) + + if self.path_file: + self.create_path_file() + + # write list of installed files, if requested. + if self.record: + outputs = self.get_outputs() + if self.root: # strip any package prefix + root_len = len(self.root) + for counter in range(len(outputs)): + outputs[counter] = outputs[counter][root_len:] + self.execute(write_file, + (self.record, outputs), + "writing list of installed files to '%s'" % + self.record) + + normpath, normcase = os.path.normpath, os.path.normcase + sys_path = [normcase(normpath(p)) for p in sys.path] + install_lib = normcase(normpath(self.install_lib)) + if (self.warn_dir and + not (self.path_file and self.install_path_file) and + install_lib not in sys_path): + logger.debug(("modules installed to '%s', which is not in " + "Python's module search path (sys.path) -- " + "you'll have to change the search path yourself"), + self.install_lib) + + def create_path_file(self): + """Creates the .pth file""" + filename = os.path.join(self.install_libbase, + self.path_file + ".pth") + if self.install_path_file: + self.execute(write_file, + (filename, [self.extra_dirs]), + "creating %s" % filename) + else: + logger.warning('%s: path file %r not created', + self.get_command_name(), filename) + + # -- Reporting methods --------------------------------------------- + + def get_outputs(self): + """Assembles the outputs of all the sub-commands.""" + outputs = [] + for cmd_name in self.get_sub_commands(): + cmd = self.get_finalized_command(cmd_name) + # Add the contents of cmd.get_outputs(), ensuring + # that outputs doesn't contain duplicate entries + for filename in cmd.get_outputs(): + if filename not in outputs: + outputs.append(filename) + + if self.path_file and self.install_path_file: + outputs.append(os.path.join(self.install_libbase, + self.path_file + ".pth")) + + return outputs + + def get_inputs(self): + """Returns the inputs of all the sub-commands""" + # XXX gee, this looks familiar ;-( + inputs = [] + for cmd_name in self.get_sub_commands(): + cmd = self.get_finalized_command(cmd_name) + inputs.extend(cmd.get_inputs()) + + return inputs + + # -- Predicates for sub-command list ------------------------------- + + def has_lib(self): + """Returns true if the current distribution has any Python + modules to install.""" + return (self.distribution.has_pure_modules() or + self.distribution.has_ext_modules()) + + def has_headers(self): + """Returns true if the current distribution has any headers to + install.""" + return self.distribution.has_headers() + + def has_scripts(self): + """Returns true if the current distribution has any scripts to. + install.""" + return self.distribution.has_scripts() + + def has_data(self): + """Returns true if the current distribution has any data to. + install.""" + return self.distribution.has_data_files() + + # 'sub_commands': a list of commands this command might have to run to + # get its work done. See cmd.py for more info. + sub_commands = [('install_lib', has_lib), + ('install_headers', has_headers), + ('install_scripts', has_scripts), + ('install_data', has_data), + # keep install_distinfo last, as it needs the record + # with files to be completely generated + ('install_distinfo', lambda self: not self.no_distinfo), + ] diff --git a/Lib/packaging/command/install_distinfo.py b/Lib/packaging/command/install_distinfo.py new file mode 100644 index 000000000000..41fe73459f20 --- /dev/null +++ b/Lib/packaging/command/install_distinfo.py @@ -0,0 +1,175 @@ +"""Create the PEP 376-compliant .dist-info directory.""" + +# Forked from the former install_egg_info command by Josip Djolonga + +import csv +import os +import re +import hashlib + +from packaging.command.cmd import Command +from packaging import logger +from shutil import rmtree + + +class install_distinfo(Command): + + description = 'create a .dist-info directory for the distribution' + + user_options = [ + ('distinfo-dir=', None, + "directory where the the .dist-info directory will be installed"), + ('installer=', None, + "the name of the installer"), + ('requested', None, + "generate a REQUESTED file"), + ('no-requested', None, + "do not generate a REQUESTED file"), + ('no-record', None, + "do not generate a RECORD file"), + ('no-resources', None, + "do not generate a RESSOURCES list installed file") + ] + + boolean_options = ['requested', 'no-record', 'no-resources'] + + negative_opt = {'no-requested': 'requested'} + + def initialize_options(self): + self.distinfo_dir = None + self.installer = None + self.requested = None + self.no_record = None + self.no_resources = None + + def finalize_options(self): + self.set_undefined_options('install_dist', + 'installer', 'requested', 'no_record') + + self.set_undefined_options('install_lib', + ('install_dir', 'distinfo_dir')) + + if self.installer is None: + # FIXME distutils or packaging? + # + document default in the option help text above and in install + self.installer = 'distutils' + if self.requested is None: + self.requested = True + if self.no_record is None: + self.no_record = False + if self.no_resources is None: + self.no_resources = False + + metadata = self.distribution.metadata + + basename = "%s-%s.dist-info" % ( + to_filename(safe_name(metadata['Name'])), + to_filename(safe_version(metadata['Version']))) + + self.distinfo_dir = os.path.join(self.distinfo_dir, basename) + self.outputs = [] + + def run(self): + # FIXME dry-run should be used at a finer level, so that people get + # useful logging output and can have an idea of what the command would + # have done + if not self.dry_run: + target = self.distinfo_dir + + if os.path.isdir(target) and not os.path.islink(target): + rmtree(target) + elif os.path.exists(target): + self.execute(os.unlink, (self.distinfo_dir,), + "removing " + target) + + self.execute(os.makedirs, (target,), "creating " + target) + + metadata_path = os.path.join(self.distinfo_dir, 'METADATA') + logger.info('creating %s', metadata_path) + self.distribution.metadata.write(metadata_path) + self.outputs.append(metadata_path) + + installer_path = os.path.join(self.distinfo_dir, 'INSTALLER') + logger.info('creating %s', installer_path) + with open(installer_path, 'w') as f: + f.write(self.installer) + self.outputs.append(installer_path) + + if self.requested: + requested_path = os.path.join(self.distinfo_dir, 'REQUESTED') + logger.info('creating %s', requested_path) + open(requested_path, 'w').close() + self.outputs.append(requested_path) + + + if not self.no_resources: + install_data = self.get_finalized_command('install_data') + if install_data.get_resources_out() != []: + resources_path = os.path.join(self.distinfo_dir, + 'RESOURCES') + logger.info('creating %s', resources_path) + with open(resources_path, 'wb') as f: + writer = csv.writer(f, delimiter=',', + lineterminator=os.linesep, + quotechar='"') + for tuple in install_data.get_resources_out(): + writer.writerow(tuple) + + self.outputs.append(resources_path) + + if not self.no_record: + record_path = os.path.join(self.distinfo_dir, 'RECORD') + logger.info('creating %s', record_path) + with open(record_path, 'w', encoding='utf-8') as f: + writer = csv.writer(f, delimiter=',', + lineterminator=os.linesep, + quotechar='"') + + install = self.get_finalized_command('install_dist') + + for fpath in install.get_outputs(): + if fpath.endswith('.pyc') or fpath.endswith('.pyo'): + # do not put size and md5 hash, as in PEP-376 + writer.writerow((fpath, '', '')) + else: + size = os.path.getsize(fpath) + with open(fpath, 'r') as fp: + hash = hashlib.md5() + hash.update(fp.read().encode()) + md5sum = hash.hexdigest() + writer.writerow((fpath, md5sum, size)) + + # add the RECORD file itself + writer.writerow((record_path, '', '')) + self.outputs.append(record_path) + + def get_outputs(self): + return self.outputs + + +# The following functions are taken from setuptools' pkg_resources module. + +def safe_name(name): + """Convert an arbitrary string to a standard distribution name + + Any runs of non-alphanumeric/. characters are replaced with a single '-'. + """ + return re.sub('[^A-Za-z0-9.]+', '-', name) + + +def safe_version(version): + """Convert an arbitrary string to a standard version string + + Spaces become dots, and all other non-alphanumeric characters become + dashes, with runs of multiple dashes condensed to a single dash. + """ + version = version.replace(' ', '.') + return re.sub('[^A-Za-z0-9.]+', '-', version) + + +def to_filename(name): + """Convert a project or version name to its filename-escaped form + + Any '-' characters are currently replaced with '_'. + """ + return name.replace('-', '_') diff --git a/Lib/packaging/command/install_headers.py b/Lib/packaging/command/install_headers.py new file mode 100644 index 000000000000..e043d6b8ed2a --- /dev/null +++ b/Lib/packaging/command/install_headers.py @@ -0,0 +1,43 @@ +"""Install C/C++ header files to the Python include directory.""" + +from packaging.command.cmd import Command + + +# XXX force is never used +class install_headers(Command): + + description = "install C/C++ header files" + + user_options = [('install-dir=', 'd', + "directory to install header files to"), + ('force', 'f', + "force installation (overwrite existing files)"), + ] + + boolean_options = ['force'] + + def initialize_options(self): + self.install_dir = None + self.force = False + self.outfiles = [] + + def finalize_options(self): + self.set_undefined_options('install_dist', + ('install_headers', 'install_dir'), + 'force') + + def run(self): + headers = self.distribution.headers + if not headers: + return + + self.mkpath(self.install_dir) + for header in headers: + out = self.copy_file(header, self.install_dir)[0] + self.outfiles.append(out) + + def get_inputs(self): + return self.distribution.headers or [] + + def get_outputs(self): + return self.outfiles diff --git a/Lib/packaging/command/install_lib.py b/Lib/packaging/command/install_lib.py new file mode 100644 index 000000000000..5ff9cee03c9c --- /dev/null +++ b/Lib/packaging/command/install_lib.py @@ -0,0 +1,222 @@ +"""Install all modules (extensions and pure Python).""" + +import os +import sys +import logging + +from packaging import logger +from packaging.command.cmd import Command +from packaging.errors import PackagingOptionError + + +# Extension for Python source files. +if hasattr(os, 'extsep'): + PYTHON_SOURCE_EXTENSION = os.extsep + "py" +else: + PYTHON_SOURCE_EXTENSION = ".py" + +class install_lib(Command): + + description = "install all modules (extensions and pure Python)" + + # The byte-compilation options are a tad confusing. Here are the + # possible scenarios: + # 1) no compilation at all (--no-compile --no-optimize) + # 2) compile .pyc only (--compile --no-optimize; default) + # 3) compile .pyc and "level 1" .pyo (--compile --optimize) + # 4) compile "level 1" .pyo only (--no-compile --optimize) + # 5) compile .pyc and "level 2" .pyo (--compile --optimize-more) + # 6) compile "level 2" .pyo only (--no-compile --optimize-more) + # + # The UI for this is two option, 'compile' and 'optimize'. + # 'compile' is strictly boolean, and only decides whether to + # generate .pyc files. 'optimize' is three-way (0, 1, or 2), and + # decides both whether to generate .pyo files and what level of + # optimization to use. + + user_options = [ + ('install-dir=', 'd', "directory to install to"), + ('build-dir=','b', "build directory (where to install from)"), + ('force', 'f', "force installation (overwrite existing files)"), + ('compile', 'c', "compile .py to .pyc [default]"), + ('no-compile', None, "don't compile .py files"), + ('optimize=', 'O', + "also compile with optimization: -O1 for \"python -O\", " + "-O2 for \"python -OO\", and -O0 to disable [default: -O0]"), + ('skip-build', None, "skip the build steps"), + ] + + boolean_options = ['force', 'compile', 'skip-build'] + negative_opt = {'no-compile' : 'compile'} + + def initialize_options(self): + # let the 'install_dist' command dictate our installation directory + self.install_dir = None + self.build_dir = None + self.force = False + self.compile = None + self.optimize = None + self.skip_build = None + + def finalize_options(self): + # Get all the information we need to install pure Python modules + # from the umbrella 'install_dist' command -- build (source) directory, + # install (target) directory, and whether to compile .py files. + self.set_undefined_options('install_dist', + ('build_lib', 'build_dir'), + ('install_lib', 'install_dir'), + 'force', 'compile', 'optimize', 'skip_build') + + if self.compile is None: + self.compile = True + if self.optimize is None: + self.optimize = 0 + + if not isinstance(self.optimize, int): + try: + self.optimize = int(self.optimize) + if self.optimize not in (0, 1, 2): + raise AssertionError + except (ValueError, AssertionError): + raise PackagingOptionError("optimize must be 0, 1, or 2") + + def run(self): + # Make sure we have built everything we need first + self.build() + + # Install everything: simply dump the entire contents of the build + # directory to the installation directory (that's the beauty of + # having a build directory!) + outfiles = self.install() + + # (Optionally) compile .py to .pyc + if outfiles is not None and self.distribution.has_pure_modules(): + self.byte_compile(outfiles) + + # -- Top-level worker functions ------------------------------------ + # (called from 'run()') + + def build(self): + if not self.skip_build: + if self.distribution.has_pure_modules(): + self.run_command('build_py') + if self.distribution.has_ext_modules(): + self.run_command('build_ext') + + def install(self): + if os.path.isdir(self.build_dir): + outfiles = self.copy_tree(self.build_dir, self.install_dir) + else: + logger.warning( + '%s: %r does not exist -- no Python modules to install', + self.get_command_name(), self.build_dir) + return + return outfiles + + def byte_compile(self, files): + if getattr(sys, 'dont_write_bytecode'): + # XXX do we want this? because a Python runs without bytecode + # doesn't mean that the *dists should not contain bytecode + #--or does it? + logger.warning('%s: byte-compiling is disabled, skipping.', + self.get_command_name()) + return + + from packaging.util import byte_compile + + # Get the "--root" directory supplied to the "install_dist" command, + # and use it as a prefix to strip off the purported filename + # encoded in bytecode files. This is far from complete, but it + # should at least generate usable bytecode in RPM distributions. + install_root = self.get_finalized_command('install_dist').root + + # Temporary kludge until we remove the verbose arguments and use + # logging everywhere + verbose = logger.getEffectiveLevel() >= logging.DEBUG + + if self.compile: + byte_compile(files, optimize=0, + force=self.force, prefix=install_root, + dry_run=self.dry_run) + if self.optimize > 0: + byte_compile(files, optimize=self.optimize, + force=self.force, prefix=install_root, + verbose=verbose, + dry_run=self.dry_run) + + + # -- Utility methods ----------------------------------------------- + + def _mutate_outputs(self, has_any, build_cmd, cmd_option, output_dir): + if not has_any: + return [] + + build_cmd = self.get_finalized_command(build_cmd) + build_files = build_cmd.get_outputs() + build_dir = getattr(build_cmd, cmd_option) + + prefix_len = len(build_dir) + len(os.sep) + outputs = [] + for file in build_files: + outputs.append(os.path.join(output_dir, file[prefix_len:])) + + return outputs + + def _bytecode_filenames(self, py_filenames): + bytecode_files = [] + for py_file in py_filenames: + # Since build_py handles package data installation, the + # list of outputs can contain more than just .py files. + # Make sure we only report bytecode for the .py files. + ext = os.path.splitext(os.path.normcase(py_file))[1] + if ext != PYTHON_SOURCE_EXTENSION: + continue + if self.compile: + bytecode_files.append(py_file + "c") + if self.optimize > 0: + bytecode_files.append(py_file + "o") + + return bytecode_files + + + # -- External interface -------------------------------------------- + # (called by outsiders) + + def get_outputs(self): + """Return the list of files that would be installed if this command + were actually run. Not affected by the "dry-run" flag or whether + modules have actually been built yet. + """ + pure_outputs = \ + self._mutate_outputs(self.distribution.has_pure_modules(), + 'build_py', 'build_lib', + self.install_dir) + if self.compile: + bytecode_outputs = self._bytecode_filenames(pure_outputs) + else: + bytecode_outputs = [] + + ext_outputs = \ + self._mutate_outputs(self.distribution.has_ext_modules(), + 'build_ext', 'build_lib', + self.install_dir) + + return pure_outputs + bytecode_outputs + ext_outputs + + def get_inputs(self): + """Get the list of files that are input to this command, ie. the + files that get installed as they are named in the build tree. + The files in this list correspond one-to-one to the output + filenames returned by 'get_outputs()'. + """ + inputs = [] + + if self.distribution.has_pure_modules(): + build_py = self.get_finalized_command('build_py') + inputs.extend(build_py.get_outputs()) + + if self.distribution.has_ext_modules(): + build_ext = self.get_finalized_command('build_ext') + inputs.extend(build_ext.get_outputs()) + + return inputs diff --git a/Lib/packaging/command/install_scripts.py b/Lib/packaging/command/install_scripts.py new file mode 100644 index 000000000000..cfacbe25fb8f --- /dev/null +++ b/Lib/packaging/command/install_scripts.py @@ -0,0 +1,59 @@ +"""Install scripts.""" + +# Contributed by Bastian Kleineidam + +import os +from packaging.command.cmd import Command +from packaging import logger + +class install_scripts(Command): + + description = "install scripts (Python or otherwise)" + + user_options = [ + ('install-dir=', 'd', "directory to install scripts to"), + ('build-dir=','b', "build directory (where to install from)"), + ('force', 'f', "force installation (overwrite existing files)"), + ('skip-build', None, "skip the build steps"), + ] + + boolean_options = ['force', 'skip-build'] + + + def initialize_options(self): + self.install_dir = None + self.force = False + self.build_dir = None + self.skip_build = None + + def finalize_options(self): + self.set_undefined_options('build', ('build_scripts', 'build_dir')) + self.set_undefined_options('install_dist', + ('install_scripts', 'install_dir'), + 'force', 'skip_build') + + def run(self): + if not self.skip_build: + self.run_command('build_scripts') + + if not os.path.exists(self.build_dir): + self.outfiles = [] + return + + self.outfiles = self.copy_tree(self.build_dir, self.install_dir) + if os.name == 'posix': + # Set the executable bits (owner, group, and world) on + # all the scripts we just installed. + for file in self.get_outputs(): + if self.dry_run: + logger.info("changing mode of %s", file) + else: + mode = (os.stat(file).st_mode | 0o555) & 0o7777 + logger.info("changing mode of %s to %o", file, mode) + os.chmod(file, mode) + + def get_inputs(self): + return self.distribution.scripts or [] + + def get_outputs(self): + return self.outfiles or [] diff --git a/Lib/packaging/command/register.py b/Lib/packaging/command/register.py new file mode 100644 index 000000000000..962afdcc9675 --- /dev/null +++ b/Lib/packaging/command/register.py @@ -0,0 +1,282 @@ +"""Register a release with a project index.""" + +# Contributed by Richard Jones + +import io +import getpass +import urllib.error +import urllib.parse +import urllib.request + +from packaging import logger +from packaging.util import (read_pypirc, generate_pypirc, DEFAULT_REPOSITORY, + DEFAULT_REALM, get_pypirc_path) +from packaging.command.cmd import Command + +class register(Command): + + description = "register a release with PyPI" + user_options = [ + ('repository=', 'r', + "repository URL [default: %s]" % DEFAULT_REPOSITORY), + ('show-response', None, + "display full response text from server"), + ('list-classifiers', None, + "list valid Trove classifiers"), + ('strict', None , + "stop the registration if the metadata is not fully compliant") + ] + + boolean_options = ['show-response', 'list-classifiers', 'strict'] + + def initialize_options(self): + self.repository = None + self.realm = None + self.show_response = False + self.list_classifiers = False + self.strict = False + + def finalize_options(self): + if self.repository is None: + self.repository = DEFAULT_REPOSITORY + if self.realm is None: + self.realm = DEFAULT_REALM + + def run(self): + self._set_config() + + # Check the package metadata + check = self.distribution.get_command_obj('check') + if check.strict != self.strict and not check.all: + # If check was already run but with different options, + # re-run it + check.strict = self.strict + check.all = True + self.distribution.have_run.pop('check', None) + self.run_command('check') + + if self.dry_run: + self.verify_metadata() + elif self.list_classifiers: + self.classifiers() + else: + self.send_metadata() + + def _set_config(self): + ''' Reads the configuration file and set attributes. + ''' + config = read_pypirc(self.repository, self.realm) + if config != {}: + self.username = config['username'] + self.password = config['password'] + self.repository = config['repository'] + self.realm = config['realm'] + self.has_config = True + else: + if self.repository not in ('pypi', DEFAULT_REPOSITORY): + raise ValueError('%s not found in .pypirc' % self.repository) + if self.repository == 'pypi': + self.repository = DEFAULT_REPOSITORY + self.has_config = False + + def classifiers(self): + ''' Fetch the list of classifiers from the server. + ''' + response = urllib.request.urlopen(self.repository+'?:action=list_classifiers') + logger.info(response.read()) + + def verify_metadata(self): + ''' Send the metadata to the package index server to be checked. + ''' + # send the info to the server and report the result + code, result = self.post_to_server(self.build_post_data('verify')) + logger.info('server response (%s): %s', code, result) + + + def send_metadata(self): + ''' Send the metadata to the package index server. + + Well, do the following: + 1. figure who the user is, and then + 2. send the data as a Basic auth'ed POST. + + First we try to read the username/password from $HOME/.pypirc, + which is a ConfigParser-formatted file with a section + [distutils] containing username and password entries (both + in clear text). Eg: + + [distutils] + index-servers = + pypi + + [pypi] + username: fred + password: sekrit + + Otherwise, to figure who the user is, we offer the user three + choices: + + 1. use existing login, + 2. register as a new user, or + 3. set the password to a random string and email the user. + + ''' + # TODO factor registration out into another method + # TODO use print to print, not logging + + # see if we can short-cut and get the username/password from the + # config + if self.has_config: + choice = '1' + username = self.username + password = self.password + else: + choice = 'x' + username = password = '' + + # get the user's login info + choices = '1 2 3 4'.split() + while choice not in choices: + logger.info('''\ +We need to know who you are, so please choose either: + 1. use your existing login, + 2. register as a new user, + 3. have the server generate a new password for you (and email it to you), or + 4. quit +Your selection [default 1]: ''') + + choice = input() + if not choice: + choice = '1' + elif choice not in choices: + print('Please choose one of the four options!') + + if choice == '1': + # get the username and password + while not username: + username = input('Username: ') + while not password: + password = getpass.getpass('Password: ') + + # set up the authentication + auth = urllib.request.HTTPPasswordMgr() + host = urllib.parse.urlparse(self.repository)[1] + auth.add_password(self.realm, host, username, password) + # send the info to the server and report the result + code, result = self.post_to_server(self.build_post_data('submit'), + auth) + logger.info('Server response (%s): %s', code, result) + + # possibly save the login + if code == 200: + if self.has_config: + # sharing the password in the distribution instance + # so the upload command can reuse it + self.distribution.password = password + else: + logger.info( + 'I can store your PyPI login so future submissions ' + 'will be faster.\n(the login will be stored in %s)', + get_pypirc_path()) + choice = 'X' + while choice.lower() not in 'yn': + choice = input('Save your login (y/N)?') + if not choice: + choice = 'n' + if choice.lower() == 'y': + generate_pypirc(username, password) + + elif choice == '2': + data = {':action': 'user'} + data['name'] = data['password'] = data['email'] = '' + data['confirm'] = None + while not data['name']: + data['name'] = input('Username: ') + while data['password'] != data['confirm']: + while not data['password']: + data['password'] = getpass.getpass('Password: ') + while not data['confirm']: + data['confirm'] = getpass.getpass(' Confirm: ') + if data['password'] != data['confirm']: + data['password'] = '' + data['confirm'] = None + print("Password and confirm don't match!") + while not data['email']: + data['email'] = input(' EMail: ') + code, result = self.post_to_server(data) + if code != 200: + logger.info('server response (%s): %s', code, result) + else: + logger.info('you will receive an email shortly; follow the ' + 'instructions in it to complete registration.') + elif choice == '3': + data = {':action': 'password_reset'} + data['email'] = '' + while not data['email']: + data['email'] = input('Your email address: ') + code, result = self.post_to_server(data) + logger.info('server response (%s): %s', code, result) + + def build_post_data(self, action): + # figure the data to send - the metadata plus some additional + # information used by the package server + data = self.distribution.metadata.todict() + data[':action'] = action + return data + + # XXX to be refactored with upload.upload_file + def post_to_server(self, data, auth=None): + ''' Post a query to the server, and return a string response. + ''' + if 'name' in data: + logger.info('Registering %s to %s', data['name'], self.repository) + # Build up the MIME payload for the urllib2 POST data + boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' + sep_boundary = '\n--' + boundary + end_boundary = sep_boundary + '--' + body = io.StringIO() + for key, value in data.items(): + # handle multiple entries for the same name + if not isinstance(value, (tuple, list)): + value = [value] + + for value in value: + body.write(sep_boundary) + body.write('\nContent-Disposition: form-data; name="%s"'%key) + body.write("\n\n") + body.write(value) + if value and value[-1] == '\r': + body.write('\n') # write an extra newline (lurve Macs) + body.write(end_boundary) + body.write("\n") + body = body.getvalue() + + # build the Request + headers = { + 'Content-type': 'multipart/form-data; boundary=%s; charset=utf-8'%boundary, + 'Content-length': str(len(body)) + } + req = urllib.request.Request(self.repository, body, headers) + + # handle HTTP and include the Basic Auth handler + opener = urllib.request.build_opener( + urllib.request.HTTPBasicAuthHandler(password_mgr=auth) + ) + data = '' + try: + result = opener.open(req) + except urllib.error.HTTPError as e: + if self.show_response: + data = e.fp.read() + result = e.code, e.msg + except urllib.error.URLError as e: + result = 500, str(e) + else: + if self.show_response: + data = result.read() + result = 200, 'OK' + if self.show_response: + dashes = '-' * 75 + logger.info('%s%s%s', dashes, data, dashes) + + return result diff --git a/Lib/packaging/command/sdist.py b/Lib/packaging/command/sdist.py new file mode 100644 index 000000000000..a28019b366e9 --- /dev/null +++ b/Lib/packaging/command/sdist.py @@ -0,0 +1,375 @@ +"""Create a source distribution.""" + +import os +import sys +import re +from io import StringIO +from glob import glob +from shutil import get_archive_formats, rmtree + +from packaging import logger +from packaging.util import resolve_name +from packaging.errors import (PackagingPlatformError, PackagingOptionError, + PackagingModuleError, PackagingFileError) +from packaging.command import get_command_names +from packaging.command.cmd import Command +from packaging.manifest import Manifest + + +def show_formats(): + """Print all possible values for the 'formats' option (used by + the "--help-formats" command-line option). + """ + from packaging.fancy_getopt import FancyGetopt + formats = sorted(('formats=' + name, None, desc) + for name, desc in get_archive_formats()) + FancyGetopt(formats).print_help( + "List of available source distribution formats:") + +# a \ followed by some spaces + EOL +_COLLAPSE_PATTERN = re.compile('\\\w\n', re.M) +_COMMENTED_LINE = re.compile('^#.*\n$|^\w*\n$', re.M) + + +class sdist(Command): + + description = "create a source distribution (tarball, zip file, etc.)" + + user_options = [ + ('manifest=', 'm', + "name of manifest file [default: MANIFEST]"), + ('use-defaults', None, + "include the default file set in the manifest " + "[default; disable with --no-defaults]"), + ('no-defaults', None, + "don't include the default file set"), + ('prune', None, + "specifically exclude files/directories that should not be " + "distributed (build tree, RCS/CVS dirs, etc.) " + "[default; disable with --no-prune]"), + ('no-prune', None, + "don't automatically exclude anything"), + ('manifest-only', 'o', + "just regenerate the manifest and then stop "), + ('formats=', None, + "formats for source distribution (comma-separated list)"), + ('keep-temp', 'k', + "keep the distribution tree around after creating " + + "archive file(s)"), + ('dist-dir=', 'd', + "directory to put the source distribution archive(s) in " + "[default: dist]"), + ('check-metadata', None, + "Ensure that all required elements of metadata " + "are supplied. Warn if any missing. [default]"), + ('owner=', 'u', + "Owner name used when creating a tar file [default: current user]"), + ('group=', 'g', + "Group name used when creating a tar file [default: current group]"), + ('manifest-builders=', None, + "manifest builders (comma-separated list)"), + ] + + boolean_options = ['use-defaults', 'prune', + 'manifest-only', 'keep-temp', 'check-metadata'] + + help_options = [ + ('help-formats', None, + "list available distribution formats", show_formats), + ] + + negative_opt = {'no-defaults': 'use-defaults', + 'no-prune': 'prune'} + + default_format = {'posix': 'gztar', + 'nt': 'zip'} + + def initialize_options(self): + self.manifest = None + # 'use_defaults': if true, we will include the default file set + # in the manifest + self.use_defaults = True + self.prune = True + self.manifest_only = False + self.formats = None + self.keep_temp = False + self.dist_dir = None + + self.archive_files = None + self.metadata_check = True + self.owner = None + self.group = None + self.filelist = None + self.manifest_builders = None + + def _check_archive_formats(self, formats): + supported_formats = [name for name, desc in get_archive_formats()] + for format in formats: + if format not in supported_formats: + return format + return None + + def finalize_options(self): + if self.manifest is None: + self.manifest = "MANIFEST" + + self.ensure_string_list('formats') + if self.formats is None: + try: + self.formats = [self.default_format[os.name]] + except KeyError: + raise PackagingPlatformError("don't know how to create source " + "distributions on platform %s" % os.name) + + bad_format = self._check_archive_formats(self.formats) + if bad_format: + raise PackagingOptionError("unknown archive format '%s'" \ + % bad_format) + + if self.dist_dir is None: + self.dist_dir = "dist" + + if self.filelist is None: + self.filelist = Manifest() + + if self.manifest_builders is None: + self.manifest_builders = [] + else: + if isinstance(self.manifest_builders, str): + self.manifest_builders = self.manifest_builders.split(',') + builders = [] + for builder in self.manifest_builders: + builder = builder.strip() + if builder == '': + continue + try: + builder = resolve_name(builder) + except ImportError as e: + raise PackagingModuleError(e) + + builders.append(builder) + + self.manifest_builders = builders + + def run(self): + # 'filelist' contains the list of files that will make up the + # manifest + self.filelist.clear() + + # Check the package metadata + if self.metadata_check: + self.run_command('check') + + # Do whatever it takes to get the list of files to process + # (process the manifest template, read an existing manifest, + # whatever). File list is accumulated in 'self.filelist'. + self.get_file_list() + + # If user just wanted us to regenerate the manifest, stop now. + if self.manifest_only: + return + + # Otherwise, go ahead and create the source distribution tarball, + # or zipfile, or whatever. + self.make_distribution() + + def get_file_list(self): + """Figure out the list of files to include in the source + distribution, and put it in 'self.filelist'. This might involve + reading the manifest template (and writing the manifest), or just + reading the manifest, or just using the default file set -- it all + depends on the user's options. + """ + template_exists = len(self.distribution.extra_files) > 0 + if not template_exists: + logger.warning('%s: using default file list', + self.get_command_name()) + self.filelist.findall() + + if self.use_defaults: + self.add_defaults() + if template_exists: + template = '\n'.join(self.distribution.extra_files) + self.filelist.read_template(StringIO(template)) + + # call manifest builders, if any. + for builder in self.manifest_builders: + builder(self.distribution, self.filelist) + + if self.prune: + self.prune_file_list() + + self.filelist.write(self.manifest) + + def add_defaults(self): + """Add all the default files to self.filelist: + - README or README.txt + - test/test*.py + - all pure Python modules mentioned in setup script + - all files pointed by package_data (build_py) + - all files defined in data_files. + - all files defined as scripts. + - all C sources listed as part of extensions or C libraries + in the setup script (doesn't catch C headers!) + Warns if (README or README.txt) or setup.py are missing; everything + else is optional. + """ + standards = [('README', 'README.txt')] + for fn in standards: + if isinstance(fn, tuple): + alts = fn + got_it = False + for fn in alts: + if os.path.exists(fn): + got_it = True + self.filelist.append(fn) + break + + if not got_it: + logger.warning( + '%s: standard file not found: should have one of %s', + self.get_command_name(), ', '.join(alts)) + else: + if os.path.exists(fn): + self.filelist.append(fn) + else: + logger.warning('%s: standard file %r not found', + self.get_command_name(), fn) + + optional = ['test/test*.py', 'setup.cfg'] + for pattern in optional: + files = [f for f in glob(pattern) if os.path.isfile(f)] + if files: + self.filelist.extend(files) + + for cmd_name in get_command_names(): + try: + cmd_obj = self.get_finalized_command(cmd_name) + except PackagingOptionError: + pass + else: + self.filelist.extend(cmd_obj.get_source_files()) + + def prune_file_list(self): + """Prune off branches that might slip into the file list as created + by 'read_template()', but really don't belong there: + * the build tree (typically "build") + * the release tree itself (only an issue if we ran "sdist" + previously with --keep-temp, or it aborted) + * any RCS, CVS, .svn, .hg, .git, .bzr, _darcs directories + """ + build = self.get_finalized_command('build') + base_dir = self.distribution.get_fullname() + + self.filelist.exclude_pattern(None, prefix=build.build_base) + self.filelist.exclude_pattern(None, prefix=base_dir) + + # pruning out vcs directories + # both separators are used under win32 + if sys.platform == 'win32': + seps = r'/|\\' + else: + seps = '/' + + vcs_dirs = ['RCS', 'CVS', r'\.svn', r'\.hg', r'\.git', r'\.bzr', + '_darcs'] + vcs_ptrn = r'(^|%s)(%s)(%s).*' % (seps, '|'.join(vcs_dirs), seps) + self.filelist.exclude_pattern(vcs_ptrn, is_regex=True) + + def make_release_tree(self, base_dir, files): + """Create the directory tree that will become the source + distribution archive. All directories implied by the filenames in + 'files' are created under 'base_dir', and then we hard link or copy + (if hard linking is unavailable) those files into place. + Essentially, this duplicates the developer's source tree, but in a + directory named after the distribution, containing only the files + to be distributed. + """ + # Create all the directories under 'base_dir' necessary to + # put 'files' there; the 'mkpath()' is just so we don't die + # if the manifest happens to be empty. + self.mkpath(base_dir) + self.create_tree(base_dir, files, dry_run=self.dry_run) + + # And walk over the list of files, either making a hard link (if + # os.link exists) to each one that doesn't already exist in its + # corresponding location under 'base_dir', or copying each file + # that's out-of-date in 'base_dir'. (Usually, all files will be + # out-of-date, because by default we blow away 'base_dir' when + # we're done making the distribution archives.) + + if hasattr(os, 'link'): # can make hard links on this system + link = 'hard' + msg = "making hard links in %s..." % base_dir + else: # nope, have to copy + link = None + msg = "copying files to %s..." % base_dir + + if not files: + logger.warning("no files to distribute -- empty manifest?") + else: + logger.info(msg) + + for file in self.distribution.metadata.requires_files: + if file not in files: + msg = "'%s' must be included explicitly in 'extra_files'" \ + % file + raise PackagingFileError(msg) + + for file in files: + if not os.path.isfile(file): + logger.warning("'%s' not a regular file -- skipping", file) + else: + dest = os.path.join(base_dir, file) + self.copy_file(file, dest, link=link) + + self.distribution.metadata.write(os.path.join(base_dir, 'PKG-INFO')) + + def make_distribution(self): + """Create the source distribution(s). First, we create the release + tree with 'make_release_tree()'; then, we create all required + archive files (according to 'self.formats') from the release tree. + Finally, we clean up by blowing away the release tree (unless + 'self.keep_temp' is true). The list of archive files created is + stored so it can be retrieved later by 'get_archive_files()'. + """ + # Don't warn about missing metadata here -- should be (and is!) + # done elsewhere. + base_dir = self.distribution.get_fullname() + base_name = os.path.join(self.dist_dir, base_dir) + + self.make_release_tree(base_dir, self.filelist.files) + archive_files = [] # remember names of files we create + # tar archive must be created last to avoid overwrite and remove + if 'tar' in self.formats: + self.formats.append(self.formats.pop(self.formats.index('tar'))) + + for fmt in self.formats: + file = self.make_archive(base_name, fmt, base_dir=base_dir, + owner=self.owner, group=self.group) + archive_files.append(file) + self.distribution.dist_files.append(('sdist', '', file)) + + self.archive_files = archive_files + + if not self.keep_temp: + if self.dry_run: + logger.info('removing %s', base_dir) + else: + rmtree(base_dir) + + def get_archive_files(self): + """Return the list of archive files created when the command + was run, or None if the command hasn't run yet. + """ + return self.archive_files + + def create_tree(self, base_dir, files, mode=0o777, verbose=1, + dry_run=False): + need_dir = set() + for file in files: + need_dir.add(os.path.join(base_dir, os.path.dirname(file))) + + # Now create them + for dir in sorted(need_dir): + self.mkpath(dir, mode, verbose=verbose, dry_run=dry_run) diff --git a/Lib/packaging/command/test.py b/Lib/packaging/command/test.py new file mode 100644 index 000000000000..7f9015bc8ec6 --- /dev/null +++ b/Lib/packaging/command/test.py @@ -0,0 +1,81 @@ +"""Run the project's test suite.""" + +import os +import sys +import logging +import unittest + +from packaging import logger +from packaging.command.cmd import Command +from packaging.database import get_distribution +from packaging.errors import PackagingOptionError +from packaging.util import resolve_name + + +class test(Command): + + description = "run the project's test suite" + + user_options = [ + ('suite=', 's', + "test suite to run (for example: 'some_module.test_suite')"), + ('runner=', None, + "test runner to be called."), + ('tests-require=', None, + "list of distributions required to run the test suite."), + ] + + def initialize_options(self): + self.suite = None + self.runner = None + self.tests_require = [] + + def finalize_options(self): + self.build_lib = self.get_finalized_command("build").build_lib + for requirement in self.tests_require: + if get_distribution(requirement) is None: + logger.warning("test dependency %s is not installed, " + "tests may fail", requirement) + if (not self.suite and not self.runner and + self.get_ut_with_discovery() is None): + raise PackagingOptionError( + "no test discovery available, please give a 'suite' or " + "'runner' option or install unittest2") + + def get_ut_with_discovery(self): + if hasattr(unittest.TestLoader, "discover"): + return unittest + else: + try: + import unittest2 + return unittest2 + except ImportError: + return None + + def run(self): + prev_syspath = sys.path[:] + try: + # build release + build = self.get_reinitialized_command('build') + self.run_command('build') + sys.path.insert(0, build.build_lib) + + # Temporary kludge until we remove the verbose arguments and use + # logging everywhere + logger = logging.getLogger('packaging') + verbose = logger.getEffectiveLevel() >= logging.DEBUG + verbosity = verbose + 1 + + # run the tests + if self.runner: + resolve_name(self.runner)() + elif self.suite: + runner = unittest.TextTestRunner(verbosity=verbosity) + runner.run(resolve_name(self.suite)()) + elif self.get_ut_with_discovery(): + ut = self.get_ut_with_discovery() + test_suite = ut.TestLoader().discover(os.curdir) + runner = ut.TextTestRunner(verbosity=verbosity) + runner.run(test_suite) + finally: + sys.path[:] = prev_syspath diff --git a/Lib/packaging/command/upload.py b/Lib/packaging/command/upload.py new file mode 100644 index 000000000000..df265c959127 --- /dev/null +++ b/Lib/packaging/command/upload.py @@ -0,0 +1,201 @@ +"""Upload a distribution to a project index.""" + +import os +import socket +import logging +import platform +import urllib.parse +from io import BytesIO +from base64 import standard_b64encode +from hashlib import md5 +from urllib.error import HTTPError +from urllib.request import urlopen, Request + +from packaging import logger +from packaging.errors import PackagingOptionError +from packaging.util import (spawn, read_pypirc, DEFAULT_REPOSITORY, + DEFAULT_REALM) +from packaging.command.cmd import Command + + +class upload(Command): + + description = "upload distribution to PyPI" + + user_options = [ + ('repository=', 'r', + "repository URL [default: %s]" % DEFAULT_REPOSITORY), + ('show-response', None, + "display full response text from server"), + ('sign', 's', + "sign files to upload using gpg"), + ('identity=', 'i', + "GPG identity used to sign files"), + ('upload-docs', None, + "upload documentation too"), + ] + + boolean_options = ['show-response', 'sign'] + + def initialize_options(self): + self.repository = None + self.realm = None + self.show_response = False + self.username = '' + self.password = '' + self.show_response = False + self.sign = False + self.identity = None + self.upload_docs = False + + def finalize_options(self): + if self.repository is None: + self.repository = DEFAULT_REPOSITORY + if self.realm is None: + self.realm = DEFAULT_REALM + if self.identity and not self.sign: + raise PackagingOptionError( + "Must use --sign for --identity to have meaning") + config = read_pypirc(self.repository, self.realm) + if config != {}: + self.username = config['username'] + self.password = config['password'] + self.repository = config['repository'] + self.realm = config['realm'] + + # getting the password from the distribution + # if previously set by the register command + if not self.password and self.distribution.password: + self.password = self.distribution.password + + def run(self): + if not self.distribution.dist_files: + raise PackagingOptionError( + "No dist file created in earlier command") + for command, pyversion, filename in self.distribution.dist_files: + self.upload_file(command, pyversion, filename) + if self.upload_docs: + upload_docs = self.get_finalized_command("upload_docs") + upload_docs.repository = self.repository + upload_docs.username = self.username + upload_docs.password = self.password + upload_docs.run() + + # XXX to be refactored with register.post_to_server + def upload_file(self, command, pyversion, filename): + # Makes sure the repository URL is compliant + scheme, netloc, url, params, query, fragments = \ + urllib.parse.urlparse(self.repository) + if params or query or fragments: + raise AssertionError("Incompatible url %s" % self.repository) + + if scheme not in ('http', 'https'): + raise AssertionError("unsupported scheme " + scheme) + + # Sign if requested + if self.sign: + gpg_args = ["gpg", "--detach-sign", "-a", filename] + if self.identity: + gpg_args[2:2] = ["--local-user", self.identity] + spawn(gpg_args, + dry_run=self.dry_run) + + # Fill in the data - send all the metadata in case we need to + # register a new release + with open(filename, 'rb') as f: + content = f.read() + + data = self.distribution.metadata.todict() + + # extra upload infos + data[':action'] = 'file_upload' + data['protcol_version'] = '1' + data['content'] = (os.path.basename(filename), content) + data['filetype'] = command + data['pyversion'] = pyversion + data['md5_digest'] = md5(content).hexdigest() + + if command == 'bdist_dumb': + data['comment'] = 'built for %s' % platform.platform(terse=True) + + if self.sign: + with open(filename + '.asc') as fp: + sig = fp.read() + data['gpg_signature'] = [ + (os.path.basename(filename) + ".asc", sig)] + + # set up the authentication + # The exact encoding of the authentication string is debated. + # Anyway PyPI only accepts ascii for both username or password. + user_pass = (self.username + ":" + self.password).encode('ascii') + auth = b"Basic " + standard_b64encode(user_pass) + + # Build up the MIME payload for the POST data + boundary = b'--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' + sep_boundary = b'\n--' + boundary + end_boundary = sep_boundary + b'--' + body = BytesIO() + + file_fields = ('content', 'gpg_signature') + + for key, value in data.items(): + # handle multiple entries for the same name + if not isinstance(value, tuple): + value = [value] + + content_dispo = '\nContent-Disposition: form-data; name="%s"' % key + + if key in file_fields: + filename_, content = value + filename_ = ';filename="%s"' % filename_ + body.write(sep_boundary) + body.write(content_dispo.encode('utf-8')) + body.write(filename_.encode('utf-8')) + body.write(b"\n\n") + body.write(content) + else: + for value in value: + value = str(value).encode('utf-8') + body.write(sep_boundary) + body.write(content_dispo.encode('utf-8')) + body.write(b"\n\n") + body.write(value) + if value and value.endswith(b'\r'): + # write an extra newline (lurve Macs) + body.write(b'\n') + + body.write(end_boundary) + body.write(b"\n") + body = body.getvalue() + + logger.info("Submitting %s to %s", filename, self.repository) + + # build the Request + headers = {'Content-type': + 'multipart/form-data; boundary=%s' % + boundary.decode('ascii'), + 'Content-length': str(len(body)), + 'Authorization': auth} + + request = Request(self.repository, data=body, + headers=headers) + # send the data + try: + result = urlopen(request) + status = result.code + reason = result.msg + except socket.error as e: + logger.error(e) + return + except HTTPError as e: + status = e.code + reason = e.msg + + if status == 200: + logger.info('Server response (%s): %s', status, reason) + else: + logger.error('Upload failed (%s): %s', status, reason) + + if self.show_response and logger.isEnabledFor(logging.INFO): + sep = '-' * 75 + logger.info('%s\n%s\n%s', sep, result.read().decode(), sep) diff --git a/Lib/packaging/command/upload_docs.py b/Lib/packaging/command/upload_docs.py new file mode 100644 index 000000000000..29ea6e92e8bd --- /dev/null +++ b/Lib/packaging/command/upload_docs.py @@ -0,0 +1,173 @@ +"""Upload HTML documentation to a project index.""" + +import os +import base64 +import socket +import zipfile +import logging +import http.client +import urllib.parse +from io import BytesIO + +from packaging import logger +from packaging.util import read_pypirc, DEFAULT_REPOSITORY, DEFAULT_REALM +from packaging.errors import PackagingFileError +from packaging.command.cmd import Command + + +def zip_dir(directory): + """Compresses recursively contents of directory into a BytesIO object""" + destination = BytesIO() + zip_file = zipfile.ZipFile(destination, "w") + for root, dirs, files in os.walk(directory): + for name in files: + full = os.path.join(root, name) + relative = root[len(directory):].lstrip(os.path.sep) + dest = os.path.join(relative, name) + zip_file.write(full, dest) + zip_file.close() + return destination + + +# grabbed from +# http://code.activestate.com/recipes/ +# 146306-http-client-to-post-using-multipartform-data/ +# TODO factor this out for use by install and command/upload + +def encode_multipart(fields, files, boundary=None): + """ + *fields* is a sequence of (name: str, value: str) elements for regular + form fields, *files* is a sequence of (name: str, filename: str, value: + bytes) elements for data to be uploaded as files. + + Returns (content_type: bytes, body: bytes) ready for http.client.HTTP. + """ + if boundary is None: + boundary = b'--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' + elif not isinstance(boundary, bytes): + raise TypeError('boundary is not bytes but %r' % type(boundary)) + + l = [] + for key, value in fields: + l.extend(( + b'--' + boundary, + ('Content-Disposition: form-data; name="%s"' % + key).encode('utf-8'), + b'', + value.encode('utf-8'))) + + for key, filename, value in files: + l.extend(( + b'--' + boundary, + ('Content-Disposition: form-data; name="%s"; filename="%s"' % + (key, filename)).encode('utf-8'), + b'', + value)) + l.append(b'--' + boundary + b'--') + l.append(b'') + + body = b'\r\n'.join(l) + + content_type = b'multipart/form-data; boundary=' + boundary + return content_type, body + + +class upload_docs(Command): + + description = "upload HTML documentation to PyPI" + + user_options = [ + ('repository=', 'r', + "repository URL [default: %s]" % DEFAULT_REPOSITORY), + ('show-response', None, + "display full response text from server"), + ('upload-dir=', None, + "directory to upload"), + ] + + def initialize_options(self): + self.repository = None + self.realm = None + self.show_response = False + self.upload_dir = None + self.username = '' + self.password = '' + + def finalize_options(self): + if self.repository is None: + self.repository = DEFAULT_REPOSITORY + if self.realm is None: + self.realm = DEFAULT_REALM + if self.upload_dir is None: + build = self.get_finalized_command('build') + self.upload_dir = os.path.join(build.build_base, "docs") + if not os.path.isdir(self.upload_dir): + self.upload_dir = os.path.join(build.build_base, "doc") + logger.info('Using upload directory %s', self.upload_dir) + self.verify_upload_dir(self.upload_dir) + config = read_pypirc(self.repository, self.realm) + if config != {}: + self.username = config['username'] + self.password = config['password'] + self.repository = config['repository'] + self.realm = config['realm'] + + def verify_upload_dir(self, upload_dir): + self.ensure_dirname('upload_dir') + index_location = os.path.join(upload_dir, "index.html") + if not os.path.exists(index_location): + mesg = "No 'index.html found in docs directory (%s)" + raise PackagingFileError(mesg % upload_dir) + + def run(self): + name = self.distribution.metadata['Name'] + version = self.distribution.metadata['Version'] + zip_file = zip_dir(self.upload_dir) + + fields = [(':action', 'doc_upload'), + ('name', name), ('version', version)] + files = [('content', name, zip_file.getvalue())] + content_type, body = encode_multipart(fields, files) + + credentials = self.username + ':' + self.password + auth = b"Basic " + base64.encodebytes(credentials.encode()).strip() + + logger.info("Submitting documentation to %s", self.repository) + + scheme, netloc, url, params, query, fragments = urllib.parse.urlparse( + self.repository) + if scheme == "http": + conn = http.client.HTTPConnection(netloc) + elif scheme == "https": + conn = http.client.HTTPSConnection(netloc) + else: + raise AssertionError("unsupported scheme %r" % scheme) + + try: + conn.connect() + conn.putrequest("POST", url) + conn.putheader('Content-type', content_type) + conn.putheader('Content-length', str(len(body))) + conn.putheader('Authorization', auth) + conn.endheaders() + conn.send(body) + + except socket.error as e: + logger.error(e) + return + + r = conn.getresponse() + + if r.status == 200: + logger.info('Server response (%s): %s', r.status, r.reason) + elif r.status == 301: + location = r.getheader('Location') + if location is None: + location = 'http://packages.python.org/%s/' % name + logger.info('Upload successful. Visit %s', location) + else: + logger.error('Upload failed (%s): %s', r.status, r.reason) + + if self.show_response and logger.isEnabledFor(logging.INFO): + sep = '-' * 75 + logger.info('%s\n%s\n%s', sep, r.read().decode('utf-8'), sep) diff --git a/Lib/packaging/command/wininst-10.0-amd64.exe b/Lib/packaging/command/wininst-10.0-amd64.exe new file mode 100644 index 0000000000000000000000000000000000000000..11f98cd2adf1075b7ff7be7f02ebc8e743bb6b9e GIT binary patch literal 222208 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

puJW;Z>wC?j_riSDZ%2DU~vX#q-nc* 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 S{tACZRn>yU^H#1j;{O6W#h3B` literal 0 Hc-jL100001 diff --git a/Lib/packaging/command/wininst-10.0.exe b/Lib/packaging/command/wininst-10.0.exe new file mode 100644 index 0000000000000000000000000000000000000000..8ac6e19b8eeaf7642387123c749f416251c496ea GIT binary patch literal 190464 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%0DnD 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�Cv5NA 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 literal 0 Hc-jL100001 diff --git a/Lib/packaging/command/wininst-6.0.exe b/Lib/packaging/command/wininst-6.0.exe new file mode 100644 index 0000000000000000000000000000000000000000..f57c855a613e2de2b00fff1df431b8d08d171077 GIT binary patch literal 61440 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

{MZ3UJ@%Cxk`nZoeb-!Ep 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 UPR*$~HK*p(oSIW}`oAUpFD=9J00000 literal 0 Hc-jL100001 diff --git a/Lib/packaging/command/wininst-7.1.exe b/Lib/packaging/command/wininst-7.1.exe new file mode 100644 index 0000000000000000000000000000000000000000..1433bc1ad3775ec9277e13ca8bdca3a5dbc23643 GIT binary patch literal 65536 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 literal 0 Hc-jL100001 diff --git a/Lib/packaging/command/wininst-8.0.exe b/Lib/packaging/command/wininst-8.0.exe new file mode 100644 index 0000000000000000000000000000000000000000..7403bfabf5cc10c13ef2b6a2ea276b4f6d26ff37 GIT binary patch literal 61440 zc-ri}4OkOr)-XIF0Rn=F3KbP?TCqh@i$z>1YJ-x9N+22uq9P&$hloJK3~DPB8|X5Q z>DsRCvb%0;S9fc7eY&>whgPc~6!528{Hfwkt!;U_Lt|IkvWiNb@0@!k5YcY;d7tmS zzU#ZL22W=0|8t+e`<#33jHbn#DJ4ZwDgaTWs9H+;=&7InbOH=~@cn_*o`8=Ys#PR^ z^w2^&zeHE;C|c>rUaiZ?E-Wl!bSrE+2V1DiFVrQbT6C+6a%~fXg99VvKDJg*+uzo) zpxpzh6UMiH23PU1vF#Vgbz1v*a(%4*B3ws&6-@F+e5GmsgtW1=KTFD=Y;PpjiE{mM zh7smH_=hz$f4x9UXcc& zTJPdK?cfQp-svC1ZD zsh+*oVC9*JpsMSdN+(oF;|n4}()slfT1&+YiYc})wThvOp3+l%XVvwL3??hR$&V64 zYl2#|@PuYFNPn?ZGZvS+Q>S0nkj7tiUJbHX)A{M!pmxml0~m6~v1-=8*`S6n zoU+mwy^E~cYPE=Lka*77Epmn)P1P^S7y`6%b}QN8BC8WwEz}T(h@uDs0iOFiZbKz! zx0wkrOX#>Ci(_?~%68Iaklja2(^b{Md?!l7*nRFEg&jkx@0F_4AtidMW1HO8AKGSQ zbo>^QP>Bh|JSE9<8M@3O8!ob8VrV;3u&f8O%x0kA6-dXiA#4ELUqXa{@N9_HuChYL z4#)sXTq0Bl9Dwc`?0YH>-UBprKVr37?LHPUR1Lhca0az_jyI^|o7kaP52sHH60Os8 zK!cdNtYu4%N>rZeEYqqje5Tr(uBmz%Cp*<;Xt7gfA=g4tydj7)wD5)yJ!d$<8^Y{U zX&PqB8QQSYw=vhvT;>_0u<3EAS{CkX3$}9|@Oi}SoK+z#k z7;L7eL7`m*a{*+oLmjiQ+=vpI!HkzGM0i(l4~@AV$7+#}VK5hq(69jb*BE4k83)0P zv-l=5AJ?u3J#cx$RXvEv>+9=5az5m$4#K4PsuuX>3|slC6Xa$SU)4r#cJftc;KsuL z0kc1KNh#B+jr6vxN5`&6~CXc`BxF)?X5qI`f?`+;Qm+EC zIc+;ASE6q%XV}6UwmPR$Ogv}U%^P-$(LxE-1!>(wQex<2tlp@ts)LG}%0npYh8?_V zhfCE0?VuIg0;mqh7E=GYq(KlQgv#5ZNHBX4#|P91Ub97Zr)Iw~$P8Zt&0=X7R1w)N zW;-R^xs95NqVzCUI-IJ8VzD%2zQ|Ua1>+A`8dg;&;o&UXNiku(VVB;_8#ZeW>jMFa zEp|UKZ6_csXNiVgafV%*O{?yRV#hrALe#@s#2A4$!&FLCFav^OsTui^H5{hRoAxMM zqS^?VO?!kDW|^#7g8pby-DK{a(PQ3}ehl-WV9!Ryw55z{+GDIw!aU3TKGN`+l^ve~ z9x-(thhaXTiSxnO-Q>S>XA={hy4X^J!f|i?6Lz@a{lOZw@Dls$(n~zDnpodv($10wRtZ!`>14*l0ErwP?EvO&F zcS^6?wDu(U?lOEqxHrz~aExmhsfM|zBfp($V>@sWtr~%jvu*r<* zAi?O(&?wzQuLz*IKM**NwAyB-Yj(qA6jplYZtQfYG?dy*7eFibuX)2ZUhPWTx`3|T zjm#JXg#*lVA{2I%0KcIe=E;Lx%{NO$5m>ayQ&dTc!lfb#i>522DM$`O53pj+Jr%y>0m`R(G1(4u3%BN#y2s3q;oY4)Rz!LcLS9=_M>^h{^SbY zg|xt6S40$e7qU1DK#NOrs0J?SlT;82GokBKq~1*uS^_b!_@*WVKelgrB8))+Ia^r zhFxYFx~mXFbwt(4k~&T*mCa}8T;LJOu$YA}=i*wgm79xac~cJ|+NX1S$rWa?^08lzg6vveh1!CyK{7vLRd=z`m&(rpjiCQG;7a647H?S$J%>GrC4Hs*T#WzHaI zDl3UA*TXy;$96GK$FbcE8^?As#c}LaW)-l1B^?fp#M69t{GAMDI;|xLr1j8hyxC67 zM_La|!S1&mMd^?L99aT}LQLy6i&LO#7ckY?q@Blp%^NzmBbRu?)$Pb_-XLs8!2=R0 ziKF3h+micu*Eji_v~I05dF@)yH0Ykbd3N^AGr1p42%%&;wD-|L06H9vGhNkG1_00E zOx>DFirjQ+DsLcNOkJAFf5J`tc}?Y4Bm*e+DQ47bDnB6^?V3vTxp-C`|E;F-eYl-E zP*eF1T<3%OB|as`5*ZonLsq&4Ng28Y7oa*ldgszgW>+ri*od}&50ukal^~9>ZNx5b zb0B%I^3B4xAZ3!T4}^6Y_LwcrhHfPZ2R3=!7j`0F*5aD&EBq}9FzQMNw?0(=l)PY+ z4^lQ+o2bAha396A&I}}4S{KxqhBz9!Gu(l8A7b8s6)xLO;06d|%tyk4q(05{7DW#? zrmgi*xU*0GdippJz1G7bzB8sSzKw~GGxRW1;tXBPq&P!2Gd{kPiKM9(B1j-;&4MqP zs~RTCcUJ-F7$5O0;L101Omv(7r58Sk8px&ykSdP+D6t{9HM4yLz2E#deLaK2i^j(;IpnFyWW4|rsxbBWMp zXcy=+cD&oeJ?N32u*;MBMC#u&4u*YN=lX~+N>hm*0G*2i+=eiqSrxH-E%7bF_-Q@Cyx1wsG9znQ<`R$= z$%v{|-bOU0F3zB~9>NF4%nY;HxGtXT#FW4su-&$I)MuTk<6I7Wx`&AESwvz zs_sG;+$JroazRpK8qtQ1TZpWyfj3E-N_>i&1lP@YWr~xSj3j1MlBViQNWU&zMDwkw zd_fL_)$0KBr#1TxTD~jQGZ1Xz7VZY^(yRHsG3gucx5c>*wP!bn_V9uGQz81 z>)k8t(KNNLZ+716oI6JzpBao}UtLQ|Jl0|&SuPtrF5NF^0n3H25=o4i!9Ic;0>Ahd zaMwNJu71wk)eq9JF6JYBmQb816LCKH=`q*kZ2_g#uAuQC0jgps?o{yIPS8tS(NZk9 zeuICt6?u-~xfM--cQB83;GDCpM`{p`l~lN@lPDf;z_C7u0eBeSv?L3>b?5YGSPCd+ zrgM7dMf7XqtRS2)@)qQG=j&L?n*;5XD{0_-W$OaF3Jt8D$Q=0T+dGLLQ{(VB<^{wt z5Ak-)HF?NWiaP!n6U?8DvZ|s^a-oQAlu^Z-s-ui*{tl;z zDpwKjOwwTB6KAN@RQ?*Ha&TMaqNS40PzWw0~-R5AxGxjRP-I_|;74dcD6QU5a0LQw6NUP{1j8LnWt__7=d@yF+B*Jgmd1 z`QX%I>Ub9ti?|{SW}^SLh3Z`e!a@A2fzIhMqC3Q8Xoo>JInX=8yUn;0qexFrCrNak zK1J@5hUPx?be08aKoKD_WkN+ZL|WOP`C4=`arLxV(_N;6B-}&(x7ih3W-Zt->^?B# zw>NImYXRMuqw}Q?i0VP``QQ#(B%!QMPsB-C#B+9qg){7dRRYq7bWLb5OA;XVGh6K% zyT-zgG^1rdO@q7u;b4rZC{seJTqn>Th`FaB_{ZxxRMBFb?LpweZeS;1l(j6TPF^h| zpxMzQT8acdouXJ9t)(boS%P=)qd_sO)sRW@)0dMyKr3h1*=(r56}{O|`PyhuAa)Z( zMB+J`dKN<@1BOTtU!xz3r+YXVQ#$XU2UO2~3M+%!kSqH&X?OON(%|*A?0@_wUR1qwL7+UN= z#S3LH$AvS(Z7SR%0?g6f*Yt{u7C33>G!rYbl&sW*iC`P;3Ld?Jv>v{NH*`g{Mj3kS z)B--OJ1VVHS-;@7QAbX8o55*}0#Bz|N|9cc6OxneYgI^I@kwvf53-e#&X=h*l`d4> zvLG$)Lse~uM7r=Y7~9u7iBZg8LeP9Wpp@8f&=fPy3a$&02m(fJuv~9zCRX|yEF-Ob z1mk13No13X%(S4Sh(DwU)u(@u6BQUOP^9y-%iN=RL&%1;>Z$1747K~f;KkCo3nkmX zsBdY@5-!Z}NK!$>=DRJKp@286CBhSiqW-PRwHCUZPY zszS4ECKItprDkz&u8cCfmJaGQp?qij_ZiJq%b~~@G8yO$@6A@2j<^@Q0nb9(Zi$6q z%@(=>+wP@F$yOq_*s;}2|E{-ydqGVGz+z7)6m-&$M&e#VMM9`v@>G6#r!Nm1fpO(c z1zAKZf~Uj!@?o*G%33i1wyS{sxfs6ska&@Bva{+s8$eb`se`KPnk{v>m4$Jx^$VrV-t3e@HgZ`9UIM&ICxnDI80}Nmh`TtiGvv;DZ5Y&)o!$nsJqdoY}zi>1lLsM zQ|?zg8Bz^w5G2(;BvIyDtHQimE%gZJB-O`5jl*}~J}jidn)oH5tqZb-;z#WBiNlzr#n42 zxXmHF`~>tvlQ?JN6J(R2qf9~Fe^?Ag=--Iw1wF&Tb#}L1~9Y-d?$cLX4<;2&#n! zS}B5YYso&7G_&1th?jR;;o%=JsBd8kE8ISIWx7n| zyKMWWh;ZlwqpX+{F?GU?snB5~YMSgZEFxQ_k=|RSJJ9YU6Nu770DeB5%iPjzsM%Hq zdgc!|o8ShpahW#FPq(YU^*WRUZ8470f)R;tW@4S)zHGQ%n@&fUAx*s^4Ojy>9~6|Z zGP0g~74g&CfZT_0^|ONUL${e$m63ffIaMdc6Bxp7$2bEwUcrS!<2;giN=~1azRmQR z^|*|ZhG?*%<{45G!x1STLOkpSd@Q>7edW0J_$(0jbHJbQ9mCB|Fno$KXI zIi0jKyx9!?GVKRT33$y=4>vs}nt-th12x#Zp0vL^_9B(g`Z~+173?r9r;(oY+Pl!v z(&$RcI1ub**`?Qek;p8le}QrrrZ>wOM4OjlSL|jnG(I?<0dZK5`yu(}zri z7cvloZ;=Tb;ypO6UEj+e7*~v4+oUIMW9%8?2(}XS(*s?_P~6Ap>j&@X_Gj*z$dglW zBD%XKa_G!6q}l&yCf57UBBl<>#Jf=5r8V<>EP9eVrH$hskLWcArDJ}VG<;hrv%H$!y+U`h7(`d>~5li5prux;Kq(p{A-5(lOWLV)22Lrg9!C`Sc246eMOq zf^I9vopoFTIko~qgA?T#8dOVW$yHSYjy5s)W?}ne%9Bx7jv3GQWnhnHft^fwCL0ER z#0ZxQ3+EGCO7AT9+8xkZ@aaL{#u{hXa__DhNC~PNZrO*J&2&w9KjR{AxyjXnrSWf*8(m>P4A{#I^T(r z;udL3&Dj`@&!(guU3PO*Vr-2ZHb7(`l*xQW^*Kwd4o+Mc4C~htw~;tSG1q}hL(JBD zViZYS*ug@=m*_5ECD8_v2&+{NH^_p6@qHv;wMW~xa1AAn=e=8oq0k7#gJqgShHjti+!uZ7oEt@M#s5$7Mo5zgDtjv%r|Zo&8&pZGli# z10ioQ2~(uAIiUq46-wjg0o%yOUMiOM|5`wfNglM~T~~Uo*?>t%(so7fR-462f?2XT z6N|@?>`TnjHhCK=%SBD)Z!lt=WzV!g9@z+&&M`IlQ?u=X>Es~}o;p!BA+39)rOs-R z9*Ri;1uk?}vbK}4ytf!1PbYCk5V{Cp3cv|t91eMBTw(cSfsqz#{3cue_WH09|Qd|>et zhFq%jqh?$RID5!GF_`Ho#D})~kOv(f3lZ(aNKeNNW%hoCIKB4~%Y@xt4^{Z-yS%Kk z^k~Inmx(>L$abUCDp}*ih{RrVY~=;*g zMAU?~!u$yoIhmnH$6ZS_`GhcY0y^!;O=1KW;U-@s@^;+>Z?~On@9}&0z%}Ng^8?%v zQ>NPkIku9^+zcK(ms!naZp&gGuvqAw8^}B)hg{#W0L(eP1Mx@*iR>)yFbj#}A-59`M;%f{!#bGZ9sr3HIfZiGiS=N8+d;{u zAZ@c~*c?~3IfFNC;#4oLAL(ok;QDX$*X&INV@RSZETgn z(4wKb<84Yh-9c)u?N@WMTyutK*wXP!@0!cKYBFbgvy;6$2u~MQ-V{Z*S$Y@%6=yrU zmCQ`hu+2Tax5-rRChBzI7B(5#TO8qC?4K?K+{P!21|V)tykWCu(-u5DLO6=DTOnC? zU%=Nr1jdm@=F8P)p$xp5jJV8g&@(e!WVean??Ob<`mEO;ONcvE&fe*rt+ngvEzc79 zHOsOfm~Y@iZ3rzip2fI)ED0?%9-lsvgcgguLyI%yVbK-xC?e)MXQ(8{zrYFK6m!(A z;`OS8_$IcSpN{UBW`8|C3$zj!?SNZ}k7K3hf9Jp-!^eTWX>i?na(oO=CI6C)fd_MO zd(L?!$XS;I5-3g)@dV^%x@AM}%rKl4(<;exenu%8Hi>Kn)Xg!bq|KsQW>gRfoW^`}x9L>N; z2WL+n3eJtc%*?{WCo}NyNgN(NnT&@|9)qPkD&}Y$t0o%(nwRT2H8~rAYdYI4%)LX_ z67aRty7(q?D8S>75Dx+m%Xu9s3hF&l1hhH%xhQ%aRD#m~sG&lrxlcps>BELcz1MU~ zTGNBHWRo|7n(!?cB_=vaQ-whyuuG~#SVp51rO8hu2-DK! zov0A2{LI1`|vi_;lefV{mCs7k0umtw|M432g%I z{er9U?p0!GDN+FE#>S{?A z*vCx~YLLEVnc(9ri$EU-r5iUUQU4i;>g?lVEoV(K z!S-0pT;?VLUx@(L=P;!P@^fq*SJo(o;zLe8Hj+H9Y;hUtpys7!LtEDvkOaetRYVNH zK#-EMM#w544?^3_WA&t@tFL6WRKm`XO1k?>3Z;^~Xe_aSL25Q^noNSV>%%t2pvy+_9Y$-e{FPs4adVrV(it6xJ|kV zDt~~PX~g9{fJVYXUvcWGLv2pnA3J9^z5xM|nnGSkn%P3PO11cpNF{uJ zD75revYW|6L<`+0)sjb@P>sQV(7YX=Bn$07pu%}}6Y)5~>sN{4!ToK<-z+Wh z9%JM$KR5?5Lt07u-w%1jtU=y0ID`h@3dTa(HK0}Gd3`phAx1>oaVU0>=fHx#5^PKr zuFy1UtE7kr5Qq10tCMdPzWL6vx(DgCejBodl05EBexCGMFd zN2p>>#UEo#d_s8q5yr@w!@2n(+m7$a&bcVIj`rGexV6YumuH(wX*uZ9^PM-m)P#zgq9>MxI8zUBE;BE^TQM*DcjTEi; z3!3<5R%P#Tn_v#w_>Vbbc$q)9R(<2Vdk}xJ{+4Q{^Cn=XE>k)`*SO|)J8$`~(O9{p z2yX3U!52@=xkJpPuLyAQ%fmbHiKzY7B8Y*T^~sBXV$J%60jR2a^P)|dS_D*}$SMp@ z#+q_dD?1d#Le6%6OrM2hJ;LUztLnf23m{1G>2>*9=&>>#XnYUb%LIAb(t z9M4%Jqq4NTF*3f*5yqzmnWI|fi=~r=)en+i)j0E_DR)AYF*?2h3`D7t%k1H22f-2_ z)nMk0laSVWsPg6UuMq6U);#;lh%zWMv9^c8#jhh+77~*_b-?*77cA9g> z0M@VBF9?}8Gl-w7<(Ex$+Ae`ZJdHE7^FDwrKT`$vjx(I${hZz5oarhUV9s=1P~hUKRmy%ST9X5S#u)Xjf<`U~X+(a;0^IGfc@ zQR_Ir(}zjeaQi_X4`AV%oh}R;1wv^Iv-`LVg1N~cJlJID;KkA~ff_{&eauBDg&vyy zP^LTnj;C9UI%l4i9DjBuMT^&sYK5kfgHp}DhWaaN{&;Uojc=%(@u0D4^k{nUe_eH^6BS+90pQ^lu5z%Zq9rOjAr2}aj-F(OC_pvoCT z1jR6%pt|0zzH!?5rH}J?0AiOAjcgkQsAhI{j7JPfA8WCXp^${BmyDArw3L(rg(!(a zSAa6gHXstzf!2=mK;(+sYHpd932L4x;d*8uMdjUSpstXtrI=MekdOgL5J;x${9M7Q z^KKZmlnjii{fubcg~f-#{|Oe{GCdL?oyrDb3l}qaagJI}$mUU4~4otG@C^MN)(Bc8SHf9{3 z5W>$1i#o!mbtzlpPnN2pTHH$B)HR zF0GXr1XGBguPX`QO|8|cL84KQzl#Fg;!WL-Mj4%pM(V)GP$H^az;~V4Fh#B= zM0X7Kmy}r^|9aq$E3;>OzF`an3I*olbFH#I=IF-N-+}Q2-Bs2r8_=rFjFuEO5EZsO z2)fNi6bS-1hjj9;_g0P=gnAYV=8xgZIx&LW*)XiqUFyae$b_@ta zT$LWxoBQLqDkarxa9&ZIbaO@(-YHX^0>wO*#EeX01}CxOfqau+Kmh^8que8#{WN$D z<(q_WZis;WU`)cw2MP`b-zOC?NbRTw2cZ`;i6$P>9SHajNOzoXxN&j}z6h?qLmOXD zm>7ag92-EGXw=K*W+3V}3LWURrn0~n`|gf2NQ%xIu6k}!=QWj22H<8Tkwvy$7(Nth zd(_o%RbglcN`u~Z991I)2YM)|{&AEC_EPXcUxfl?dwIRvm!$P@!x?k&UgNA&!D8sV zHgH4v@=>JPQkcM+!OrsBQER}dp@F{mNZGvrCfezOiR#s!HGiYJ-{{iNxZorIQfSTo zAX6K7Vz4?pulU!ym3drZO0j69(J#2BG>N*oU}`#rX5r7lMh?fb_oPS?|vvzMD}& z$UFw28=>^^pnu%|N6_a}783uEPf1Q1-N36h@GY>=YW5ukpViQ|rXG{pJm*bqNs6b$ z{jk*M8vlN7kY?Wr;t4^w@i!e8API9t*~{Zi7xK7dqfCyDRY3eaXW0cUo1xj)=zdV@ z?@|BzxmD1aE6E4@<6EGwZs@WKgJw0|)jhZ_HI1m&bwJJr|7e8{daS$1-V99tJh{4!Q^j&26Uq zh+Z#uCE~z7!q5Ttid`jkG7LJAY!DF4&#`nv*;OB4j>7_VcvHK`Ug45pow>>-X@S8U z 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 literal 0 Hc-jL100001 diff --git a/Lib/packaging/command/wininst-9.0-amd64.exe b/Lib/packaging/command/wininst-9.0-amd64.exe new file mode 100644 index 0000000000000000000000000000000000000000..11d8011c717c3a1c54afbe00e002fab6d37766c6 GIT binary patch literal 223744 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

*fXaaT~vFh}gL+%0h0vLL`8; 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 .o/.obj) + # * library files (shared or static) are named by plugging the + # library name and extension into a format string, eg. + # "lib%s.%s" % (lib_name, ".a") for Unix static libraries + # * executables are named by appending an extension (possibly + # empty) to the program name: eg. progname + ".exe" for + # Windows + # + # To reduce redundant code, these methods expect to find + # several attributes in the current object (presumably defined + # as class attributes): + # * src_extensions - + # list of C/C++ source file extensions, eg. ['.c', '.cpp'] + # * obj_extension - + # object file extension, eg. '.o' or '.obj' + # * static_lib_extension - + # extension for static library files, eg. '.a' or '.lib' + # * shared_lib_extension - + # extension for shared library/object files, eg. '.so', '.dll' + # * static_lib_format - + # format string for generating static library filenames, + # eg. 'lib%s.%s' or '%s.%s' + # * shared_lib_format + # format string for generating shared library filenames + # (probably same as static_lib_format, since the extension + # is one of the intended parameters to the format string) + # * exe_extension - + # extension for executable files, eg. '' or '.exe' + + def object_filenames(self, source_filenames, strip_dir=False, output_dir=''): + if output_dir is None: + output_dir = '' + obj_names = [] + for src_name in source_filenames: + base, ext = os.path.splitext(src_name) + base = os.path.splitdrive(base)[1] # Chop off the drive + base = base[os.path.isabs(base):] # If abs, chop off leading / + if ext not in self.src_extensions: + raise UnknownFileError("unknown file type '%s' (from '%s')" % + (ext, src_name)) + if strip_dir: + base = os.path.basename(base) + obj_names.append(os.path.join(output_dir, + base + self.obj_extension)) + return obj_names + + def shared_object_filename(self, basename, strip_dir=False, output_dir=''): + assert output_dir is not None + if strip_dir: + basename = os.path.basename(basename) + return os.path.join(output_dir, basename + self.shared_lib_extension) + + def executable_filename(self, basename, strip_dir=False, output_dir=''): + assert output_dir is not None + if strip_dir: + basename = os.path.basename(basename) + return os.path.join(output_dir, basename + (self.exe_extension or '')) + + def library_filename(self, libname, lib_type='static', # or 'shared' + strip_dir=False, output_dir=''): + assert output_dir is not None + if lib_type not in ("static", "shared", "dylib"): + raise ValueError( + "'lib_type' must be 'static', 'shared' or 'dylib'") + fmt = getattr(self, lib_type + "_lib_format") + ext = getattr(self, lib_type + "_lib_extension") + + dir, base = os.path.split(libname) + filename = fmt % (base, ext) + if strip_dir: + dir = '' + + return os.path.join(output_dir, dir, filename) + + + # -- Utility methods ----------------------------------------------- + + def execute(self, func, args, msg=None, level=1): + execute(func, args, msg, self.dry_run) + + def spawn(self, cmd): + spawn(cmd, dry_run=self.dry_run) + + def move_file(self, src, dst): + logger.info("moving %r to %r", src, dst) + if self.dry_run: + return + return move(src, dst) + + def mkpath(self, name, mode=0o777): + name = os.path.normpath(name) + if os.path.isdir(name) or name == '': + return + if self.dry_run: + head = '' + for part in name.split(os.sep): + logger.info("created directory %s%s", head, part) + head += part + os.sep + return + os.makedirs(name, mode) diff --git a/Lib/packaging/compiler/cygwinccompiler.py b/Lib/packaging/compiler/cygwinccompiler.py new file mode 100644 index 000000000000..7bfa611ea5b8 --- /dev/null +++ b/Lib/packaging/compiler/cygwinccompiler.py @@ -0,0 +1,355 @@ +"""CCompiler implementations for Cygwin and mingw32 versions of GCC. + +This module contains the CygwinCCompiler class, a subclass of +UnixCCompiler that handles the Cygwin port of the GNU C compiler to +Windows, and the Mingw32CCompiler class which handles the mingw32 port +of GCC (same as cygwin in no-cygwin mode). +""" + +# problems: +# +# * if you use a msvc compiled python version (1.5.2) +# 1. you have to insert a __GNUC__ section in its config.h +# 2. you have to generate a import library for its dll +# - create a def-file for python??.dll +# - create a import library using +# dlltool --dllname python15.dll --def python15.def \ +# --output-lib libpython15.a +# +# see also http://starship.python.net/crew/kernr/mingw32/Notes.html +# +# * We put export_symbols in a def-file, and don't use +# --export-all-symbols because it doesn't worked reliable in some +# tested configurations. And because other windows compilers also +# need their symbols specified this no serious problem. +# +# tested configurations: +# +# * cygwin gcc 2.91.57/ld 2.9.4/dllwrap 0.2.4 works +# (after patching python's config.h and for C++ some other include files) +# see also http://starship.python.net/crew/kernr/mingw32/Notes.html +# * mingw32 gcc 2.95.2/ld 2.9.4/dllwrap 0.2.4 works +# (ld doesn't support -shared, so we use dllwrap) +# * cygwin gcc 2.95.2/ld 2.10.90/dllwrap 2.10.90 works now +# - its dllwrap doesn't work, there is a bug in binutils 2.10.90 +# see also http://sources.redhat.com/ml/cygwin/2000-06/msg01274.html +# - using gcc -mdll instead dllwrap doesn't work without -static because +# it tries to link against dlls instead their import libraries. (If +# it finds the dll first.) +# By specifying -static we force ld to link against the import libraries, +# this is windows standard and there are normally not the necessary symbols +# in the dlls. +# *** only the version of June 2000 shows these problems +# * cygwin gcc 3.2/ld 2.13.90 works +# (ld supports -shared) +# * mingw gcc 3.2/ld 2.13 works +# (ld supports -shared) + + +import os +import sys +import copy + +from packaging import logger +from packaging.compiler.unixccompiler import UnixCCompiler +from packaging.util import write_file +from packaging.errors import PackagingExecError, CompileError, UnknownFileError +from packaging.util import get_compiler_versions +import sysconfig + + +def get_msvcr(): + """Include the appropriate MSVC runtime library if Python was built + with MSVC 7.0 or later. + """ + msc_pos = sys.version.find('MSC v.') + if msc_pos != -1: + msc_ver = sys.version[msc_pos+6:msc_pos+10] + if msc_ver == '1300': + # MSVC 7.0 + return ['msvcr70'] + elif msc_ver == '1310': + # MSVC 7.1 + return ['msvcr71'] + elif msc_ver == '1400': + # VS2005 / MSVC 8.0 + return ['msvcr80'] + elif msc_ver == '1500': + # VS2008 / MSVC 9.0 + return ['msvcr90'] + else: + raise ValueError("Unknown MS Compiler version %s " % msc_ver) + + +class CygwinCCompiler(UnixCCompiler): + """ Handles the Cygwin port of the GNU C compiler to Windows. + """ + name = 'cygwin' + description = 'Cygwin port of GNU C Compiler for Win32' + obj_extension = ".o" + static_lib_extension = ".a" + shared_lib_extension = ".dll" + static_lib_format = "lib%s%s" + shared_lib_format = "%s%s" + exe_extension = ".exe" + + def __init__(self, verbose=0, dry_run=False, force=False): + + UnixCCompiler.__init__(self, verbose, dry_run, force) + + status, details = check_config_h() + logger.debug("Python's GCC status: %s (details: %s)", status, details) + if status is not CONFIG_H_OK: + self.warn( + "Python's pyconfig.h doesn't seem to support your compiler. " + "Reason: %s. " + "Compiling may fail because of undefined preprocessor macros." + % details) + + self.gcc_version, self.ld_version, self.dllwrap_version = \ + get_compiler_versions() + logger.debug(self.name + ": gcc %s, ld %s, dllwrap %s\n", + self.gcc_version, + self.ld_version, + self.dllwrap_version) + + # ld_version >= "2.10.90" and < "2.13" should also be able to use + # gcc -mdll instead of dllwrap + # Older dllwraps had own version numbers, newer ones use the + # same as the rest of binutils ( also ld ) + # dllwrap 2.10.90 is buggy + if self.ld_version >= "2.10.90": + self.linker_dll = "gcc" + else: + self.linker_dll = "dllwrap" + + # ld_version >= "2.13" support -shared so use it instead of + # -mdll -static + if self.ld_version >= "2.13": + shared_option = "-shared" + else: + shared_option = "-mdll -static" + + # Hard-code GCC because that's what this is all about. + # XXX optimization, warnings etc. should be customizable. + self.set_executables(compiler='gcc -mcygwin -O -Wall', + compiler_so='gcc -mcygwin -mdll -O -Wall', + compiler_cxx='g++ -mcygwin -O -Wall', + linker_exe='gcc -mcygwin', + linker_so=('%s -mcygwin %s' % + (self.linker_dll, shared_option))) + + # cygwin and mingw32 need different sets of libraries + if self.gcc_version == "2.91.57": + # cygwin shouldn't need msvcrt, but without the dlls will crash + # (gcc version 2.91.57) -- perhaps something about initialization + self.dll_libraries=["msvcrt"] + self.warn( + "Consider upgrading to a newer version of gcc") + else: + # Include the appropriate MSVC runtime library if Python was built + # with MSVC 7.0 or later. + self.dll_libraries = get_msvcr() + + def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts): + """Compile the source by spawning GCC and windres if needed.""" + if ext == '.rc' or ext == '.res': + # gcc needs '.res' and '.rc' compiled to object files !!! + try: + self.spawn(["windres", "-i", src, "-o", obj]) + except PackagingExecError as msg: + raise CompileError(msg) + else: # for other files use the C-compiler + try: + self.spawn(self.compiler_so + cc_args + [src, '-o', obj] + + extra_postargs) + except PackagingExecError as msg: + raise CompileError(msg) + + def link(self, target_desc, objects, output_filename, output_dir=None, + libraries=None, library_dirs=None, runtime_library_dirs=None, + export_symbols=None, debug=False, extra_preargs=None, + extra_postargs=None, build_temp=None, target_lang=None): + """Link the objects.""" + # use separate copies, so we can modify the lists + extra_preargs = copy.copy(extra_preargs or []) + libraries = copy.copy(libraries or []) + objects = copy.copy(objects or []) + + # Additional libraries + libraries.extend(self.dll_libraries) + + # handle export symbols by creating a def-file + # with executables this only works with gcc/ld as linker + if ((export_symbols is not None) and + (target_desc != self.EXECUTABLE or self.linker_dll == "gcc")): + # (The linker doesn't do anything if output is up-to-date. + # So it would probably better to check if we really need this, + # but for this we had to insert some unchanged parts of + # UnixCCompiler, and this is not what we want.) + + # we want to put some files in the same directory as the + # object files are, build_temp doesn't help much + # where are the object files + temp_dir = os.path.dirname(objects[0]) + # name of dll to give the helper files the same base name + dll_name, dll_extension = os.path.splitext( + os.path.basename(output_filename)) + + # generate the filenames for these files + def_file = os.path.join(temp_dir, dll_name + ".def") + lib_file = os.path.join(temp_dir, 'lib' + dll_name + ".a") + + # Generate .def file + contents = [ + "LIBRARY %s" % os.path.basename(output_filename), + "EXPORTS"] + for sym in export_symbols: + contents.append(sym) + self.execute(write_file, (def_file, contents), + "writing %s" % def_file) + + # next add options for def-file and to creating import libraries + + # dllwrap uses different options than gcc/ld + if self.linker_dll == "dllwrap": + extra_preargs.extend(("--output-lib", lib_file)) + # for dllwrap we have to use a special option + extra_preargs.extend(("--def", def_file)) + # we use gcc/ld here and can be sure ld is >= 2.9.10 + else: + # doesn't work: bfd_close build\...\libfoo.a: Invalid operation + #extra_preargs.extend(("-Wl,--out-implib,%s" % lib_file)) + # for gcc/ld the def-file is specified as any object files + objects.append(def_file) + + #end: if ((export_symbols is not None) and + # (target_desc != self.EXECUTABLE or self.linker_dll == "gcc")): + + # who wants symbols and a many times larger output file + # should explicitly switch the debug mode on + # otherwise we let dllwrap/ld strip the output file + # (On my machine: 10KB < stripped_file < ??100KB + # unstripped_file = stripped_file + XXX KB + # ( XXX=254 for a typical python extension)) + if not debug: + extra_preargs.append("-s") + + UnixCCompiler.link(self, target_desc, objects, output_filename, + output_dir, libraries, library_dirs, + runtime_library_dirs, + None, # export_symbols, we do this in our def-file + debug, extra_preargs, extra_postargs, build_temp, + target_lang) + + # -- Miscellaneous methods ----------------------------------------- + + def object_filenames(self, source_filenames, strip_dir=False, + output_dir=''): + """Adds supports for rc and res files.""" + if output_dir is None: + output_dir = '' + obj_names = [] + for src_name in source_filenames: + # use normcase to make sure '.rc' is really '.rc' and not '.RC' + base, ext = os.path.splitext(os.path.normcase(src_name)) + if ext not in (self.src_extensions + ['.rc','.res']): + raise UnknownFileError("unknown file type '%s' (from '%s')" % (ext, src_name)) + if strip_dir: + base = os.path.basename (base) + if ext in ('.res', '.rc'): + # these need to be compiled to object files + obj_names.append (os.path.join(output_dir, + base + ext + self.obj_extension)) + else: + obj_names.append (os.path.join(output_dir, + base + self.obj_extension)) + return obj_names + +# the same as cygwin plus some additional parameters +class Mingw32CCompiler(CygwinCCompiler): + """ Handles the Mingw32 port of the GNU C compiler to Windows. + """ + name = 'mingw32' + description = 'MinGW32 compiler' + + def __init__(self, verbose=0, dry_run=False, force=False): + + CygwinCCompiler.__init__ (self, verbose, dry_run, force) + + # ld_version >= "2.13" support -shared so use it instead of + # -mdll -static + if self.ld_version >= "2.13": + shared_option = "-shared" + else: + shared_option = "-mdll -static" + + # A real mingw32 doesn't need to specify a different entry point, + # but cygwin 2.91.57 in no-cygwin-mode needs it. + if self.gcc_version <= "2.91.57": + entry_point = '--entry _DllMain@12' + else: + entry_point = '' + + self.set_executables(compiler='gcc -mno-cygwin -O -Wall', + compiler_so='gcc -mno-cygwin -mdll -O -Wall', + compiler_cxx='g++ -mno-cygwin -O -Wall', + linker_exe='gcc -mno-cygwin', + linker_so='%s -mno-cygwin %s %s' + % (self.linker_dll, shared_option, + entry_point)) + # Maybe we should also append -mthreads, but then the finished + # dlls need another dll (mingwm10.dll see Mingw32 docs) + # (-mthreads: Support thread-safe exception handling on `Mingw32') + + # no additional libraries needed + self.dll_libraries=[] + + # Include the appropriate MSVC runtime library if Python was built + # with MSVC 7.0 or later. + self.dll_libraries = get_msvcr() + +# Because these compilers aren't configured in Python's pyconfig.h file by +# default, we should at least warn the user if he is using a unmodified +# version. + +CONFIG_H_OK = "ok" +CONFIG_H_NOTOK = "not ok" +CONFIG_H_UNCERTAIN = "uncertain" + +def check_config_h(): + """Check if the current Python installation appears amenable to building + extensions with GCC. + + Returns a tuple (status, details), where 'status' is one of the following + constants: + + - CONFIG_H_OK: all is well, go ahead and compile + - CONFIG_H_NOTOK: doesn't look good + - CONFIG_H_UNCERTAIN: not sure -- unable to read pyconfig.h + + 'details' is a human-readable string explaining the situation. + + Note there are two ways to conclude "OK": either 'sys.version' contains + the string "GCC" (implying that this Python was built with GCC), or the + installed "pyconfig.h" contains the string "__GNUC__". + """ + + # XXX since this function also checks sys.version, it's not strictly a + # "pyconfig.h" check -- should probably be renamed... + # if sys.version contains GCC then python was compiled with GCC, and the + # pyconfig.h file should be OK + if "GCC" in sys.version: + return CONFIG_H_OK, "sys.version mentions 'GCC'" + + # let's see if __GNUC__ is mentioned in python.h + fn = sysconfig.get_config_h_filename() + try: + with open(fn) as config_h: + if "__GNUC__" in config_h.read(): + return CONFIG_H_OK, "'%s' mentions '__GNUC__'" % fn + else: + return CONFIG_H_NOTOK, "'%s' does not mention '__GNUC__'" % fn + except IOError as exc: + return (CONFIG_H_UNCERTAIN, + "couldn't read '%s': %s" % (fn, exc.strerror)) diff --git a/Lib/packaging/compiler/extension.py b/Lib/packaging/compiler/extension.py new file mode 100644 index 000000000000..66f6e9a6bb07 --- /dev/null +++ b/Lib/packaging/compiler/extension.py @@ -0,0 +1,121 @@ +"""Class representing C/C++ extension modules.""" + +from packaging import logger + +# This class is really only used by the "build_ext" command, so it might +# make sense to put it in distutils.command.build_ext. However, that +# module is already big enough, and I want to make this class a bit more +# complex to simplify some common cases ("foo" module in "foo.c") and do +# better error-checking ("foo.c" actually exists). +# +# Also, putting this in build_ext.py means every setup script would have to +# import that large-ish module (indirectly, through distutils.core) in +# order to do anything. + + +class Extension: + """Just a collection of attributes that describes an extension + module and everything needed to build it (hopefully in a portable + way, but there are hooks that let you be as unportable as you need). + + Instance attributes: + name : string + the full name of the extension, including any packages -- ie. + *not* a filename or pathname, but Python dotted name + sources : [string] + list of source filenames, relative to the distribution root + (where the setup script lives), in Unix form (slash-separated) + for portability. Source files may be C, C++, SWIG (.i), + platform-specific resource files, or whatever else is recognized + by the "build_ext" command as source for a Python extension. + include_dirs : [string] + list of directories to search for C/C++ header files (in Unix + form for portability) + define_macros : [(name : string, value : string|None)] + list of macros to define; each macro is defined using a 2-tuple, + where 'value' is either the string to define it to or None to + define it without a particular value (equivalent of "#define + FOO" in source or -DFOO on Unix C compiler command line) + undef_macros : [string] + list of macros to undefine explicitly + library_dirs : [string] + list of directories to search for C/C++ libraries at link time + libraries : [string] + list of library names (not filenames or paths) to link against + runtime_library_dirs : [string] + list of directories to search for C/C++ libraries at run time + (for shared extensions, this is when the extension is loaded) + extra_objects : [string] + list of extra files to link with (eg. object files not implied + by 'sources', static library that must be explicitly specified, + binary resource files, etc.) + extra_compile_args : [string] + any extra platform- and compiler-specific information to use + when compiling the source files in 'sources'. For platforms and + compilers where "command line" makes sense, this is typically a + list of command-line arguments, but for other platforms it could + be anything. + extra_link_args : [string] + any extra platform- and compiler-specific information to use + when linking object files together to create the extension (or + to create a new static Python interpreter). Similar + interpretation as for 'extra_compile_args'. + export_symbols : [string] + list of symbols to be exported from a shared extension. Not + used on all platforms, and not generally necessary for Python + extensions, which typically export exactly one symbol: "init" + + extension_name. + swig_opts : [string] + any extra options to pass to SWIG if a source file has the .i + extension. + depends : [string] + list of files that the extension depends on + language : string + extension language (i.e. "c", "c++", "objc"). Will be detected + from the source extensions if not provided. + optional : boolean + specifies that a build failure in the extension should not abort the + build process, but simply not install the failing extension. + """ + + # **kwargs are allowed so that a warning is emitted instead of an + # exception + def __init__(self, name, sources, include_dirs=None, define_macros=None, + undef_macros=None, library_dirs=None, libraries=None, + runtime_library_dirs=None, extra_objects=None, + extra_compile_args=None, extra_link_args=None, + export_symbols=None, swig_opts=None, depends=None, + language=None, optional=None, **kw): + if not isinstance(name, str): + raise AssertionError("'name' must be a string") + + if not isinstance(sources, list): + raise AssertionError("'sources' must be a list of strings") + + for v in sources: + if not isinstance(v, str): + raise AssertionError("'sources' must be a list of strings") + + self.name = name + self.sources = sources + self.include_dirs = include_dirs or [] + self.define_macros = define_macros or [] + self.undef_macros = undef_macros or [] + self.library_dirs = library_dirs or [] + self.libraries = libraries or [] + self.runtime_library_dirs = runtime_library_dirs or [] + self.extra_objects = extra_objects or [] + self.extra_compile_args = extra_compile_args or [] + self.extra_link_args = extra_link_args or [] + self.export_symbols = export_symbols or [] + self.swig_opts = swig_opts or [] + self.depends = depends or [] + self.language = language + self.optional = optional + + # If there are unknown keyword options, warn about them + if len(kw) > 0: + options = [repr(option) for option in kw] + options = ', '.join(sorted(options)) + logger.warning( + 'unknown arguments given to Extension: %s', options) diff --git a/Lib/packaging/compiler/msvc9compiler.py b/Lib/packaging/compiler/msvc9compiler.py new file mode 100644 index 000000000000..d30444697b21 --- /dev/null +++ b/Lib/packaging/compiler/msvc9compiler.py @@ -0,0 +1,720 @@ +"""CCompiler implementation for the Microsoft Visual Studio 2008 compiler. + +The MSVCCompiler class is compatible with VS 2005 and VS 2008. Legacy +support for older versions of VS are in the msvccompiler module. +""" + +# Written by Perry Stoll +# hacked by Robin Becker and Thomas Heller to do a better job of +# finding DevStudio (through the registry) +# ported to VS2005 and VS 2008 by Christian Heimes +import os +import subprocess +import sys +import re + +from packaging.errors import (PackagingExecError, PackagingPlatformError, + CompileError, LibError, LinkError) +from packaging.compiler.ccompiler import CCompiler +from packaging.compiler import gen_lib_options +from packaging import logger +from packaging.util import get_platform + +import winreg + +RegOpenKeyEx = winreg.OpenKeyEx +RegEnumKey = winreg.EnumKey +RegEnumValue = winreg.EnumValue +RegError = winreg.error + +HKEYS = (winreg.HKEY_USERS, + winreg.HKEY_CURRENT_USER, + winreg.HKEY_LOCAL_MACHINE, + winreg.HKEY_CLASSES_ROOT) + +VS_BASE = r"Software\Microsoft\VisualStudio\%0.1f" +WINSDK_BASE = r"Software\Microsoft\Microsoft SDKs\Windows" +NET_BASE = r"Software\Microsoft\.NETFramework" + +# A map keyed by get_platform() return values to values accepted by +# 'vcvarsall.bat'. Note a cross-compile may combine these (eg, 'x86_amd64' is +# the param to cross-compile on x86 targetting amd64.) +PLAT_TO_VCVARS = { + 'win32' : 'x86', + 'win-amd64' : 'amd64', + 'win-ia64' : 'ia64', +} + + +class Reg: + """Helper class to read values from the registry + """ + + def get_value(cls, path, key): + for base in HKEYS: + d = cls.read_values(base, path) + if d and key in d: + return d[key] + raise KeyError(key) + get_value = classmethod(get_value) + + def read_keys(cls, base, key): + """Return list of registry keys.""" + try: + handle = RegOpenKeyEx(base, key) + except RegError: + return None + L = [] + i = 0 + while True: + try: + k = RegEnumKey(handle, i) + except RegError: + break + L.append(k) + i += 1 + return L + read_keys = classmethod(read_keys) + + def read_values(cls, base, key): + """Return dict of registry keys and values. + + All names are converted to lowercase. + """ + try: + handle = RegOpenKeyEx(base, key) + except RegError: + return None + d = {} + i = 0 + while True: + try: + name, value, type = RegEnumValue(handle, i) + except RegError: + break + name = name.lower() + d[cls.convert_mbcs(name)] = cls.convert_mbcs(value) + i += 1 + return d + read_values = classmethod(read_values) + + def convert_mbcs(s): + dec = getattr(s, "decode", None) + if dec is not None: + try: + s = dec("mbcs") + except UnicodeError: + pass + return s + convert_mbcs = staticmethod(convert_mbcs) + +class MacroExpander: + + def __init__(self, version): + self.macros = {} + self.vsbase = VS_BASE % version + self.load_macros(version) + + def set_macro(self, macro, path, key): + self.macros["$(%s)" % macro] = Reg.get_value(path, key) + + def load_macros(self, version): + self.set_macro("VCInstallDir", self.vsbase + r"\Setup\VC", "productdir") + self.set_macro("VSInstallDir", self.vsbase + r"\Setup\VS", "productdir") + self.set_macro("FrameworkDir", NET_BASE, "installroot") + try: + if version >= 8.0: + self.set_macro("FrameworkSDKDir", NET_BASE, + "sdkinstallrootv2.0") + else: + raise KeyError("sdkinstallrootv2.0") + except KeyError: + raise PackagingPlatformError( + """Python was built with Visual Studio 2008; +extensions must be built with a compiler than can generate compatible binaries. +Visual Studio 2008 was not found on this system. If you have Cygwin installed, +you can try compiling with MingW32, by passing "-c mingw32" to setup.py.""") + + if version >= 9.0: + self.set_macro("FrameworkVersion", self.vsbase, "clr version") + self.set_macro("WindowsSdkDir", WINSDK_BASE, "currentinstallfolder") + else: + p = r"Software\Microsoft\NET Framework Setup\Product" + for base in HKEYS: + try: + h = RegOpenKeyEx(base, p) + except RegError: + continue + key = RegEnumKey(h, 0) + d = Reg.get_value(base, r"%s\%s" % (p, key)) + self.macros["$(FrameworkVersion)"] = d["version"] + + def sub(self, s): + for k, v in self.macros.items(): + s = s.replace(k, v) + return s + +def get_build_version(): + """Return the version of MSVC that was used to build Python. + + For Python 2.3 and up, the version number is included in + sys.version. For earlier versions, assume the compiler is MSVC 6. + """ + prefix = "MSC v." + i = sys.version.find(prefix) + if i == -1: + return 6 + i = i + len(prefix) + s, rest = sys.version[i:].split(" ", 1) + majorVersion = int(s[:-2]) - 6 + minorVersion = int(s[2:3]) / 10.0 + # I don't think paths are affected by minor version in version 6 + if majorVersion == 6: + minorVersion = 0 + if majorVersion >= 6: + return majorVersion + minorVersion + # else we don't know what version of the compiler this is + return None + +def normalize_and_reduce_paths(paths): + """Return a list of normalized paths with duplicates removed. + + The current order of paths is maintained. + """ + # Paths are normalized so things like: /a and /a/ aren't both preserved. + reduced_paths = [] + for p in paths: + np = os.path.normpath(p) + # XXX(nnorwitz): O(n**2), if reduced_paths gets long perhaps use a set. + if np not in reduced_paths: + reduced_paths.append(np) + return reduced_paths + +def removeDuplicates(variable): + """Remove duplicate values of an environment variable. + """ + oldList = variable.split(os.pathsep) + newList = [] + for i in oldList: + if i not in newList: + newList.append(i) + newVariable = os.pathsep.join(newList) + return newVariable + +def find_vcvarsall(version): + """Find the vcvarsall.bat file + + At first it tries to find the productdir of VS 2008 in the registry. If + that fails it falls back to the VS90COMNTOOLS env var. + """ + vsbase = VS_BASE % version + try: + productdir = Reg.get_value(r"%s\Setup\VC" % vsbase, + "productdir") + except KeyError: + logger.debug("Unable to find productdir in registry") + productdir = None + + if not productdir or not os.path.isdir(productdir): + toolskey = "VS%0.f0COMNTOOLS" % version + toolsdir = os.environ.get(toolskey, None) + + if toolsdir and os.path.isdir(toolsdir): + productdir = os.path.join(toolsdir, os.pardir, os.pardir, "VC") + productdir = os.path.abspath(productdir) + if not os.path.isdir(productdir): + logger.debug("%s is not a valid directory", productdir) + return None + else: + logger.debug("env var %s is not set or invalid", toolskey) + if not productdir: + logger.debug("no productdir found") + return None + vcvarsall = os.path.join(productdir, "vcvarsall.bat") + if os.path.isfile(vcvarsall): + return vcvarsall + logger.debug("unable to find vcvarsall.bat") + return None + +def query_vcvarsall(version, arch="x86"): + """Launch vcvarsall.bat and read the settings from its environment + """ + vcvarsall = find_vcvarsall(version) + interesting = set(("include", "lib", "libpath", "path")) + result = {} + + if vcvarsall is None: + raise PackagingPlatformError("Unable to find vcvarsall.bat") + logger.debug("calling 'vcvarsall.bat %s' (version=%s)", arch, version) + popen = subprocess.Popen('"%s" %s & set' % (vcvarsall, arch), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + stdout, stderr = popen.communicate() + if popen.wait() != 0: + raise PackagingPlatformError(stderr.decode("mbcs")) + + stdout = stdout.decode("mbcs") + for line in stdout.split("\n"): + line = Reg.convert_mbcs(line) + if '=' not in line: + continue + line = line.strip() + key, value = line.split('=', 1) + key = key.lower() + if key in interesting: + if value.endswith(os.pathsep): + value = value[:-1] + result[key] = removeDuplicates(value) + + if len(result) != len(interesting): + raise ValueError(str(list(result))) + + return result + +# More globals +VERSION = get_build_version() +if VERSION < 8.0: + raise PackagingPlatformError("VC %0.1f is not supported by this module" % VERSION) +# MACROS = MacroExpander(VERSION) + +class MSVCCompiler(CCompiler) : + """Concrete class that implements an interface to Microsoft Visual C++, + as defined by the CCompiler abstract class.""" + + name = 'msvc' + description = 'Microsoft Visual C++' + + # Just set this so CCompiler's constructor doesn't barf. We currently + # don't use the 'set_executables()' bureaucracy provided by CCompiler, + # as it really isn't necessary for this sort of single-compiler class. + # Would be nice to have a consistent interface with UnixCCompiler, + # though, so it's worth thinking about. + executables = {} + + # Private class data (need to distinguish C from C++ source for compiler) + _c_extensions = ['.c'] + _cpp_extensions = ['.cc', '.cpp', '.cxx'] + _rc_extensions = ['.rc'] + _mc_extensions = ['.mc'] + + # Needed for the filename generation methods provided by the + # base class, CCompiler. + src_extensions = (_c_extensions + _cpp_extensions + + _rc_extensions + _mc_extensions) + res_extension = '.res' + obj_extension = '.obj' + static_lib_extension = '.lib' + shared_lib_extension = '.dll' + static_lib_format = shared_lib_format = '%s%s' + exe_extension = '.exe' + + def __init__(self, verbose=0, dry_run=False, force=False): + CCompiler.__init__(self, verbose, dry_run, force) + self.__version = VERSION + self.__root = r"Software\Microsoft\VisualStudio" + # self.__macros = MACROS + self.__paths = [] + # target platform (.plat_name is consistent with 'bdist') + self.plat_name = None + self.__arch = None # deprecated name + self.initialized = False + + def initialize(self, plat_name=None): + # multi-init means we would need to check platform same each time... + assert not self.initialized, "don't init multiple times" + if plat_name is None: + plat_name = get_platform() + # sanity check for platforms to prevent obscure errors later. + ok_plats = 'win32', 'win-amd64', 'win-ia64' + if plat_name not in ok_plats: + raise PackagingPlatformError("--plat-name must be one of %s" % + (ok_plats,)) + + if "DISTUTILS_USE_SDK" in os.environ and "MSSdk" in os.environ and self.find_exe("cl.exe"): + # Assume that the SDK set up everything alright; don't try to be + # smarter + self.cc = "cl.exe" + self.linker = "link.exe" + self.lib = "lib.exe" + self.rc = "rc.exe" + self.mc = "mc.exe" + else: + # On x86, 'vcvars32.bat amd64' creates an env that doesn't work; + # to cross compile, you use 'x86_amd64'. + # On AMD64, 'vcvars32.bat amd64' is a native build env; to cross + # compile use 'x86' (ie, it runs the x86 compiler directly) + # No idea how itanium handles this, if at all. + if plat_name == get_platform() or plat_name == 'win32': + # native build or cross-compile to win32 + plat_spec = PLAT_TO_VCVARS[plat_name] + else: + # cross compile from win32 -> some 64bit + plat_spec = PLAT_TO_VCVARS[get_platform()] + '_' + \ + PLAT_TO_VCVARS[plat_name] + + vc_env = query_vcvarsall(VERSION, plat_spec) + + # take care to only use strings in the environment. + self.__paths = vc_env['path'].encode('mbcs').split(os.pathsep) + os.environ['lib'] = vc_env['lib'].encode('mbcs') + os.environ['include'] = vc_env['include'].encode('mbcs') + + if len(self.__paths) == 0: + raise PackagingPlatformError("Python was built with %s, " + "and extensions need to be built with the same " + "version of the compiler, but it isn't installed." + % self.__product) + + self.cc = self.find_exe("cl.exe") + self.linker = self.find_exe("link.exe") + self.lib = self.find_exe("lib.exe") + self.rc = self.find_exe("rc.exe") # resource compiler + self.mc = self.find_exe("mc.exe") # message compiler + #self.set_path_env_var('lib') + #self.set_path_env_var('include') + + # extend the MSVC path with the current path + try: + for p in os.environ['path'].split(';'): + self.__paths.append(p) + except KeyError: + pass + self.__paths = normalize_and_reduce_paths(self.__paths) + os.environ['path'] = ";".join(self.__paths) + + self.preprocess_options = None + if self.__arch == "x86": + self.compile_options = [ '/nologo', '/Ox', '/MD', '/W3', + '/DNDEBUG'] + self.compile_options_debug = ['/nologo', '/Od', '/MDd', '/W3', + '/Z7', '/D_DEBUG'] + else: + # Win64 + self.compile_options = [ '/nologo', '/Ox', '/MD', '/W3', '/GS-' , + '/DNDEBUG'] + self.compile_options_debug = ['/nologo', '/Od', '/MDd', '/W3', '/GS-', + '/Z7', '/D_DEBUG'] + + self.ldflags_shared = ['/DLL', '/nologo', '/INCREMENTAL:NO'] + if self.__version >= 7: + self.ldflags_shared_debug = [ + '/DLL', '/nologo', '/INCREMENTAL:no', '/DEBUG', '/pdb:None' + ] + self.ldflags_static = [ '/nologo'] + + self.initialized = True + + # -- Worker methods ------------------------------------------------ + + def object_filenames(self, + source_filenames, + strip_dir=False, + output_dir=''): + # Copied from ccompiler.py, extended to return .res as 'object'-file + # for .rc input file + if output_dir is None: output_dir = '' + obj_names = [] + for src_name in source_filenames: + base, ext = os.path.splitext(src_name) + base = os.path.splitdrive(base)[1] # Chop off the drive + base = base[os.path.isabs(base):] # If abs, chop off leading / + if ext not in self.src_extensions: + # Better to raise an exception instead of silently continuing + # and later complain about sources and targets having + # different lengths + raise CompileError("Don't know how to compile %s" % src_name) + if strip_dir: + base = os.path.basename(base) + if ext in self._rc_extensions: + obj_names.append(os.path.join(output_dir, + base + self.res_extension)) + elif ext in self._mc_extensions: + obj_names.append(os.path.join(output_dir, + base + self.res_extension)) + else: + obj_names.append(os.path.join(output_dir, + base + self.obj_extension)) + return obj_names + + + def compile(self, sources, + output_dir=None, macros=None, include_dirs=None, debug=False, + extra_preargs=None, extra_postargs=None, depends=None): + + if not self.initialized: + self.initialize() + compile_info = self._setup_compile(output_dir, macros, include_dirs, + sources, depends, extra_postargs) + macros, objects, extra_postargs, pp_opts, build = compile_info + + compile_opts = extra_preargs or [] + compile_opts.append('/c') + if debug: + compile_opts.extend(self.compile_options_debug) + else: + compile_opts.extend(self.compile_options) + + for obj in objects: + try: + src, ext = build[obj] + except KeyError: + continue + if debug: + # pass the full pathname to MSVC in debug mode, + # this allows the debugger to find the source file + # without asking the user to browse for it + src = os.path.abspath(src) + + if ext in self._c_extensions: + input_opt = "/Tc" + src + elif ext in self._cpp_extensions: + input_opt = "/Tp" + src + elif ext in self._rc_extensions: + # compile .RC to .RES file + input_opt = src + output_opt = "/fo" + obj + try: + self.spawn([self.rc] + pp_opts + + [output_opt] + [input_opt]) + except PackagingExecError as msg: + raise CompileError(msg) + continue + elif ext in self._mc_extensions: + # Compile .MC to .RC file to .RES file. + # * '-h dir' specifies the directory for the + # generated include file + # * '-r dir' specifies the target directory of the + # generated RC file and the binary message resource + # it includes + # + # For now (since there are no options to change this), + # we use the source-directory for the include file and + # the build directory for the RC file and message + # resources. This works at least for win32all. + h_dir = os.path.dirname(src) + rc_dir = os.path.dirname(obj) + try: + # first compile .MC to .RC and .H file + self.spawn([self.mc] + + ['-h', h_dir, '-r', rc_dir] + [src]) + base, _ = os.path.splitext(os.path.basename(src)) + rc_file = os.path.join(rc_dir, base + '.rc') + # then compile .RC to .RES file + self.spawn([self.rc] + + ["/fo" + obj] + [rc_file]) + + except PackagingExecError as msg: + raise CompileError(msg) + continue + else: + # how to handle this file? + raise CompileError("Don't know how to compile %s to %s" + % (src, obj)) + + output_opt = "/Fo" + obj + try: + self.spawn([self.cc] + compile_opts + pp_opts + + [input_opt, output_opt] + + extra_postargs) + except PackagingExecError as msg: + raise CompileError(msg) + + return objects + + + def create_static_lib(self, + objects, + output_libname, + output_dir=None, + debug=False, + target_lang=None): + + if not self.initialized: + self.initialize() + objects, output_dir = self._fix_object_args(objects, output_dir) + output_filename = self.library_filename(output_libname, + output_dir=output_dir) + + if self._need_link(objects, output_filename): + lib_args = objects + ['/OUT:' + output_filename] + if debug: + pass # XXX what goes here? + try: + self.spawn([self.lib] + lib_args) + except PackagingExecError as msg: + raise LibError(msg) + else: + logger.debug("skipping %s (up-to-date)", output_filename) + + + def link(self, target_desc, objects, output_filename, output_dir=None, + libraries=None, library_dirs=None, runtime_library_dirs=None, + export_symbols=None, debug=False, extra_preargs=None, + extra_postargs=None, build_temp=None, target_lang=None): + if not self.initialized: + self.initialize() + objects, output_dir = self._fix_object_args(objects, output_dir) + fixed_args = self._fix_lib_args(libraries, library_dirs, + runtime_library_dirs) + libraries, library_dirs, runtime_library_dirs = fixed_args + + if runtime_library_dirs: + self.warn("don't know what to do with 'runtime_library_dirs': " + + str(runtime_library_dirs)) + + lib_opts = gen_lib_options(self, + library_dirs, runtime_library_dirs, + libraries) + if output_dir is not None: + output_filename = os.path.join(output_dir, output_filename) + + if self._need_link(objects, output_filename): + if target_desc == CCompiler.EXECUTABLE: + if debug: + ldflags = self.ldflags_shared_debug[1:] + else: + ldflags = self.ldflags_shared[1:] + else: + if debug: + ldflags = self.ldflags_shared_debug + else: + ldflags = self.ldflags_shared + + export_opts = [] + for sym in (export_symbols or []): + export_opts.append("/EXPORT:" + sym) + + ld_args = (ldflags + lib_opts + export_opts + + objects + ['/OUT:' + output_filename]) + + # The MSVC linker generates .lib and .exp files, which cannot be + # suppressed by any linker switches. The .lib files may even be + # needed! Make sure they are generated in the temporary build + # directory. Since they have different names for debug and release + # builds, they can go into the same directory. + build_temp = os.path.dirname(objects[0]) + if export_symbols is not None: + dll_name, dll_ext = os.path.splitext( + os.path.basename(output_filename)) + implib_file = os.path.join( + build_temp, + self.library_filename(dll_name)) + ld_args.append('/IMPLIB:' + implib_file) + + # Embedded manifests are recommended - see MSDN article titled + # "How to: Embed a Manifest Inside a C/C++ Application" + # (currently at http://msdn2.microsoft.com/en-us/library/ms235591(VS.80).aspx) + # Ask the linker to generate the manifest in the temp dir, so + # we can embed it later. + temp_manifest = os.path.join( + build_temp, + os.path.basename(output_filename) + ".manifest") + ld_args.append('/MANIFESTFILE:' + temp_manifest) + + if extra_preargs: + ld_args[:0] = extra_preargs + if extra_postargs: + ld_args.extend(extra_postargs) + + self.mkpath(os.path.dirname(output_filename)) + try: + self.spawn([self.linker] + ld_args) + except PackagingExecError as msg: + raise LinkError(msg) + + # embed the manifest + # XXX - this is somewhat fragile - if mt.exe fails, distutils + # will still consider the DLL up-to-date, but it will not have a + # manifest. Maybe we should link to a temp file? OTOH, that + # implies a build environment error that shouldn't go undetected. + if target_desc == CCompiler.EXECUTABLE: + mfid = 1 + else: + mfid = 2 + self._remove_visual_c_ref(temp_manifest) + out_arg = '-outputresource:%s;%s' % (output_filename, mfid) + try: + self.spawn(['mt.exe', '-nologo', '-manifest', + temp_manifest, out_arg]) + except PackagingExecError as msg: + raise LinkError(msg) + else: + logger.debug("skipping %s (up-to-date)", output_filename) + + def _remove_visual_c_ref(self, manifest_file): + try: + # Remove references to the Visual C runtime, so they will + # fall through to the Visual C dependency of Python.exe. + # This way, when installed for a restricted user (e.g. + # runtimes are not in WinSxS folder, but in Python's own + # folder), the runtimes do not need to be in every folder + # with .pyd's. + with open(manifest_file) as manifest_f: + manifest_buf = manifest_f.read() + pattern = re.compile( + r"""|)""", + re.DOTALL) + manifest_buf = re.sub(pattern, "", manifest_buf) + pattern = "\s*" + manifest_buf = re.sub(pattern, "", manifest_buf) + with open(manifest_file, 'w') as manifest_f: + manifest_f.write(manifest_buf) + except IOError: + pass + + # -- Miscellaneous methods ----------------------------------------- + # These are all used by the 'gen_lib_options() function, in + # ccompiler.py. + + def library_dir_option(self, dir): + return "/LIBPATH:" + dir + + def runtime_library_dir_option(self, dir): + raise PackagingPlatformError( + "don't know how to set runtime library search path for MSVC++") + + def library_option(self, lib): + return self.library_filename(lib) + + + def find_library_file(self, dirs, lib, debug=False): + # Prefer a debugging library if found (and requested), but deal + # with it if we don't have one. + if debug: + try_names = [lib + "_d", lib] + else: + try_names = [lib] + for dir in dirs: + for name in try_names: + libfile = os.path.join(dir, self.library_filename(name)) + if os.path.exists(libfile): + return libfile + else: + # Oops, didn't find it in *any* of 'dirs' + return None + + # Helper methods for using the MSVC registry settings + + def find_exe(self, exe): + """Return path to an MSVC executable program. + + Tries to find the program in several places: first, one of the + MSVC program search paths from the registry; next, the directories + in the PATH environment variable. If any of those work, return an + absolute path that is known to exist. If none of them work, just + return the original program name, 'exe'. + """ + for p in self.__paths: + fn = os.path.join(os.path.abspath(p), exe) + if os.path.isfile(fn): + return fn + + # didn't find it; try existing path + for p in os.environ['Path'].split(';'): + fn = os.path.join(os.path.abspath(p),exe) + if os.path.isfile(fn): + return fn + + return exe diff --git a/Lib/packaging/compiler/msvccompiler.py b/Lib/packaging/compiler/msvccompiler.py new file mode 100644 index 000000000000..97f76bb99f1e --- /dev/null +++ b/Lib/packaging/compiler/msvccompiler.py @@ -0,0 +1,636 @@ +"""CCompiler implementation for old Microsoft Visual Studio compilers. + +For a compiler compatible with VS 2005 and 2008, use msvc9compiler. +""" + +# Written by Perry Stoll +# hacked by Robin Becker and Thomas Heller to do a better job of +# finding DevStudio (through the registry) + + +import sys +import os + +from packaging.errors import (PackagingExecError, PackagingPlatformError, + CompileError, LibError, LinkError) +from packaging.compiler.ccompiler import CCompiler +from packaging.compiler import gen_lib_options +from packaging import logger + +_can_read_reg = False +try: + import winreg + + _can_read_reg = True + hkey_mod = winreg + + RegOpenKeyEx = winreg.OpenKeyEx + RegEnumKey = winreg.EnumKey + RegEnumValue = winreg.EnumValue + RegError = winreg.error + +except ImportError: + try: + import win32api + import win32con + _can_read_reg = True + hkey_mod = win32con + + RegOpenKeyEx = win32api.RegOpenKeyEx + RegEnumKey = win32api.RegEnumKey + RegEnumValue = win32api.RegEnumValue + RegError = win32api.error + + except ImportError: + logger.warning( + "can't read registry to find the necessary compiler setting;\n" + "make sure that Python modules _winreg, win32api or win32con " + "are installed.") + +if _can_read_reg: + HKEYS = (hkey_mod.HKEY_USERS, + hkey_mod.HKEY_CURRENT_USER, + hkey_mod.HKEY_LOCAL_MACHINE, + hkey_mod.HKEY_CLASSES_ROOT) + + +def read_keys(base, key): + """Return list of registry keys.""" + + try: + handle = RegOpenKeyEx(base, key) + except RegError: + return None + L = [] + i = 0 + while True: + try: + k = RegEnumKey(handle, i) + except RegError: + break + L.append(k) + i = i + 1 + return L + + +def read_values(base, key): + """Return dict of registry keys and values. + + All names are converted to lowercase. + """ + try: + handle = RegOpenKeyEx(base, key) + except RegError: + return None + d = {} + i = 0 + while True: + try: + name, value, type = RegEnumValue(handle, i) + except RegError: + break + name = name.lower() + d[convert_mbcs(name)] = convert_mbcs(value) + i = i + 1 + return d + + +def convert_mbcs(s): + enc = getattr(s, "encode", None) + if enc is not None: + try: + s = enc("mbcs") + except UnicodeError: + pass + return s + + +class MacroExpander: + + def __init__(self, version): + self.macros = {} + self.load_macros(version) + + def set_macro(self, macro, path, key): + for base in HKEYS: + d = read_values(base, path) + if d: + self.macros["$(%s)" % macro] = d[key] + break + + def load_macros(self, version): + vsbase = r"Software\Microsoft\VisualStudio\%0.1f" % version + self.set_macro("VCInstallDir", vsbase + r"\Setup\VC", "productdir") + self.set_macro("VSInstallDir", vsbase + r"\Setup\VS", "productdir") + net = r"Software\Microsoft\.NETFramework" + self.set_macro("FrameworkDir", net, "installroot") + try: + if version > 7.0: + self.set_macro("FrameworkSDKDir", net, "sdkinstallrootv1.1") + else: + self.set_macro("FrameworkSDKDir", net, "sdkinstallroot") + except KeyError: + raise PackagingPlatformError( +"""Python was built with Visual Studio 2003; extensions must be built with +a compiler than can generate compatible binaries. Visual Studio 2003 was +not found on this system. If you have Cygwin installed, you can try +compiling with MingW32, by passing "-c mingw32" to setup.py.""") +# XXX update this comment for setup.cfg + + p = r"Software\Microsoft\NET Framework Setup\Product" + for base in HKEYS: + try: + h = RegOpenKeyEx(base, p) + except RegError: + continue + key = RegEnumKey(h, 0) + d = read_values(base, r"%s\%s" % (p, key)) + self.macros["$(FrameworkVersion)"] = d["version"] + + def sub(self, s): + for k, v in self.macros.items(): + s = s.replace(k, v) + return s + + +def get_build_version(): + """Return the version of MSVC that was used to build Python. + + For Python 2.3 and up, the version number is included in + sys.version. For earlier versions, assume the compiler is MSVC 6. + """ + + prefix = "MSC v." + i = sys.version.find(prefix) + if i == -1: + return 6 + i = i + len(prefix) + s, rest = sys.version[i:].split(" ", 1) + majorVersion = int(s[:-2]) - 6 + minorVersion = int(s[2:3]) / 10.0 + # I don't think paths are affected by minor version in version 6 + if majorVersion == 6: + minorVersion = 0 + if majorVersion >= 6: + return majorVersion + minorVersion + # else we don't know what version of the compiler this is + return None + + +def get_build_architecture(): + """Return the processor architecture. + + Possible results are "Intel", "Itanium", or "AMD64". + """ + + prefix = " bit (" + i = sys.version.find(prefix) + if i == -1: + return "Intel" + j = sys.version.find(")", i) + return sys.version[i+len(prefix):j] + + +def normalize_and_reduce_paths(paths): + """Return a list of normalized paths with duplicates removed. + + The current order of paths is maintained. + """ + # Paths are normalized so things like: /a and /a/ aren't both preserved. + reduced_paths = [] + for p in paths: + np = os.path.normpath(p) + # XXX(nnorwitz): O(n**2), if reduced_paths gets long perhaps use a set. + if np not in reduced_paths: + reduced_paths.append(np) + return reduced_paths + + +class MSVCCompiler(CCompiler): + """Concrete class that implements an interface to Microsoft Visual C++, + as defined by the CCompiler abstract class.""" + + name = 'msvc' + description = "Microsoft Visual C++" + + # Just set this so CCompiler's constructor doesn't barf. We currently + # don't use the 'set_executables()' bureaucracy provided by CCompiler, + # as it really isn't necessary for this sort of single-compiler class. + # Would be nice to have a consistent interface with UnixCCompiler, + # though, so it's worth thinking about. + executables = {} + + # Private class data (need to distinguish C from C++ source for compiler) + _c_extensions = ['.c'] + _cpp_extensions = ['.cc', '.cpp', '.cxx'] + _rc_extensions = ['.rc'] + _mc_extensions = ['.mc'] + + # Needed for the filename generation methods provided by the + # base class, CCompiler. + src_extensions = (_c_extensions + _cpp_extensions + + _rc_extensions + _mc_extensions) + res_extension = '.res' + obj_extension = '.obj' + static_lib_extension = '.lib' + shared_lib_extension = '.dll' + static_lib_format = shared_lib_format = '%s%s' + exe_extension = '.exe' + + def __init__(self, verbose=0, dry_run=False, force=False): + CCompiler.__init__(self, verbose, dry_run, force) + self.__version = get_build_version() + self.__arch = get_build_architecture() + if self.__arch == "Intel": + # x86 + if self.__version >= 7: + self.__root = r"Software\Microsoft\VisualStudio" + self.__macros = MacroExpander(self.__version) + else: + self.__root = r"Software\Microsoft\Devstudio" + self.__product = "Visual Studio version %s" % self.__version + else: + # Win64. Assume this was built with the platform SDK + self.__product = "Microsoft SDK compiler %s" % (self.__version + 6) + + self.initialized = False + + def initialize(self): + self.__paths = [] + if ("DISTUTILS_USE_SDK" in os.environ and "MSSdk" in os.environ and + self.find_exe("cl.exe")): + # Assume that the SDK set up everything alright; don't try to be + # smarter + self.cc = "cl.exe" + self.linker = "link.exe" + self.lib = "lib.exe" + self.rc = "rc.exe" + self.mc = "mc.exe" + else: + self.__paths = self.get_msvc_paths("path") + + if len(self.__paths) == 0: + raise PackagingPlatformError("Python was built with %s " + "and extensions need to be built with the same " + "version of the compiler, but it isn't installed." % + self.__product) + + self.cc = self.find_exe("cl.exe") + self.linker = self.find_exe("link.exe") + self.lib = self.find_exe("lib.exe") + self.rc = self.find_exe("rc.exe") # resource compiler + self.mc = self.find_exe("mc.exe") # message compiler + self.set_path_env_var('lib') + self.set_path_env_var('include') + + # extend the MSVC path with the current path + try: + for p in os.environ['path'].split(';'): + self.__paths.append(p) + except KeyError: + pass + self.__paths = normalize_and_reduce_paths(self.__paths) + os.environ['path'] = ';'.join(self.__paths) + + self.preprocess_options = None + if self.__arch == "Intel": + self.compile_options = ['/nologo', '/Ox', '/MD', '/W3', '/GX', + '/DNDEBUG'] + self.compile_options_debug = ['/nologo', '/Od', '/MDd', '/W3', '/GX', + '/Z7', '/D_DEBUG'] + else: + # Win64 + self.compile_options = ['/nologo', '/Ox', '/MD', '/W3', '/GS-', + '/DNDEBUG'] + self.compile_options_debug = ['/nologo', '/Od', '/MDd', '/W3', '/GS-', + '/Z7', '/D_DEBUG'] + + self.ldflags_shared = ['/DLL', '/nologo', '/INCREMENTAL:NO'] + if self.__version >= 7: + self.ldflags_shared_debug = [ + '/DLL', '/nologo', '/INCREMENTAL:no', '/DEBUG' + ] + else: + self.ldflags_shared_debug = [ + '/DLL', '/nologo', '/INCREMENTAL:no', '/pdb:None', '/DEBUG' + ] + self.ldflags_static = [ '/nologo'] + + self.initialized = True + + # -- Worker methods ------------------------------------------------ + + def object_filenames(self, source_filenames, strip_dir=False, output_dir=''): + # Copied from ccompiler.py, extended to return .res as 'object'-file + # for .rc input file + if output_dir is None: + output_dir = '' + obj_names = [] + for src_name in source_filenames: + base, ext = os.path.splitext(src_name) + base = os.path.splitdrive(base)[1] # Chop off the drive + base = base[os.path.isabs(base):] # If abs, chop off leading / + if ext not in self.src_extensions: + # Better to raise an exception instead of silently continuing + # and later complain about sources and targets having + # different lengths + raise CompileError("Don't know how to compile %s" % src_name) + if strip_dir: + base = os.path.basename(base) + if ext in self._rc_extensions: + obj_names.append(os.path.join(output_dir, + base + self.res_extension)) + elif ext in self._mc_extensions: + obj_names.append(os.path.join(output_dir, + base + self.res_extension)) + else: + obj_names.append(os.path.join(output_dir, + base + self.obj_extension)) + return obj_names + + def compile(self, sources, + output_dir=None, macros=None, include_dirs=None, debug=False, + extra_preargs=None, extra_postargs=None, depends=None): + + if not self.initialized: + self.initialize() + macros, objects, extra_postargs, pp_opts, build = \ + self._setup_compile(output_dir, macros, include_dirs, sources, + depends, extra_postargs) + + compile_opts = extra_preargs or [] + compile_opts.append('/c') + if debug: + compile_opts.extend(self.compile_options_debug) + else: + compile_opts.extend(self.compile_options) + + for obj in objects: + try: + src, ext = build[obj] + except KeyError: + continue + if debug: + # pass the full pathname to MSVC in debug mode, + # this allows the debugger to find the source file + # without asking the user to browse for it + src = os.path.abspath(src) + + if ext in self._c_extensions: + input_opt = "/Tc" + src + elif ext in self._cpp_extensions: + input_opt = "/Tp" + src + elif ext in self._rc_extensions: + # compile .RC to .RES file + input_opt = src + output_opt = "/fo" + obj + try: + self.spawn([self.rc] + pp_opts + + [output_opt] + [input_opt]) + except PackagingExecError as msg: + raise CompileError(msg) + continue + elif ext in self._mc_extensions: + + # Compile .MC to .RC file to .RES file. + # * '-h dir' specifies the directory for the + # generated include file + # * '-r dir' specifies the target directory of the + # generated RC file and the binary message resource + # it includes + # + # For now (since there are no options to change this), + # we use the source-directory for the include file and + # the build directory for the RC file and message + # resources. This works at least for win32all. + + h_dir = os.path.dirname(src) + rc_dir = os.path.dirname(obj) + try: + # first compile .MC to .RC and .H file + self.spawn([self.mc] + + ['-h', h_dir, '-r', rc_dir] + [src]) + base, _ = os.path.splitext(os.path.basename(src)) + rc_file = os.path.join(rc_dir, base + '.rc') + # then compile .RC to .RES file + self.spawn([self.rc] + + ["/fo" + obj] + [rc_file]) + + except PackagingExecError as msg: + raise CompileError(msg) + continue + else: + # how to handle this file? + raise CompileError( + "Don't know how to compile %s to %s" % + (src, obj)) + + output_opt = "/Fo" + obj + try: + self.spawn([self.cc] + compile_opts + pp_opts + + [input_opt, output_opt] + + extra_postargs) + except PackagingExecError as msg: + raise CompileError(msg) + + return objects + + def create_static_lib(self, objects, output_libname, output_dir=None, + debug=False, target_lang=None): + if not self.initialized: + self.initialize() + objects, output_dir = self._fix_object_args(objects, output_dir) + output_filename = \ + self.library_filename(output_libname, output_dir=output_dir) + + if self._need_link(objects, output_filename): + lib_args = objects + ['/OUT:' + output_filename] + if debug: + pass # XXX what goes here? + try: + self.spawn([self.lib] + lib_args) + except PackagingExecError as msg: + raise LibError(msg) + + else: + logger.debug("skipping %s (up-to-date)", output_filename) + + def link(self, target_desc, objects, output_filename, output_dir=None, + libraries=None, library_dirs=None, runtime_library_dirs=None, + export_symbols=None, debug=False, extra_preargs=None, + extra_postargs=None, build_temp=None, target_lang=None): + + if not self.initialized: + self.initialize() + objects, output_dir = self._fix_object_args(objects, output_dir) + libraries, library_dirs, runtime_library_dirs = \ + self._fix_lib_args(libraries, library_dirs, runtime_library_dirs) + + if runtime_library_dirs: + self.warn("don't know what to do with 'runtime_library_dirs': %s" + % (runtime_library_dirs,)) + + lib_opts = gen_lib_options(self, library_dirs, runtime_library_dirs, + libraries) + if output_dir is not None: + output_filename = os.path.join(output_dir, output_filename) + + if self._need_link(objects, output_filename): + + if target_desc == CCompiler.EXECUTABLE: + if debug: + ldflags = self.ldflags_shared_debug[1:] + else: + ldflags = self.ldflags_shared[1:] + else: + if debug: + ldflags = self.ldflags_shared_debug + else: + ldflags = self.ldflags_shared + + export_opts = [] + for sym in (export_symbols or []): + export_opts.append("/EXPORT:" + sym) + + ld_args = (ldflags + lib_opts + export_opts + + objects + ['/OUT:' + output_filename]) + + # The MSVC linker generates .lib and .exp files, which cannot be + # suppressed by any linker switches. The .lib files may even be + # needed! Make sure they are generated in the temporary build + # directory. Since they have different names for debug and release + # builds, they can go into the same directory. + if export_symbols is not None: + dll_name, dll_ext = os.path.splitext( + os.path.basename(output_filename)) + implib_file = os.path.join( + os.path.dirname(objects[0]), + self.library_filename(dll_name)) + ld_args.append('/IMPLIB:' + implib_file) + + if extra_preargs: + ld_args[:0] = extra_preargs + if extra_postargs: + ld_args.extend(extra_postargs) + + self.mkpath(os.path.dirname(output_filename)) + try: + self.spawn([self.linker] + ld_args) + except PackagingExecError as msg: + raise LinkError(msg) + + else: + logger.debug("skipping %s (up-to-date)", output_filename) + + # -- Miscellaneous methods ----------------------------------------- + # These are all used by the 'gen_lib_options() function, in + # ccompiler.py. + + def library_dir_option(self, dir): + return "/LIBPATH:" + dir + + def runtime_library_dir_option(self, dir): + raise PackagingPlatformError("don't know how to set runtime library search path for MSVC++") + + def library_option(self, lib): + return self.library_filename(lib) + + def find_library_file(self, dirs, lib, debug=False): + # Prefer a debugging library if found (and requested), but deal + # with it if we don't have one. + if debug: + try_names = [lib + "_d", lib] + else: + try_names = [lib] + for dir in dirs: + for name in try_names: + libfile = os.path.join(dir, self.library_filename(name)) + if os.path.exists(libfile): + return libfile + else: + # Oops, didn't find it in *any* of 'dirs' + return None + + # Helper methods for using the MSVC registry settings + + def find_exe(self, exe): + """Return path to an MSVC executable program. + + Tries to find the program in several places: first, one of the + MSVC program search paths from the registry; next, the directories + in the PATH environment variable. If any of those work, return an + absolute path that is known to exist. If none of them work, just + return the original program name, 'exe'. + """ + + for p in self.__paths: + fn = os.path.join(os.path.abspath(p), exe) + if os.path.isfile(fn): + return fn + + # didn't find it; try existing path + for p in os.environ['Path'].split(';'): + fn = os.path.join(os.path.abspath(p), exe) + if os.path.isfile(fn): + return fn + + return exe + + def get_msvc_paths(self, path, platform='x86'): + """Get a list of devstudio directories (include, lib or path). + + Return a list of strings. The list will be empty if unable to + access the registry or appropriate registry keys not found. + """ + + if not _can_read_reg: + return [] + + path = path + " dirs" + if self.__version >= 7: + key = (r"%s\%0.1f\VC\VC_OBJECTS_PLATFORM_INFO\Win32\Directories" + % (self.__root, self.__version)) + else: + key = (r"%s\6.0\Build System\Components\Platforms" + r"\Win32 (%s)\Directories" % (self.__root, platform)) + + for base in HKEYS: + d = read_values(base, key) + if d: + if self.__version >= 7: + return self.__macros.sub(d[path]).split(";") + else: + return d[path].split(";") + # MSVC 6 seems to create the registry entries we need only when + # the GUI is run. + if self.__version == 6: + for base in HKEYS: + if read_values(base, r"%s\6.0" % self.__root) is not None: + self.warn("It seems you have Visual Studio 6 installed, " + "but the expected registry settings are not present.\n" + "You must at least run the Visual Studio GUI once " + "so that these entries are created.") + break + return [] + + def set_path_env_var(self, name): + """Set environment variable 'name' to an MSVC path type value. + + This is equivalent to a SET command prior to execution of spawned + commands. + """ + + if name == "lib": + p = self.get_msvc_paths("library") + else: + p = self.get_msvc_paths(name) + if p: + os.environ[name] = ';'.join(p) + + +if get_build_version() >= 8.0: + logger.debug("importing new compiler from distutils.msvc9compiler") + OldMSVCCompiler = MSVCCompiler + from packaging.compiler.msvc9compiler import MSVCCompiler + # get_build_architecture not really relevant now we support cross-compile + from packaging.compiler.msvc9compiler import MacroExpander diff --git a/Lib/packaging/compiler/unixccompiler.py b/Lib/packaging/compiler/unixccompiler.py new file mode 100644 index 000000000000..8c24c0f6e38a --- /dev/null +++ b/Lib/packaging/compiler/unixccompiler.py @@ -0,0 +1,339 @@ +"""CCompiler implementation for Unix compilers. + +This module contains the UnixCCompiler class, a subclass of CCompiler +that handles the "typical" Unix-style command-line C compiler: + * macros defined with -Dname[=value] + * macros undefined with -Uname + * include search directories specified with -Idir + * libraries specified with -lllib + * library search directories specified with -Ldir + * compile handled by 'cc' (or similar) executable with -c option: + compiles .c to .o + * link static library handled by 'ar' command (possibly with 'ranlib') + * link shared library handled by 'cc -shared' +""" + +import os, sys + +from packaging.util import newer +from packaging.compiler.ccompiler import CCompiler +from packaging.compiler import gen_preprocess_options, gen_lib_options +from packaging.errors import (PackagingExecError, CompileError, + LibError, LinkError) +from packaging import logger +import sysconfig + + +# XXX Things not currently handled: +# * optimization/debug/warning flags; we just use whatever's in Python's +# Makefile and live with it. Is this adequate? If not, we might +# have to have a bunch of subclasses GNUCCompiler, SGICCompiler, +# SunCCompiler, and I suspect down that road lies madness. +# * even if we don't know a warning flag from an optimization flag, +# we need some way for outsiders to feed preprocessor/compiler/linker +# flags in to us -- eg. a sysadmin might want to mandate certain flags +# via a site config file, or a user might want to set something for +# compiling this module distribution only via the setup.py command +# line, whatever. As long as these options come from something on the +# current system, they can be as system-dependent as they like, and we +# should just happily stuff them into the preprocessor/compiler/linker +# options and carry on. + +def _darwin_compiler_fixup(compiler_so, cc_args): + """ + This function will strip '-isysroot PATH' and '-arch ARCH' from the + compile flags if the user has specified one them in extra_compile_flags. + + This is needed because '-arch ARCH' adds another architecture to the + build, without a way to remove an architecture. Furthermore GCC will + barf if multiple '-isysroot' arguments are present. + """ + stripArch = stripSysroot = False + + compiler_so = list(compiler_so) + kernel_version = os.uname()[2] # 8.4.3 + major_version = int(kernel_version.split('.')[0]) + + if major_version < 8: + # OSX before 10.4.0, these don't support -arch and -isysroot at + # all. + stripArch = stripSysroot = True + else: + stripArch = '-arch' in cc_args + stripSysroot = '-isysroot' in cc_args + + if stripArch or 'ARCHFLAGS' in os.environ: + while True: + try: + index = compiler_so.index('-arch') + # Strip this argument and the next one: + del compiler_so[index:index+2] + except ValueError: + break + + if 'ARCHFLAGS' in os.environ and not stripArch: + # User specified different -arch flags in the environ, + # see also the sysconfig + compiler_so = compiler_so + os.environ['ARCHFLAGS'].split() + + if stripSysroot: + try: + index = compiler_so.index('-isysroot') + # Strip this argument and the next one: + del compiler_so[index:index+2] + except ValueError: + pass + + # Check if the SDK that is used during compilation actually exists, + # the universal build requires the usage of a universal SDK and not all + # users have that installed by default. + sysroot = None + if '-isysroot' in cc_args: + idx = cc_args.index('-isysroot') + sysroot = cc_args[idx+1] + elif '-isysroot' in compiler_so: + idx = compiler_so.index('-isysroot') + sysroot = compiler_so[idx+1] + + if sysroot and not os.path.isdir(sysroot): + logger.warning( + "compiling with an SDK that doesn't seem to exist: %r;\n" + "please check your Xcode installation", sysroot) + + return compiler_so + +class UnixCCompiler(CCompiler): + + name = 'unix' + description = 'Standard UNIX-style compiler' + + # These are used by CCompiler in two places: the constructor sets + # instance attributes 'preprocessor', 'compiler', etc. from them, and + # 'set_executable()' allows any of these to be set. The defaults here + # are pretty generic; they will probably have to be set by an outsider + # (eg. using information discovered by the sysconfig about building + # Python extensions). + executables = {'preprocessor' : None, + 'compiler' : ["cc"], + 'compiler_so' : ["cc"], + 'compiler_cxx' : ["cc"], + 'linker_so' : ["cc", "-shared"], + 'linker_exe' : ["cc"], + 'archiver' : ["ar", "-cr"], + 'ranlib' : None, + } + + if sys.platform[:6] == "darwin": + executables['ranlib'] = ["ranlib"] + + # Needed for the filename generation methods provided by the base + # class, CCompiler. NB. whoever instantiates/uses a particular + # UnixCCompiler instance should set 'shared_lib_ext' -- we set a + # reasonable common default here, but it's not necessarily used on all + # Unices! + + src_extensions = [".c",".C",".cc",".cxx",".cpp",".m"] + obj_extension = ".o" + static_lib_extension = ".a" + shared_lib_extension = ".so" + dylib_lib_extension = ".dylib" + static_lib_format = shared_lib_format = dylib_lib_format = "lib%s%s" + if sys.platform == "cygwin": + exe_extension = ".exe" + + def preprocess(self, source, + output_file=None, macros=None, include_dirs=None, + extra_preargs=None, extra_postargs=None): + ignore, macros, include_dirs = \ + self._fix_compile_args(None, macros, include_dirs) + pp_opts = gen_preprocess_options(macros, include_dirs) + pp_args = self.preprocessor + pp_opts + if output_file: + pp_args.extend(('-o', output_file)) + if extra_preargs: + pp_args[:0] = extra_preargs + if extra_postargs: + pp_args.extend(extra_postargs) + pp_args.append(source) + + # We need to preprocess: either we're being forced to, or we're + # generating output to stdout, or there's a target output file and + # the source file is newer than the target (or the target doesn't + # exist). + if self.force or output_file is None or newer(source, output_file): + if output_file: + self.mkpath(os.path.dirname(output_file)) + try: + self.spawn(pp_args) + except PackagingExecError as msg: + raise CompileError(msg) + + def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts): + compiler_so = self.compiler_so + if sys.platform == 'darwin': + compiler_so = _darwin_compiler_fixup(compiler_so, cc_args + extra_postargs) + try: + self.spawn(compiler_so + cc_args + [src, '-o', obj] + + extra_postargs) + except PackagingExecError as msg: + raise CompileError(msg) + + def create_static_lib(self, objects, output_libname, + output_dir=None, debug=False, target_lang=None): + objects, output_dir = self._fix_object_args(objects, output_dir) + + output_filename = \ + self.library_filename(output_libname, output_dir=output_dir) + + if self._need_link(objects, output_filename): + self.mkpath(os.path.dirname(output_filename)) + self.spawn(self.archiver + + [output_filename] + + objects + self.objects) + + # Not many Unices required ranlib anymore -- SunOS 4.x is, I + # think the only major Unix that does. Maybe we need some + # platform intelligence here to skip ranlib if it's not + # needed -- or maybe Python's configure script took care of + # it for us, hence the check for leading colon. + if self.ranlib: + try: + self.spawn(self.ranlib + [output_filename]) + except PackagingExecError as msg: + raise LibError(msg) + else: + logger.debug("skipping %s (up-to-date)", output_filename) + + def link(self, target_desc, objects, + output_filename, output_dir=None, libraries=None, + library_dirs=None, runtime_library_dirs=None, + export_symbols=None, debug=False, extra_preargs=None, + extra_postargs=None, build_temp=None, target_lang=None): + objects, output_dir = self._fix_object_args(objects, output_dir) + libraries, library_dirs, runtime_library_dirs = \ + self._fix_lib_args(libraries, library_dirs, runtime_library_dirs) + + lib_opts = gen_lib_options(self, library_dirs, runtime_library_dirs, + libraries) + if type(output_dir) not in (str, type(None)): + raise TypeError("'output_dir' must be a string or None") + if output_dir is not None: + output_filename = os.path.join(output_dir, output_filename) + + if self._need_link(objects, output_filename): + ld_args = (objects + self.objects + + lib_opts + ['-o', output_filename]) + if debug: + ld_args[:0] = ['-g'] + if extra_preargs: + ld_args[:0] = extra_preargs + if extra_postargs: + ld_args.extend(extra_postargs) + self.mkpath(os.path.dirname(output_filename)) + try: + if target_desc == CCompiler.EXECUTABLE: + linker = self.linker_exe[:] + else: + linker = self.linker_so[:] + if target_lang == "c++" and self.compiler_cxx: + # skip over environment variable settings if /usr/bin/env + # is used to set up the linker's environment. + # This is needed on OSX. Note: this assumes that the + # normal and C++ compiler have the same environment + # settings. + i = 0 + if os.path.basename(linker[0]) == "env": + i = 1 + while '=' in linker[i]: + i = i + 1 + + linker[i] = self.compiler_cxx[i] + + if sys.platform == 'darwin': + linker = _darwin_compiler_fixup(linker, ld_args) + + self.spawn(linker + ld_args) + except PackagingExecError as msg: + raise LinkError(msg) + else: + logger.debug("skipping %s (up-to-date)", output_filename) + + # -- Miscellaneous methods ----------------------------------------- + # These are all used by the 'gen_lib_options() function, in + # ccompiler.py. + + def library_dir_option(self, dir): + return "-L" + dir + + def _is_gcc(self, compiler_name): + return "gcc" in compiler_name or "g++" in compiler_name + + def runtime_library_dir_option(self, dir): + # XXX Hackish, at the very least. See Python bug #445902: + # http://sourceforge.net/tracker/index.php + # ?func=detail&aid=445902&group_id=5470&atid=105470 + # Linkers on different platforms need different options to + # specify that directories need to be added to the list of + # directories searched for dependencies when a dynamic library + # is sought. GCC on GNU systems (Linux, FreeBSD, ...) has to + # be told to pass the -R option through to the linker, whereas + # other compilers and gcc on other systems just know this. + # Other compilers may need something slightly different. At + # this time, there's no way to determine this information from + # the configuration data stored in the Python installation, so + # we use this hack. + + compiler = os.path.basename(sysconfig.get_config_var("CC")) + if sys.platform[:6] == "darwin": + # MacOSX's linker doesn't understand the -R flag at all + return "-L" + dir + elif sys.platform[:5] == "hp-ux": + if self._is_gcc(compiler): + return ["-Wl,+s", "-L" + dir] + return ["+s", "-L" + dir] + elif sys.platform[:7] == "irix646" or sys.platform[:6] == "osf1V5": + return ["-rpath", dir] + elif self._is_gcc(compiler): + # gcc on non-GNU systems does not need -Wl, but can + # use it anyway. Since distutils has always passed in + # -Wl whenever gcc was used in the past it is probably + # safest to keep doing so. + if sysconfig.get_config_var("GNULD") == "yes": + # GNU ld needs an extra option to get a RUNPATH + # instead of just an RPATH. + return "-Wl,--enable-new-dtags,-R" + dir + else: + return "-Wl,-R" + dir + elif sys.platform[:3] == "aix": + return "-blibpath:" + dir + else: + # No idea how --enable-new-dtags would be passed on to + # ld if this system was using GNU ld. Don't know if a + # system like this even exists. + return "-R" + dir + + def library_option(self, lib): + return "-l" + lib + + def find_library_file(self, dirs, lib, debug=False): + shared_f = self.library_filename(lib, lib_type='shared') + dylib_f = self.library_filename(lib, lib_type='dylib') + static_f = self.library_filename(lib, lib_type='static') + + for dir in dirs: + shared = os.path.join(dir, shared_f) + dylib = os.path.join(dir, dylib_f) + static = os.path.join(dir, static_f) + # We're second-guessing the linker here, with not much hard + # data to go on: GCC seems to prefer the shared library, so I'm + # assuming that *all* Unix C compilers do. And of course I'm + # ignoring even GCC's "-static" option. So sue me. + if os.path.exists(dylib): + return dylib + elif os.path.exists(shared): + return shared + elif os.path.exists(static): + return static + + # Oops, didn't find it in *any* of 'dirs' + return None diff --git a/Lib/packaging/config.py b/Lib/packaging/config.py new file mode 100644 index 000000000000..9239f4a83b6f --- /dev/null +++ b/Lib/packaging/config.py @@ -0,0 +1,357 @@ +"""Utilities to find and read config files used by packaging.""" + +import os +import sys +import logging + +from shlex import split +from configparser import RawConfigParser +from packaging import logger +from packaging.errors import PackagingOptionError +from packaging.compiler.extension import Extension +from packaging.util import check_environ, iglob, resolve_name, strtobool +from packaging.compiler import set_compiler +from packaging.command import set_command +from packaging.markers import interpret + + +def _pop_values(values_dct, key): + """Remove values from the dictionary and convert them as a list""" + vals_str = values_dct.pop(key, '') + if not vals_str: + return + fields = [] + for field in vals_str.split(os.linesep): + tmp_vals = field.split('--') + if len(tmp_vals) == 2 and not interpret(tmp_vals[1]): + continue + fields.append(tmp_vals[0]) + # Get bash options like `gcc -print-file-name=libgcc.a` XXX bash options? + vals = split(' '.join(fields)) + if vals: + return vals + + +def _rel_path(base, path): + assert path.startswith(base) + return path[len(base):].lstrip('/') + + +def get_resources_dests(resources_root, rules): + """Find destinations for resources files""" + destinations = {} + for base, suffix, dest in rules: + prefix = os.path.join(resources_root, base) + for abs_base in iglob(prefix): + abs_glob = os.path.join(abs_base, suffix) + for abs_path in iglob(abs_glob): + resource_file = _rel_path(resources_root, abs_path) + if dest is None: # remove the entry if it was here + destinations.pop(resource_file, None) + else: + rel_path = _rel_path(abs_base, abs_path) + destinations[resource_file] = os.path.join(dest, rel_path) + return destinations + + +class Config: + """Reads configuration files and work with the Distribution instance + """ + def __init__(self, dist): + self.dist = dist + self.setup_hook = None + + def run_hook(self, config): + if self.setup_hook is None: + return + # the hook gets only the config + self.setup_hook(config) + + def find_config_files(self): + """Find as many configuration files as should be processed for this + platform, and return a list of filenames in the order in which they + should be parsed. The filenames returned are guaranteed to exist + (modulo nasty race conditions). + + There are three possible config files: packaging.cfg in the + Packaging installation directory (ie. where the top-level + Packaging __inst__.py file lives), a file in the user's home + directory named .pydistutils.cfg on Unix and pydistutils.cfg + on Windows/Mac; and setup.cfg in the current directory. + + The file in the user's home directory can be disabled with the + --no-user-cfg option. + """ + files = [] + check_environ() + + # Where to look for the system-wide Packaging config file + sys_dir = os.path.dirname(sys.modules['packaging'].__file__) + + # Look for the system config file + sys_file = os.path.join(sys_dir, "packaging.cfg") + if os.path.isfile(sys_file): + files.append(sys_file) + + # What to call the per-user config file + if os.name == 'posix': + user_filename = ".pydistutils.cfg" + else: + user_filename = "pydistutils.cfg" + + # And look for the user config file + if self.dist.want_user_cfg: + user_file = os.path.join(os.path.expanduser('~'), user_filename) + if os.path.isfile(user_file): + files.append(user_file) + + # All platforms support local setup.cfg + local_file = "setup.cfg" + if os.path.isfile(local_file): + files.append(local_file) + + if logger.isEnabledFor(logging.DEBUG): + logger.debug("using config files: %s", ', '.join(files)) + return files + + def _convert_metadata(self, name, value): + # converts a value found in setup.cfg into a valid metadata + # XXX + return value + + def _multiline(self, value): + value = [v for v in + [v.strip() for v in value.split('\n')] + if v != ''] + return value + + def _read_setup_cfg(self, parser, cfg_filename): + cfg_directory = os.path.dirname(os.path.abspath(cfg_filename)) + content = {} + for section in parser.sections(): + content[section] = dict(parser.items(section)) + + # global:setup_hook is called *first* + if 'global' in content: + if 'setup_hook' in content['global']: + setup_hook = content['global']['setup_hook'] + try: + self.setup_hook = resolve_name(setup_hook) + except ImportError as e: + logger.warning('could not import setup_hook: %s', + e.args[0]) + else: + self.run_hook(content) + + metadata = self.dist.metadata + + # setting the metadata values + if 'metadata' in content: + for key, value in content['metadata'].items(): + key = key.replace('_', '-') + if metadata.is_multi_field(key): + value = self._multiline(value) + + if key == 'project-url': + value = [(label.strip(), url.strip()) + for label, url in + [v.split(',') for v in value]] + + if key == 'description-file': + if 'description' in content['metadata']: + msg = ("description and description-file' are " + "mutually exclusive") + raise PackagingOptionError(msg) + + if isinstance(value, list): + filenames = value + else: + filenames = value.split() + + # concatenate each files + value = '' + for filename in filenames: + # will raise if file not found + with open(filename) as description_file: + value += description_file.read().strip() + '\n' + # add filename as a required file + if filename not in metadata.requires_files: + metadata.requires_files.append(filename) + value = value.strip() + key = 'description' + + if metadata.is_metadata_field(key): + metadata[key] = self._convert_metadata(key, value) + + if 'files' in content: + files = content['files'] + self.dist.package_dir = files.pop('packages_root', None) + + files = dict((key, self._multiline(value)) for key, value in + files.items()) + + self.dist.packages = [] + + packages = files.get('packages', []) + if isinstance(packages, str): + packages = [packages] + + for package in packages: + if ':' in package: + dir_, package = package.split(':') + self.dist.package_dir[package] = dir_ + self.dist.packages.append(package) + + self.dist.py_modules = files.get('modules', []) + if isinstance(self.dist.py_modules, str): + self.dist.py_modules = [self.dist.py_modules] + self.dist.scripts = files.get('scripts', []) + if isinstance(self.dist.scripts, str): + self.dist.scripts = [self.dist.scripts] + + self.dist.package_data = {} + for data in files.get('package_data', []): + data = data.split('=') + if len(data) != 2: + continue # XXX error should never pass silently + key, value = data + self.dist.package_data[key.strip()] = value.strip() + + self.dist.data_files = [] + for data in files.get('data_files', []): + data = data.split('=') + if len(data) != 2: + continue + key, value = data + values = [v.strip() for v in value.split(',')] + self.dist.data_files.append((key, values)) + + # manifest template + self.dist.extra_files = files.get('extra_files', []) + + resources = [] + for rule in files.get('resources', []): + glob, destination = rule.split('=', 1) + rich_glob = glob.strip().split(' ', 1) + if len(rich_glob) == 2: + prefix, suffix = rich_glob + else: + assert len(rich_glob) == 1 + prefix = '' + suffix = glob + if destination == '': + destination = None + resources.append( + (prefix.strip(), suffix.strip(), destination.strip())) + self.dist.data_files = get_resources_dests( + cfg_directory, resources) + + ext_modules = self.dist.ext_modules + for section_key in content: + labels = section_key.split('=') + if len(labels) == 2 and labels[0] == 'extension': + # labels[1] not used from now but should be implemented + # for extension build dependency + values_dct = content[section_key] + ext_modules.append(Extension( + values_dct.pop('name'), + _pop_values(values_dct, 'sources'), + _pop_values(values_dct, 'include_dirs'), + _pop_values(values_dct, 'define_macros'), + _pop_values(values_dct, 'undef_macros'), + _pop_values(values_dct, 'library_dirs'), + _pop_values(values_dct, 'libraries'), + _pop_values(values_dct, 'runtime_library_dirs'), + _pop_values(values_dct, 'extra_objects'), + _pop_values(values_dct, 'extra_compile_args'), + _pop_values(values_dct, 'extra_link_args'), + _pop_values(values_dct, 'export_symbols'), + _pop_values(values_dct, 'swig_opts'), + _pop_values(values_dct, 'depends'), + values_dct.pop('language', None), + values_dct.pop('optional', None), + **values_dct)) + + def parse_config_files(self, filenames=None): + if filenames is None: + filenames = self.find_config_files() + + logger.debug("Distribution.parse_config_files():") + + parser = RawConfigParser() + + for filename in filenames: + logger.debug(" reading %s", filename) + parser.read(filename) + + if os.path.split(filename)[-1] == 'setup.cfg': + self._read_setup_cfg(parser, filename) + + for section in parser.sections(): + if section == 'global': + if parser.has_option('global', 'compilers'): + self._load_compilers(parser.get('global', 'compilers')) + + if parser.has_option('global', 'commands'): + self._load_commands(parser.get('global', 'commands')) + + options = parser.options(section) + opt_dict = self.dist.get_option_dict(section) + + for opt in options: + if opt == '__name__': + continue + val = parser.get(section, opt) + opt = opt.replace('-', '_') + + if opt == 'sub_commands': + val = self._multiline(val) + if isinstance(val, str): + val = [val] + + # Hooks use a suffix system to prevent being overriden + # by a config file processed later (i.e. a hook set in + # the user config file cannot be replaced by a hook + # set in a project config file, unless they have the + # same suffix). + if (opt.startswith("pre_hook.") or + opt.startswith("post_hook.")): + hook_type, alias = opt.split(".") + hook_dict = opt_dict.setdefault( + hook_type, (filename, {}))[1] + hook_dict[alias] = val + else: + opt_dict[opt] = filename, val + + # Make the RawConfigParser forget everything (so we retain + # the original filenames that options come from) + parser.__init__() + + # If there was a "global" section in the config file, use it + # to set Distribution options. + if 'global' in self.dist.command_options: + for opt, (src, val) in self.dist.command_options['global'].items(): + alias = self.dist.negative_opt.get(opt) + try: + if alias: + setattr(self.dist, alias, not strtobool(val)) + elif opt == 'dry_run': # FIXME ugh! + setattr(self.dist, opt, strtobool(val)) + else: + setattr(self.dist, opt, val) + except ValueError as msg: + raise PackagingOptionError(msg) + + def _load_compilers(self, compilers): + compilers = self._multiline(compilers) + if isinstance(compilers, str): + compilers = [compilers] + for compiler in compilers: + set_compiler(compiler.strip()) + + def _load_commands(self, commands): + commands = self._multiline(commands) + if isinstance(commands, str): + commands = [commands] + for command in commands: + set_command(command.strip()) diff --git a/Lib/packaging/create.py b/Lib/packaging/create.py new file mode 100644 index 000000000000..837d0b6b8813 --- /dev/null +++ b/Lib/packaging/create.py @@ -0,0 +1,693 @@ +#!/usr/bin/env python +"""Interactive helper used to create a setup.cfg file. + +This script will generate a packaging configuration file by looking at +the current directory and asking the user questions. It is intended to +be called as + + pysetup create + +or + + python3.3 -m packaging.create +""" + +# Original code by Sean Reifschneider + +# Original TODO list: +# Look for a license file and automatically add the category. +# When a .c file is found during the walk, can we add it as an extension? +# Ask if there is a maintainer different that the author +# Ask for the platform (can we detect this via "import win32" or something?) +# Ask for the dependencies. +# Ask for the Requires-Dist +# Ask for the Provides-Dist +# Ask for a description +# Detect scripts (not sure how. #! outside of package?) + +import os +import imp +import sys +import glob +import re +import shutil +import sysconfig +from configparser import RawConfigParser +from textwrap import dedent +from hashlib import md5 +from functools import cmp_to_key +# importing this with an underscore as it should be replaced by the +# dict form or another structures for all purposes +from packaging._trove import all_classifiers as _CLASSIFIERS_LIST +from packaging.version import is_valid_version + +_FILENAME = 'setup.cfg' +_DEFAULT_CFG = '.pypkgcreate' + +_helptext = { + 'name': ''' +The name of the program to be packaged, usually a single word composed +of lower-case characters such as "python", "sqlalchemy", or "CherryPy". +''', + 'version': ''' +Version number of the software, typically 2 or 3 numbers separated by dots +such as "1.00", "0.6", or "3.02.01". "0.1.0" is recommended for initial +development. +''', + 'summary': ''' +A one-line summary of what this project is or does, typically a sentence 80 +characters or less in length. +''', + 'author': ''' +The full name of the author (typically you). +''', + 'author_email': ''' +E-mail address of the project author (typically you). +''', + 'do_classifier': ''' +Trove classifiers are optional identifiers that allow you to specify the +intended audience by saying things like "Beta software with a text UI +for Linux under the PSF license. However, this can be a somewhat involved +process. +''', + 'packages': ''' +You can provide a package name contained in your project. +''', + 'modules': ''' +You can provide a python module contained in your project. +''', + 'extra_files': ''' +You can provide extra files/dirs contained in your project. +It has to follow the template syntax. XXX add help here. +''', + + 'home_page': ''' +The home page for the project, typically starting with "http://". +''', + 'trove_license': ''' +Optionally you can specify a license. Type a string that identifies a common +license, and then you can select a list of license specifiers. +''', + 'trove_generic': ''' +Optionally, you can set other trove identifiers for things such as the +human language, programming language, user interface, etc... +''', + 'setup.py found': ''' +The setup.py script will be executed to retrieve the metadata. +A wizard will be run if you answer "n", +''', +} + +PROJECT_MATURITY = ['Development Status :: 1 - Planning', + 'Development Status :: 2 - Pre-Alpha', + 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', + 'Development Status :: 6 - Mature', + 'Development Status :: 7 - Inactive'] + +# XXX everything needs docstrings and tests (both low-level tests of various +# methods and functional tests of running the script) + + +def load_setup(): + """run the setup script (i.e the setup.py file) + + This function load the setup file in all cases (even if it have already + been loaded before, because we are monkey patching its setup function with + a particular one""" + with open("setup.py") as f: + imp.load_module("setup", f, "setup.py", (".py", "r", imp.PY_SOURCE)) + + +def ask_yn(question, default=None, helptext=None): + question += ' (y/n)' + while True: + answer = ask(question, default, helptext, required=True) + if answer and answer[0].lower() in 'yn': + return answer[0].lower() + + print('\nERROR: You must select "Y" or "N".\n') + + +def ask(question, default=None, helptext=None, required=True, + lengthy=False, multiline=False): + prompt = '%s: ' % (question,) + if default: + prompt = '%s [%s]: ' % (question, default) + if default and len(question) + len(default) > 70: + prompt = '%s\n [%s]: ' % (question, default) + if lengthy or multiline: + prompt += '\n > ' + + if not helptext: + helptext = 'No additional help available.' + + helptext = helptext.strip("\n") + + while True: + sys.stdout.write(prompt) + sys.stdout.flush() + + line = sys.stdin.readline().strip() + if line == '?': + print('=' * 70) + print(helptext) + print('=' * 70) + continue + if default and not line: + return default + if not line and required: + print('*' * 70) + print('This value cannot be empty.') + print('===========================') + if helptext: + print(helptext) + print('*' * 70) + continue + return line + + +def convert_yn_to_bool(yn, yes=True, no=False): + """Convert a y/yes or n/no to a boolean value.""" + if yn.lower().startswith('y'): + return yes + else: + return no + + +def _build_classifiers_dict(classifiers): + d = {} + for key in classifiers: + subDict = d + for subkey in key.split(' :: '): + if not subkey in subDict: + subDict[subkey] = {} + subDict = subDict[subkey] + return d + +CLASSIFIERS = _build_classifiers_dict(_CLASSIFIERS_LIST) + + +def _build_licences(classifiers): + res = [] + for index, item in enumerate(classifiers): + if not item.startswith('License :: '): + continue + res.append((index, item.split(' :: ')[-1].lower())) + return res + +LICENCES = _build_licences(_CLASSIFIERS_LIST) + + +class MainProgram: + """Make a project setup configuration file (setup.cfg).""" + + def __init__(self): + self.configparser = None + self.classifiers = set() + self.data = {'name': '', + 'version': '1.0.0', + 'classifier': self.classifiers, + 'packages': [], + 'modules': [], + 'platform': [], + 'resources': [], + 'extra_files': [], + 'scripts': [], + } + self._load_defaults() + + def __call__(self): + setupcfg_defined = False + if self.has_setup_py() and self._prompt_user_for_conversion(): + setupcfg_defined = self.convert_py_to_cfg() + if not setupcfg_defined: + self.define_cfg_values() + self._write_cfg() + + def has_setup_py(self): + """Test for the existance of a setup.py file.""" + return os.path.exists('setup.py') + + def define_cfg_values(self): + self.inspect() + self.query_user() + + def _lookup_option(self, key): + if not self.configparser.has_option('DEFAULT', key): + return None + return self.configparser.get('DEFAULT', key) + + def _load_defaults(self): + # Load default values from a user configuration file + self.configparser = RawConfigParser() + # TODO replace with section in distutils config file + default_cfg = os.path.expanduser(os.path.join('~', _DEFAULT_CFG)) + self.configparser.read(default_cfg) + self.data['author'] = self._lookup_option('author') + self.data['author_email'] = self._lookup_option('author_email') + + def _prompt_user_for_conversion(self): + # Prompt the user about whether they would like to use the setup.py + # conversion utility to generate a setup.cfg or generate the setup.cfg + # from scratch + answer = ask_yn(('A legacy setup.py has been found.\n' + 'Would you like to convert it to a setup.cfg?'), + default="y", + helptext=_helptext['setup.py found']) + return convert_yn_to_bool(answer) + + def _dotted_packages(self, data): + packages = sorted(data) + modified_pkgs = [] + for pkg in packages: + pkg = pkg.lstrip('./') + pkg = pkg.replace('/', '.') + modified_pkgs.append(pkg) + return modified_pkgs + + def _write_cfg(self): + if os.path.exists(_FILENAME): + if os.path.exists('%s.old' % _FILENAME): + print("ERROR: %(name)s.old backup exists, please check that " + "current %(name)s is correct and remove %(name)s.old" % + {'name': _FILENAME}) + return + shutil.move(_FILENAME, '%s.old' % _FILENAME) + + with open(_FILENAME, 'w') as fp: + fp.write('[metadata]\n') + # simple string entries + for name in ('name', 'version', 'summary', 'download_url'): + fp.write('%s = %s\n' % (name, self.data.get(name, 'UNKNOWN'))) + # optional string entries + if 'keywords' in self.data and self.data['keywords']: + fp.write('keywords = %s\n' % ' '.join(self.data['keywords'])) + for name in ('home_page', 'author', 'author_email', + 'maintainer', 'maintainer_email', 'description-file'): + if name in self.data and self.data[name]: + fp.write('%s = %s\n' % (name, self.data[name])) + if 'description' in self.data: + fp.write( + 'description = %s\n' + % '\n |'.join(self.data['description'].split('\n'))) + # multiple use string entries + for name in ('platform', 'supported-platform', 'classifier', + 'requires-dist', 'provides-dist', 'obsoletes-dist', + 'requires-external'): + if not(name in self.data and self.data[name]): + continue + fp.write('%s = ' % name) + fp.write(''.join(' %s\n' % val + for val in self.data[name]).lstrip()) + fp.write('\n[files]\n') + for name in ('packages', 'modules', 'scripts', + 'package_data', 'extra_files'): + if not(name in self.data and self.data[name]): + continue + fp.write('%s = %s\n' + % (name, '\n '.join(self.data[name]).strip())) + fp.write('\nresources =\n') + for src, dest in self.data['resources']: + fp.write(' %s = %s\n' % (src, dest)) + fp.write('\n') + + os.chmod(_FILENAME, 0o644) + print('Wrote "%s".' % _FILENAME) + + def convert_py_to_cfg(self): + """Generate a setup.cfg from an existing setup.py. + + It only exports the distutils metadata (setuptools specific metadata + is not currently supported). + """ + data = self.data + + def setup_mock(**attrs): + """Mock the setup(**attrs) in order to retrieve metadata.""" + # use the distutils v1 processings to correctly parse metadata. + #XXX we could also use the setuptools distibution ??? + from distutils.dist import Distribution + dist = Distribution(attrs) + dist.parse_config_files() + + # 1. retrieve metadata fields that are quite similar in + # PEP 314 and PEP 345 + labels = (('name',) * 2, + ('version',) * 2, + ('author',) * 2, + ('author_email',) * 2, + ('maintainer',) * 2, + ('maintainer_email',) * 2, + ('description', 'summary'), + ('long_description', 'description'), + ('url', 'home_page'), + ('platforms', 'platform'), + # backport only for 2.5+ + ('provides', 'provides-dist'), + ('obsoletes', 'obsoletes-dist'), + ('requires', 'requires-dist')) + + get = lambda lab: getattr(dist.metadata, lab.replace('-', '_')) + data.update((new, get(old)) for old, new in labels if get(old)) + + # 2. retrieve data that requires special processing + data['classifier'].update(dist.get_classifiers() or []) + data['scripts'].extend(dist.scripts or []) + data['packages'].extend(dist.packages or []) + data['modules'].extend(dist.py_modules or []) + # 2.1 data_files -> resources + if dist.data_files: + if len(dist.data_files) < 2 or \ + isinstance(dist.data_files[1], str): + dist.data_files = [('', dist.data_files)] + # add tokens in the destination paths + vars = {'distribution.name': data['name']} + path_tokens = list(sysconfig.get_paths(vars=vars).items()) + + def length_comparison(x, y): + len_x = len(x[1]) + len_y = len(y[1]) + if len_x == len_y: + return 0 + elif len_x < len_y: + return -1 + else: + return 1 + + # sort tokens to use the longest one first + path_tokens.sort(key=cmp_to_key(length_comparison)) + for dest, srcs in (dist.data_files or []): + dest = os.path.join(sys.prefix, dest) + for tok, path in path_tokens: + if dest.startswith(path): + dest = ('{%s}' % tok) + dest[len(path):] + files = [('/ '.join(src.rsplit('/', 1)), dest) + for src in srcs] + data['resources'].extend(files) + continue + # 2.2 package_data -> extra_files + package_dirs = dist.package_dir or {} + for package, extras in iter(dist.package_data.items()) or []: + package_dir = package_dirs.get(package, package) + files = [os.path.join(package_dir, f) for f in extras] + data['extra_files'].extend(files) + + # Use README file if its content is the desciption + if "description" in data: + ref = md5(re.sub('\s', '', + self.data['description']).lower().encode()) + ref = ref.digest() + for readme in glob.glob('README*'): + with open(readme) as fp: + contents = fp.read() + val = md5(re.sub('\s', '', + contents.lower()).encode()).digest() + if val == ref: + del data['description'] + data['description-file'] = readme + break + + # apply monkey patch to distutils (v1) and setuptools (if needed) + # (abort the feature if distutils v1 has been killed) + try: + from distutils import core + core.setup # make sure it's not d2 maskerading as d1 + except (ImportError, AttributeError): + return + saved_setups = [(core, core.setup)] + core.setup = setup_mock + try: + import setuptools + except ImportError: + pass + else: + saved_setups.append((setuptools, setuptools.setup)) + setuptools.setup = setup_mock + # get metadata by executing the setup.py with the patched setup(...) + success = False # for python < 2.4 + try: + load_setup() + success = True + finally: # revert monkey patches + for patched_module, original_setup in saved_setups: + patched_module.setup = original_setup + if not self.data: + raise ValueError('Unable to load metadata from setup.py') + return success + + def inspect_file(self, path): + with open(path, 'r') as fp: + for _ in range(10): + line = fp.readline() + m = re.match(r'^#!.*python((?P\d)(\.\d+)?)?$', line) + if m: + if m.group('major') == '3': + self.classifiers.add( + 'Programming Language :: Python :: 3') + else: + self.classifiers.add( + 'Programming Language :: Python :: 2') + + def inspect(self): + """Inspect the current working diretory for a name and version. + + This information is harvested in where the directory is named + like [name]-[version]. + """ + dir_name = os.path.basename(os.getcwd()) + self.data['name'] = dir_name + match = re.match(r'(.*)-(\d.+)', dir_name) + if match: + self.data['name'] = match.group(1) + self.data['version'] = match.group(2) + # TODO Needs tested! + if not is_valid_version(self.data['version']): + msg = "Invalid version discovered: %s" % self.data['version'] + raise RuntimeError(msg) + + def query_user(self): + self.data['name'] = ask('Project name', self.data['name'], + _helptext['name']) + + self.data['version'] = ask('Current version number', + self.data.get('version'), _helptext['version']) + self.data['summary'] = ask('Package summary', + self.data.get('summary'), _helptext['summary'], + lengthy=True) + self.data['author'] = ask('Author name', + self.data.get('author'), _helptext['author']) + self.data['author_email'] = ask('Author e-mail address', + self.data.get('author_email'), _helptext['author_email']) + self.data['home_page'] = ask('Project Home Page', + self.data.get('home_page'), _helptext['home_page'], + required=False) + + if ask_yn('Do you want me to automatically build the file list ' + 'with everything I can find in the current directory ? ' + 'If you say no, you will have to define them manually.') == 'y': + self._find_files() + else: + while ask_yn('Do you want to add a single module ?' + ' (you will be able to add full packages next)', + helptext=_helptext['modules']) == 'y': + self._set_multi('Module name', 'modules') + + while ask_yn('Do you want to add a package ?', + helptext=_helptext['packages']) == 'y': + self._set_multi('Package name', 'packages') + + while ask_yn('Do you want to add an extra file ?', + helptext=_helptext['extra_files']) == 'y': + self._set_multi('Extra file/dir name', 'extra_files') + + if ask_yn('Do you want to set Trove classifiers?', + helptext=_helptext['do_classifier']) == 'y': + self.set_classifier() + + def _find_files(self): + # we are looking for python modules and packages, + # other stuff are added as regular files + pkgs = self.data['packages'] + modules = self.data['modules'] + extra_files = self.data['extra_files'] + + def is_package(path): + return os.path.exists(os.path.join(path, '__init__.py')) + + curdir = os.getcwd() + scanned = [] + _pref = ['lib', 'include', 'dist', 'build', '.', '~'] + _suf = ['.pyc'] + + def to_skip(path): + path = relative(path) + + for pref in _pref: + if path.startswith(pref): + return True + + for suf in _suf: + if path.endswith(suf): + return True + + return False + + def relative(path): + return path[len(curdir) + 1:] + + def dotted(path): + res = relative(path).replace(os.path.sep, '.') + if res.endswith('.py'): + res = res[:-len('.py')] + return res + + # first pass: packages + for root, dirs, files in os.walk(curdir): + if to_skip(root): + continue + for dir_ in sorted(dirs): + if to_skip(dir_): + continue + fullpath = os.path.join(root, dir_) + dotted_name = dotted(fullpath) + if is_package(fullpath) and dotted_name not in pkgs: + pkgs.append(dotted_name) + scanned.append(fullpath) + + # modules and extra files + for root, dirs, files in os.walk(curdir): + if to_skip(root): + continue + + if any(root.startswith(path) for path in scanned): + continue + + for file in sorted(files): + fullpath = os.path.join(root, file) + if to_skip(fullpath): + continue + # single module? + if os.path.splitext(file)[-1] == '.py': + modules.append(dotted(fullpath)) + else: + extra_files.append(relative(fullpath)) + + def _set_multi(self, question, name): + existing_values = self.data[name] + value = ask(question, helptext=_helptext[name]).strip() + if value not in existing_values: + existing_values.append(value) + + def set_classifier(self): + self.set_maturity_status(self.classifiers) + self.set_license(self.classifiers) + self.set_other_classifier(self.classifiers) + + def set_other_classifier(self, classifiers): + if ask_yn('Do you want to set other trove identifiers', 'n', + _helptext['trove_generic']) != 'y': + return + self.walk_classifiers(classifiers, [CLASSIFIERS], '') + + def walk_classifiers(self, classifiers, trovepath, desc): + trove = trovepath[-1] + + if not trove: + return + + for key in sorted(trove): + if len(trove[key]) == 0: + if ask_yn('Add "%s"' % desc[4:] + ' :: ' + key, 'n') == 'y': + classifiers.add(desc[4:] + ' :: ' + key) + continue + + if ask_yn('Do you want to set items under\n "%s" (%d sub-items)' + % (key, len(trove[key])), 'n', + _helptext['trove_generic']) == 'y': + self.walk_classifiers(classifiers, trovepath + [trove[key]], + desc + ' :: ' + key) + + def set_license(self, classifiers): + while True: + license = ask('What license do you use', + helptext=_helptext['trove_license'], required=False) + if not license: + return + + license_words = license.lower().split(' ') + found_list = [] + + for index, licence in LICENCES: + for word in license_words: + if word in licence: + found_list.append(index) + break + + if len(found_list) == 0: + print('ERROR: Could not find a matching license for "%s"' % + license) + continue + + question = 'Matching licenses:\n\n' + + for index, list_index in enumerate(found_list): + question += ' %s) %s\n' % (index + 1, + _CLASSIFIERS_LIST[list_index]) + + question += ('\nType the number of the license you wish to use or ' + '? to try again:') + choice = ask(question, required=False) + + if choice == '?': + continue + if choice == '': + return + + try: + index = found_list[int(choice) - 1] + except ValueError: + print("ERROR: Invalid selection, type a number from the list " + "above.") + + classifiers.add(_CLASSIFIERS_LIST[index]) + + def set_maturity_status(self, classifiers): + maturity_name = lambda mat: mat.split('- ')[-1] + maturity_question = '''\ + Please select the project status: + + %s + + Status''' % '\n'.join('%s - %s' % (i, maturity_name(n)) + for i, n in enumerate(PROJECT_MATURITY)) + while True: + choice = ask(dedent(maturity_question), required=False) + + if choice: + try: + choice = int(choice) - 1 + key = PROJECT_MATURITY[choice] + classifiers.add(key) + return + except (IndexError, ValueError): + print("ERROR: Invalid selection, type a single digit " + "number.") + + +def main(): + """Main entry point.""" + program = MainProgram() + # # uncomment when implemented + # if not program.load_existing_setup_script(): + # program.inspect_directory() + # program.query_user() + # program.update_config_file() + # program.write_setup_script() + # packaging.util.cfg_to_args() + program() + + +if __name__ == '__main__': + main() diff --git a/Lib/packaging/database.py b/Lib/packaging/database.py new file mode 100644 index 000000000000..087a6ecadde3 --- /dev/null +++ b/Lib/packaging/database.py @@ -0,0 +1,627 @@ +"""PEP 376 implementation.""" + +import io +import os +import re +import csv +import sys +import zipimport +from hashlib import md5 +from packaging import logger +from packaging.errors import PackagingError +from packaging.version import suggest_normalized_version, VersionPredicate +from packaging.metadata import Metadata + + +__all__ = [ + 'Distribution', 'EggInfoDistribution', 'distinfo_dirname', + 'get_distributions', 'get_distribution', 'get_file_users', + 'provides_distribution', 'obsoletes_distribution', + 'enable_cache', 'disable_cache', 'clear_cache', +] + + +# TODO update docs + +DIST_FILES = ('INSTALLER', 'METADATA', 'RECORD', 'REQUESTED', 'RESOURCES') + +# Cache +_cache_name = {} # maps names to Distribution instances +_cache_name_egg = {} # maps names to EggInfoDistribution instances +_cache_path = {} # maps paths to Distribution instances +_cache_path_egg = {} # maps paths to EggInfoDistribution instances +_cache_generated = False # indicates if .dist-info distributions are cached +_cache_generated_egg = False # indicates if .dist-info and .egg are cached +_cache_enabled = True + + +def enable_cache(): + """ + Enables the internal cache. + + Note that this function will not clear the cache in any case, for that + functionality see :func:`clear_cache`. + """ + global _cache_enabled + + _cache_enabled = True + + +def disable_cache(): + """ + Disables the internal cache. + + Note that this function will not clear the cache in any case, for that + functionality see :func:`clear_cache`. + """ + global _cache_enabled + + _cache_enabled = False + + +def clear_cache(): + """ Clears the internal cache. """ + global _cache_name, _cache_name_egg, _cache_path, _cache_path_egg, \ + _cache_generated, _cache_generated_egg + + _cache_name = {} + _cache_name_egg = {} + _cache_path = {} + _cache_path_egg = {} + _cache_generated = False + _cache_generated_egg = False + + +def _yield_distributions(include_dist, include_egg, paths=sys.path): + """ + Yield .dist-info and .egg(-info) distributions, based on the arguments + + :parameter include_dist: yield .dist-info distributions + :parameter include_egg: yield .egg(-info) distributions + """ + for path in paths: + realpath = os.path.realpath(path) + if not os.path.isdir(realpath): + continue + for dir in os.listdir(realpath): + dist_path = os.path.join(realpath, dir) + if include_dist and dir.endswith('.dist-info'): + yield Distribution(dist_path) + elif include_egg and (dir.endswith('.egg-info') or + dir.endswith('.egg')): + yield EggInfoDistribution(dist_path) + + +def _generate_cache(use_egg_info=False, paths=sys.path): + global _cache_generated, _cache_generated_egg + + if _cache_generated_egg or (_cache_generated and not use_egg_info): + return + else: + gen_dist = not _cache_generated + gen_egg = use_egg_info + + for dist in _yield_distributions(gen_dist, gen_egg, paths): + if isinstance(dist, Distribution): + _cache_path[dist.path] = dist + if not dist.name in _cache_name: + _cache_name[dist.name] = [] + _cache_name[dist.name].append(dist) + else: + _cache_path_egg[dist.path] = dist + if not dist.name in _cache_name_egg: + _cache_name_egg[dist.name] = [] + _cache_name_egg[dist.name].append(dist) + + if gen_dist: + _cache_generated = True + if gen_egg: + _cache_generated_egg = True + + +class Distribution: + """Created with the *path* of the ``.dist-info`` directory provided to the + constructor. It reads the metadata contained in ``METADATA`` when it is + instantiated.""" + + name = '' + """The name of the distribution.""" + + version = '' + """The version of the distribution.""" + + metadata = None + """A :class:`packaging.metadata.Metadata` instance loaded with + the distribution's ``METADATA`` file.""" + + requested = False + """A boolean that indicates whether the ``REQUESTED`` metadata file is + present (in other words, whether the package was installed by user + request or it was installed as a dependency).""" + + def __init__(self, path): + if _cache_enabled and path in _cache_path: + self.metadata = _cache_path[path].metadata + else: + metadata_path = os.path.join(path, 'METADATA') + self.metadata = Metadata(path=metadata_path) + + self.name = self.metadata['Name'] + self.version = self.metadata['Version'] + self.path = path + + if _cache_enabled and not path in _cache_path: + _cache_path[path] = self + + def __repr__(self): + return '' % ( + self.name, self.version, self.path) + + def _get_records(self, local=False): + with self.get_distinfo_file('RECORD') as record: + record_reader = csv.reader(record, delimiter=',') + # XXX needs an explaining comment + for row in record_reader: + path, checksum, size = (row[:] + + [None for i in range(len(row), 3)]) + if local: + path = path.replace('/', os.sep) + path = os.path.join(sys.prefix, path) + yield path, checksum, size + + def get_resource_path(self, relative_path): + with self.get_distinfo_file('RESOURCES') as resources_file: + resources_reader = csv.reader(resources_file, delimiter=',') + for relative, destination in resources_reader: + if relative == relative_path: + return destination + raise KeyError( + 'no resource file with relative path %r is installed' % + relative_path) + + def list_installed_files(self, local=False): + """ + Iterates over the ``RECORD`` entries and returns a tuple + ``(path, md5, size)`` for each line. If *local* is ``True``, + the returned path is transformed into a local absolute path. + Otherwise the raw value from RECORD is returned. + + A local absolute path is an absolute path in which occurrences of + ``'/'`` have been replaced by the system separator given by ``os.sep``. + + :parameter local: flag to say if the path should be returned a local + absolute path + + :type local: boolean + :returns: iterator of (path, md5, size) + """ + return self._get_records(local) + + def uses(self, path): + """ + Returns ``True`` if path is listed in ``RECORD``. *path* can be a local + absolute path or a relative ``'/'``-separated path. + + :rtype: boolean + """ + for p, checksum, size in self._get_records(): + local_absolute = os.path.join(sys.prefix, p) + if path == p or path == local_absolute: + return True + return False + + def get_distinfo_file(self, path, binary=False): + """ + Returns a file located under the ``.dist-info`` directory. Returns a + ``file`` instance for the file pointed by *path*. + + :parameter path: a ``'/'``-separated path relative to the + ``.dist-info`` directory or an absolute path; + If *path* is an absolute path and doesn't start + with the ``.dist-info`` directory path, + a :class:`PackagingError` is raised + :type path: string + :parameter binary: If *binary* is ``True``, opens the file in read-only + binary mode (``rb``), otherwise opens it in + read-only mode (``r``). + :rtype: file object + """ + open_flags = 'r' + if binary: + open_flags += 'b' + + # Check if it is an absolute path # XXX use relpath, add tests + if path.find(os.sep) >= 0: + # it's an absolute path? + distinfo_dirname, path = path.split(os.sep)[-2:] + if distinfo_dirname != self.path.split(os.sep)[-1]: + raise PackagingError( + 'dist-info file %r does not belong to the %r %s ' + 'distribution' % (path, self.name, self.version)) + + # The file must be relative + if path not in DIST_FILES: + raise PackagingError('invalid path for a dist-info file: %r' % + path) + + path = os.path.join(self.path, path) + return open(path, open_flags) + + def list_distinfo_files(self, local=False): + """ + Iterates over the ``RECORD`` entries and returns paths for each line if + the path is pointing to a file located in the ``.dist-info`` directory + or one of its subdirectories. + + :parameter local: If *local* is ``True``, each returned path is + transformed into a local absolute path. Otherwise the + raw value from ``RECORD`` is returned. + :type local: boolean + :returns: iterator of paths + """ + for path, checksum, size in self._get_records(local): + yield path + + def __eq__(self, other): + return isinstance(other, Distribution) and self.path == other.path + + # See http://docs.python.org/reference/datamodel#object.__hash__ + __hash__ = object.__hash__ + + +class EggInfoDistribution: + """Created with the *path* of the ``.egg-info`` directory or file provided + to the constructor. It reads the metadata contained in the file itself, or + if the given path happens to be a directory, the metadata is read from the + file ``PKG-INFO`` under that directory.""" + + name = '' + """The name of the distribution.""" + + version = '' + """The version of the distribution.""" + + metadata = None + """A :class:`packaging.metadata.Metadata` instance loaded with + the distribution's ``METADATA`` file.""" + + _REQUIREMENT = re.compile( + r'(?P[-A-Za-z0-9_.]+)\s*' + r'(?P(?:<|<=|!=|==|>=|>)[-A-Za-z0-9_.]+)?\s*' + r'(?P(?:\s*,\s*(?:<|<=|!=|==|>=|>)[-A-Za-z0-9_.]+)*)\s*' + r'(?P\[.*\])?') + + def __init__(self, path): + self.path = path + if _cache_enabled and path in _cache_path_egg: + self.metadata = _cache_path_egg[path].metadata + self.name = self.metadata['Name'] + self.version = self.metadata['Version'] + return + + # reused from Distribute's pkg_resources + def yield_lines(strs): + """Yield non-empty/non-comment lines of a ``basestring`` + or sequence""" + if isinstance(strs, str): + for s in strs.splitlines(): + s = s.strip() + # skip blank lines/comments + if s and not s.startswith('#'): + yield s + else: + for ss in strs: + for s in yield_lines(ss): + yield s + + requires = None + + if path.endswith('.egg'): + if os.path.isdir(path): + meta_path = os.path.join(path, 'EGG-INFO', 'PKG-INFO') + self.metadata = Metadata(path=meta_path) + try: + req_path = os.path.join(path, 'EGG-INFO', 'requires.txt') + with open(req_path, 'r') as fp: + requires = fp.read() + except IOError: + requires = None + else: + # FIXME handle the case where zipfile is not available + zipf = zipimport.zipimporter(path) + fileobj = io.StringIO( + zipf.get_data('EGG-INFO/PKG-INFO').decode('utf8')) + self.metadata = Metadata(fileobj=fileobj) + try: + requires = zipf.get_data('EGG-INFO/requires.txt') + except IOError: + requires = None + self.name = self.metadata['Name'] + self.version = self.metadata['Version'] + + elif path.endswith('.egg-info'): + if os.path.isdir(path): + path = os.path.join(path, 'PKG-INFO') + try: + with open(os.path.join(path, 'requires.txt'), 'r') as fp: + requires = fp.read() + except IOError: + requires = None + self.metadata = Metadata(path=path) + self.name = self.metadata['name'] + self.version = self.metadata['Version'] + + else: + raise ValueError('path must end with .egg-info or .egg, got %r' % + path) + + if requires is not None: + if self.metadata['Metadata-Version'] == '1.1': + # we can't have 1.1 metadata *and* Setuptools requires + for field in ('Obsoletes', 'Requires', 'Provides'): + del self.metadata[field] + + reqs = [] + + if requires is not None: + for line in yield_lines(requires): + if line.startswith('['): + logger.warning( + 'extensions in requires.txt are not supported ' + '(used by %r %s)', self.name, self.version) + break + else: + match = self._REQUIREMENT.match(line.strip()) + if not match: + # this happens when we encounter extras; since they + # are written at the end of the file we just exit + break + else: + if match.group('extras'): + msg = ('extra requirements are not supported ' + '(used by %r %s)', self.name, self.version) + logger.warning(msg, self.name) + name = match.group('name') + version = None + if match.group('first'): + version = match.group('first') + if match.group('rest'): + version += match.group('rest') + version = version.replace(' ', '') # trim spaces + if version is None: + reqs.append(name) + else: + reqs.append('%s (%s)' % (name, version)) + + if len(reqs) > 0: + self.metadata['Requires-Dist'] += reqs + + if _cache_enabled: + _cache_path_egg[self.path] = self + + def __repr__(self): + return '' % ( + self.name, self.version, self.path) + + def list_installed_files(self, local=False): + + def _md5(path): + with open(path, 'rb') as f: + content = f.read() + return md5(content).hexdigest() + + def _size(path): + return os.stat(path).st_size + + path = self.path + if local: + path = path.replace('/', os.sep) + + # XXX What about scripts and data files ? + if os.path.isfile(path): + return [(path, _md5(path), _size(path))] + else: + files = [] + for root, dir, files_ in os.walk(path): + for item in files_: + item = os.path.join(root, item) + files.append((item, _md5(item), _size(item))) + return files + + return [] + + def uses(self, path): + return False + + def __eq__(self, other): + return (isinstance(other, EggInfoDistribution) and + self.path == other.path) + + # See http://docs.python.org/reference/datamodel#object.__hash__ + __hash__ = object.__hash__ + + +def distinfo_dirname(name, version): + """ + The *name* and *version* parameters are converted into their + filename-escaped form, i.e. any ``'-'`` characters are replaced + with ``'_'`` other than the one in ``'dist-info'`` and the one + separating the name from the version number. + + :parameter name: is converted to a standard distribution name by replacing + any runs of non- alphanumeric characters with a single + ``'-'``. + :type name: string + :parameter version: is converted to a standard version string. Spaces + become dots, and all other non-alphanumeric characters + (except dots) become dashes, with runs of multiple + dashes condensed to a single dash. + :type version: string + :returns: directory name + :rtype: string""" + file_extension = '.dist-info' + name = name.replace('-', '_') + normalized_version = suggest_normalized_version(version) + # Because this is a lookup procedure, something will be returned even if + # it is a version that cannot be normalized + if normalized_version is None: + # Unable to achieve normality? + normalized_version = version + return '-'.join([name, normalized_version]) + file_extension + + +def get_distributions(use_egg_info=False, paths=sys.path): + """ + Provides an iterator that looks for ``.dist-info`` directories in + ``sys.path`` and returns :class:`Distribution` instances for each one of + them. If the parameters *use_egg_info* is ``True``, then the ``.egg-info`` + files and directores are iterated as well. + + :rtype: iterator of :class:`Distribution` and :class:`EggInfoDistribution` + instances + """ + if not _cache_enabled: + for dist in _yield_distributions(True, use_egg_info, paths): + yield dist + else: + _generate_cache(use_egg_info, paths) + + for dist in _cache_path.values(): + yield dist + + if use_egg_info: + for dist in _cache_path_egg.values(): + yield dist + + +def get_distribution(name, use_egg_info=False, paths=None): + """ + Scans all elements in ``sys.path`` and looks for all directories + ending with ``.dist-info``. Returns a :class:`Distribution` + corresponding to the ``.dist-info`` directory that contains the + ``METADATA`` that matches *name* for the *name* metadata field. + If no distribution exists with the given *name* and the parameter + *use_egg_info* is set to ``True``, then all files and directories ending + with ``.egg-info`` are scanned. A :class:`EggInfoDistribution` instance is + returned if one is found that has metadata that matches *name* for the + *name* metadata field. + + This function only returns the first result found, as no more than one + value is expected. If the directory is not found, ``None`` is returned. + + :rtype: :class:`Distribution` or :class:`EggInfoDistribution` or None + """ + if paths == None: + paths = sys.path + + if not _cache_enabled: + for dist in _yield_distributions(True, use_egg_info, paths): + if dist.name == name: + return dist + else: + _generate_cache(use_egg_info, paths) + + if name in _cache_name: + return _cache_name[name][0] + elif use_egg_info and name in _cache_name_egg: + return _cache_name_egg[name][0] + else: + return None + + +def obsoletes_distribution(name, version=None, use_egg_info=False): + """ + Iterates over all distributions to find which distributions obsolete + *name*. + + If a *version* is provided, it will be used to filter the results. + If the argument *use_egg_info* is set to ``True``, then ``.egg-info`` + distributions will be considered as well. + + :type name: string + :type version: string + :parameter name: + """ + for dist in get_distributions(use_egg_info): + obsoleted = (dist.metadata['Obsoletes-Dist'] + + dist.metadata['Obsoletes']) + for obs in obsoleted: + o_components = obs.split(' ', 1) + if len(o_components) == 1 or version is None: + if name == o_components[0]: + yield dist + break + else: + try: + predicate = VersionPredicate(obs) + except ValueError: + raise PackagingError( + 'distribution %r has ill-formed obsoletes field: ' + '%r' % (dist.name, obs)) + if name == o_components[0] and predicate.match(version): + yield dist + break + + +def provides_distribution(name, version=None, use_egg_info=False): + """ + Iterates over all distributions to find which distributions provide *name*. + If a *version* is provided, it will be used to filter the results. Scans + all elements in ``sys.path`` and looks for all directories ending with + ``.dist-info``. Returns a :class:`Distribution` corresponding to the + ``.dist-info`` directory that contains a ``METADATA`` that matches *name* + for the name metadata. If the argument *use_egg_info* is set to ``True``, + then all files and directories ending with ``.egg-info`` are considered + as well and returns an :class:`EggInfoDistribution` instance. + + This function only returns the first result found, since no more than + one values are expected. If the directory is not found, returns ``None``. + + :parameter version: a version specifier that indicates the version + required, conforming to the format in ``PEP-345`` + + :type name: string + :type version: string + """ + predicate = None + if not version is None: + try: + predicate = VersionPredicate(name + ' (' + version + ')') + except ValueError: + raise PackagingError('invalid name or version: %r, %r' % + (name, version)) + + for dist in get_distributions(use_egg_info): + provided = dist.metadata['Provides-Dist'] + dist.metadata['Provides'] + + for p in provided: + p_components = p.rsplit(' ', 1) + if len(p_components) == 1 or predicate is None: + if name == p_components[0]: + yield dist + break + else: + p_name, p_ver = p_components + if len(p_ver) < 2 or p_ver[0] != '(' or p_ver[-1] != ')': + raise PackagingError( + 'distribution %r has invalid Provides field: %r' % + (dist.name, p)) + p_ver = p_ver[1:-1] # trim off the parenthesis + if p_name == name and predicate.match(p_ver): + yield dist + break + + +def get_file_users(path): + """ + Iterates over all distributions to find out which distributions use + *path*. + + :parameter path: can be a local absolute path or a relative + ``'/'``-separated path. + :type path: string + :rtype: iterator of :class:`Distribution` instances + """ + for dist in get_distributions(): + if dist.uses(path): + yield dist diff --git a/Lib/packaging/depgraph.py b/Lib/packaging/depgraph.py new file mode 100644 index 000000000000..48ea3d925216 --- /dev/null +++ b/Lib/packaging/depgraph.py @@ -0,0 +1,270 @@ +"""Class and functions dealing with dependencies between distributions. + +This module provides a DependencyGraph class to represent the +dependencies between distributions. Auxiliary functions can generate a +graph, find reverse dependencies, and print a graph in DOT format. +""" + +import sys + +from io import StringIO +from packaging.errors import PackagingError +from packaging.version import VersionPredicate, IrrationalVersionError + +__all__ = ['DependencyGraph', 'generate_graph', 'dependent_dists', + 'graph_to_dot'] + + +class DependencyGraph: + """ + Represents a dependency graph between distributions. + + The dependency relationships are stored in an ``adjacency_list`` that maps + distributions to a list of ``(other, label)`` tuples where ``other`` + is a distribution and the edge is labeled with ``label`` (i.e. the version + specifier, if such was provided). Also, for more efficient traversal, for + every distribution ``x``, a list of predecessors is kept in + ``reverse_list[x]``. An edge from distribution ``a`` to + distribution ``b`` means that ``a`` depends on ``b``. If any missing + dependencies are found, they are stored in ``missing``, which is a + dictionary that maps distributions to a list of requirements that were not + provided by any other distributions. + """ + + def __init__(self): + self.adjacency_list = {} + self.reverse_list = {} + self.missing = {} + + def add_distribution(self, distribution): + """Add the *distribution* to the graph. + + :type distribution: :class:`packaging.database.Distribution` or + :class:`packaging.database.EggInfoDistribution` + """ + self.adjacency_list[distribution] = [] + self.reverse_list[distribution] = [] + self.missing[distribution] = [] + + def add_edge(self, x, y, label=None): + """Add an edge from distribution *x* to distribution *y* with the given + *label*. + + :type x: :class:`packaging.database.Distribution` or + :class:`packaging.database.EggInfoDistribution` + :type y: :class:`packaging.database.Distribution` or + :class:`packaging.database.EggInfoDistribution` + :type label: ``str`` or ``None`` + """ + self.adjacency_list[x].append((y, label)) + # multiple edges are allowed, so be careful + if not x in self.reverse_list[y]: + self.reverse_list[y].append(x) + + def add_missing(self, distribution, requirement): + """ + Add a missing *requirement* for the given *distribution*. + + :type distribution: :class:`packaging.database.Distribution` or + :class:`packaging.database.EggInfoDistribution` + :type requirement: ``str`` + """ + self.missing[distribution].append(requirement) + + def _repr_dist(self, dist): + return '%s %s' % (dist.name, dist.metadata['Version']) + + def repr_node(self, dist, level=1): + """Prints only a subgraph""" + output = [] + output.append(self._repr_dist(dist)) + for other, label in self.adjacency_list[dist]: + dist = self._repr_dist(other) + if label is not None: + dist = '%s [%s]' % (dist, label) + output.append(' ' * level + str(dist)) + suboutput = self.repr_node(other, level + 1) + subs = suboutput.split('\n') + output.extend(subs[1:]) + return '\n'.join(output) + + def __repr__(self): + """Representation of the graph""" + output = [] + for dist, adjs in self.adjacency_list.items(): + output.append(self.repr_node(dist)) + return '\n'.join(output) + + +def graph_to_dot(graph, f, skip_disconnected=True): + """Writes a DOT output for the graph to the provided file *f*. + + If *skip_disconnected* is set to ``True``, then all distributions + that are not dependent on any other distribution are skipped. + + :type f: has to support ``file``-like operations + :type skip_disconnected: ``bool`` + """ + disconnected = [] + + f.write("digraph dependencies {\n") + for dist, adjs in graph.adjacency_list.items(): + if len(adjs) == 0 and not skip_disconnected: + disconnected.append(dist) + for other, label in adjs: + if not label is None: + f.write('"%s" -> "%s" [label="%s"]\n' % + (dist.name, other.name, label)) + else: + f.write('"%s" -> "%s"\n' % (dist.name, other.name)) + if not skip_disconnected and len(disconnected) > 0: + f.write('subgraph disconnected {\n') + f.write('label = "Disconnected"\n') + f.write('bgcolor = red\n') + + for dist in disconnected: + f.write('"%s"' % dist.name) + f.write('\n') + f.write('}\n') + f.write('}\n') + + +def generate_graph(dists): + """Generates a dependency graph from the given distributions. + + :parameter dists: a list of distributions + :type dists: list of :class:`packaging.database.Distribution` and + :class:`packaging.database.EggInfoDistribution` instances + :rtype: a :class:`DependencyGraph` instance + """ + graph = DependencyGraph() + provided = {} # maps names to lists of (version, dist) tuples + + # first, build the graph and find out the provides + for dist in dists: + graph.add_distribution(dist) + provides = (dist.metadata['Provides-Dist'] + + dist.metadata['Provides'] + + ['%s (%s)' % (dist.name, dist.metadata['Version'])]) + + for p in provides: + comps = p.strip().rsplit(" ", 1) + name = comps[0] + version = None + if len(comps) == 2: + version = comps[1] + if len(version) < 3 or version[0] != '(' or version[-1] != ')': + raise PackagingError('Distribution %s has ill formed' \ + 'provides field: %s' % (dist.name, p)) + version = version[1:-1] # trim off parenthesis + if not name in provided: + provided[name] = [] + provided[name].append((version, dist)) + + # now make the edges + for dist in dists: + requires = dist.metadata['Requires-Dist'] + dist.metadata['Requires'] + for req in requires: + try: + predicate = VersionPredicate(req) + except IrrationalVersionError: + # XXX compat-mode if cannot read the version + name = req.split()[0] + predicate = VersionPredicate(name) + + name = predicate.name + + if not name in provided: + graph.add_missing(dist, req) + else: + matched = False + for version, provider in provided[name]: + try: + match = predicate.match(version) + except IrrationalVersionError: + # XXX small compat-mode + if version.split(' ') == 1: + match = True + else: + match = False + + if match: + graph.add_edge(dist, provider, req) + matched = True + break + if not matched: + graph.add_missing(dist, req) + return graph + + +def dependent_dists(dists, dist): + """Recursively generate a list of distributions from *dists* that are + dependent on *dist*. + + :param dists: a list of distributions + :param dist: a distribution, member of *dists* for which we are interested + """ + if not dist in dists: + raise ValueError('The given distribution is not a member of the list') + graph = generate_graph(dists) + + dep = [dist] # dependent distributions + fringe = graph.reverse_list[dist] # list of nodes we should inspect + + while not len(fringe) == 0: + node = fringe.pop() + dep.append(node) + for prev in graph.reverse_list[node]: + if not prev in dep: + fringe.append(prev) + + dep.pop(0) # remove dist from dep, was there to prevent infinite loops + return dep + + +def main(): + from packaging.database import get_distributions + tempout = StringIO() + try: + old = sys.stderr + sys.stderr = tempout + try: + dists = list(get_distributions(use_egg_info=True)) + graph = generate_graph(dists) + finally: + sys.stderr = old + except Exception as e: + tempout.seek(0) + tempout = tempout.read() + print('Could not generate the graph\n%s\n%s\n' % (tempout, e)) + sys.exit(1) + + for dist, reqs in graph.missing.items(): + if len(reqs) > 0: + print("Warning: Missing dependencies for %s:" % dist.name, + ", ".join(reqs)) + # XXX replace with argparse + if len(sys.argv) == 1: + print('Dependency graph:') + print(' ' + repr(graph).replace('\n', '\n ')) + sys.exit(0) + elif len(sys.argv) > 1 and sys.argv[1] in ('-d', '--dot'): + if len(sys.argv) > 2: + filename = sys.argv[2] + else: + filename = 'depgraph.dot' + + with open(filename, 'w') as f: + graph_to_dot(graph, f, True) + tempout.seek(0) + tempout = tempout.read() + print(tempout) + print('Dot file written at "%s"' % filename) + sys.exit(0) + else: + print('Supported option: -d [filename]') + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/Lib/packaging/dist.py b/Lib/packaging/dist.py new file mode 100644 index 000000000000..6065e78a6a93 --- /dev/null +++ b/Lib/packaging/dist.py @@ -0,0 +1,819 @@ +"""Class representing the distribution being built/installed/etc.""" + +import os +import re + +from packaging.errors import (PackagingOptionError, PackagingArgError, + PackagingModuleError, PackagingClassError) +from packaging.fancy_getopt import FancyGetopt +from packaging.util import strtobool, resolve_name +from packaging import logger +from packaging.metadata import Metadata +from packaging.config import Config +from packaging.command import get_command_class, STANDARD_COMMANDS + +# Regex to define acceptable Packaging command names. This is not *quite* +# the same as a Python NAME -- I don't allow leading underscores. The fact +# that they're very similar is no coincidence; the default naming scheme is +# to look for a Python module named after the command. +command_re = re.compile(r'^[a-zA-Z]([a-zA-Z0-9_]*)$') + +USAGE = """\ +usage: %(script)s [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...] + or: %(script)s --help [cmd1 cmd2 ...] + or: %(script)s --help-commands + or: %(script)s cmd --help +""" + + +def gen_usage(script_name): + script = os.path.basename(script_name) + return USAGE % {'script': script} + + +class Distribution: + """The core of the Packaging. Most of the work hiding behind 'setup' + is really done within a Distribution instance, which farms the work out + to the Packaging commands specified on the command line. + + Setup scripts will almost never instantiate Distribution directly, + unless the 'setup()' function is totally inadequate to their needs. + However, it is conceivable that a setup script might wish to subclass + Distribution for some specialized purpose, and then pass the subclass + to 'setup()' as the 'distclass' keyword argument. If so, it is + necessary to respect the expectations that 'setup' has of Distribution. + See the code for 'setup()', in run.py, for details. + """ + + # 'global_options' describes the command-line options that may be + # supplied to the setup script prior to any actual commands. + # Eg. "./setup.py -n" or "./setup.py --dry-run" both take advantage of + # these global options. This list should be kept to a bare minimum, + # since every global option is also valid as a command option -- and we + # don't want to pollute the commands with too many options that they + # have minimal control over. + global_options = [ + ('dry-run', 'n', "don't actually do anything"), + ('help', 'h', "show detailed help message"), + ('no-user-cfg', None, 'ignore pydistutils.cfg in your home directory'), + ] + + # 'common_usage' is a short (2-3 line) string describing the common + # usage of the setup script. + common_usage = """\ +Common commands: (see '--help-commands' for more) + + setup.py build will build the package underneath 'build/' + setup.py install will install the package +""" + + # options that are not propagated to the commands + display_options = [ + ('help-commands', None, + "list all available commands"), + ('name', None, + "print package name"), + ('version', 'V', + "print package version"), + ('fullname', None, + "print -"), + ('author', None, + "print the author's name"), + ('author-email', None, + "print the author's email address"), + ('maintainer', None, + "print the maintainer's name"), + ('maintainer-email', None, + "print the maintainer's email address"), + ('contact', None, + "print the maintainer's name if known, else the author's"), + ('contact-email', None, + "print the maintainer's email address if known, else the author's"), + ('url', None, + "print the URL for this package"), + ('license', None, + "print the license of the package"), + ('licence', None, + "alias for --license"), + ('description', None, + "print the package description"), + ('long-description', None, + "print the long package description"), + ('platforms', None, + "print the list of platforms"), + ('classifier', None, + "print the list of classifiers"), + ('keywords', None, + "print the list of keywords"), + ('provides', None, + "print the list of packages/modules provided"), + ('requires', None, + "print the list of packages/modules required"), + ('obsoletes', None, + "print the list of packages/modules made obsolete"), + ('use-2to3', None, + "use 2to3 to make source python 3.x compatible"), + ('convert-2to3-doctests', None, + "use 2to3 to convert doctests in seperate text files"), + ] + display_option_names = [x[0].replace('-', '_') for x in display_options] + + # negative options are options that exclude other options + negative_opt = {} + + # -- Creation/initialization methods ------------------------------- + def __init__(self, attrs=None): + """Construct a new Distribution instance: initialize all the + attributes of a Distribution, and then use 'attrs' (a dictionary + mapping attribute names to values) to assign some of those + attributes their "real" values. (Any attributes not mentioned in + 'attrs' will be assigned to some null value: 0, None, an empty list + or dictionary, etc.) Most importantly, initialize the + 'command_obj' attribute to the empty dictionary; this will be + filled in with real command objects by 'parse_command_line()'. + """ + + # Default values for our command-line options + self.dry_run = False + self.help = False + for attr in self.display_option_names: + setattr(self, attr, False) + + # Store the configuration + self.config = Config(self) + + # Store the distribution metadata (name, version, author, and so + # forth) in a separate object -- we're getting to have enough + # information here (and enough command-line options) that it's + # worth it. + self.metadata = Metadata() + + # 'cmdclass' maps command names to class objects, so we + # can 1) quickly figure out which class to instantiate when + # we need to create a new command object, and 2) have a way + # for the setup script to override command classes + self.cmdclass = {} + + # 'script_name' and 'script_args' are usually set to sys.argv[0] + # and sys.argv[1:], but they can be overridden when the caller is + # not necessarily a setup script run from the command line. + self.script_name = None + self.script_args = None + + # 'command_options' is where we store command options between + # parsing them (from config files, the command line, etc.) and when + # they are actually needed -- ie. when the command in question is + # instantiated. It is a dictionary of dictionaries of 2-tuples: + # command_options = { command_name : { option : (source, value) } } + self.command_options = {} + + # 'dist_files' is the list of (command, pyversion, file) that + # have been created by any dist commands run so far. This is + # filled regardless of whether the run is dry or not. pyversion + # gives sysconfig.get_python_version() if the dist file is + # specific to a Python version, 'any' if it is good for all + # Python versions on the target platform, and '' for a source + # file. pyversion should not be used to specify minimum or + # maximum required Python versions; use the metainfo for that + # instead. + self.dist_files = [] + + # These options are really the business of various commands, rather + # than of the Distribution itself. We provide aliases for them in + # Distribution as a convenience to the developer. + self.packages = [] + self.package_data = {} + self.package_dir = None + self.py_modules = [] + self.libraries = [] + self.headers = [] + self.ext_modules = [] + self.ext_package = None + self.include_dirs = [] + self.extra_path = None + self.scripts = [] + self.data_files = {} + self.password = '' + self.use_2to3 = False + self.convert_2to3_doctests = [] + self.extra_files = [] + + # And now initialize bookkeeping stuff that can't be supplied by + # the caller at all. 'command_obj' maps command names to + # Command instances -- that's how we enforce that every command + # class is a singleton. + self.command_obj = {} + + # 'have_run' maps command names to boolean values; it keeps track + # of whether we have actually run a particular command, to make it + # cheap to "run" a command whenever we think we might need to -- if + # it's already been done, no need for expensive filesystem + # operations, we just check the 'have_run' dictionary and carry on. + # It's only safe to query 'have_run' for a command class that has + # been instantiated -- a false value will be inserted when the + # command object is created, and replaced with a true value when + # the command is successfully run. Thus it's probably best to use + # '.get()' rather than a straight lookup. + self.have_run = {} + + # Now we'll use the attrs dictionary (ultimately, keyword args from + # the setup script) to possibly override any or all of these + # distribution options. + + if attrs is not None: + # Pull out the set of command options and work on them + # specifically. Note that this order guarantees that aliased + # command options will override any supplied redundantly + # through the general options dictionary. + options = attrs.get('options') + if options is not None: + del attrs['options'] + for command, cmd_options in options.items(): + opt_dict = self.get_option_dict(command) + for opt, val in cmd_options.items(): + opt_dict[opt] = ("setup script", val) + + # Now work on the rest of the attributes. Any attribute that's + # not already defined is invalid! + for key, val in attrs.items(): + if self.metadata.is_metadata_field(key): + self.metadata[key] = val + elif hasattr(self, key): + setattr(self, key, val) + else: + logger.warning( + 'unknown argument given to Distribution: %r', key) + + # no-user-cfg is handled before other command line args + # because other args override the config files, and this + # one is needed before we can load the config files. + # If attrs['script_args'] wasn't passed, assume false. + # + # This also make sure we just look at the global options + self.want_user_cfg = True + + if self.script_args is not None: + for arg in self.script_args: + if not arg.startswith('-'): + break + if arg == '--no-user-cfg': + self.want_user_cfg = False + break + + self.finalize_options() + + def get_option_dict(self, command): + """Get the option dictionary for a given command. If that + command's option dictionary hasn't been created yet, then create it + and return the new dictionary; otherwise, return the existing + option dictionary. + """ + d = self.command_options.get(command) + if d is None: + d = self.command_options[command] = {} + return d + + def get_fullname(self): + return self.metadata.get_fullname() + + def dump_option_dicts(self, header=None, commands=None, indent=""): + from pprint import pformat + + if commands is None: # dump all command option dicts + commands = sorted(self.command_options) + + if header is not None: + logger.info(indent + header) + indent = indent + " " + + if not commands: + logger.info(indent + "no commands known yet") + return + + for cmd_name in commands: + opt_dict = self.command_options.get(cmd_name) + if opt_dict is None: + logger.info(indent + "no option dict for %r command", + cmd_name) + else: + logger.info(indent + "option dict for %r command:", cmd_name) + out = pformat(opt_dict) + for line in out.split('\n'): + logger.info(indent + " " + line) + + # -- Config file finding/parsing methods --------------------------- + # XXX to be removed + def parse_config_files(self, filenames=None): + return self.config.parse_config_files(filenames) + + def find_config_files(self): + return self.config.find_config_files() + + # -- Command-line parsing methods ---------------------------------- + + def parse_command_line(self): + """Parse the setup script's command line, taken from the + 'script_args' instance attribute (which defaults to 'sys.argv[1:]' + -- see 'setup()' in run.py). This list is first processed for + "global options" -- options that set attributes of the Distribution + instance. Then, it is alternately scanned for Packaging commands + and options for that command. Each new command terminates the + options for the previous command. The allowed options for a + command are determined by the 'user_options' attribute of the + command class -- thus, we have to be able to load command classes + in order to parse the command line. Any error in that 'options' + attribute raises PackagingGetoptError; any error on the + command line raises PackagingArgError. If no Packaging commands + were found on the command line, raises PackagingArgError. Return + true if command line was successfully parsed and we should carry + on with executing commands; false if no errors but we shouldn't + execute commands (currently, this only happens if user asks for + help). + """ + # + # We now have enough information to show the Macintosh dialog + # that allows the user to interactively specify the "command line". + # + toplevel_options = self._get_toplevel_options() + + # We have to parse the command line a bit at a time -- global + # options, then the first command, then its options, and so on -- + # because each command will be handled by a different class, and + # the options that are valid for a particular class aren't known + # until we have loaded the command class, which doesn't happen + # until we know what the command is. + + self.commands = [] + parser = FancyGetopt(toplevel_options + self.display_options) + parser.set_negative_aliases(self.negative_opt) + parser.set_aliases({'licence': 'license'}) + args = parser.getopt(args=self.script_args, object=self) + option_order = parser.get_option_order() + + # for display options we return immediately + if self.handle_display_options(option_order): + return + + while args: + args = self._parse_command_opts(parser, args) + if args is None: # user asked for help (and got it) + return + + # Handle the cases of --help as a "global" option, ie. + # "setup.py --help" and "setup.py --help command ...". For the + # former, we show global options (--dry-run, etc.) + # and display-only options (--name, --version, etc.); for the + # latter, we omit the display-only options and show help for + # each command listed on the command line. + if self.help: + self._show_help(parser, + display_options=len(self.commands) == 0, + commands=self.commands) + return + + return 1 + + def _get_toplevel_options(self): + """Return the non-display options recognized at the top level. + + This includes options that are recognized *only* at the top + level as well as options recognized for commands. + """ + return self.global_options + + def _parse_command_opts(self, parser, args): + """Parse the command-line options for a single command. + 'parser' must be a FancyGetopt instance; 'args' must be the list + of arguments, starting with the current command (whose options + we are about to parse). Returns a new version of 'args' with + the next command at the front of the list; will be the empty + list if there are no more commands on the command line. Returns + None if the user asked for help on this command. + """ + # Pull the current command from the head of the command line + command = args[0] + if not command_re.match(command): + raise SystemExit("invalid command name %r" % command) + self.commands.append(command) + + # Dig up the command class that implements this command, so we + # 1) know that it's a valid command, and 2) know which options + # it takes. + try: + cmd_class = get_command_class(command) + except PackagingModuleError as msg: + raise PackagingArgError(msg) + + # XXX We want to push this in packaging.command + # + # Require that the command class be derived from Command -- want + # to be sure that the basic "command" interface is implemented. + for meth in ('initialize_options', 'finalize_options', 'run'): + if hasattr(cmd_class, meth): + continue + raise PackagingClassError( + 'command %r must implement %r' % (cmd_class, meth)) + + # Also make sure that the command object provides a list of its + # known options. + if not (hasattr(cmd_class, 'user_options') and + isinstance(cmd_class.user_options, list)): + raise PackagingClassError( + "command class %s must provide " + "'user_options' attribute (a list of tuples)" % cmd_class) + + # If the command class has a list of negative alias options, + # merge it in with the global negative aliases. + negative_opt = self.negative_opt + if hasattr(cmd_class, 'negative_opt'): + negative_opt = negative_opt.copy() + negative_opt.update(cmd_class.negative_opt) + + # Check for help_options in command class. They have a different + # format (tuple of four) so we need to preprocess them here. + if (hasattr(cmd_class, 'help_options') and + isinstance(cmd_class.help_options, list)): + help_options = cmd_class.help_options[:] + else: + help_options = [] + + # All commands support the global options too, just by adding + # in 'global_options'. + parser.set_option_table(self.global_options + + cmd_class.user_options + + help_options) + parser.set_negative_aliases(negative_opt) + args, opts = parser.getopt(args[1:]) + if hasattr(opts, 'help') and opts.help: + self._show_help(parser, display_options=False, + commands=[cmd_class]) + return + + if (hasattr(cmd_class, 'help_options') and + isinstance(cmd_class.help_options, list)): + help_option_found = False + for help_option, short, desc, func in cmd_class.help_options: + if hasattr(opts, help_option.replace('-', '_')): + help_option_found = True + if hasattr(func, '__call__'): + func() + else: + raise PackagingClassError( + "invalid help function %r for help option %r: " + "must be a callable object (function, etc.)" + % (func, help_option)) + + if help_option_found: + return + + # Put the options from the command line into their official + # holding pen, the 'command_options' dictionary. + opt_dict = self.get_option_dict(command) + for name, value in vars(opts).items(): + opt_dict[name] = ("command line", value) + + return args + + def finalize_options(self): + """Set final values for all the options on the Distribution + instance, analogous to the .finalize_options() method of Command + objects. + """ + if getattr(self, 'convert_2to3_doctests', None): + self.convert_2to3_doctests = [os.path.join(p) + for p in self.convert_2to3_doctests] + else: + self.convert_2to3_doctests = [] + + def _show_help(self, parser, global_options=True, display_options=True, + commands=[]): + """Show help for the setup script command line in the form of + several lists of command-line options. 'parser' should be a + FancyGetopt instance; do not expect it to be returned in the + same state, as its option table will be reset to make it + generate the correct help text. + + If 'global_options' is true, lists the global options: + --dry-run, etc. If 'display_options' is true, lists + the "display-only" options: --name, --version, etc. Finally, + lists per-command help for every command name or command class + in 'commands'. + """ + # late import because of mutual dependence between these modules + from packaging.command.cmd import Command + + if global_options: + if display_options: + options = self._get_toplevel_options() + else: + options = self.global_options + parser.set_option_table(options) + parser.print_help(self.common_usage + "\nGlobal options:") + print('') + + if display_options: + parser.set_option_table(self.display_options) + parser.print_help( + "Information display options (just display " + + "information, ignore any commands)") + print('') + + for command in self.commands: + if isinstance(command, type) and issubclass(command, Command): + cls = command + else: + cls = get_command_class(command) + if (hasattr(cls, 'help_options') and + isinstance(cls.help_options, list)): + parser.set_option_table(cls.user_options + cls.help_options) + else: + parser.set_option_table(cls.user_options) + parser.print_help("Options for %r command:" % cls.__name__) + print('') + + print(gen_usage(self.script_name)) + + def handle_display_options(self, option_order): + """If there were any non-global "display-only" options + (--help-commands or the metadata display options) on the command + line, display the requested info and return true; else return + false. + """ + # User just wants a list of commands -- we'll print it out and stop + # processing now (ie. if they ran "setup --help-commands foo bar", + # we ignore "foo bar"). + if self.help_commands: + self.print_commands() + print('') + print(gen_usage(self.script_name)) + return 1 + + # If user supplied any of the "display metadata" options, then + # display that metadata in the order in which the user supplied the + # metadata options. + any_display_options = False + is_display_option = set() + for option in self.display_options: + is_display_option.add(option[0]) + + for opt, val in option_order: + if val and opt in is_display_option: + opt = opt.replace('-', '_') + value = self.metadata[opt] + if opt in ('keywords', 'platform'): + print(','.join(value)) + elif opt in ('classifier', 'provides', 'requires', + 'obsoletes'): + print('\n'.join(value)) + else: + print(value) + any_display_options = True + + return any_display_options + + def print_command_list(self, commands, header, max_length): + """Print a subset of the list of all commands -- used by + 'print_commands()'. + """ + print(header + ":") + + for cmd in commands: + cls = self.cmdclass.get(cmd) or get_command_class(cmd) + description = getattr(cls, 'description', + '(no description available)') + + print(" %-*s %s" % (max_length, cmd, description)) + + def _get_command_groups(self): + """Helper function to retrieve all the command class names divided + into standard commands (listed in + packaging2.command.STANDARD_COMMANDS) and extra commands (given in + self.cmdclass and not standard commands). + """ + extra_commands = [cmd for cmd in self.cmdclass + if cmd not in STANDARD_COMMANDS] + return STANDARD_COMMANDS, extra_commands + + def print_commands(self): + """Print out a help message listing all available commands with a + description of each. The list is divided into standard commands + (listed in packaging2.command.STANDARD_COMMANDS) and extra commands + (given in self.cmdclass and not standard commands). The + descriptions come from the command class attribute + 'description'. + """ + std_commands, extra_commands = self._get_command_groups() + max_length = 0 + for cmd in (std_commands + extra_commands): + if len(cmd) > max_length: + max_length = len(cmd) + + self.print_command_list(std_commands, + "Standard commands", + max_length) + if extra_commands: + print() + self.print_command_list(extra_commands, + "Extra commands", + max_length) + + # -- Command class/object methods ---------------------------------- + + def get_command_obj(self, command, create=True): + """Return the command object for 'command'. Normally this object + is cached on a previous call to 'get_command_obj()'; if no command + object for 'command' is in the cache, then we either create and + return it (if 'create' is true) or return None. + """ + cmd_obj = self.command_obj.get(command) + if not cmd_obj and create: + logger.debug("Distribution.get_command_obj(): " \ + "creating %r command object", command) + + cls = get_command_class(command) + cmd_obj = self.command_obj[command] = cls(self) + self.have_run[command] = 0 + + # Set any options that were supplied in config files + # or on the command line. (NB. support for error + # reporting is lame here: any errors aren't reported + # until 'finalize_options()' is called, which means + # we won't report the source of the error.) + options = self.command_options.get(command) + if options: + self._set_command_options(cmd_obj, options) + + return cmd_obj + + def _set_command_options(self, command_obj, option_dict=None): + """Set the options for 'command_obj' from 'option_dict'. Basically + this means copying elements of a dictionary ('option_dict') to + attributes of an instance ('command'). + + 'command_obj' must be a Command instance. If 'option_dict' is not + supplied, uses the standard option dictionary for this command + (from 'self.command_options'). + """ + command_name = command_obj.get_command_name() + if option_dict is None: + option_dict = self.get_option_dict(command_name) + + logger.debug(" setting options for %r command:", command_name) + + for option, (source, value) in option_dict.items(): + logger.debug(" %s = %s (from %s)", option, value, source) + try: + bool_opts = [x.replace('-', '_') + for x in command_obj.boolean_options] + except AttributeError: + bool_opts = [] + try: + neg_opt = command_obj.negative_opt + except AttributeError: + neg_opt = {} + + try: + is_string = isinstance(value, str) + if option in neg_opt and is_string: + setattr(command_obj, neg_opt[option], not strtobool(value)) + elif option in bool_opts and is_string: + setattr(command_obj, option, strtobool(value)) + elif hasattr(command_obj, option): + setattr(command_obj, option, value) + else: + raise PackagingOptionError( + "error in %s: command %r has no such option %r" % + (source, command_name, option)) + except ValueError as msg: + raise PackagingOptionError(msg) + + def get_reinitialized_command(self, command, reinit_subcommands=False): + """Reinitializes a command to the state it was in when first + returned by 'get_command_obj()': ie., initialized but not yet + finalized. This provides the opportunity to sneak option + values in programmatically, overriding or supplementing + user-supplied values from the config files and command line. + You'll have to re-finalize the command object (by calling + 'finalize_options()' or 'ensure_finalized()') before using it for + real. + + 'command' should be a command name (string) or command object. If + 'reinit_subcommands' is true, also reinitializes the command's + sub-commands, as declared by the 'sub_commands' class attribute (if + it has one). See the "install_dist" command for an example. Only + reinitializes the sub-commands that actually matter, ie. those + whose test predicates return true. + + Returns the reinitialized command object. + """ + from packaging.command.cmd import Command + if not isinstance(command, Command): + command_name = command + command = self.get_command_obj(command_name) + else: + command_name = command.get_command_name() + + if not command.finalized: + return command + command.initialize_options() + self.have_run[command_name] = 0 + command.finalized = False + self._set_command_options(command) + + if reinit_subcommands: + for sub in command.get_sub_commands(): + self.get_reinitialized_command(sub, reinit_subcommands) + + return command + + # -- Methods that operate on the Distribution ---------------------- + + def run_commands(self): + """Run each command that was seen on the setup script command line. + Uses the list of commands found and cache of command objects + created by 'get_command_obj()'. + """ + for cmd in self.commands: + self.run_command(cmd) + + # -- Methods that operate on its Commands -------------------------- + + def run_command(self, command, options=None): + """Do whatever it takes to run a command (including nothing at all, + if the command has already been run). Specifically: if we have + already created and run the command named by 'command', return + silently without doing anything. If the command named by 'command' + doesn't even have a command object yet, create one. Then invoke + 'run()' on that command object (or an existing one). + """ + # Already been here, done that? then return silently. + if self.have_run.get(command): + return + + if options is not None: + self.command_options[command] = options + + cmd_obj = self.get_command_obj(command) + cmd_obj.ensure_finalized() + self.run_command_hooks(cmd_obj, 'pre_hook') + logger.info("running %s", command) + cmd_obj.run() + self.run_command_hooks(cmd_obj, 'post_hook') + self.have_run[command] = 1 + + def run_command_hooks(self, cmd_obj, hook_kind): + """Run hooks registered for that command and phase. + + *cmd_obj* is a finalized command object; *hook_kind* is either + 'pre_hook' or 'post_hook'. + """ + if hook_kind not in ('pre_hook', 'post_hook'): + raise ValueError('invalid hook kind: %r' % hook_kind) + + hooks = getattr(cmd_obj, hook_kind, None) + + if hooks is None: + return + + for hook in hooks.values(): + if isinstance(hook, str): + try: + hook_obj = resolve_name(hook) + except ImportError as e: + raise PackagingModuleError(e) + else: + hook_obj = hook + + if not hasattr(hook_obj, '__call__'): + raise PackagingOptionError('hook %r is not callable' % hook) + + logger.info('running %s %s for command %s', + hook_kind, hook, cmd_obj.get_command_name()) + hook_obj(cmd_obj) + + # -- Distribution query methods ------------------------------------ + def has_pure_modules(self): + return len(self.packages or self.py_modules or []) > 0 + + def has_ext_modules(self): + return self.ext_modules and len(self.ext_modules) > 0 + + def has_c_libraries(self): + return self.libraries and len(self.libraries) > 0 + + def has_modules(self): + return self.has_pure_modules() or self.has_ext_modules() + + def has_headers(self): + return self.headers and len(self.headers) > 0 + + def has_scripts(self): + return self.scripts and len(self.scripts) > 0 + + def has_data_files(self): + return self.data_files and len(self.data_files) > 0 + + def is_pure(self): + return (self.has_pure_modules() and + not self.has_ext_modules() and + not self.has_c_libraries()) diff --git a/Lib/packaging/errors.py b/Lib/packaging/errors.py new file mode 100644 index 000000000000..8924a2dcfc3e --- /dev/null +++ b/Lib/packaging/errors.py @@ -0,0 +1,142 @@ +"""Exceptions used throughout the package. + +Submodules of packaging may raise exceptions defined in this module as +well as standard exceptions; in particular, SystemExit is usually raised +for errors that are obviously the end-user's fault (e.g. bad +command-line arguments). +""" + + +class PackagingError(Exception): + """The root of all Packaging evil.""" + + +class PackagingModuleError(PackagingError): + """Unable to load an expected module, or to find an expected class + within some module (in particular, command modules and classes).""" + + +class PackagingClassError(PackagingError): + """Some command class (or possibly distribution class, if anyone + feels a need to subclass Distribution) is found not to be holding + up its end of the bargain, ie. implementing some part of the + "command "interface.""" + + +class PackagingGetoptError(PackagingError): + """The option table provided to 'fancy_getopt()' is bogus.""" + + +class PackagingArgError(PackagingError): + """Raised by fancy_getopt in response to getopt.error -- ie. an + error in the command line usage.""" + + +class PackagingFileError(PackagingError): + """Any problems in the filesystem: expected file not found, etc. + Typically this is for problems that we detect before IOError or + OSError could be raised.""" + + +class PackagingOptionError(PackagingError): + """Syntactic/semantic errors in command options, such as use of + mutually conflicting options, or inconsistent options, + badly-spelled values, etc. No distinction is made between option + values originating in the setup script, the command line, config + files, or what-have-you -- but if we *know* something originated in + the setup script, we'll raise PackagingSetupError instead.""" + + +class PackagingSetupError(PackagingError): + """For errors that can be definitely blamed on the setup script, + such as invalid keyword arguments to 'setup()'.""" + + +class PackagingPlatformError(PackagingError): + """We don't know how to do something on the current platform (but + we do know how to do it on some platform) -- eg. trying to compile + C files on a platform not supported by a CCompiler subclass.""" + + +class PackagingExecError(PackagingError): + """Any problems executing an external program (such as the C + compiler, when compiling C files).""" + + +class PackagingInternalError(PackagingError): + """Internal inconsistencies or impossibilities (obviously, this + should never be seen if the code is working!).""" + + +class PackagingTemplateError(PackagingError): + """Syntax error in a file list template.""" + + +class PackagingByteCompileError(PackagingError): + """Byte compile error.""" + + +class PackagingPyPIError(PackagingError): + """Any problem occuring during using the indexes.""" + + +# Exception classes used by the CCompiler implementation classes +class CCompilerError(Exception): + """Some compile/link operation failed.""" + + +class PreprocessError(CCompilerError): + """Failure to preprocess one or more C/C++ files.""" + + +class CompileError(CCompilerError): + """Failure to compile one or more C/C++ source files.""" + + +class LibError(CCompilerError): + """Failure to create a static library from one or more C/C++ object + files.""" + + +class LinkError(CCompilerError): + """Failure to link one or more C/C++ object files into an executable + or shared library file.""" + + +class UnknownFileError(CCompilerError): + """Attempt to process an unknown file type.""" + + +class MetadataMissingError(PackagingError): + """A required metadata is missing""" + + +class MetadataConflictError(PackagingError): + """Attempt to read or write metadata fields that are conflictual.""" + + +class MetadataUnrecognizedVersionError(PackagingError): + """Unknown metadata version number.""" + + +class IrrationalVersionError(Exception): + """This is an irrational version.""" + pass + + +class HugeMajorVersionNumError(IrrationalVersionError): + """An irrational version because the major version number is huge + (often because a year or date was used). + + See `error_on_huge_major_num` option in `NormalizedVersion` for details. + This guard can be disabled by setting that option False. + """ + pass + + +class InstallationException(Exception): + """Base exception for installation scripts""" + + +class InstallationConflict(InstallationException): + """Raised when a conflict is detected""" diff --git a/Lib/packaging/fancy_getopt.py b/Lib/packaging/fancy_getopt.py new file mode 100644 index 000000000000..049086429040 --- /dev/null +++ b/Lib/packaging/fancy_getopt.py @@ -0,0 +1,451 @@ +"""Command line parsing machinery. + +The FancyGetopt class is a Wrapper around the getopt module that +provides the following additional features: + * short and long options are tied together + * options have help strings, so fancy_getopt could potentially + create a complete usage summary + * options set attributes of a passed-in object. + +It is used under the hood by the command classes. Do not use directly. +""" + +import getopt +import re +import sys +import string +import textwrap + +from packaging.errors import PackagingGetoptError, PackagingArgError + +# Much like command_re in packaging.core, this is close to but not quite +# the same as a Python NAME -- except, in the spirit of most GNU +# utilities, we use '-' in place of '_'. (The spirit of LISP lives on!) +# The similarities to NAME are again not a coincidence... +longopt_pat = r'[a-zA-Z](?:[a-zA-Z0-9-]*)' +longopt_re = re.compile(r'^%s$' % longopt_pat) + +# For recognizing "negative alias" options, eg. "quiet=!verbose" +neg_alias_re = re.compile("^(%s)=!(%s)$" % (longopt_pat, longopt_pat)) + + +class FancyGetopt: + """Wrapper around the standard 'getopt()' module that provides some + handy extra functionality: + * short and long options are tied together + * options have help strings, and help text can be assembled + from them + * options set attributes of a passed-in object + * boolean options can have "negative aliases" -- eg. if + --quiet is the "negative alias" of --verbose, then "--quiet" + on the command line sets 'verbose' to false + """ + + def __init__(self, option_table=None): + + # The option table is (currently) a list of tuples. The + # tuples may have 3 or four values: + # (long_option, short_option, help_string [, repeatable]) + # if an option takes an argument, its long_option should have '=' + # appended; short_option should just be a single character, no ':' + # in any case. If a long_option doesn't have a corresponding + # short_option, short_option should be None. All option tuples + # must have long options. + self.option_table = option_table + + # 'option_index' maps long option names to entries in the option + # table (ie. those 3-tuples). + self.option_index = {} + if self.option_table: + self._build_index() + + # 'alias' records (duh) alias options; {'foo': 'bar'} means + # --foo is an alias for --bar + self.alias = {} + + # 'negative_alias' keeps track of options that are the boolean + # opposite of some other option + self.negative_alias = {} + + # These keep track of the information in the option table. We + # don't actually populate these structures until we're ready to + # parse the command line, since the 'option_table' passed in here + # isn't necessarily the final word. + self.short_opts = [] + self.long_opts = [] + self.short2long = {} + self.attr_name = {} + self.takes_arg = {} + + # And 'option_order' is filled up in 'getopt()'; it records the + # original order of options (and their values) on the command line, + # but expands short options, converts aliases, etc. + self.option_order = [] + + def _build_index(self): + self.option_index.clear() + for option in self.option_table: + self.option_index[option[0]] = option + + def set_option_table(self, option_table): + self.option_table = option_table + self._build_index() + + def add_option(self, long_option, short_option=None, help_string=None): + if long_option in self.option_index: + raise PackagingGetoptError( + "option conflict: already an option '%s'" % long_option) + else: + option = (long_option, short_option, help_string) + self.option_table.append(option) + self.option_index[long_option] = option + + def has_option(self, long_option): + """Return true if the option table for this parser has an + option with long name 'long_option'.""" + return long_option in self.option_index + + def _check_alias_dict(self, aliases, what): + assert isinstance(aliases, dict) + for alias, opt in aliases.items(): + if alias not in self.option_index: + raise PackagingGetoptError( + ("invalid %s '%s': " + "option '%s' not defined") % (what, alias, alias)) + if opt not in self.option_index: + raise PackagingGetoptError( + ("invalid %s '%s': " + "aliased option '%s' not defined") % (what, alias, opt)) + + def set_aliases(self, alias): + """Set the aliases for this option parser.""" + self._check_alias_dict(alias, "alias") + self.alias = alias + + def set_negative_aliases(self, negative_alias): + """Set the negative aliases for this option parser. + 'negative_alias' should be a dictionary mapping option names to + option names, both the key and value must already be defined + in the option table.""" + self._check_alias_dict(negative_alias, "negative alias") + self.negative_alias = negative_alias + + def _grok_option_table(self): + """Populate the various data structures that keep tabs on the + option table. Called by 'getopt()' before it can do anything + worthwhile. + """ + self.long_opts = [] + self.short_opts = [] + self.short2long.clear() + self.repeat = {} + + for option in self.option_table: + if len(option) == 3: + integer, short, help = option + repeat = 0 + elif len(option) == 4: + integer, short, help, repeat = option + else: + # the option table is part of the code, so simply + # assert that it is correct + raise ValueError("invalid option tuple: %r" % option) + + # Type- and value-check the option names + if not isinstance(integer, str) or len(integer) < 2: + raise PackagingGetoptError( + ("invalid long option '%s': " + "must be a string of length >= 2") % integer) + + if (not ((short is None) or + (isinstance(short, str) and len(short) == 1))): + raise PackagingGetoptError( + ("invalid short option '%s': " + "must be a single character or None") % short) + + self.repeat[integer] = repeat + self.long_opts.append(integer) + + if integer[-1] == '=': # option takes an argument? + if short: + short = short + ':' + integer = integer[0:-1] + self.takes_arg[integer] = 1 + else: + + # Is option is a "negative alias" for some other option (eg. + # "quiet" == "!verbose")? + alias_to = self.negative_alias.get(integer) + if alias_to is not None: + if self.takes_arg[alias_to]: + raise PackagingGetoptError( + ("invalid negative alias '%s': " + "aliased option '%s' takes a value") % \ + (integer, alias_to)) + + self.long_opts[-1] = integer # XXX redundant?! + self.takes_arg[integer] = 0 + + else: + self.takes_arg[integer] = 0 + + # If this is an alias option, make sure its "takes arg" flag is + # the same as the option it's aliased to. + alias_to = self.alias.get(integer) + if alias_to is not None: + if self.takes_arg[integer] != self.takes_arg[alias_to]: + raise PackagingGetoptError( + ("invalid alias '%s': inconsistent with " + "aliased option '%s' (one of them takes a value, " + "the other doesn't") % (integer, alias_to)) + + # Now enforce some bondage on the long option name, so we can + # later translate it to an attribute name on some object. Have + # to do this a bit late to make sure we've removed any trailing + # '='. + if not longopt_re.match(integer): + raise PackagingGetoptError( + ("invalid long option name '%s' " + + "(must be letters, numbers, hyphens only") % integer) + + self.attr_name[integer] = integer.replace('-', '_') + if short: + self.short_opts.append(short) + self.short2long[short[0]] = integer + + def getopt(self, args=None, object=None): + """Parse command-line options in args. Store as attributes on object. + + If 'args' is None or not supplied, uses 'sys.argv[1:]'. If + 'object' is None or not supplied, creates a new OptionDummy + object, stores option values there, and returns a tuple (args, + object). If 'object' is supplied, it is modified in place and + 'getopt()' just returns 'args'; in both cases, the returned + 'args' is a modified copy of the passed-in 'args' list, which + is left untouched. + """ + if args is None: + args = sys.argv[1:] + if object is None: + object = OptionDummy() + created_object = 1 + else: + created_object = 0 + + self._grok_option_table() + + short_opts = ' '.join(self.short_opts) + + try: + opts, args = getopt.getopt(args, short_opts, self.long_opts) + except getopt.error as msg: + raise PackagingArgError(msg) + + for opt, val in opts: + if len(opt) == 2 and opt[0] == '-': # it's a short option + opt = self.short2long[opt[1]] + else: + assert len(opt) > 2 and opt[:2] == '--' + opt = opt[2:] + + alias = self.alias.get(opt) + if alias: + opt = alias + + if not self.takes_arg[opt]: # boolean option? + assert val == '', "boolean option can't have value" + alias = self.negative_alias.get(opt) + if alias: + opt = alias + val = 0 + else: + val = 1 + + attr = self.attr_name[opt] + # The only repeating option at the moment is 'verbose'. + # It has a negative option -q quiet, which should set verbose = 0. + if val and self.repeat.get(attr) is not None: + val = getattr(object, attr, 0) + 1 + setattr(object, attr, val) + self.option_order.append((opt, val)) + + # for opts + if created_object: + return args, object + else: + return args + + def get_option_order(self): + """Returns the list of (option, value) tuples processed by the + previous run of 'getopt()'. Raises RuntimeError if + 'getopt()' hasn't been called yet. + """ + if self.option_order is None: + raise RuntimeError("'getopt()' hasn't been called yet") + else: + return self.option_order + + return self.option_order + + def generate_help(self, header=None): + """Generate help text (a list of strings, one per suggested line of + output) from the option table for this FancyGetopt object. + """ + # Blithely assume the option table is good: probably wouldn't call + # 'generate_help()' unless you've already called 'getopt()'. + + # First pass: determine maximum length of long option names + max_opt = 0 + for option in self.option_table: + integer = option[0] + short = option[1] + l = len(integer) + if integer[-1] == '=': + l = l - 1 + if short is not None: + l = l + 5 # " (-x)" where short == 'x' + if l > max_opt: + max_opt = l + + opt_width = max_opt + 2 + 2 + 2 # room for indent + dashes + gutter + + # Typical help block looks like this: + # --foo controls foonabulation + # Help block for longest option looks like this: + # --flimflam set the flim-flam level + # and with wrapped text: + # --flimflam set the flim-flam level (must be between + # 0 and 100, except on Tuesdays) + # Options with short names will have the short name shown (but + # it doesn't contribute to max_opt): + # --foo (-f) controls foonabulation + # If adding the short option would make the left column too wide, + # we push the explanation off to the next line + # --flimflam (-l) + # set the flim-flam level + # Important parameters: + # - 2 spaces before option block start lines + # - 2 dashes for each long option name + # - min. 2 spaces between option and explanation (gutter) + # - 5 characters (incl. space) for short option name + + # Now generate lines of help text. (If 80 columns were good enough + # for Jesus, then 78 columns are good enough for me!) + line_width = 78 + text_width = line_width - opt_width + big_indent = ' ' * opt_width + if header: + lines = [header] + else: + lines = ['Option summary:'] + + for option in self.option_table: + integer, short, help = option[:3] + text = textwrap.wrap(help, text_width) + + # Case 1: no short option at all (makes life easy) + if short is None: + if text: + lines.append(" --%-*s %s" % (max_opt, integer, text[0])) + else: + lines.append(" --%-*s " % (max_opt, integer)) + + # Case 2: we have a short option, so we have to include it + # just after the long option + else: + opt_names = "%s (-%s)" % (integer, short) + if text: + lines.append(" --%-*s %s" % + (max_opt, opt_names, text[0])) + else: + lines.append(" --%-*s" % opt_names) + + for l in text[1:]: + lines.append(big_indent + l) + + return lines + + def print_help(self, header=None, file=None): + if file is None: + file = sys.stdout + for line in self.generate_help(header): + file.write(line + "\n") + + +def fancy_getopt(options, negative_opt, object, args): + parser = FancyGetopt(options) + parser.set_negative_aliases(negative_opt) + return parser.getopt(args, object) + + +WS_TRANS = str.maketrans(string.whitespace, ' ' * len(string.whitespace)) + + +def wrap_text(text, width): + """Split *text* into lines of no more than *width* characters each. + + *text* is a str and *width* an int. Returns a list of str. + """ + + if text is None: + return [] + if len(text) <= width: + return [text] + + text = text.expandtabs() + text = text.translate(WS_TRANS) + + chunks = re.split(r'( +|-+)', text) + chunks = [_f for _f in chunks if _f] # ' - ' results in empty strings + lines = [] + + while chunks: + + cur_line = [] # list of chunks (to-be-joined) + cur_len = 0 # length of current line + + while chunks: + l = len(chunks[0]) + if cur_len + l <= width: # can squeeze (at least) this chunk in + cur_line.append(chunks[0]) + del chunks[0] + cur_len = cur_len + l + else: # this line is full + # drop last chunk if all space + if cur_line and cur_line[-1][0] == ' ': + del cur_line[-1] + break + + if chunks: # any chunks left to process? + + # if the current line is still empty, then we had a single + # chunk that's too big too fit on a line -- so we break + # down and break it up at the line width + if cur_len == 0: + cur_line.append(chunks[0][0:width]) + chunks[0] = chunks[0][width:] + + # all-whitespace chunks at the end of a line can be discarded + # (and we know from the re.split above that if a chunk has + # *any* whitespace, it is *all* whitespace) + if chunks[0][0] == ' ': + del chunks[0] + + # and store this line in the list-of-all-lines -- as a single + # string, of course! + lines.append(''.join(cur_line)) + + # while chunks + + return lines + + +class OptionDummy: + """Dummy class just used as a place to hold command-line option + values as instance attributes.""" + + def __init__(self, options=[]): + """Create a new OptionDummy instance. The attributes listed in + 'options' will be initialized to None.""" + for opt in options: + setattr(self, opt, None) diff --git a/Lib/packaging/install.py b/Lib/packaging/install.py new file mode 100644 index 000000000000..3904727d4503 --- /dev/null +++ b/Lib/packaging/install.py @@ -0,0 +1,483 @@ +"""Building blocks for installers. + +When used as a script, this module installs a release thanks to info +obtained from an index (e.g. PyPI), with dependencies. + +This is a higher-level module built on packaging.database and +packaging.pypi. +""" + +import os +import sys +import stat +import errno +import shutil +import logging +import tempfile +from sysconfig import get_config_var + +from packaging import logger +from packaging.dist import Distribution +from packaging.util import (_is_archive_file, ask, get_install_method, + egginfo_to_distinfo) +from packaging.pypi import wrapper +from packaging.version import get_version_predicate +from packaging.database import get_distributions, get_distribution +from packaging.depgraph import generate_graph + +from packaging.errors import (PackagingError, InstallationException, + InstallationConflict, CCompilerError) +from packaging.pypi.errors import ProjectNotFound, ReleaseNotFound + +__all__ = ['install_dists', 'install_from_infos', 'get_infos', 'remove', + 'install', 'install_local_project'] + + +def _move_files(files, destination): + """Move the list of files in the destination folder, keeping the same + structure. + + Return a list of tuple (old, new) emplacement of files + + :param files: a list of files to move. + :param destination: the destination directory to put on the files. + if not defined, create a new one, using mkdtemp + """ + if not destination: + destination = tempfile.mkdtemp() + + for old in files: + # not using os.path.join() because basename() might not be + # unique in destination + new = "%s%s" % (destination, old) + + # try to make the paths. + try: + os.makedirs(os.path.dirname(new)) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + raise e + os.rename(old, new) + yield old, new + + +def _run_distutils_install(path): + # backward compat: using setuptools or plain-distutils + cmd = '%s setup.py install --record=%s' + record_file = os.path.join(path, 'RECORD') + os.system(cmd % (sys.executable, record_file)) + if not os.path.exists(record_file): + raise ValueError('failed to install') + else: + egginfo_to_distinfo(record_file, remove_egginfo=True) + + +def _run_setuptools_install(path): + cmd = '%s setup.py install --record=%s --single-version-externally-managed' + record_file = os.path.join(path, 'RECORD') + os.system(cmd % (sys.executable, record_file)) + if not os.path.exists(record_file): + raise ValueError('failed to install') + else: + egginfo_to_distinfo(record_file, remove_egginfo=True) + + +def _run_packaging_install(path): + # XXX check for a valid setup.cfg? + dist = Distribution() + dist.parse_config_files() + try: + dist.run_command('install_dist') + except (IOError, os.error, PackagingError, CCompilerError) as msg: + raise SystemExit("error: " + str(msg)) + + +def _install_dist(dist, path): + """Install a distribution into a path. + + This: + + * unpack the distribution + * copy the files in "path" + * determine if the distribution is packaging or distutils1. + """ + where = dist.unpack() + + if where is None: + raise ValueError('Cannot locate the unpacked archive') + + return _run_install_from_archive(where) + + +def install_local_project(path): + """Install a distribution from a source directory. + + If the source directory contains a setup.py install using distutils1. + If a setup.cfg is found, install using the install_dist command. + + """ + path = os.path.abspath(path) + if os.path.isdir(path): + logger.info('installing from source directory: %s', path) + _run_install_from_dir(path) + elif _is_archive_file(path): + logger.info('installing from archive: %s', path) + _unpacked_dir = tempfile.mkdtemp() + shutil.unpack_archive(path, _unpacked_dir) + _run_install_from_archive(_unpacked_dir) + else: + logger.warning('no projects to install') + + +def _run_install_from_archive(source_dir): + # XXX need a better way + for item in os.listdir(source_dir): + fullpath = os.path.join(source_dir, item) + if os.path.isdir(fullpath): + source_dir = fullpath + break + return _run_install_from_dir(source_dir) + + +install_methods = { + 'packaging': _run_packaging_install, + 'setuptools': _run_setuptools_install, + 'distutils': _run_distutils_install} + + +def _run_install_from_dir(source_dir): + old_dir = os.getcwd() + os.chdir(source_dir) + install_method = get_install_method(source_dir) + func = install_methods[install_method] + try: + func = install_methods[install_method] + return func(source_dir) + finally: + os.chdir(old_dir) + + +def install_dists(dists, path, paths=sys.path): + """Install all distributions provided in dists, with the given prefix. + + If an error occurs while installing one of the distributions, uninstall all + the installed distribution (in the context if this function). + + Return a list of installed dists. + + :param dists: distributions to install + :param path: base path to install distribution in + :param paths: list of paths (defaults to sys.path) to look for info + """ + if not path: + path = tempfile.mkdtemp() + + installed_dists = [] + for dist in dists: + logger.info('installing %s %s', dist.name, dist.version) + try: + _install_dist(dist, path) + installed_dists.append(dist) + except Exception as e: + logger.info('failed: %s', e) + + # reverting + for installed_dist in installed_dists: + logger.info('reverting %s', installed_dist) + _remove_dist(installed_dist, paths) + raise e + return installed_dists + + +def install_from_infos(install_path=None, install=[], remove=[], conflicts=[], + paths=sys.path): + """Install and remove the given distributions. + + The function signature is made to be compatible with the one of get_infos. + The aim of this script is to povide a way to install/remove what's asked, + and to rollback if needed. + + So, it's not possible to be in an inconsistant state, it could be either + installed, either uninstalled, not half-installed. + + The process follow those steps: + + 1. Move all distributions that will be removed in a temporary location + 2. Install all the distributions that will be installed in a temp. loc. + 3. If the installation fails, rollback (eg. move back) those + distributions, or remove what have been installed. + 4. Else, move the distributions to the right locations, and remove for + real the distributions thats need to be removed. + + :param install_path: the installation path where we want to install the + distributions. + :param install: list of distributions that will be installed; install_path + must be provided if this list is not empty. + :param remove: list of distributions that will be removed. + :param conflicts: list of conflicting distributions, eg. that will be in + conflict once the install and remove distribution will be + processed. + :param paths: list of paths (defaults to sys.path) to look for info + """ + # first of all, if we have conflicts, stop here. + if conflicts: + raise InstallationConflict(conflicts) + + if install and not install_path: + raise ValueError("Distributions are to be installed but `install_path`" + " is not provided.") + + # before removing the files, we will start by moving them away + # then, if any error occurs, we could replace them in the good place. + temp_files = {} # contains lists of {dist: (old, new)} paths + temp_dir = None + if remove: + temp_dir = tempfile.mkdtemp() + for dist in remove: + files = dist.list_installed_files() + temp_files[dist] = _move_files(files, temp_dir) + try: + if install: + install_dists(install, install_path, paths) + except: + # if an error occurs, put back the files in the right place. + for files in temp_files.values(): + for old, new in files: + shutil.move(new, old) + if temp_dir: + shutil.rmtree(temp_dir) + # now re-raising + raise + + # we can remove them for good + for files in temp_files.values(): + for old, new in files: + os.remove(new) + if temp_dir: + shutil.rmtree(temp_dir) + + +def _get_setuptools_deps(release): + # NotImplementedError + pass + + +def get_infos(requirements, index=None, installed=None, prefer_final=True): + """Return the informations on what's going to be installed and upgraded. + + :param requirements: is a *string* containing the requirements for this + project (for instance "FooBar 1.1" or "BarBaz (<1.2)") + :param index: If an index is specified, use this one, otherwise, use + :class index.ClientWrapper: to get project metadatas. + :param installed: a list of already installed distributions. + :param prefer_final: when picking up the releases, prefer a "final" one + over a beta/alpha/etc one. + + The results are returned in a dict, containing all the operations + needed to install the given requirements:: + + >>> get_install_info("FooBar (<=1.2)") + {'install': [], 'remove': [], 'conflict': []} + + Conflict contains all the conflicting distributions, if there is a + conflict. + """ + # this function does several things: + # 1. get a release specified by the requirements + # 2. gather its metadata, using setuptools compatibility if needed + # 3. compare this tree with what is currently installed on the system, + # return the requirements of what is missing + # 4. do that recursively and merge back the results + # 5. return a dict containing information about what is needed to install + # or remove + + if not installed: + logger.info('reading installed distributions') + installed = list(get_distributions(use_egg_info=True)) + + infos = {'install': [], 'remove': [], 'conflict': []} + # Is a compatible version of the project already installed ? + predicate = get_version_predicate(requirements) + found = False + + # check that the project isn't already installed + for installed_project in installed: + # is it a compatible project ? + if predicate.name.lower() != installed_project.name.lower(): + continue + found = True + logger.info('found %s %s', installed_project.name, + installed_project.metadata['version']) + + # if we already have something installed, check it matches the + # requirements + if predicate.match(installed_project.metadata['version']): + return infos + break + + if not found: + logger.info('project not installed') + + if not index: + index = wrapper.ClientWrapper() + + if not installed: + installed = get_distributions(use_egg_info=True) + + # Get all the releases that match the requirements + try: + release = index.get_release(requirements) + except (ReleaseNotFound, ProjectNotFound): + raise InstallationException('Release not found: "%s"' % requirements) + + if release is None: + logger.info('could not find a matching project') + return infos + + metadata = release.fetch_metadata() + + # we need to build setuptools deps if any + if 'requires_dist' not in metadata: + metadata['requires_dist'] = _get_setuptools_deps(release) + + # build the dependency graph with local and required dependencies + dists = list(installed) + dists.append(release) + depgraph = generate_graph(dists) + + # Get what the missing deps are + dists = depgraph.missing[release] + if dists: + logger.info("missing dependencies found, retrieving metadata") + # we have missing deps + for dist in dists: + _update_infos(infos, get_infos(dist, index, installed)) + + # Fill in the infos + existing = [d for d in installed if d.name == release.name] + if existing: + infos['remove'].append(existing[0]) + infos['conflict'].extend(depgraph.reverse_list[existing[0]]) + infos['install'].append(release) + return infos + + +def _update_infos(infos, new_infos): + """extends the lists contained in the `info` dict with those contained + in the `new_info` one + """ + for key, value in infos.items(): + if key in new_infos: + infos[key].extend(new_infos[key]) + + +def _remove_dist(dist, paths=sys.path): + remove(dist.name, paths) + + +def remove(project_name, paths=sys.path, auto_confirm=True): + """Removes a single project from the installation""" + dist = get_distribution(project_name, use_egg_info=True, paths=paths) + if dist is None: + raise PackagingError('Distribution "%s" not found' % project_name) + files = dist.list_installed_files(local=True) + rmdirs = [] + rmfiles = [] + tmp = tempfile.mkdtemp(prefix=project_name + '-uninstall') + try: + for file_, md5, size in files: + if os.path.isfile(file_): + dirname, filename = os.path.split(file_) + tmpfile = os.path.join(tmp, filename) + try: + os.rename(file_, tmpfile) + finally: + if not os.path.isfile(file_): + os.rename(tmpfile, file_) + if file_ not in rmfiles: + rmfiles.append(file_) + if dirname not in rmdirs: + rmdirs.append(dirname) + finally: + shutil.rmtree(tmp) + + logger.info('removing %r: ', project_name) + + for file_ in rmfiles: + logger.info(' %s', file_) + + # Taken from the pip project + if auto_confirm: + response = 'y' + else: + response = ask('Proceed (y/n)? ', ('y', 'n')) + + if response == 'y': + file_count = 0 + for file_ in rmfiles: + os.remove(file_) + file_count += 1 + + dir_count = 0 + for dirname in rmdirs: + if not os.path.exists(dirname): + # could + continue + + files_count = 0 + for root, dir, files in os.walk(dirname): + files_count += len(files) + + if files_count > 0: + # XXX Warning + continue + + # empty dirs with only empty dirs + if os.stat(dirname).st_mode & stat.S_IWUSR: + # XXX Add a callable in shutil.rmtree to count + # the number of deleted elements + shutil.rmtree(dirname) + dir_count += 1 + + # removing the top path + # XXX count it ? + if os.path.exists(dist.path): + shutil.rmtree(dist.path) + + logger.info('success: removed %d files and %d dirs', + file_count, dir_count) + + +def install(project): + logger.info('getting information about %r', project) + try: + info = get_infos(project) + except InstallationException: + logger.info('cound not find %r', project) + return + + if info['install'] == []: + logger.info('nothing to install') + return + + install_path = get_config_var('base') + try: + install_from_infos(install_path, + info['install'], info['remove'], info['conflict']) + + except InstallationConflict as e: + if logger.isEnabledFor(logging.INFO): + projects = ['%s %s' % (p.name, p.version) for p in e.args[0]] + logger.info('%r conflicts with %s', project, ','.join(projects)) + + +def _main(**attrs): + if 'script_args' not in attrs: + import sys + attrs['requirements'] = sys.argv[1] + get_infos(**attrs) + +if __name__ == '__main__': + _main() diff --git a/Lib/packaging/manifest.py b/Lib/packaging/manifest.py new file mode 100644 index 000000000000..a3798530a582 --- /dev/null +++ b/Lib/packaging/manifest.py @@ -0,0 +1,372 @@ +"""Class representing the list of files in a distribution. + +The Manifest class can be used to: + + - read or write a MANIFEST file + - read a template file and find out the file list +""" +# XXX todo: document + add tests +import re +import os +import fnmatch + +from packaging import logger +from packaging.util import write_file, convert_path +from packaging.errors import (PackagingTemplateError, + PackagingInternalError) + +__all__ = ['Manifest'] + +# a \ followed by some spaces + EOL +_COLLAPSE_PATTERN = re.compile('\\\w*\n', re.M) +_COMMENTED_LINE = re.compile('#.*?(?=\n)|\n(?=$)', re.M | re.S) + + +class Manifest(object): + """A list of files built by on exploring the filesystem and filtered by + applying various patterns to what we find there. + """ + + def __init__(self): + self.allfiles = None + self.files = [] + + # + # Public API + # + + def findall(self, dir=os.curdir): + self.allfiles = _findall(dir) + + def append(self, item): + self.files.append(item) + + def extend(self, items): + self.files.extend(items) + + def sort(self): + # Not a strict lexical sort! + self.files = [os.path.join(*path_tuple) for path_tuple in + sorted(os.path.split(path) for path in self.files)] + + def clear(self): + """Clear all collected files.""" + self.files = [] + if self.allfiles is not None: + self.allfiles = [] + + def remove_duplicates(self): + # Assumes list has been sorted! + for i in range(len(self.files) - 1, 0, -1): + if self.files[i] == self.files[i - 1]: + del self.files[i] + + def read_template(self, path_or_file): + """Read and parse a manifest template file. + 'path' can be a path or a file-like object. + + Updates the list accordingly. + """ + if isinstance(path_or_file, str): + f = open(path_or_file) + else: + f = path_or_file + + try: + content = f.read() + # first, let's unwrap collapsed lines + content = _COLLAPSE_PATTERN.sub('', content) + # next, let's remove commented lines and empty lines + content = _COMMENTED_LINE.sub('', content) + + # now we have our cleaned up lines + lines = [line.strip() for line in content.split('\n')] + finally: + f.close() + + for line in lines: + if line == '': + continue + try: + self._process_template_line(line) + except PackagingTemplateError as msg: + logger.warning("%s, %s", path_or_file, msg) + + def write(self, path): + """Write the file list in 'self.filelist' (presumably as filled in + by 'add_defaults()' and 'read_template()') to the manifest file + named by 'self.manifest'. + """ + if os.path.isfile(path): + with open(path) as fp: + first_line = fp.readline() + + if first_line != '# file GENERATED by packaging, do NOT edit\n': + logger.info("not writing to manually maintained " + "manifest file %r", path) + return + + self.sort() + self.remove_duplicates() + content = self.files[:] + content.insert(0, '# file GENERATED by packaging, do NOT edit') + logger.info("writing manifest file %r", path) + write_file(path, content) + + def read(self, path): + """Read the manifest file (named by 'self.manifest') and use it to + fill in 'self.filelist', the list of files to include in the source + distribution. + """ + logger.info("reading manifest file %r", path) + with open(path) as manifest: + for line in manifest.readlines(): + self.append(line) + + def exclude_pattern(self, pattern, anchor=True, prefix=None, + is_regex=False): + """Remove strings (presumably filenames) from 'files' that match + 'pattern'. + + Other parameters are the same as for 'include_pattern()', above. + The list 'self.files' is modified in place. Return True if files are + found. + """ + files_found = False + pattern_re = _translate_pattern(pattern, anchor, prefix, is_regex) + for i in range(len(self.files) - 1, -1, -1): + if pattern_re.search(self.files[i]): + del self.files[i] + files_found = True + + return files_found + + # + # Private API + # + + def _parse_template_line(self, line): + words = line.split() + if len(words) == 1: + # no action given, let's use the default 'include' + words.insert(0, 'include') + + action = words[0] + patterns = dir = dir_pattern = None + + if action in ('include', 'exclude', + 'global-include', 'global-exclude'): + if len(words) < 2: + raise PackagingTemplateError( + "%r expects ..." % action) + + patterns = [convert_path(word) for word in words[1:]] + + elif action in ('recursive-include', 'recursive-exclude'): + if len(words) < 3: + raise PackagingTemplateError( + "%r expects

..." % action) + + dir = convert_path(words[1]) + patterns = [convert_path(word) for word in words[2:]] + + elif action in ('graft', 'prune'): + if len(words) != 2: + raise PackagingTemplateError( + "%r expects a single " % action) + + dir_pattern = convert_path(words[1]) + + else: + raise PackagingTemplateError("unknown action %r" % action) + + return action, patterns, dir, dir_pattern + + def _process_template_line(self, line): + # Parse the line: split it up, make sure the right number of words + # is there, and return the relevant words. 'action' is always + # defined: it's the first word of the line. Which of the other + # three are defined depends on the action; it'll be either + # patterns, (dir and patterns), or (dir_pattern). + action, patterns, dir, dir_pattern = self._parse_template_line(line) + + # OK, now we know that the action is valid and we have the + # right number of words on the line for that action -- so we + # can proceed with minimal error-checking. + if action == 'include': + for pattern in patterns: + if not self._include_pattern(pattern, anchor=True): + logger.warning("no files found matching %r", pattern) + + elif action == 'exclude': + for pattern in patterns: + if not self.exclude_pattern(pattern, anchor=True): + logger.warning("no previously-included files " + "found matching %r", pattern) + + elif action == 'global-include': + for pattern in patterns: + if not self._include_pattern(pattern, anchor=False): + logger.warning("no files found matching %r " + "anywhere in distribution", pattern) + + elif action == 'global-exclude': + for pattern in patterns: + if not self.exclude_pattern(pattern, anchor=False): + logger.warning("no previously-included files " + "matching %r found anywhere in " + "distribution", pattern) + + elif action == 'recursive-include': + for pattern in patterns: + if not self._include_pattern(pattern, prefix=dir): + logger.warning("no files found matching %r " + "under directory %r", pattern, dir) + + elif action == 'recursive-exclude': + for pattern in patterns: + if not self.exclude_pattern(pattern, prefix=dir): + logger.warning("no previously-included files " + "matching %r found under directory %r", + pattern, dir) + + elif action == 'graft': + if not self._include_pattern(None, prefix=dir_pattern): + logger.warning("no directories found matching %r", + dir_pattern) + + elif action == 'prune': + if not self.exclude_pattern(None, prefix=dir_pattern): + logger.warning("no previously-included directories found " + "matching %r", dir_pattern) + else: + raise PackagingInternalError( + "this cannot happen: invalid action %r" % action) + + def _include_pattern(self, pattern, anchor=True, prefix=None, + is_regex=False): + """Select strings (presumably filenames) from 'self.files' that + match 'pattern', a Unix-style wildcard (glob) pattern. + + Patterns are not quite the same as implemented by the 'fnmatch' + module: '*' and '?' match non-special characters, where "special" + is platform-dependent: slash on Unix; colon, slash, and backslash on + DOS/Windows; and colon on Mac OS. + + If 'anchor' is true (the default), then the pattern match is more + stringent: "*.py" will match "foo.py" but not "foo/bar.py". If + 'anchor' is false, both of these will match. + + If 'prefix' is supplied, then only filenames starting with 'prefix' + (itself a pattern) and ending with 'pattern', with anything in between + them, will match. 'anchor' is ignored in this case. + + If 'is_regex' is true, 'anchor' and 'prefix' are ignored, and + 'pattern' is assumed to be either a string containing a regex or a + regex object -- no translation is done, the regex is just compiled + and used as-is. + + Selected strings will be added to self.files. + + Return True if files are found. + """ + files_found = False + pattern_re = _translate_pattern(pattern, anchor, prefix, is_regex) + + # delayed loading of allfiles list + if self.allfiles is None: + self.findall() + + for name in self.allfiles: + if pattern_re.search(name): + self.files.append(name) + files_found = True + + return files_found + + +# +# Utility functions +# +def _findall(dir=os.curdir): + """Find all files under 'dir' and return the list of full filenames + (relative to 'dir'). + """ + from stat import S_ISREG, S_ISDIR, S_ISLNK + + list = [] + stack = [dir] + pop = stack.pop + push = stack.append + + while stack: + dir = pop() + names = os.listdir(dir) + + for name in names: + if dir != os.curdir: # avoid the dreaded "./" syndrome + fullname = os.path.join(dir, name) + else: + fullname = name + + # Avoid excess stat calls -- just one will do, thank you! + stat = os.stat(fullname) + mode = stat.st_mode + if S_ISREG(mode): + list.append(fullname) + elif S_ISDIR(mode) and not S_ISLNK(mode): + push(fullname) + + return list + + +def _glob_to_re(pattern): + """Translate a shell-like glob pattern to a regular expression. + + Return a string containing the regex. Differs from + 'fnmatch.translate()' in that '*' does not match "special characters" + (which are platform-specific). + """ + pattern_re = fnmatch.translate(pattern) + + # '?' and '*' in the glob pattern become '.' and '.*' in the RE, which + # IMHO is wrong -- '?' and '*' aren't supposed to match slash in Unix, + # and by extension they shouldn't match such "special characters" under + # any OS. So change all non-escaped dots in the RE to match any + # character except the special characters. + # XXX currently the "special characters" are just slash -- i.e. this is + # Unix-only. + pattern_re = re.sub(r'((?': lambda x, y: x > y, + '>=': lambda x, y: x >= y, + '<': lambda x, y: x < y, + '<=': lambda x, y: x <= y, + 'in': lambda x, y: x in y, + 'not in': lambda x, y: x not in y} + + +def _operate(operation, x, y): + return _OPERATORS[operation](x, y) + + +# restricted set of variables +_VARS = {'sys.platform': sys.platform, + 'python_version': sys.version[:3], + 'python_full_version': sys.version.split(' ', 1)[0], + 'os.name': os.name, + 'platform.version': platform.version(), + 'platform.machine': platform.machine(), + 'platform.python_implementation': platform.python_implementation()} + + +class _Operation: + + def __init__(self, execution_context=None): + self.left = None + self.op = None + self.right = None + if execution_context is None: + execution_context = {} + self.execution_context = execution_context + + def _get_var(self, name): + if name in self.execution_context: + return self.execution_context[name] + return _VARS[name] + + def __repr__(self): + return '%s %s %s' % (self.left, self.op, self.right) + + def _is_string(self, value): + if value is None or len(value) < 2: + return False + for delimiter in '"\'': + if value[0] == value[-1] == delimiter: + return True + return False + + def _is_name(self, value): + return value in _VARS + + def _convert(self, value): + if value in _VARS: + return self._get_var(value) + return value.strip('"\'') + + def _check_name(self, value): + if value not in _VARS: + raise NameError(value) + + def _nonsense_op(self): + msg = 'This operation is not supported : "%s"' % self + raise SyntaxError(msg) + + def __call__(self): + # make sure we do something useful + if self._is_string(self.left): + if self._is_string(self.right): + self._nonsense_op() + self._check_name(self.right) + else: + if not self._is_string(self.right): + self._nonsense_op() + self._check_name(self.left) + + if self.op not in _OPERATORS: + raise TypeError('Operator not supported "%s"' % self.op) + + left = self._convert(self.left) + right = self._convert(self.right) + return _operate(self.op, left, right) + + +class _OR: + def __init__(self, left, right=None): + self.left = left + self.right = right + + def filled(self): + return self.right is not None + + def __repr__(self): + return 'OR(%r, %r)' % (self.left, self.right) + + def __call__(self): + return self.left() or self.right() + + +class _AND: + def __init__(self, left, right=None): + self.left = left + self.right = right + + def filled(self): + return self.right is not None + + def __repr__(self): + return 'AND(%r, %r)' % (self.left, self.right) + + def __call__(self): + return self.left() and self.right() + + +def interpret(marker, execution_context=None): + """Interpret a marker and return a result depending on environment.""" + marker = marker.strip().encode() + ops = [] + op_starting = True + for token in tokenize(BytesIO(marker).readline): + # Unpack token + toktype, tokval, rowcol, line, logical_line = token + if toktype not in (NAME, OP, STRING, ENDMARKER, ENCODING): + raise SyntaxError('Type not supported "%s"' % tokval) + + if op_starting: + op = _Operation(execution_context) + if len(ops) > 0: + last = ops[-1] + if isinstance(last, (_OR, _AND)) and not last.filled(): + last.right = op + else: + ops.append(op) + else: + ops.append(op) + op_starting = False + else: + op = ops[-1] + + if (toktype == ENDMARKER or + (toktype == NAME and tokval in ('and', 'or'))): + if toktype == NAME and tokval == 'and': + ops.append(_AND(ops.pop())) + elif toktype == NAME and tokval == 'or': + ops.append(_OR(ops.pop())) + op_starting = True + continue + + if isinstance(op, (_OR, _AND)) and op.right is not None: + op = op.right + + if ((toktype in (NAME, STRING) and tokval not in ('in', 'not')) + or (toktype == OP and tokval == '.')): + if op.op is None: + if op.left is None: + op.left = tokval + else: + op.left += tokval + else: + if op.right is None: + op.right = tokval + else: + op.right += tokval + elif toktype == OP or tokval in ('in', 'not'): + if tokval == 'in' and op.op == 'not': + op.op = 'not in' + else: + op.op = tokval + + for op in ops: + if not op(): + return False + return True diff --git a/Lib/packaging/metadata.py b/Lib/packaging/metadata.py new file mode 100644 index 000000000000..8abbe384a726 --- /dev/null +++ b/Lib/packaging/metadata.py @@ -0,0 +1,552 @@ +"""Implementation of the Metadata for Python packages PEPs. + +Supports all metadata formats (1.0, 1.1, 1.2). +""" + +import re +import logging + +from io import StringIO +from email import message_from_file +from packaging import logger +from packaging.markers import interpret +from packaging.version import (is_valid_predicate, is_valid_version, + is_valid_versions) +from packaging.errors import (MetadataMissingError, + MetadataConflictError, + MetadataUnrecognizedVersionError) + +try: + # docutils is installed + from docutils.utils import Reporter + from docutils.parsers.rst import Parser + from docutils import frontend + from docutils import nodes + + class SilentReporter(Reporter): + + def __init__(self, source, report_level, halt_level, stream=None, + debug=0, encoding='ascii', error_handler='replace'): + self.messages = [] + Reporter.__init__(self, source, report_level, halt_level, stream, + debug, encoding, error_handler) + + def system_message(self, level, message, *children, **kwargs): + self.messages.append((level, message, children, kwargs)) + + _HAS_DOCUTILS = True +except ImportError: + # docutils is not installed + _HAS_DOCUTILS = False + +# public API of this module +__all__ = ['Metadata', 'PKG_INFO_ENCODING', 'PKG_INFO_PREFERRED_VERSION'] + +# Encoding used for the PKG-INFO files +PKG_INFO_ENCODING = 'utf-8' + +# preferred version. Hopefully will be changed +# to 1.2 once PEP 345 is supported everywhere +PKG_INFO_PREFERRED_VERSION = '1.0' + +_LINE_PREFIX = re.compile('\n \|') +_241_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform', + 'Summary', 'Description', + 'Keywords', 'Home-page', 'Author', 'Author-email', + 'License') + +_314_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform', + 'Supported-Platform', 'Summary', 'Description', + 'Keywords', 'Home-page', 'Author', 'Author-email', + 'License', 'Classifier', 'Download-URL', 'Obsoletes', + 'Provides', 'Requires') + +_314_MARKERS = ('Obsoletes', 'Provides', 'Requires') + +_345_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform', + 'Supported-Platform', 'Summary', 'Description', + 'Keywords', 'Home-page', 'Author', 'Author-email', + 'Maintainer', 'Maintainer-email', 'License', + 'Classifier', 'Download-URL', 'Obsoletes-Dist', + 'Project-URL', 'Provides-Dist', 'Requires-Dist', + 'Requires-Python', 'Requires-External') + +_345_MARKERS = ('Provides-Dist', 'Requires-Dist', 'Requires-Python', + 'Obsoletes-Dist', 'Requires-External', 'Maintainer', + 'Maintainer-email', 'Project-URL') + +_ALL_FIELDS = set() +_ALL_FIELDS.update(_241_FIELDS) +_ALL_FIELDS.update(_314_FIELDS) +_ALL_FIELDS.update(_345_FIELDS) + + +def _version2fieldlist(version): + if version == '1.0': + return _241_FIELDS + elif version == '1.1': + return _314_FIELDS + elif version == '1.2': + return _345_FIELDS + raise MetadataUnrecognizedVersionError(version) + + +def _best_version(fields): + """Detect the best version depending on the fields used.""" + def _has_marker(keys, markers): + for marker in markers: + if marker in keys: + return True + return False + + keys = list(fields) + possible_versions = ['1.0', '1.1', '1.2'] + + # first let's try to see if a field is not part of one of the version + for key in keys: + if key not in _241_FIELDS and '1.0' in possible_versions: + possible_versions.remove('1.0') + if key not in _314_FIELDS and '1.1' in possible_versions: + possible_versions.remove('1.1') + if key not in _345_FIELDS and '1.2' in possible_versions: + possible_versions.remove('1.2') + + # possible_version contains qualified versions + if len(possible_versions) == 1: + return possible_versions[0] # found ! + elif len(possible_versions) == 0: + raise MetadataConflictError('Unknown metadata set') + + # let's see if one unique marker is found + is_1_1 = '1.1' in possible_versions and _has_marker(keys, _314_MARKERS) + is_1_2 = '1.2' in possible_versions and _has_marker(keys, _345_MARKERS) + if is_1_1 and is_1_2: + raise MetadataConflictError('You used incompatible 1.1 and 1.2 fields') + + # we have the choice, either 1.0, or 1.2 + # - 1.0 has a broken Summary field but works with all tools + # - 1.1 is to avoid + # - 1.2 fixes Summary but is not widespread yet + if not is_1_1 and not is_1_2: + # we couldn't find any specific marker + if PKG_INFO_PREFERRED_VERSION in possible_versions: + return PKG_INFO_PREFERRED_VERSION + if is_1_1: + return '1.1' + + # default marker when 1.0 is disqualified + return '1.2' + + +_ATTR2FIELD = { + 'metadata_version': 'Metadata-Version', + 'name': 'Name', + 'version': 'Version', + 'platform': 'Platform', + 'supported_platform': 'Supported-Platform', + 'summary': 'Summary', + 'description': 'Description', + 'keywords': 'Keywords', + 'home_page': 'Home-page', + 'author': 'Author', + 'author_email': 'Author-email', + 'maintainer': 'Maintainer', + 'maintainer_email': 'Maintainer-email', + 'license': 'License', + 'classifier': 'Classifier', + 'download_url': 'Download-URL', + 'obsoletes_dist': 'Obsoletes-Dist', + 'provides_dist': 'Provides-Dist', + 'requires_dist': 'Requires-Dist', + 'requires_python': 'Requires-Python', + 'requires_external': 'Requires-External', + 'requires': 'Requires', + 'provides': 'Provides', + 'obsoletes': 'Obsoletes', + 'project_url': 'Project-URL', +} + +_PREDICATE_FIELDS = ('Requires-Dist', 'Obsoletes-Dist', 'Provides-Dist') +_VERSIONS_FIELDS = ('Requires-Python',) +_VERSION_FIELDS = ('Version',) +_LISTFIELDS = ('Platform', 'Classifier', 'Obsoletes', + 'Requires', 'Provides', 'Obsoletes-Dist', + 'Provides-Dist', 'Requires-Dist', 'Requires-External', + 'Project-URL', 'Supported-Platform') +_LISTTUPLEFIELDS = ('Project-URL',) + +_ELEMENTSFIELD = ('Keywords',) + +_UNICODEFIELDS = ('Author', 'Maintainer', 'Summary', 'Description') + +_MISSING = object() + + +class NoDefault: + """Marker object used for clean representation""" + def __repr__(self): + return '' + +_MISSING = NoDefault() + + +class Metadata: + """The metadata of a release. + + Supports versions 1.0, 1.1 and 1.2 (auto-detected). You can + instantiate the class with one of these arguments (or none): + - *path*, the path to a METADATA file + - *fileobj* give a file-like object with METADATA as content + - *mapping* is a dict-like object + """ + # TODO document that execution_context and platform_dependent are used + # to filter on query, not when setting a key + # also document the mapping API and UNKNOWN default key + + def __init__(self, path=None, platform_dependent=False, + execution_context=None, fileobj=None, mapping=None): + self._fields = {} + self.requires_files = [] + self.docutils_support = _HAS_DOCUTILS + self.platform_dependent = platform_dependent + self.execution_context = execution_context + if [path, fileobj, mapping].count(None) < 2: + raise TypeError('path, fileobj and mapping are exclusive') + if path is not None: + self.read(path) + elif fileobj is not None: + self.read_file(fileobj) + elif mapping is not None: + self.update(mapping) + + def _set_best_version(self): + self._fields['Metadata-Version'] = _best_version(self._fields) + + def _write_field(self, file, name, value): + file.write('%s: %s\n' % (name, value)) + + def __getitem__(self, name): + return self.get(name) + + def __setitem__(self, name, value): + return self.set(name, value) + + def __delitem__(self, name): + field_name = self._convert_name(name) + try: + del self._fields[field_name] + except KeyError: + raise KeyError(name) + self._set_best_version() + + def __contains__(self, name): + return (name in self._fields or + self._convert_name(name) in self._fields) + + def _convert_name(self, name): + if name in _ALL_FIELDS: + return name + name = name.replace('-', '_').lower() + return _ATTR2FIELD.get(name, name) + + def _default_value(self, name): + if name in _LISTFIELDS or name in _ELEMENTSFIELD: + return [] + return 'UNKNOWN' + + def _check_rst_data(self, data): + """Return warnings when the provided data has syntax errors.""" + source_path = StringIO() + parser = Parser() + settings = frontend.OptionParser().get_default_values() + settings.tab_width = 4 + settings.pep_references = None + settings.rfc_references = None + reporter = SilentReporter(source_path, + settings.report_level, + settings.halt_level, + stream=settings.warning_stream, + debug=settings.debug, + encoding=settings.error_encoding, + error_handler=settings.error_encoding_error_handler) + + document = nodes.document(settings, reporter, source=source_path) + document.note_source(source_path, -1) + try: + parser.parse(data, document) + except AttributeError: + reporter.messages.append((-1, 'Could not finish the parsing.', + '', {})) + + return reporter.messages + + def _platform(self, value): + if not self.platform_dependent or ';' not in value: + return True, value + value, marker = value.split(';') + return interpret(marker, self.execution_context), value + + def _remove_line_prefix(self, value): + return _LINE_PREFIX.sub('\n', value) + + # + # Public API + # + def get_fullname(self): + """Return the distribution name with version""" + return '%s-%s' % (self['Name'], self['Version']) + + def is_metadata_field(self, name): + """return True if name is a valid metadata key""" + name = self._convert_name(name) + return name in _ALL_FIELDS + + def is_multi_field(self, name): + name = self._convert_name(name) + return name in _LISTFIELDS + + def read(self, filepath): + """Read the metadata values from a file path.""" + with open(filepath, 'r', encoding='ascii') as fp: + self.read_file(fp) + + def read_file(self, fileob): + """Read the metadata values from a file object.""" + msg = message_from_file(fileob) + self._fields['Metadata-Version'] = msg['metadata-version'] + + for field in _version2fieldlist(self['Metadata-Version']): + if field in _LISTFIELDS: + # we can have multiple lines + values = msg.get_all(field) + if field in _LISTTUPLEFIELDS and values is not None: + values = [tuple(value.split(',')) for value in values] + self.set(field, values) + else: + # single line + value = msg[field] + if value is not None and value != 'UNKNOWN': + self.set(field, value) + + def write(self, filepath): + """Write the metadata fields to filepath.""" + with open(filepath, 'w') as fp: + self.write_file(fp) + + def write_file(self, fileobject): + """Write the PKG-INFO format data to a file object.""" + self._set_best_version() + for field in _version2fieldlist(self['Metadata-Version']): + values = self.get(field) + if field in _ELEMENTSFIELD: + self._write_field(fileobject, field, ','.join(values)) + continue + if field not in _LISTFIELDS: + if field == 'Description': + values = values.replace('\n', '\n |') + values = [values] + + if field in _LISTTUPLEFIELDS: + values = [','.join(value) for value in values] + + for value in values: + self._write_field(fileobject, field, value) + + def update(self, other=None, **kwargs): + """Set metadata values from the given iterable `other` and kwargs. + + Behavior is like `dict.update`: If `other` has a ``keys`` method, + they are looped over and ``self[key]`` is assigned ``other[key]``. + Else, ``other`` is an iterable of ``(key, value)`` iterables. + + Keys that don't match a metadata field or that have an empty value are + dropped. + """ + def _set(key, value): + if key in _ATTR2FIELD and value: + self.set(self._convert_name(key), value) + + if other is None: + pass + elif hasattr(other, 'keys'): + for k in other.keys(): + _set(k, other[k]) + else: + for k, v in other: + _set(k, v) + + if kwargs: + self.update(kwargs) + + def set(self, name, value): + """Control then set a metadata field.""" + name = self._convert_name(name) + + if ((name in _ELEMENTSFIELD or name == 'Platform') and + not isinstance(value, (list, tuple))): + if isinstance(value, str): + value = [v.strip() for v in value.split(',')] + else: + value = [] + elif (name in _LISTFIELDS and + not isinstance(value, (list, tuple))): + if isinstance(value, str): + value = [value] + else: + value = [] + + if logger.isEnabledFor(logging.WARNING): + if name in _PREDICATE_FIELDS and value is not None: + for v in value: + # check that the values are valid predicates + if not is_valid_predicate(v.split(';')[0]): + logger.warning( + '%r is not a valid predicate (field %r)', + v, name) + # FIXME this rejects UNKNOWN, is that right? + elif name in _VERSIONS_FIELDS and value is not None: + if not is_valid_versions(value): + logger.warning('%r is not a valid version (field %r)', + value, name) + elif name in _VERSION_FIELDS and value is not None: + if not is_valid_version(value): + logger.warning('%r is not a valid version (field %r)', + value, name) + + if name in _UNICODEFIELDS: + if name == 'Description': + value = self._remove_line_prefix(value) + + self._fields[name] = value + self._set_best_version() + + def get(self, name, default=_MISSING): + """Get a metadata field.""" + name = self._convert_name(name) + if name not in self._fields: + if default is _MISSING: + default = self._default_value(name) + return default + if name in _UNICODEFIELDS: + value = self._fields[name] + return value + elif name in _LISTFIELDS: + value = self._fields[name] + if value is None: + return [] + res = [] + for val in value: + valid, val = self._platform(val) + if not valid: + continue + if name not in _LISTTUPLEFIELDS: + res.append(val) + else: + # That's for Project-URL + res.append((val[0], val[1])) + return res + + elif name in _ELEMENTSFIELD: + valid, value = self._platform(self._fields[name]) + if not valid: + return [] + if isinstance(value, str): + return value.split(',') + valid, value = self._platform(self._fields[name]) + if not valid: + return None + return value + + def check(self, strict=False, restructuredtext=False): + """Check if the metadata is compliant. If strict is False then raise if + no Name or Version are provided""" + # XXX should check the versions (if the file was loaded) + missing, warnings = [], [] + + for attr in ('Name', 'Version'): # required by PEP 345 + if attr not in self: + missing.append(attr) + + if strict and missing != []: + msg = 'missing required metadata: %s' % ', '.join(missing) + raise MetadataMissingError(msg) + + for attr in ('Home-page', 'Author'): + if attr not in self: + missing.append(attr) + + if _HAS_DOCUTILS and restructuredtext: + warnings.extend(self._check_rst_data(self['Description'])) + + # checking metadata 1.2 (XXX needs to check 1.1, 1.0) + if self['Metadata-Version'] != '1.2': + return missing, warnings + + def is_valid_predicates(value): + for v in value: + if not is_valid_predicate(v.split(';')[0]): + return False + return True + + for fields, controller in ((_PREDICATE_FIELDS, is_valid_predicates), + (_VERSIONS_FIELDS, is_valid_versions), + (_VERSION_FIELDS, is_valid_version)): + for field in fields: + value = self.get(field, None) + if value is not None and not controller(value): + warnings.append('Wrong value for %r: %s' % (field, value)) + + return missing, warnings + + def todict(self): + """Return fields as a dict. + + Field names will be converted to use the underscore-lowercase style + instead of hyphen-mixed case (i.e. home_page instead of Home-page). + """ + data = { + 'metadata_version': self['Metadata-Version'], + 'name': self['Name'], + 'version': self['Version'], + 'summary': self['Summary'], + 'home_page': self['Home-page'], + 'author': self['Author'], + 'author_email': self['Author-email'], + 'license': self['License'], + 'description': self['Description'], + 'keywords': self['Keywords'], + 'platform': self['Platform'], + 'classifier': self['Classifier'], + 'download_url': self['Download-URL'], + } + + if self['Metadata-Version'] == '1.2': + data['requires_dist'] = self['Requires-Dist'] + data['requires_python'] = self['Requires-Python'] + data['requires_external'] = self['Requires-External'] + data['provides_dist'] = self['Provides-Dist'] + data['obsoletes_dist'] = self['Obsoletes-Dist'] + data['project_url'] = [','.join(url) for url in + self['Project-URL']] + + elif self['Metadata-Version'] == '1.1': + data['provides'] = self['Provides'] + data['requires'] = self['Requires'] + data['obsoletes'] = self['Obsoletes'] + + return data + + # Mapping API + + def keys(self): + return _version2fieldlist(self['Metadata-Version']) + + def __iter__(self): + for key in self.keys(): + yield key + + def values(self): + return [self[key] for key in list(self.keys())] + + def items(self): + return [(key, self[key]) for key in list(self.keys())] diff --git a/Lib/packaging/pypi/__init__.py b/Lib/packaging/pypi/__init__.py new file mode 100644 index 000000000000..5660c50ade58 --- /dev/null +++ b/Lib/packaging/pypi/__init__.py @@ -0,0 +1,9 @@ +"""Low-level and high-level APIs to interact with project indexes.""" + +__all__ = ['simple', + 'xmlrpc', + 'dist', + 'errors', + 'mirrors'] + +from packaging.pypi.dist import ReleaseInfo, ReleasesList, DistInfo diff --git a/Lib/packaging/pypi/base.py b/Lib/packaging/pypi/base.py new file mode 100644 index 000000000000..305fca9cc8f1 --- /dev/null +++ b/Lib/packaging/pypi/base.py @@ -0,0 +1,48 @@ +"""Base class for index crawlers.""" + +from packaging.pypi.dist import ReleasesList + + +class BaseClient: + """Base class containing common methods for the index crawlers/clients""" + + def __init__(self, prefer_final, prefer_source): + self._prefer_final = prefer_final + self._prefer_source = prefer_source + self._index = self + + def _get_prefer_final(self, prefer_final=None): + """Return the prefer_final internal parameter or the specified one if + provided""" + if prefer_final: + return prefer_final + else: + return self._prefer_final + + def _get_prefer_source(self, prefer_source=None): + """Return the prefer_source internal parameter or the specified one if + provided""" + if prefer_source: + return prefer_source + else: + return self._prefer_source + + def _get_project(self, project_name): + """Return an project instance, create it if necessary""" + return self._projects.setdefault(project_name.lower(), + ReleasesList(project_name, index=self._index)) + + def download_distribution(self, requirements, temp_path=None, + prefer_source=None, prefer_final=None): + """Download a distribution from the last release according to the + requirements. + + If temp_path is provided, download to this path, otherwise, create a + temporary location for the download and return it. + """ + prefer_final = self._get_prefer_final(prefer_final) + prefer_source = self._get_prefer_source(prefer_source) + release = self.get_release(requirements, prefer_final) + if release: + dist = release.get_distribution(prefer_source=prefer_source) + return dist.download(temp_path) diff --git a/Lib/packaging/pypi/dist.py b/Lib/packaging/pypi/dist.py new file mode 100644 index 000000000000..16510dffd843 --- /dev/null +++ b/Lib/packaging/pypi/dist.py @@ -0,0 +1,547 @@ +"""Classes representing releases and distributions retrieved from indexes. + +A project (= unique name) can have several releases (= versions) and +each release can have several distributions (= sdist and bdists). + +Release objects contain metadata-related information (see PEP 376); +distribution objects contain download-related information. +""" + +import sys +import mimetypes +import re +import tempfile +import urllib.request +import urllib.parse +import urllib.error +import urllib.parse +import hashlib +from shutil import unpack_archive + +from packaging.errors import IrrationalVersionError +from packaging.version import (suggest_normalized_version, NormalizedVersion, + get_version_predicate) +from packaging.metadata import Metadata +from packaging.pypi.errors import (HashDoesNotMatch, UnsupportedHashName, + CantParseArchiveName) + + +__all__ = ['ReleaseInfo', 'DistInfo', 'ReleasesList', 'get_infos_from_url'] + +EXTENSIONS = ".tar.gz .tar.bz2 .tar .zip .tgz .egg".split() +MD5_HASH = re.compile(r'^.*#md5=([a-f0-9]+)$') +DIST_TYPES = ['bdist', 'sdist'] + + +class IndexReference: + """Mixin used to store the index reference""" + def set_index(self, index=None): + self._index = index + + +class ReleaseInfo(IndexReference): + """Represent a release of a project (a project with a specific version). + The release contain the _metadata informations related to this specific + version, and is also a container for distribution related informations. + + See the DistInfo class for more information about distributions. + """ + + def __init__(self, name, version, metadata=None, hidden=False, + index=None, **kwargs): + """ + :param name: the name of the distribution + :param version: the version of the distribution + :param metadata: the metadata fields of the release. + :type metadata: dict + :param kwargs: optional arguments for a new distribution. + """ + self.set_index(index) + self.name = name + self._version = None + self.version = version + if metadata: + self.metadata = Metadata(mapping=metadata) + else: + self.metadata = None + self.dists = {} + self.hidden = hidden + + if 'dist_type' in kwargs: + dist_type = kwargs.pop('dist_type') + self.add_distribution(dist_type, **kwargs) + + def set_version(self, version): + try: + self._version = NormalizedVersion(version) + except IrrationalVersionError: + suggestion = suggest_normalized_version(version) + if suggestion: + self.version = suggestion + else: + raise IrrationalVersionError(version) + + def get_version(self): + return self._version + + version = property(get_version, set_version) + + def fetch_metadata(self): + """If the metadata is not set, use the indexes to get it""" + if not self.metadata: + self._index.get_metadata(self.name, str(self.version)) + return self.metadata + + @property + def is_final(self): + """proxy to version.is_final""" + return self.version.is_final + + def fetch_distributions(self): + if self.dists is None: + self._index.get_distributions(self.name, str(self.version)) + if self.dists is None: + self.dists = {} + return self.dists + + def add_distribution(self, dist_type='sdist', python_version=None, + **params): + """Add distribution informations to this release. + If distribution information is already set for this distribution type, + add the given url paths to the distribution. This can be useful while + some of them fails to download. + + :param dist_type: the distribution type (eg. "sdist", "bdist", etc.) + :param params: the fields to be passed to the distribution object + (see the :class:DistInfo constructor). + """ + if dist_type not in DIST_TYPES: + raise ValueError(dist_type) + if dist_type in self.dists: + self.dists[dist_type].add_url(**params) + else: + self.dists[dist_type] = DistInfo(self, dist_type, + index=self._index, **params) + if python_version: + self.dists[dist_type].python_version = python_version + + def get_distribution(self, dist_type=None, prefer_source=True): + """Return a distribution. + + If dist_type is set, find first for this distribution type, and just + act as an alias of __get_item__. + + If prefer_source is True, search first for source distribution, and if + not return one existing distribution. + """ + if len(self.dists) == 0: + raise LookupError() + if dist_type: + return self[dist_type] + if prefer_source: + if "sdist" in self.dists: + dist = self["sdist"] + else: + dist = next(self.dists.values()) + return dist + + def unpack(self, path=None, prefer_source=True): + """Unpack the distribution to the given path. + + If not destination is given, creates a temporary location. + + Returns the location of the extracted files (root). + """ + return self.get_distribution(prefer_source=prefer_source)\ + .unpack(path=path) + + def download(self, temp_path=None, prefer_source=True): + """Download the distribution, using the requirements. + + If more than one distribution match the requirements, use the last + version. + Download the distribution, and put it in the temp_path. If no temp_path + is given, creates and return one. + + Returns the complete absolute path to the downloaded archive. + """ + return self.get_distribution(prefer_source=prefer_source)\ + .download(path=temp_path) + + def set_metadata(self, metadata): + if not self.metadata: + self.metadata = Metadata() + self.metadata.update(metadata) + + def __getitem__(self, item): + """distributions are available using release["sdist"]""" + return self.dists[item] + + def _check_is_comparable(self, other): + if not isinstance(other, ReleaseInfo): + raise TypeError("cannot compare %s and %s" + % (type(self).__name__, type(other).__name__)) + elif self.name != other.name: + raise TypeError("cannot compare %s and %s" + % (self.name, other.name)) + + def __repr__(self): + return "<%s %s>" % (self.name, self.version) + + def __eq__(self, other): + self._check_is_comparable(other) + return self.version == other.version + + def __lt__(self, other): + self._check_is_comparable(other) + return self.version < other.version + + def __ne__(self, other): + return not self.__eq__(other) + + def __gt__(self, other): + return not (self.__lt__(other) or self.__eq__(other)) + + def __le__(self, other): + return self.__eq__(other) or self.__lt__(other) + + def __ge__(self, other): + return self.__eq__(other) or self.__gt__(other) + + # See http://docs.python.org/reference/datamodel#object.__hash__ + __hash__ = object.__hash__ + + +class DistInfo(IndexReference): + """Represents a distribution retrieved from an index (sdist, bdist, ...) + """ + + def __init__(self, release, dist_type=None, url=None, hashname=None, + hashval=None, is_external=True, python_version=None, + index=None): + """Create a new instance of DistInfo. + + :param release: a DistInfo class is relative to a release. + :param dist_type: the type of the dist (eg. source, bin-*, etc.) + :param url: URL where we found this distribution + :param hashname: the name of the hash we want to use. Refer to the + hashlib.new documentation for more information. + :param hashval: the hash value. + :param is_external: we need to know if the provided url comes from + an index browsing, or from an external resource. + + """ + self.set_index(index) + self.release = release + self.dist_type = dist_type + self.python_version = python_version + self._unpacked_dir = None + # set the downloaded path to None by default. The goal here + # is to not download distributions multiple times + self.downloaded_location = None + # We store urls in dict, because we need to have a bit more infos + # than the simple URL. It will be used later to find the good url to + # use. + # We have two _url* attributes: _url and urls. urls contains a list + # of dict for the different urls, and _url contains the choosen url, in + # order to dont make the selection process multiple times. + self.urls = [] + self._url = None + self.add_url(url, hashname, hashval, is_external) + + def add_url(self, url=None, hashname=None, hashval=None, is_external=True): + """Add a new url to the list of urls""" + if hashname is not None: + try: + hashlib.new(hashname) + except ValueError: + raise UnsupportedHashName(hashname) + if not url in [u['url'] for u in self.urls]: + self.urls.append({ + 'url': url, + 'hashname': hashname, + 'hashval': hashval, + 'is_external': is_external, + }) + # reset the url selection process + self._url = None + + @property + def url(self): + """Pick up the right url for the list of urls in self.urls""" + # We return internal urls over externals. + # If there is more than one internal or external, return the first + # one. + if self._url is None: + if len(self.urls) > 1: + internals_urls = [u for u in self.urls \ + if u['is_external'] == False] + if len(internals_urls) >= 1: + self._url = internals_urls[0] + if self._url is None: + self._url = self.urls[0] + return self._url + + @property + def is_source(self): + """return if the distribution is a source one or not""" + return self.dist_type == 'sdist' + + def download(self, path=None): + """Download the distribution to a path, and return it. + + If the path is given in path, use this, otherwise, generates a new one + Return the download location. + """ + if path is None: + path = tempfile.mkdtemp() + + # if we do not have downloaded it yet, do it. + if self.downloaded_location is None: + url = self.url['url'] + archive_name = urllib.parse.urlparse(url)[2].split('/')[-1] + filename, headers = urllib.request.urlretrieve(url, + path + "/" + archive_name) + self.downloaded_location = filename + self._check_md5(filename) + return self.downloaded_location + + def unpack(self, path=None): + """Unpack the distribution to the given path. + + If not destination is given, creates a temporary location. + + Returns the location of the extracted files (root). + """ + if not self._unpacked_dir: + if path is None: + path = tempfile.mkdtemp() + + filename = self.download(path) + content_type = mimetypes.guess_type(filename)[0] + unpack_archive(filename, path) + self._unpacked_dir = path + + return path + + def _check_md5(self, filename): + """Check that the md5 checksum of the given file matches the one in + url param""" + hashname = self.url['hashname'] + expected_hashval = self.url['hashval'] + if not None in (expected_hashval, hashname): + with open(filename, 'rb') as f: + hashval = hashlib.new(hashname) + hashval.update(f.read()) + + if hashval.hexdigest() != expected_hashval: + raise HashDoesNotMatch("got %s instead of %s" + % (hashval.hexdigest(), expected_hashval)) + + def __repr__(self): + if self.release is None: + return "" % self.dist_type + + return "<%s %s %s>" % ( + self.release.name, self.release.version, self.dist_type or "") + + +class ReleasesList(IndexReference): + """A container of Release. + + Provides useful methods and facilities to sort and filter releases. + """ + def __init__(self, name, releases=None, contains_hidden=False, index=None): + self.set_index(index) + self.releases = [] + self.name = name + self.contains_hidden = contains_hidden + if releases: + self.add_releases(releases) + + def fetch_releases(self): + self._index.get_releases(self.name) + return self.releases + + def filter(self, predicate): + """Filter and return a subset of releases matching the given predicate. + """ + return ReleasesList(self.name, [release for release in self.releases + if predicate.match(release.version)], + index=self._index) + + def get_last(self, requirements, prefer_final=None): + """Return the "last" release, that satisfy the given predicates. + + "last" is defined by the version number of the releases, you also could + set prefer_final parameter to True or False to change the order results + """ + predicate = get_version_predicate(requirements) + releases = self.filter(predicate) + if len(releases) == 0: + return None + releases.sort_releases(prefer_final, reverse=True) + return releases[0] + + def add_releases(self, releases): + """Add releases in the release list. + + :param: releases is a list of ReleaseInfo objects. + """ + for r in releases: + self.add_release(release=r) + + def add_release(self, version=None, dist_type='sdist', release=None, + **dist_args): + """Add a release to the list. + + The release can be passed in the `release` parameter, and in this case, + it will be crawled to extract the useful informations if necessary, or + the release informations can be directly passed in the `version` and + `dist_type` arguments. + + Other keywords arguments can be provided, and will be forwarded to the + distribution creation (eg. the arguments of the DistInfo constructor). + """ + if release: + if release.name.lower() != self.name.lower(): + raise ValueError("%s is not the same project as %s" % + (release.name, self.name)) + version = str(release.version) + + if not version in self.get_versions(): + # append only if not already exists + self.releases.append(release) + for dist in release.dists.values(): + for url in dist.urls: + self.add_release(version, dist.dist_type, **url) + else: + matches = [r for r in self.releases + if str(r.version) == version and r.name == self.name] + if not matches: + release = ReleaseInfo(self.name, version, index=self._index) + self.releases.append(release) + else: + release = matches[0] + + release.add_distribution(dist_type=dist_type, **dist_args) + + def sort_releases(self, prefer_final=False, reverse=True, *args, **kwargs): + """Sort the results with the given properties. + + The `prefer_final` argument can be used to specify if final + distributions (eg. not dev, bet or alpha) would be prefered or not. + + Results can be inverted by using `reverse`. + + Any other parameter provided will be forwarded to the sorted call. You + cannot redefine the key argument of "sorted" here, as it is used + internally to sort the releases. + """ + + sort_by = [] + if prefer_final: + sort_by.append("is_final") + sort_by.append("version") + + self.releases.sort( + key=lambda i: tuple(getattr(i, arg) for arg in sort_by), + reverse=reverse, *args, **kwargs) + + def get_release(self, version): + """Return a release from its version.""" + matches = [r for r in self.releases if str(r.version) == version] + if len(matches) != 1: + raise KeyError(version) + return matches[0] + + def get_versions(self): + """Return a list of releases versions contained""" + return [str(r.version) for r in self.releases] + + def __getitem__(self, key): + return self.releases[key] + + def __len__(self): + return len(self.releases) + + def __repr__(self): + string = 'Project "%s"' % self.name + if self.get_versions(): + string += ' versions: %s' % ', '.join(self.get_versions()) + return '<%s>' % string + + +def get_infos_from_url(url, probable_dist_name=None, is_external=True): + """Get useful informations from an URL. + + Return a dict of (name, version, url, hashtype, hash, is_external) + + :param url: complete url of the distribution + :param probable_dist_name: A probable name of the project. + :param is_external: Tell if the url commes from an index or from + an external URL. + """ + # if the url contains a md5 hash, get it. + md5_hash = None + match = MD5_HASH.match(url) + if match is not None: + md5_hash = match.group(1) + # remove the hash + url = url.replace("#md5=%s" % md5_hash, "") + + # parse the archive name to find dist name and version + archive_name = urllib.parse.urlparse(url)[2].split('/')[-1] + extension_matched = False + # remove the extension from the name + for ext in EXTENSIONS: + if archive_name.endswith(ext): + archive_name = archive_name[:-len(ext)] + extension_matched = True + + name, version = split_archive_name(archive_name) + if extension_matched is True: + return {'name': name, + 'version': version, + 'url': url, + 'hashname': "md5", + 'hashval': md5_hash, + 'is_external': is_external, + 'dist_type': 'sdist'} + + +def split_archive_name(archive_name, probable_name=None): + """Split an archive name into two parts: name and version. + + Return the tuple (name, version) + """ + # Try to determine wich part is the name and wich is the version using the + # "-" separator. Take the larger part to be the version number then reduce + # if this not works. + def eager_split(str, maxsplit=2): + # split using the "-" separator + splits = str.rsplit("-", maxsplit) + name = splits[0] + version = "-".join(splits[1:]) + if version.startswith("-"): + version = version[1:] + if suggest_normalized_version(version) is None and maxsplit >= 0: + # we dont get a good version number: recurse ! + return eager_split(str, maxsplit - 1) + else: + return name, version + if probable_name is not None: + probable_name = probable_name.lower() + name = None + if probable_name is not None and probable_name in archive_name: + # we get the name from probable_name, if given. + name = probable_name + version = archive_name.lstrip(name) + else: + name, version = eager_split(archive_name) + + version = suggest_normalized_version(version) + if version is not None and name != "": + return name.lower(), version + else: + raise CantParseArchiveName(archive_name) diff --git a/Lib/packaging/pypi/errors.py b/Lib/packaging/pypi/errors.py new file mode 100644 index 000000000000..2191ac100c7d --- /dev/null +++ b/Lib/packaging/pypi/errors.py @@ -0,0 +1,39 @@ +"""Exceptions raised by packaging.pypi code.""" + +from packaging.errors import PackagingPyPIError + + +class ProjectNotFound(PackagingPyPIError): + """Project has not been found""" + + +class DistributionNotFound(PackagingPyPIError): + """The release has not been found""" + + +class ReleaseNotFound(PackagingPyPIError): + """The release has not been found""" + + +class CantParseArchiveName(PackagingPyPIError): + """An archive name can't be parsed to find distribution name and version""" + + +class DownloadError(PackagingPyPIError): + """An error has occurs while downloading""" + + +class HashDoesNotMatch(DownloadError): + """Compared hashes does not match""" + + +class UnsupportedHashName(PackagingPyPIError): + """A unsupported hashname has been used""" + + +class UnableToDownload(PackagingPyPIError): + """All mirrors have been tried, without success""" + + +class InvalidSearchField(PackagingPyPIError): + """An invalid search field has been used""" diff --git a/Lib/packaging/pypi/mirrors.py b/Lib/packaging/pypi/mirrors.py new file mode 100644 index 000000000000..a646acff3cdd --- /dev/null +++ b/Lib/packaging/pypi/mirrors.py @@ -0,0 +1,52 @@ +"""Utilities related to the mirror infrastructure defined in PEP 381.""" + +from string import ascii_lowercase +import socket + +DEFAULT_MIRROR_URL = "last.pypi.python.org" + + +def get_mirrors(hostname=None): + """Return the list of mirrors from the last record found on the DNS + entry:: + + >>> from packaging.pypi.mirrors import get_mirrors + >>> get_mirrors() + ['a.pypi.python.org', 'b.pypi.python.org', 'c.pypi.python.org', + 'd.pypi.python.org'] + + """ + if hostname is None: + hostname = DEFAULT_MIRROR_URL + + # return the last mirror registered on PyPI. + try: + hostname = socket.gethostbyname_ex(hostname)[0] + except socket.gaierror: + return [] + end_letter = hostname.split(".", 1) + + # determine the list from the last one. + return ["%s.%s" % (s, end_letter[1]) for s in string_range(end_letter[0])] + + +def string_range(last): + """Compute the range of string between "a" and last. + + This works for simple "a to z" lists, but also for "a to zz" lists. + """ + for k in range(len(last)): + for x in product(ascii_lowercase, repeat=(k + 1)): + result = ''.join(x) + yield result + if result == last: + return + + +def product(*args, **kwds): + pools = [tuple(arg) for arg in args] * kwds.get('repeat', 1) + result = [[]] + for pool in pools: + result = [x + [y] for x in result for y in pool] + for prod in result: + yield tuple(prod) diff --git a/Lib/packaging/pypi/simple.py b/Lib/packaging/pypi/simple.py new file mode 100644 index 000000000000..85851938833a --- /dev/null +++ b/Lib/packaging/pypi/simple.py @@ -0,0 +1,452 @@ +"""Spider using the screen-scraping "simple" PyPI API. + +This module contains the class SimpleIndexCrawler, a simple spider that +can be used to find and retrieve distributions from a project index +(like the Python Package Index), using its so-called simple API (see +reference implementation available at http://pypi.python.org/simple/). +""" + +import http.client +import re +import socket +import sys +import urllib.request +import urllib.parse +import urllib.error +import os + + +from fnmatch import translate +from packaging import logger +from packaging.metadata import Metadata +from packaging.version import get_version_predicate +from packaging import __version__ as packaging_version +from packaging.pypi.base import BaseClient +from packaging.pypi.dist import (ReleasesList, EXTENSIONS, + get_infos_from_url, MD5_HASH) +from packaging.pypi.errors import (PackagingPyPIError, DownloadError, + UnableToDownload, CantParseArchiveName, + ReleaseNotFound, ProjectNotFound) +from packaging.pypi.mirrors import get_mirrors +from packaging.metadata import Metadata + +__all__ = ['Crawler', 'DEFAULT_SIMPLE_INDEX_URL'] + +# -- Constants ----------------------------------------------- +DEFAULT_SIMPLE_INDEX_URL = "http://a.pypi.python.org/simple/" +DEFAULT_HOSTS = ("*",) +SOCKET_TIMEOUT = 15 +USER_AGENT = "Python-urllib/%s packaging/%s" % ( + sys.version[:3], packaging_version) + +# -- Regexps ------------------------------------------------- +EGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.]+)$') +HREF = re.compile("""href\\s*=\\s*['"]?([^'"> ]+)""", re.I) +URL_SCHEME = re.compile('([-+.a-z0-9]{2,}):', re.I).match + +# This pattern matches a character entity reference (a decimal numeric +# references, a hexadecimal numeric reference, or a named reference). +ENTITY_SUB = re.compile(r'&(#(\d+|x[\da-fA-F]+)|[\w.:-]+);?').sub +REL = re.compile("""<([^>]*\srel\s*=\s*['"]?([^'">]+)[^>]*)>""", re.I) + + +def socket_timeout(timeout=SOCKET_TIMEOUT): + """Decorator to add a socket timeout when requesting pages on PyPI. + """ + def _socket_timeout(func): + def _socket_timeout(self, *args, **kwargs): + old_timeout = socket.getdefaulttimeout() + if hasattr(self, "_timeout"): + timeout = self._timeout + socket.setdefaulttimeout(timeout) + try: + return func(self, *args, **kwargs) + finally: + socket.setdefaulttimeout(old_timeout) + return _socket_timeout + return _socket_timeout + + +def with_mirror_support(): + """Decorator that makes the mirroring support easier""" + def wrapper(func): + def wrapped(self, *args, **kwargs): + try: + return func(self, *args, **kwargs) + except DownloadError: + # if an error occurs, try with the next index_url + if self._mirrors_tries >= self._mirrors_max_tries: + try: + self._switch_to_next_mirror() + except KeyError: + raise UnableToDownload("Tried all mirrors") + else: + self._mirrors_tries += 1 + self._projects.clear() + return wrapped(self, *args, **kwargs) + return wrapped + return wrapper + + +class Crawler(BaseClient): + """Provides useful tools to request the Python Package Index simple API. + + You can specify both mirrors and mirrors_url, but mirrors_url will only be + used if mirrors is set to None. + + :param index_url: the url of the simple index to search on. + :param prefer_final: if the version is not mentioned, and the last + version is not a "final" one (alpha, beta, etc.), + pick up the last final version. + :param prefer_source: if the distribution type is not mentioned, pick up + the source one if available. + :param follow_externals: tell if following external links is needed or + not. Default is False. + :param hosts: a list of hosts allowed to be processed while using + follow_externals=True. Default behavior is to follow all + hosts. + :param follow_externals: tell if following external links is needed or + not. Default is False. + :param mirrors_url: the url to look on for DNS records giving mirror + adresses. + :param mirrors: a list of mirrors (see PEP 381). + :param timeout: time in seconds to consider a url has timeouted. + :param mirrors_max_tries": number of times to try requesting informations + on mirrors before switching. + """ + + def __init__(self, index_url=DEFAULT_SIMPLE_INDEX_URL, prefer_final=False, + prefer_source=True, hosts=DEFAULT_HOSTS, + follow_externals=False, mirrors_url=None, mirrors=None, + timeout=SOCKET_TIMEOUT, mirrors_max_tries=0): + super(Crawler, self).__init__(prefer_final, prefer_source) + self.follow_externals = follow_externals + + # mirroring attributes. + if not index_url.endswith("/"): + index_url += "/" + # if no mirrors are defined, use the method described in PEP 381. + if mirrors is None: + mirrors = get_mirrors(mirrors_url) + self._mirrors = set(mirrors) + self._mirrors_used = set() + self.index_url = index_url + self._mirrors_max_tries = mirrors_max_tries + self._mirrors_tries = 0 + self._timeout = timeout + + # create a regexp to match all given hosts + self._allowed_hosts = re.compile('|'.join(map(translate, hosts))).match + + # we keep an index of pages we have processed, in order to avoid + # scanning them multple time (eg. if there is multiple pages pointing + # on one) + self._processed_urls = [] + self._projects = {} + + @with_mirror_support() + def search_projects(self, name=None, **kwargs): + """Search the index for projects containing the given name. + + Return a list of names. + """ + with self._open_url(self.index_url) as index: + if '*' in name: + name.replace('*', '.*') + else: + name = "%s%s%s" % ('*.?', name, '*.?') + name = name.replace('*', '[^<]*') # avoid matching end tag + projectname = re.compile(']*>(%s)' % name, re.I) + matching_projects = [] + + index_content = index.read() + + # FIXME should use bytes I/O and regexes instead of decoding + index_content = index_content.decode() + + for match in projectname.finditer(index_content): + project_name = match.group(1) + matching_projects.append(self._get_project(project_name)) + return matching_projects + + def get_releases(self, requirements, prefer_final=None, + force_update=False): + """Search for releases and return a ReleaseList object containing + the results. + """ + predicate = get_version_predicate(requirements) + if predicate.name.lower() in self._projects and not force_update: + return self._projects.get(predicate.name.lower()) + prefer_final = self._get_prefer_final(prefer_final) + logger.info('reading info on PyPI about %s', predicate.name) + self._process_index_page(predicate.name) + + if predicate.name.lower() not in self._projects: + raise ProjectNotFound() + + releases = self._projects.get(predicate.name.lower()) + releases.sort_releases(prefer_final=prefer_final) + return releases + + def get_release(self, requirements, prefer_final=None): + """Return only one release that fulfill the given requirements""" + predicate = get_version_predicate(requirements) + release = self.get_releases(predicate, prefer_final)\ + .get_last(predicate) + if not release: + raise ReleaseNotFound("No release matches the given criterias") + return release + + def get_distributions(self, project_name, version): + """Return the distributions found on the index for the specific given + release""" + # as the default behavior of get_release is to return a release + # containing the distributions, just alias it. + return self.get_release("%s (%s)" % (project_name, version)) + + def get_metadata(self, project_name, version): + """Return the metadatas from the simple index. + + Currently, download one archive, extract it and use the PKG-INFO file. + """ + release = self.get_distributions(project_name, version) + if not release.metadata: + location = release.get_distribution().unpack() + pkg_info = os.path.join(location, 'PKG-INFO') + release.metadata = Metadata(pkg_info) + return release + + def _switch_to_next_mirror(self): + """Switch to the next mirror (eg. point self.index_url to the next + mirror url. + + Raise a KeyError if all mirrors have been tried. + """ + self._mirrors_used.add(self.index_url) + index_url = self._mirrors.pop() + if not ("http://" or "https://" or "file://") in index_url: + index_url = "http://%s" % index_url + + if not index_url.endswith("/simple"): + index_url = "%s/simple/" % index_url + + self.index_url = index_url + + def _is_browsable(self, url): + """Tell if the given URL can be browsed or not. + + It uses the follow_externals and the hosts list to tell if the given + url is browsable or not. + """ + # if _index_url is contained in the given URL, we are browsing the + # index, and it's always "browsable". + # local files are always considered browable resources + if self.index_url in url or urllib.parse.urlparse(url)[0] == "file": + return True + elif self.follow_externals: + if self._allowed_hosts(urllib.parse.urlparse(url)[1]): # 1 is netloc + return True + else: + return False + return False + + def _is_distribution(self, link): + """Tell if the given URL matches to a distribution name or not. + """ + #XXX find a better way to check that links are distributions + # Using a regexp ? + for ext in EXTENSIONS: + if ext in link: + return True + return False + + def _register_release(self, release=None, release_info={}): + """Register a new release. + + Both a release or a dict of release_info can be provided, the prefered + way (eg. the quicker) is the dict one. + + Return the list of existing releases for the given project. + """ + # Check if the project already has a list of releases (refering to + # the project name). If not, create a new release list. + # Then, add the release to the list. + if release: + name = release.name + else: + name = release_info['name'] + if not name.lower() in self._projects: + self._projects[name.lower()] = ReleasesList(name, index=self._index) + + if release: + self._projects[name.lower()].add_release(release=release) + else: + name = release_info.pop('name') + version = release_info.pop('version') + dist_type = release_info.pop('dist_type') + self._projects[name.lower()].add_release(version, dist_type, + **release_info) + return self._projects[name.lower()] + + def _process_url(self, url, project_name=None, follow_links=True): + """Process an url and search for distributions packages. + + For each URL found, if it's a download, creates a PyPIdistribution + object. If it's a homepage and we can follow links, process it too. + + :param url: the url to process + :param project_name: the project name we are searching for. + :param follow_links: Do not want to follow links more than from one + level. This parameter tells if we want to follow + the links we find (eg. run recursively this + method on it) + """ + with self._open_url(url) as f: + base_url = f.url + if url not in self._processed_urls: + self._processed_urls.append(url) + link_matcher = self._get_link_matcher(url) + for link, is_download in link_matcher(f.read().decode(), base_url): + if link not in self._processed_urls: + if self._is_distribution(link) or is_download: + self._processed_urls.append(link) + # it's a distribution, so create a dist object + try: + infos = get_infos_from_url(link, project_name, + is_external=not self.index_url in url) + except CantParseArchiveName as e: + logger.warning( + "version has not been parsed: %s", e) + else: + self._register_release(release_info=infos) + else: + if self._is_browsable(link) and follow_links: + self._process_url(link, project_name, + follow_links=False) + + def _get_link_matcher(self, url): + """Returns the right link matcher function of the given url + """ + if self.index_url in url: + return self._simple_link_matcher + else: + return self._default_link_matcher + + def _get_full_url(self, url, base_url): + return urllib.parse.urljoin(base_url, self._htmldecode(url)) + + def _simple_link_matcher(self, content, base_url): + """Yield all links with a rel="download" or rel="homepage". + + This matches the simple index requirements for matching links. + If follow_externals is set to False, dont yeld the external + urls. + + :param content: the content of the page we want to parse + :param base_url: the url of this page. + """ + for match in HREF.finditer(content): + url = self._get_full_url(match.group(1), base_url) + if MD5_HASH.match(url): + yield (url, True) + + for match in REL.finditer(content): + # search for rel links. + tag, rel = match.groups() + rels = [s.strip() for s in rel.lower().split(',')] + if 'homepage' in rels or 'download' in rels: + for match in HREF.finditer(tag): + url = self._get_full_url(match.group(1), base_url) + if 'download' in rels or self._is_browsable(url): + # yield a list of (url, is_download) + yield (url, 'download' in rels) + + def _default_link_matcher(self, content, base_url): + """Yield all links found on the page. + """ + for match in HREF.finditer(content): + url = self._get_full_url(match.group(1), base_url) + if self._is_browsable(url): + yield (url, False) + + @with_mirror_support() + def _process_index_page(self, name): + """Find and process a PyPI page for the given project name. + + :param name: the name of the project to find the page + """ + # Browse and index the content of the given PyPI page. + url = self.index_url + name + "/" + self._process_url(url, name) + + @socket_timeout() + def _open_url(self, url): + """Open a urllib2 request, handling HTTP authentication, and local + files support. + + """ + scheme, netloc, path, params, query, frag = urllib.parse.urlparse(url) + + # authentication stuff + if scheme in ('http', 'https'): + auth, host = urllib.parse.splituser(netloc) + else: + auth = None + + # add index.html automatically for filesystem paths + if scheme == 'file': + if url.endswith('/'): + url += "index.html" + + # add authorization headers if auth is provided + if auth: + auth = "Basic " + \ + urllib.parse.unquote(auth).encode('base64').strip() + new_url = urllib.parse.urlunparse(( + scheme, host, path, params, query, frag)) + request = urllib.request.Request(new_url) + request.add_header("Authorization", auth) + else: + request = urllib.request.Request(url) + request.add_header('User-Agent', USER_AGENT) + try: + fp = urllib.request.urlopen(request) + except (ValueError, http.client.InvalidURL) as v: + msg = ' '.join([str(arg) for arg in v.args]) + raise PackagingPyPIError('%s %s' % (url, msg)) + except urllib.error.HTTPError as v: + return v + except urllib.error.URLError as v: + raise DownloadError("Download error for %s: %s" % (url, v.reason)) + except http.client.BadStatusLine as v: + raise DownloadError('%s returned a bad status line. ' + 'The server might be down, %s' % (url, v.line)) + except http.client.HTTPException as v: + raise DownloadError("Download error for %s: %s" % (url, v)) + except socket.timeout: + raise DownloadError("The server timeouted") + + if auth: + # Put authentication info back into request URL if same host, + # so that links found on the page will work + s2, h2, path2, param2, query2, frag2 = \ + urllib.parse.urlparse(fp.url) + if s2 == scheme and h2 == host: + fp.url = urllib.parse.urlunparse( + (s2, netloc, path2, param2, query2, frag2)) + return fp + + def _decode_entity(self, match): + what = match.group(1) + if what.startswith('#x'): + what = int(what[2:], 16) + elif what.startswith('#'): + what = int(what[1:]) + else: + from html.entities import name2codepoint + what = name2codepoint.get(what, match.group(0)) + return chr(what) + + def _htmldecode(self, text): + """Decode HTML entities in the given text.""" + return ENTITY_SUB(self._decode_entity, text) diff --git a/Lib/packaging/pypi/wrapper.py b/Lib/packaging/pypi/wrapper.py new file mode 100644 index 000000000000..945d08abb78a --- /dev/null +++ b/Lib/packaging/pypi/wrapper.py @@ -0,0 +1,99 @@ +"""Convenient client for all PyPI APIs. + +This module provides a ClientWrapper class which will use the "simple" +or XML-RPC API to request information or files from an index. +""" + +from packaging.pypi import simple, xmlrpc + +_WRAPPER_MAPPINGS = {'get_release': 'simple', + 'get_releases': 'simple', + 'search_projects': 'simple', + 'get_metadata': 'xmlrpc', + 'get_distributions': 'simple'} + +_WRAPPER_INDEXES = {'xmlrpc': xmlrpc.Client, + 'simple': simple.Crawler} + + +def switch_index_if_fails(func, wrapper): + """Decorator that switch of index (for instance from xmlrpc to simple) + if the first mirror return an empty list or raises an exception. + """ + def decorator(*args, **kwargs): + retry = True + exception = None + methods = [func] + for f in wrapper._indexes.values(): + if f != func.__self__ and hasattr(f, func.__name__): + methods.append(getattr(f, func.__name__)) + for method in methods: + try: + response = method(*args, **kwargs) + retry = False + except Exception as e: + exception = e + if not retry: + break + if retry and exception: + raise exception + else: + return response + return decorator + + +class ClientWrapper: + """Wrapper around simple and xmlrpc clients, + + Choose the best implementation to use depending the needs, using the given + mappings. + If one of the indexes returns an error, tries to use others indexes. + + :param index: tell which index to rely on by default. + :param index_classes: a dict of name:class to use as indexes. + :param indexes: a dict of name:index already instantiated + :param mappings: the mappings to use for this wrapper + """ + + def __init__(self, default_index='simple', index_classes=_WRAPPER_INDEXES, + indexes={}, mappings=_WRAPPER_MAPPINGS): + self._projects = {} + self._mappings = mappings + self._indexes = indexes + self._default_index = default_index + + # instantiate the classes and set their _project attribute to the one + # of the wrapper. + for name, cls in index_classes.items(): + obj = self._indexes.setdefault(name, cls()) + obj._projects = self._projects + obj._index = self + + def __getattr__(self, method_name): + """When asking for methods of the wrapper, return the implementation of + the wrapped classes, depending the mapping. + + Decorate the methods to switch of implementation if an error occurs + """ + real_method = None + if method_name in _WRAPPER_MAPPINGS: + obj = self._indexes[_WRAPPER_MAPPINGS[method_name]] + real_method = getattr(obj, method_name) + else: + # the method is not defined in the mappings, so we try first to get + # it via the default index, and rely on others if needed. + try: + real_method = getattr(self._indexes[self._default_index], + method_name) + except AttributeError: + other_indexes = [i for i in self._indexes + if i != self._default_index] + for index in other_indexes: + real_method = getattr(self._indexes[index], method_name, + None) + if real_method: + break + if real_method: + return switch_index_if_fails(real_method, self) + else: + raise AttributeError("No index have attribute '%s'" % method_name) diff --git a/Lib/packaging/pypi/xmlrpc.py b/Lib/packaging/pypi/xmlrpc.py new file mode 100644 index 000000000000..7a9f6cc7137d --- /dev/null +++ b/Lib/packaging/pypi/xmlrpc.py @@ -0,0 +1,200 @@ +"""Spider using the XML-RPC PyPI API. + +This module contains the class Client, a spider that can be used to find +and retrieve distributions from a project index (like the Python Package +Index), using its XML-RPC API (see documentation of the reference +implementation at http://wiki.python.org/moin/PyPiXmlRpc). +""" + +import xmlrpc.client + +from packaging import logger +from packaging.errors import IrrationalVersionError +from packaging.version import get_version_predicate +from packaging.pypi.base import BaseClient +from packaging.pypi.errors import (ProjectNotFound, InvalidSearchField, + ReleaseNotFound) +from packaging.pypi.dist import ReleaseInfo + +__all__ = ['Client', 'DEFAULT_XMLRPC_INDEX_URL'] + +DEFAULT_XMLRPC_INDEX_URL = 'http://python.org/pypi' + +_SEARCH_FIELDS = ['name', 'version', 'author', 'author_email', 'maintainer', + 'maintainer_email', 'home_page', 'license', 'summary', + 'description', 'keywords', 'platform', 'download_url'] + + +class Client(BaseClient): + """Client to query indexes using XML-RPC method calls. + + If no server_url is specified, use the default PyPI XML-RPC URL, + defined in the DEFAULT_XMLRPC_INDEX_URL constant:: + + >>> client = XMLRPCClient() + >>> client.server_url == DEFAULT_XMLRPC_INDEX_URL + True + + >>> client = XMLRPCClient("http://someurl/") + >>> client.server_url + 'http://someurl/' + """ + + def __init__(self, server_url=DEFAULT_XMLRPC_INDEX_URL, prefer_final=False, + prefer_source=True): + super(Client, self).__init__(prefer_final, prefer_source) + self.server_url = server_url + self._projects = {} + + def get_release(self, requirements, prefer_final=False): + """Return a release with all complete metadata and distribution + related informations. + """ + prefer_final = self._get_prefer_final(prefer_final) + predicate = get_version_predicate(requirements) + releases = self.get_releases(predicate.name) + release = releases.get_last(predicate, prefer_final) + self.get_metadata(release.name, str(release.version)) + self.get_distributions(release.name, str(release.version)) + return release + + def get_releases(self, requirements, prefer_final=None, show_hidden=True, + force_update=False): + """Return the list of existing releases for a specific project. + + Cache the results from one call to another. + + If show_hidden is True, return the hidden releases too. + If force_update is True, reprocess the index to update the + informations (eg. make a new XML-RPC call). + :: + + >>> client = XMLRPCClient() + >>> client.get_releases('Foo') + ['1.1', '1.2', '1.3'] + + If no such project exists, raise a ProjectNotFound exception:: + + >>> client.get_project_versions('UnexistingProject') + ProjectNotFound: UnexistingProject + + """ + def get_versions(project_name, show_hidden): + return self.proxy.package_releases(project_name, show_hidden) + + predicate = get_version_predicate(requirements) + prefer_final = self._get_prefer_final(prefer_final) + project_name = predicate.name + if not force_update and (project_name.lower() in self._projects): + project = self._projects[project_name.lower()] + if not project.contains_hidden and show_hidden: + # if hidden releases are requested, and have an existing + # list of releases that does not contains hidden ones + all_versions = get_versions(project_name, show_hidden) + existing_versions = project.get_versions() + hidden_versions = set(all_versions) - set(existing_versions) + for version in hidden_versions: + project.add_release(release=ReleaseInfo(project_name, + version, index=self._index)) + else: + versions = get_versions(project_name, show_hidden) + if not versions: + raise ProjectNotFound(project_name) + project = self._get_project(project_name) + project.add_releases([ReleaseInfo(project_name, version, + index=self._index) + for version in versions]) + project = project.filter(predicate) + if len(project) == 0: + raise ReleaseNotFound("%s" % predicate) + project.sort_releases(prefer_final) + return project + + + def get_distributions(self, project_name, version): + """Grab informations about distributions from XML-RPC. + + Return a ReleaseInfo object, with distribution-related informations + filled in. + """ + url_infos = self.proxy.release_urls(project_name, version) + project = self._get_project(project_name) + if version not in project.get_versions(): + project.add_release(release=ReleaseInfo(project_name, version, + index=self._index)) + release = project.get_release(version) + for info in url_infos: + packagetype = info['packagetype'] + dist_infos = {'url': info['url'], + 'hashval': info['md5_digest'], + 'hashname': 'md5', + 'is_external': False, + 'python_version': info['python_version']} + release.add_distribution(packagetype, **dist_infos) + return release + + def get_metadata(self, project_name, version): + """Retrieve project metadata. + + Return a ReleaseInfo object, with metadata informations filled in. + """ + # to be case-insensitive, get the informations from the XMLRPC API + projects = [d['name'] for d in + self.proxy.search({'name': project_name}) + if d['name'].lower() == project_name] + if len(projects) > 0: + project_name = projects[0] + + metadata = self.proxy.release_data(project_name, version) + project = self._get_project(project_name) + if version not in project.get_versions(): + project.add_release(release=ReleaseInfo(project_name, version, + index=self._index)) + release = project.get_release(version) + release.set_metadata(metadata) + return release + + def search_projects(self, name=None, operator="or", **kwargs): + """Find using the keys provided in kwargs. + + You can set operator to "and" or "or". + """ + for key in kwargs: + if key not in _SEARCH_FIELDS: + raise InvalidSearchField(key) + if name: + kwargs["name"] = name + projects = self.proxy.search(kwargs, operator) + for p in projects: + project = self._get_project(p['name']) + try: + project.add_release(release=ReleaseInfo(p['name'], + p['version'], metadata={'summary': p['summary']}, + index=self._index)) + except IrrationalVersionError as e: + logger.warning("Irrational version error found: %s", e) + return [self._projects[p['name'].lower()] for p in projects] + + def get_all_projects(self): + """Return the list of all projects registered in the package index""" + projects = self.proxy.list_packages() + for name in projects: + self.get_releases(name, show_hidden=True) + + return [self._projects[name.lower()] for name in set(projects)] + + @property + def proxy(self): + """Property used to return the XMLRPC server proxy. + + If no server proxy is defined yet, creates a new one:: + + >>> client = XmlRpcClient() + >>> client.proxy() + + + """ + if not hasattr(self, '_server_proxy'): + self._server_proxy = xmlrpc.client.ServerProxy(self.server_url) + + return self._server_proxy diff --git a/Lib/packaging/resources.py b/Lib/packaging/resources.py new file mode 100644 index 000000000000..e5904f360dfa --- /dev/null +++ b/Lib/packaging/resources.py @@ -0,0 +1,25 @@ +"""Data file path abstraction. + +Functions in this module use sysconfig to find the paths to the resource +files registered in project's setup.cfg file. See the documentation for +more information. +""" +# TODO write that documentation + +from packaging.database import get_distribution + +__all__ = ['get_file_path', 'get_file'] + + +def get_file_path(distribution_name, relative_path): + """Return the path to a resource file.""" + dist = get_distribution(distribution_name) + if dist != None: + return dist.get_resource_path(relative_path) + raise LookupError('no distribution named %r found' % distribution_name) + + +def get_file(distribution_name, relative_path, *args, **kwargs): + """Open and return a resource file.""" + return open(get_file_path(distribution_name, relative_path), + *args, **kwargs) diff --git a/Lib/packaging/run.py b/Lib/packaging/run.py new file mode 100644 index 000000000000..1d4fadb880e0 --- /dev/null +++ b/Lib/packaging/run.py @@ -0,0 +1,645 @@ +"""Main command line parser. Implements the pysetup script.""" + +import os +import re +import sys +import getopt +import logging + +from packaging import logger +from packaging.dist import Distribution +from packaging.util import _is_archive_file +from packaging.command import get_command_class, STANDARD_COMMANDS +from packaging.install import install, install_local_project, remove +from packaging.database import get_distribution, get_distributions +from packaging.depgraph import generate_graph +from packaging.fancy_getopt import FancyGetopt +from packaging.errors import (PackagingArgError, PackagingError, + PackagingModuleError, PackagingClassError, + CCompilerError) + + +command_re = re.compile(r'^[a-zA-Z]([a-zA-Z0-9_]*)$') + +common_usage = """\ +Actions: +%(actions)s + +To get more help on an action, use: + + pysetup action --help +""" + +create_usage = """\ +Usage: pysetup create + or: pysetup create --help + +Create a new Python package. +""" + +graph_usage = """\ +Usage: pysetup graph dist + or: pysetup graph --help + +Print dependency graph for the distribution. + +positional arguments: + dist installed distribution name +""" + +install_usage = """\ +Usage: pysetup install [dist] + or: pysetup install [archive] + or: pysetup install [src_dir] + or: pysetup install --help + +Install a Python distribution from the indexes, source directory, or sdist. + +positional arguments: + archive path to source distribution (zip, tar.gz) + dist distribution name to install from the indexes + scr_dir path to source directory + +""" + +metadata_usage = """\ +Usage: pysetup metadata [dist] [-f field ...] + or: pysetup metadata [dist] [--all] + or: pysetup metadata --help + +Print metadata for the distribution. + +positional arguments: + dist installed distribution name + +optional arguments: + -f metadata field to print + --all print all metadata fields +""" + +remove_usage = """\ +Usage: pysetup remove dist [-y] + or: pysetup remove --help + +Uninstall a Python distribution. + +positional arguments: + dist installed distribution name + +optional arguments: + -y auto confirm package removal +""" + +run_usage = """\ +Usage: pysetup run [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...] + or: pysetup run --help + or: pysetup run --list-commands + or: pysetup run cmd --help +""" + +list_usage = """\ +Usage: pysetup list dist [dist ...] + or: pysetup list --help + or: pysetup list --all + +Print name, version and location for the matching installed distributions. + +positional arguments: + dist installed distribution name + +optional arguments: + --all list all installed distributions +""" + +search_usage = """\ +Usage: pysetup search [project] [--simple [url]] [--xmlrpc [url] [--fieldname value ...] --operator or|and] + or: pysetup search --help + +Search the indexes for the matching projects. + +positional arguments: + project the project pattern to search for + +optional arguments: + --xmlrpc [url] wether to use the xmlrpc index or not. If an url is + specified, it will be used rather than the default one. + + --simple [url] wether to use the simple index or not. If an url is + specified, it will be used rather than the default one. + + --fieldname value Make a search on this field. Can only be used if + --xmlrpc has been selected or is the default index. + + --operator or|and Defines what is the operator to use when doing xmlrpc + searchs with multiple fieldnames. Can only be used if + --xmlrpc has been selected or is the default index. +""" + +global_options = [ + # The fourth entry for verbose means that it can be repeated. + ('verbose', 'v', "run verbosely (default)", True), + ('quiet', 'q', "run quietly (turns verbosity off)"), + ('dry-run', 'n', "don't actually do anything"), + ('help', 'h', "show detailed help message"), + ('no-user-cfg', None, 'ignore pydistutils.cfg in your home directory'), + ('version', None, 'Display the version'), +] + +negative_opt = {'quiet': 'verbose'} + +display_options = [ + ('help-commands', None, "list all available commands"), +] + +display_option_names = [x[0].replace('-', '_') for x in display_options] + + +def _parse_args(args, options, long_options): + """Transform sys.argv input into a dict. + + :param args: the args to parse (i.e sys.argv) + :param options: the list of options to pass to getopt + :param long_options: the list of string with the names of the long options + to be passed to getopt. + + The function returns a dict with options/long_options as keys and matching + values as values. + """ + optlist, args = getopt.gnu_getopt(args, options, long_options) + optdict = {} + optdict['args'] = args + for k, v in optlist: + k = k.lstrip('-') + if k not in optdict: + optdict[k] = [] + if v: + optdict[k].append(v) + else: + optdict[k].append(v) + return optdict + + +class action_help: + """Prints a help message when the standard help flags: -h and --help + are used on the commandline. + """ + + def __init__(self, help_msg): + self.help_msg = help_msg + + def __call__(self, f): + def wrapper(*args, **kwargs): + f_args = args[1] + if '--help' in f_args or '-h' in f_args: + print(self.help_msg) + return + return f(*args, **kwargs) + return wrapper + + +@action_help(create_usage) +def _create(distpatcher, args, **kw): + from packaging.create import main + return main() + + +@action_help(graph_usage) +def _graph(dispatcher, args, **kw): + name = args[1] + dist = get_distribution(name, use_egg_info=True) + if dist is None: + print('Distribution not found.') + else: + dists = get_distributions(use_egg_info=True) + graph = generate_graph(dists) + print(graph.repr_node(dist)) + + +@action_help(install_usage) +def _install(dispatcher, args, **kw): + # first check if we are in a source directory + if len(args) < 2: + # are we inside a project dir? + listing = os.listdir(os.getcwd()) + if 'setup.py' in listing or 'setup.cfg' in listing: + args.insert(1, os.getcwd()) + else: + logger.warning('no project to install') + return + + # installing from a source dir or archive file? + if os.path.isdir(args[1]) or _is_archive_file(args[1]): + install_local_project(args[1]) + else: + # download from PyPI + install(args[1]) + + +@action_help(metadata_usage) +def _metadata(dispatcher, args, **kw): + opts = _parse_args(args[1:], 'f:', ['all']) + if opts['args']: + name = opts['args'][0] + dist = get_distribution(name, use_egg_info=True) + if dist is None: + logger.warning('%s not installed', name) + return + else: + logger.info('searching local dir for metadata') + dist = Distribution() + dist.parse_config_files() + + metadata = dist.metadata + + if 'all' in opts: + keys = metadata.keys() + else: + if 'f' in opts: + keys = (k for k in opts['f'] if k in metadata) + else: + keys = () + + for key in keys: + if key in metadata: + print(metadata._convert_name(key) + ':') + value = metadata[key] + if isinstance(value, list): + for v in value: + print(' ' + v) + else: + print(' ' + value.replace('\n', '\n ')) + + +@action_help(remove_usage) +def _remove(distpatcher, args, **kw): + opts = _parse_args(args[1:], 'y', []) + if 'y' in opts: + auto_confirm = True + else: + auto_confirm = False + + for dist in set(opts['args']): + try: + remove(dist, auto_confirm=auto_confirm) + except PackagingError: + logger.warning('%s not installed', dist) + + +@action_help(run_usage) +def _run(dispatcher, args, **kw): + parser = dispatcher.parser + args = args[1:] + + commands = STANDARD_COMMANDS # + extra commands + + if args == ['--list-commands']: + print('List of available commands:') + cmds = sorted(commands) + + for cmd in cmds: + cls = dispatcher.cmdclass.get(cmd) or get_command_class(cmd) + desc = getattr(cls, 'description', + '(no description available)') + print(' %s: %s' % (cmd, desc)) + return + + while args: + args = dispatcher._parse_command_opts(parser, args) + if args is None: + return + + # create the Distribution class + # need to feed setup.cfg here ! + dist = Distribution() + + # Find and parse the config file(s): they will override options from + # the setup script, but be overridden by the command line. + + # XXX still need to be extracted from Distribution + dist.parse_config_files() + + try: + for cmd in dispatcher.commands: + dist.run_command(cmd, dispatcher.command_options[cmd]) + + except KeyboardInterrupt: + raise SystemExit("interrupted") + except (IOError, os.error, PackagingError, CCompilerError) as msg: + raise SystemExit("error: " + str(msg)) + + # XXX this is crappy + return dist + + +@action_help(list_usage) +def _list(dispatcher, args, **kw): + opts = _parse_args(args[1:], '', ['all']) + dists = get_distributions(use_egg_info=True) + if 'all' in opts: + results = dists + else: + results = [d for d in dists if d.name.lower() in opts['args']] + + for dist in results: + print('%s %s at %s' % (dist.name, dist.metadata['version'], dist.path)) + + +@action_help(search_usage) +def _search(dispatcher, args, **kw): + """The search action. + + It is able to search for a specific index (specified with --index), using + the simple or xmlrpc index types (with --type xmlrpc / --type simple) + """ + opts = _parse_args(args[1:], '', ['simple', 'xmlrpc']) + # 1. what kind of index is requested ? (xmlrpc / simple) + + +actions = [ + ('run', 'Run one or several commands', _run), + ('metadata', 'Display the metadata of a project', _metadata), + ('install', 'Install a project', _install), + ('remove', 'Remove a project', _remove), + ('search', 'Search for a project in the indexes', _search), + ('list', 'Search for local projects', _list), + ('graph', 'Display a graph', _graph), + ('create', 'Create a Project', _create), +] + + +class Dispatcher: + """Reads the command-line options + """ + def __init__(self, args=None): + self.verbose = 1 + self.dry_run = False + self.help = False + self.script_name = 'pysetup' + self.cmdclass = {} + self.commands = [] + self.command_options = {} + + for attr in display_option_names: + setattr(self, attr, False) + + self.parser = FancyGetopt(global_options + display_options) + self.parser.set_negative_aliases(negative_opt) + # FIXME this parses everything, including command options (e.g. "run + # build -i" errors with "option -i not recognized") + args = self.parser.getopt(args=args, object=self) + + # if first arg is "run", we have some commands + if len(args) == 0: + self.action = None + else: + self.action = args[0] + + allowed = [action[0] for action in actions] + [None] + if self.action not in allowed: + msg = 'Unrecognized action "%s"' % self.action + raise PackagingArgError(msg) + + # setting up the logging level from the command-line options + # -q gets warning, error and critical + if self.verbose == 0: + level = logging.WARNING + # default level or -v gets info too + # XXX there's a bug somewhere: the help text says that -v is default + # (and verbose is set to 1 above), but when the user explicitly gives + # -v on the command line, self.verbose is incremented to 2! Here we + # compensate for that (I tested manually). On a related note, I think + # it's a good thing to use -q/nothing/-v/-vv on the command line + # instead of logging constants; it will be easy to add support for + # logging configuration in setup.cfg for advanced users. --merwok + elif self.verbose in (1, 2): + level = logging.INFO + else: # -vv and more for debug + level = logging.DEBUG + + # for display options we return immediately + option_order = self.parser.get_option_order() + + self.args = args + + if self.help or self.action is None: + self._show_help(self.parser, display_options_=False) + + def _parse_command_opts(self, parser, args): + # Pull the current command from the head of the command line + command = args[0] + if not command_re.match(command): + raise SystemExit("invalid command name %r" % (command,)) + self.commands.append(command) + + # Dig up the command class that implements this command, so we + # 1) know that it's a valid command, and 2) know which options + # it takes. + try: + cmd_class = get_command_class(command) + except PackagingModuleError as msg: + raise PackagingArgError(msg) + + # XXX We want to push this in packaging.command + # + # Require that the command class be derived from Command -- want + # to be sure that the basic "command" interface is implemented. + for meth in ('initialize_options', 'finalize_options', 'run'): + if hasattr(cmd_class, meth): + continue + raise PackagingClassError( + 'command %r must implement %r' % (cmd_class, meth)) + + # Also make sure that the command object provides a list of its + # known options. + if not (hasattr(cmd_class, 'user_options') and + isinstance(cmd_class.user_options, list)): + raise PackagingClassError( + "command class %s must provide " + "'user_options' attribute (a list of tuples)" % cmd_class) + + # If the command class has a list of negative alias options, + # merge it in with the global negative aliases. + _negative_opt = negative_opt.copy() + + if hasattr(cmd_class, 'negative_opt'): + _negative_opt.update(cmd_class.negative_opt) + + # Check for help_options in command class. They have a different + # format (tuple of four) so we need to preprocess them here. + if (hasattr(cmd_class, 'help_options') and + isinstance(cmd_class.help_options, list)): + help_options = cmd_class.help_options[:] + else: + help_options = [] + + # All commands support the global options too, just by adding + # in 'global_options'. + parser.set_option_table(global_options + + cmd_class.user_options + + help_options) + parser.set_negative_aliases(_negative_opt) + args, opts = parser.getopt(args[1:]) + + if hasattr(opts, 'help') and opts.help: + self._show_command_help(cmd_class) + return + + if (hasattr(cmd_class, 'help_options') and + isinstance(cmd_class.help_options, list)): + help_option_found = False + for help_option, short, desc, func in cmd_class.help_options: + if hasattr(opts, help_option.replace('-', '_')): + help_option_found = True + if hasattr(func, '__call__'): + func() + else: + raise PackagingClassError( + "invalid help function %r for help option %r: " + "must be a callable object (function, etc.)" + % (func, help_option)) + + if help_option_found: + return + + # Put the options from the command line into their official + # holding pen, the 'command_options' dictionary. + opt_dict = self.get_option_dict(command) + for name, value in vars(opts).items(): + opt_dict[name] = ("command line", value) + + return args + + def get_option_dict(self, command): + """Get the option dictionary for a given command. If that + command's option dictionary hasn't been created yet, then create it + and return the new dictionary; otherwise, return the existing + option dictionary. + """ + d = self.command_options.get(command) + if d is None: + d = self.command_options[command] = {} + return d + + def show_help(self): + self._show_help(self.parser) + + def print_usage(self, parser): + parser.set_option_table(global_options) + + actions_ = [' %s: %s' % (name, desc) for name, desc, __ in actions] + usage = common_usage % {'actions': '\n'.join(actions_)} + + parser.print_help(usage + "\nGlobal options:") + + def _show_help(self, parser, global_options_=True, display_options_=True, + commands=[]): + # late import because of mutual dependence between these modules + from packaging.command.cmd import Command + + print('Usage: pysetup [options] action [action_options]') + print('') + if global_options_: + self.print_usage(self.parser) + print('') + + if display_options_: + parser.set_option_table(display_options) + parser.print_help( + "Information display options (just display " + + "information, ignore any commands)") + print('') + + for command in commands: + if isinstance(command, type) and issubclass(command, Command): + cls = command + else: + cls = get_command_class(command) + if (hasattr(cls, 'help_options') and + isinstance(cls.help_options, list)): + parser.set_option_table(cls.user_options + cls.help_options) + else: + parser.set_option_table(cls.user_options) + + parser.print_help("Options for %r command:" % cls.__name__) + print('') + + def _show_command_help(self, command): + if isinstance(command, str): + command = get_command_class(command) + + name = command.get_command_name() + + desc = getattr(command, 'description', '(no description available)') + print('Description: %s' % desc) + print('') + + if (hasattr(command, 'help_options') and + isinstance(command.help_options, list)): + self.parser.set_option_table(command.user_options + + command.help_options) + else: + self.parser.set_option_table(command.user_options) + + self.parser.print_help("Options:") + print('') + + def _get_command_groups(self): + """Helper function to retrieve all the command class names divided + into standard commands (listed in + packaging.command.STANDARD_COMMANDS) and extra commands (given in + self.cmdclass and not standard commands). + """ + extra_commands = [cmd for cmd in self.cmdclass + if cmd not in STANDARD_COMMANDS] + return STANDARD_COMMANDS, extra_commands + + def print_commands(self): + """Print out a help message listing all available commands with a + description of each. The list is divided into standard commands + (listed in packaging.command.STANDARD_COMMANDS) and extra commands + (given in self.cmdclass and not standard commands). The + descriptions come from the command class attribute + 'description'. + """ + std_commands, extra_commands = self._get_command_groups() + max_length = max(len(command) + for commands in (std_commands, extra_commands) + for command in commands) + + self.print_command_list(std_commands, "Standard commands", max_length) + if extra_commands: + print() + self.print_command_list(extra_commands, "Extra commands", + max_length) + + def print_command_list(self, commands, header, max_length): + """Print a subset of the list of all commands -- used by + 'print_commands()'. + """ + print(header + ":") + + for cmd in commands: + cls = self.cmdclass.get(cmd) or get_command_class(cmd) + description = getattr(cls, 'description', + '(no description available)') + + print(" %-*s %s" % (max_length, cmd, description)) + + def __call__(self): + if self.action is None: + return + for action, desc, func in actions: + if action == self.action: + return func(self, self.args) + return -1 + + +def main(args=None): + dispatcher = Dispatcher(args) + if dispatcher.action is None: + return + + return dispatcher() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/Lib/packaging/tests/LONG_DESC.txt b/Lib/packaging/tests/LONG_DESC.txt new file mode 100644 index 000000000000..2b4358aae6cc --- /dev/null +++ b/Lib/packaging/tests/LONG_DESC.txt @@ -0,0 +1,44 @@ +CLVault +======= + +CLVault uses Keyring to provide a command-line utility to safely store +and retrieve passwords. + +Install it using pip or the setup.py script:: + + $ python setup.py install + + $ pip install clvault + +Once it's installed, you will have three scripts installed in your +Python scripts folder, you can use to list, store and retrieve passwords:: + + $ clvault-set blog + Set your password: + Set the associated username (can be blank): tarek + Set a description (can be blank): My blog password + Password set. + + $ clvault-get blog + The username is "tarek" + The password has been copied in your clipboard + + $ clvault-list + Registered services: + blog My blog password + + +*clvault-set* takes a service name then prompt you for a password, and some +optional information about your service. The password is safely stored in +a keyring while the description is saved in a ``.clvault`` file in your +home directory. This file is created automatically the first time the command +is used. + +*clvault-get* copies the password for a given service in your clipboard, and +displays the associated user if any. + +*clvault-list* lists all registered services, with their description when +given. + + +Project page: http://bitbucket.org/tarek/clvault diff --git a/Lib/packaging/tests/PKG-INFO b/Lib/packaging/tests/PKG-INFO new file mode 100644 index 000000000000..f48546e5a88d --- /dev/null +++ b/Lib/packaging/tests/PKG-INFO @@ -0,0 +1,57 @@ +Metadata-Version: 1.2 +Name: CLVault +Version: 0.5 +Summary: Command-Line utility to store and retrieve passwords +Home-page: http://bitbucket.org/tarek/clvault +Author: Tarek Ziade +Author-email: tarek@ziade.org +License: PSF +Keywords: keyring,password,crypt +Requires-Dist: foo; sys.platform == 'okook' +Requires-Dist: bar; sys.platform == '%s' +Platform: UNKNOWN +Description: CLVault + |======= + | + |CLVault uses Keyring to provide a command-line utility to safely store + |and retrieve passwords. + | + |Install it using pip or the setup.py script:: + | + | $ python setup.py install + | + | $ pip install clvault + | + |Once it's installed, you will have three scripts installed in your + |Python scripts folder, you can use to list, store and retrieve passwords:: + | + | $ clvault-set blog + | Set your password: + | Set the associated username (can be blank): tarek + | Set a description (can be blank): My blog password + | Password set. + | + | $ clvault-get blog + | The username is "tarek" + | The password has been copied in your clipboard + | + | $ clvault-list + | Registered services: + | blog My blog password + | + | + |*clvault-set* takes a service name then prompt you for a password, and some + |optional information about your service. The password is safely stored in + |a keyring while the description is saved in a ``.clvault`` file in your + |home directory. This file is created automatically the first time the command + |is used. + | + |*clvault-get* copies the password for a given service in your clipboard, and + |displays the associated user if any. + | + |*clvault-list* lists all registered services, with their description when + |given. + | + | + |Project page: http://bitbucket.org/tarek/clvault + | diff --git a/Lib/packaging/tests/SETUPTOOLS-PKG-INFO b/Lib/packaging/tests/SETUPTOOLS-PKG-INFO new file mode 100644 index 000000000000..dff8d0056715 --- /dev/null +++ b/Lib/packaging/tests/SETUPTOOLS-PKG-INFO @@ -0,0 +1,182 @@ +Metadata-Version: 1.0 +Name: setuptools +Version: 0.6c9 +Summary: Download, build, install, upgrade, and uninstall Python packages -- easily! +Home-page: http://pypi.python.org/pypi/setuptools +Author: Phillip J. Eby +Author-email: distutils-sig@python.org +License: PSF or ZPL +Description: =============================== + Installing and Using Setuptools + =============================== + + .. contents:: **Table of Contents** + + + ------------------------- + Installation Instructions + ------------------------- + + Windows + ======= + + Install setuptools using the provided ``.exe`` installer. If you've previously + installed older versions of setuptools, please delete all ``setuptools*.egg`` + and ``setuptools.pth`` files from your system's ``site-packages`` directory + (and any other ``sys.path`` directories) FIRST. + + If you are upgrading a previous version of setuptools that was installed using + an ``.exe`` installer, please be sure to also *uninstall that older version* + via your system's "Add/Remove Programs" feature, BEFORE installing the newer + version. + + Once installation is complete, you will find an ``easy_install.exe`` program in + your Python ``Scripts`` subdirectory. Be sure to add this directory to your + ``PATH`` environment variable, if you haven't already done so. + + + RPM-Based Systems + ================= + + Install setuptools using the provided source RPM. The included ``.spec`` file + assumes you are installing using the default ``python`` executable, and is not + specific to a particular Python version. The ``easy_install`` executable will + be installed to a system ``bin`` directory such as ``/usr/bin``. + + If you wish to install to a location other than the default Python + installation's default ``site-packages`` directory (and ``$prefix/bin`` for + scripts), please use the ``.egg``-based installation approach described in the + following section. + + + Cygwin, Mac OS X, Linux, Other + ============================== + + 1. Download the appropriate egg for your version of Python (e.g. + ``setuptools-0.6c9-py2.4.egg``). Do NOT rename it. + + 2. Run it as if it were a shell script, e.g. ``sh setuptools-0.6c9-py2.4.egg``. + Setuptools will install itself using the matching version of Python (e.g. + ``python2.4``), and will place the ``easy_install`` executable in the + default location for installing Python scripts (as determined by the + standard distutils configuration files, or by the Python installation). + + If you want to install setuptools to somewhere other than ``site-packages`` or + your default distutils installation locations for libraries and scripts, you + may include EasyInstall command-line options such as ``--prefix``, + ``--install-dir``, and so on, following the ``.egg`` filename on the same + command line. For example:: + + sh setuptools-0.6c9-py2.4.egg --prefix=~ + + You can use ``--help`` to get a full options list, but we recommend consulting + the `EasyInstall manual`_ for detailed instructions, especially `the section + on custom installation locations`_. + + .. _EasyInstall manual: http://peak.telecommunity.com/DevCenter/EasyInstall + .. _the section on custom installation locations: http://peak.telecommunity.com/DevCenter/EasyInstall#custom-installation-locations + + + Cygwin Note + ----------- + + If you are trying to install setuptools for the **Windows** version of Python + (as opposed to the Cygwin version that lives in ``/usr/bin``), you must make + sure that an appropriate executable (``python2.3``, ``python2.4``, or + ``python2.5``) is on your **Cygwin** ``PATH`` when invoking the egg. For + example, doing the following at a Cygwin bash prompt will install setuptools + for the **Windows** Python found at ``C:\\Python24``:: + + ln -s /cygdrive/c/Python24/python.exe python2.4 + PATH=.:$PATH sh setuptools-0.6c9-py2.4.egg + rm python2.4 + + + Downloads + ========= + + All setuptools downloads can be found at `the project's home page in the Python + Package Index`_. Scroll to the very bottom of the page to find the links. + + .. _the project's home page in the Python Package Index: http://pypi.python.org/pypi/setuptools + + In addition to the PyPI downloads, the development version of ``setuptools`` + is available from the `Python SVN sandbox`_, and in-development versions of the + `0.6 branch`_ are available as well. + + .. _0.6 branch: http://svn.python.org/projects/sandbox/branches/setuptools-0.6/#egg=setuptools-dev06 + + .. _Python SVN sandbox: http://svn.python.org/projects/sandbox/trunk/setuptools/#egg=setuptools-dev + + -------------------------------- + Using Setuptools and EasyInstall + -------------------------------- + + Here are some of the available manuals, tutorials, and other resources for + learning about Setuptools, Python Eggs, and EasyInstall: + + * `The EasyInstall user's guide and reference manual`_ + * `The setuptools Developer's Guide`_ + * `The pkg_resources API reference`_ + * `Package Compatibility Notes`_ (user-maintained) + * `The Internal Structure of Python Eggs`_ + + Questions, comments, and bug reports should be directed to the `distutils-sig + mailing list`_. If you have written (or know of) any tutorials, documentation, + plug-ins, or other resources for setuptools users, please let us know about + them there, so this reference list can be updated. If you have working, + *tested* patches to correct problems or add features, you may submit them to + the `setuptools bug tracker`_. + + .. _setuptools bug tracker: http://bugs.python.org/setuptools/ + .. _Package Compatibility Notes: http://peak.telecommunity.com/DevCenter/PackageNotes + .. _The Internal Structure of Python Eggs: http://peak.telecommunity.com/DevCenter/EggFormats + .. _The setuptools Developer's Guide: http://peak.telecommunity.com/DevCenter/setuptools + .. _The pkg_resources API reference: http://peak.telecommunity.com/DevCenter/PkgResources + .. _The EasyInstall user's guide and reference manual: http://peak.telecommunity.com/DevCenter/EasyInstall + .. _distutils-sig mailing list: http://mail.python.org/pipermail/distutils-sig/ + + + ------- + Credits + ------- + + * The original design for the ``.egg`` format and the ``pkg_resources`` API was + co-created by Phillip Eby and Bob Ippolito. Bob also implemented the first + version of ``pkg_resources``, and supplied the OS X operating system version + compatibility algorithm. + + * Ian Bicking implemented many early "creature comfort" features of + easy_install, including support for downloading via Sourceforge and + Subversion repositories. Ian's comments on the Web-SIG about WSGI + application deployment also inspired the concept of "entry points" in eggs, + and he has given talks at PyCon and elsewhere to inform and educate the + community about eggs and setuptools. + + * Jim Fulton contributed time and effort to build automated tests of various + aspects of ``easy_install``, and supplied the doctests for the command-line + ``.exe`` wrappers on Windows. + + * Phillip J. Eby is the principal author and maintainer of setuptools, and + first proposed the idea of an importable binary distribution format for + Python application plug-ins. + + * Significant parts of the implementation of setuptools were funded by the Open + Source Applications Foundation, to provide a plug-in infrastructure for the + Chandler PIM application. In addition, many OSAF staffers (such as Mike + "Code Bear" Taylor) contributed their time and stress as guinea pigs for the + use of eggs and setuptools, even before eggs were "cool". (Thanks, guys!) + + +Keywords: CPAN PyPI distutils eggs package management +Platform: UNKNOWN +Classifier: Development Status :: 3 - Alpha +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Python Software Foundation License +Classifier: License :: OSI Approved :: Zope Public License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: System :: Archiving :: Packaging +Classifier: Topic :: System :: Systems Administration +Classifier: Topic :: Utilities diff --git a/Lib/packaging/tests/SETUPTOOLS-PKG-INFO2 b/Lib/packaging/tests/SETUPTOOLS-PKG-INFO2 new file mode 100644 index 000000000000..4b3906a41bcc --- /dev/null +++ b/Lib/packaging/tests/SETUPTOOLS-PKG-INFO2 @@ -0,0 +1,183 @@ +Metadata-Version: 1.1 +Name: setuptools +Version: 0.6c9 +Summary: Download, build, install, upgrade, and uninstall Python packages -- easily! +Home-page: http://pypi.python.org/pypi/setuptools +Author: Phillip J. Eby +Author-email: distutils-sig@python.org +License: PSF or ZPL +Description: =============================== + Installing and Using Setuptools + =============================== + + .. contents:: **Table of Contents** + + + ------------------------- + Installation Instructions + ------------------------- + + Windows + ======= + + Install setuptools using the provided ``.exe`` installer. If you've previously + installed older versions of setuptools, please delete all ``setuptools*.egg`` + and ``setuptools.pth`` files from your system's ``site-packages`` directory + (and any other ``sys.path`` directories) FIRST. + + If you are upgrading a previous version of setuptools that was installed using + an ``.exe`` installer, please be sure to also *uninstall that older version* + via your system's "Add/Remove Programs" feature, BEFORE installing the newer + version. + + Once installation is complete, you will find an ``easy_install.exe`` program in + your Python ``Scripts`` subdirectory. Be sure to add this directory to your + ``PATH`` environment variable, if you haven't already done so. + + + RPM-Based Systems + ================= + + Install setuptools using the provided source RPM. The included ``.spec`` file + assumes you are installing using the default ``python`` executable, and is not + specific to a particular Python version. The ``easy_install`` executable will + be installed to a system ``bin`` directory such as ``/usr/bin``. + + If you wish to install to a location other than the default Python + installation's default ``site-packages`` directory (and ``$prefix/bin`` for + scripts), please use the ``.egg``-based installation approach described in the + following section. + + + Cygwin, Mac OS X, Linux, Other + ============================== + + 1. Download the appropriate egg for your version of Python (e.g. + ``setuptools-0.6c9-py2.4.egg``). Do NOT rename it. + + 2. Run it as if it were a shell script, e.g. ``sh setuptools-0.6c9-py2.4.egg``. + Setuptools will install itself using the matching version of Python (e.g. + ``python2.4``), and will place the ``easy_install`` executable in the + default location for installing Python scripts (as determined by the + standard distutils configuration files, or by the Python installation). + + If you want to install setuptools to somewhere other than ``site-packages`` or + your default distutils installation locations for libraries and scripts, you + may include EasyInstall command-line options such as ``--prefix``, + ``--install-dir``, and so on, following the ``.egg`` filename on the same + command line. For example:: + + sh setuptools-0.6c9-py2.4.egg --prefix=~ + + You can use ``--help`` to get a full options list, but we recommend consulting + the `EasyInstall manual`_ for detailed instructions, especially `the section + on custom installation locations`_. + + .. _EasyInstall manual: http://peak.telecommunity.com/DevCenter/EasyInstall + .. _the section on custom installation locations: http://peak.telecommunity.com/DevCenter/EasyInstall#custom-installation-locations + + + Cygwin Note + ----------- + + If you are trying to install setuptools for the **Windows** version of Python + (as opposed to the Cygwin version that lives in ``/usr/bin``), you must make + sure that an appropriate executable (``python2.3``, ``python2.4``, or + ``python2.5``) is on your **Cygwin** ``PATH`` when invoking the egg. For + example, doing the following at a Cygwin bash prompt will install setuptools + for the **Windows** Python found at ``C:\\Python24``:: + + ln -s /cygdrive/c/Python24/python.exe python2.4 + PATH=.:$PATH sh setuptools-0.6c9-py2.4.egg + rm python2.4 + + + Downloads + ========= + + All setuptools downloads can be found at `the project's home page in the Python + Package Index`_. Scroll to the very bottom of the page to find the links. + + .. _the project's home page in the Python Package Index: http://pypi.python.org/pypi/setuptools + + In addition to the PyPI downloads, the development version of ``setuptools`` + is available from the `Python SVN sandbox`_, and in-development versions of the + `0.6 branch`_ are available as well. + + .. _0.6 branch: http://svn.python.org/projects/sandbox/branches/setuptools-0.6/#egg=setuptools-dev06 + + .. _Python SVN sandbox: http://svn.python.org/projects/sandbox/trunk/setuptools/#egg=setuptools-dev + + -------------------------------- + Using Setuptools and EasyInstall + -------------------------------- + + Here are some of the available manuals, tutorials, and other resources for + learning about Setuptools, Python Eggs, and EasyInstall: + + * `The EasyInstall user's guide and reference manual`_ + * `The setuptools Developer's Guide`_ + * `The pkg_resources API reference`_ + * `Package Compatibility Notes`_ (user-maintained) + * `The Internal Structure of Python Eggs`_ + + Questions, comments, and bug reports should be directed to the `distutils-sig + mailing list`_. If you have written (or know of) any tutorials, documentation, + plug-ins, or other resources for setuptools users, please let us know about + them there, so this reference list can be updated. If you have working, + *tested* patches to correct problems or add features, you may submit them to + the `setuptools bug tracker`_. + + .. _setuptools bug tracker: http://bugs.python.org/setuptools/ + .. _Package Compatibility Notes: http://peak.telecommunity.com/DevCenter/PackageNotes + .. _The Internal Structure of Python Eggs: http://peak.telecommunity.com/DevCenter/EggFormats + .. _The setuptools Developer's Guide: http://peak.telecommunity.com/DevCenter/setuptools + .. _The pkg_resources API reference: http://peak.telecommunity.com/DevCenter/PkgResources + .. _The EasyInstall user's guide and reference manual: http://peak.telecommunity.com/DevCenter/EasyInstall + .. _distutils-sig mailing list: http://mail.python.org/pipermail/distutils-sig/ + + + ------- + Credits + ------- + + * The original design for the ``.egg`` format and the ``pkg_resources`` API was + co-created by Phillip Eby and Bob Ippolito. Bob also implemented the first + version of ``pkg_resources``, and supplied the OS X operating system version + compatibility algorithm. + + * Ian Bicking implemented many early "creature comfort" features of + easy_install, including support for downloading via Sourceforge and + Subversion repositories. Ian's comments on the Web-SIG about WSGI + application deployment also inspired the concept of "entry points" in eggs, + and he has given talks at PyCon and elsewhere to inform and educate the + community about eggs and setuptools. + + * Jim Fulton contributed time and effort to build automated tests of various + aspects of ``easy_install``, and supplied the doctests for the command-line + ``.exe`` wrappers on Windows. + + * Phillip J. Eby is the principal author and maintainer of setuptools, and + first proposed the idea of an importable binary distribution format for + Python application plug-ins. + + * Significant parts of the implementation of setuptools were funded by the Open + Source Applications Foundation, to provide a plug-in infrastructure for the + Chandler PIM application. In addition, many OSAF staffers (such as Mike + "Code Bear" Taylor) contributed their time and stress as guinea pigs for the + use of eggs and setuptools, even before eggs were "cool". (Thanks, guys!) + + +Keywords: CPAN PyPI distutils eggs package management +Platform: UNKNOWN +Classifier: Development Status :: 3 - Alpha +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Python Software Foundation License +Classifier: License :: OSI Approved :: Zope Public License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: System :: Archiving :: Packaging +Classifier: Topic :: System :: Systems Administration +Classifier: Topic :: Utilities +Requires: Foo diff --git a/Lib/packaging/tests/__init__.py b/Lib/packaging/tests/__init__.py new file mode 100644 index 000000000000..0b0e3c55438d --- /dev/null +++ b/Lib/packaging/tests/__init__.py @@ -0,0 +1,133 @@ +"""Test suite for packaging. + +This test suite consists of a collection of test modules in the +packaging.tests package. Each test module has a name starting with +'test' and contains a function test_suite(). The function is expected +to return an initialized unittest.TestSuite instance. + +Utility code is included in packaging.tests.support. +""" + +# Put this text back for the backport +#Always import unittest from this module, it will be the right version +#(standard library unittest for 3.2 and higher, third-party unittest2 +#elease for older versions). + +import os +import sys +import unittest +from test.support import TESTFN + +# XXX move helpers to support, add tests for them, remove things that +# duplicate test.support (or keep them for the backport; needs thinking) + +here = os.path.dirname(__file__) or os.curdir +verbose = 1 + +def test_suite(): + suite = unittest.TestSuite() + for fn in os.listdir(here): + if fn.startswith("test") and fn.endswith(".py"): + modname = "packaging.tests." + fn[:-3] + __import__(modname) + module = sys.modules[modname] + suite.addTest(module.test_suite()) + return suite + + +class Error(Exception): + """Base class for regression test exceptions.""" + + +class TestFailed(Error): + """Test failed.""" + + +class BasicTestRunner: + def run(self, test): + result = unittest.TestResult() + test(result) + return result + + +def _run_suite(suite, verbose_=1): + """Run tests from a unittest.TestSuite-derived class.""" + global verbose + verbose = verbose_ + if verbose_: + runner = unittest.TextTestRunner(sys.stdout, verbosity=2) + else: + runner = BasicTestRunner() + + result = runner.run(suite) + if not result.wasSuccessful(): + if len(result.errors) == 1 and not result.failures: + err = result.errors[0][1] + elif len(result.failures) == 1 and not result.errors: + err = result.failures[0][1] + else: + err = "errors occurred; run in verbose mode for details" + raise TestFailed(err) + + +def run_unittest(classes, verbose_=1): + """Run tests from unittest.TestCase-derived classes. + + Originally extracted from stdlib test.test_support and modified to + support unittest2. + """ + valid_types = (unittest.TestSuite, unittest.TestCase) + suite = unittest.TestSuite() + for cls in classes: + if isinstance(cls, str): + if cls in sys.modules: + suite.addTest(unittest.findTestCases(sys.modules[cls])) + else: + raise ValueError("str arguments must be keys in sys.modules") + elif isinstance(cls, valid_types): + suite.addTest(cls) + else: + suite.addTest(unittest.makeSuite(cls)) + _run_suite(suite, verbose_) + + +def reap_children(): + """Use this function at the end of test_main() whenever sub-processes + are started. This will help ensure that no extra children (zombies) + stick around to hog resources and create problems when looking + for refleaks. + + Extracted from stdlib test.support. + """ + + # Reap all our dead child processes so we don't leave zombies around. + # These hog resources and might be causing some of the buildbots to die. + if hasattr(os, 'waitpid'): + any_process = -1 + while True: + try: + # This will raise an exception on Windows. That's ok. + pid, status = os.waitpid(any_process, os.WNOHANG) + if pid == 0: + break + except: + break + + +def captured_stdout(func, *args, **kw): + import io + orig_stdout = getattr(sys, 'stdout') + setattr(sys, 'stdout', io.StringIO()) + try: + res = func(*args, **kw) + sys.stdout.seek(0) + return res, sys.stdout.read() + finally: + setattr(sys, 'stdout', orig_stdout) + + +def unload(name): + try: + del sys.modules[name] + except KeyError: + pass diff --git a/Lib/packaging/tests/__main__.py b/Lib/packaging/tests/__main__.py new file mode 100644 index 000000000000..68ee229e5ad7 --- /dev/null +++ b/Lib/packaging/tests/__main__.py @@ -0,0 +1,20 @@ +"""Packaging test suite runner.""" + +# Ripped from importlib tests, thanks Brett! + +import os +import sys +import unittest +from test.support import run_unittest, reap_children + + +def test_main(): + start_dir = os.path.dirname(__file__) + top_dir = os.path.dirname(os.path.dirname(start_dir)) + test_loader = unittest.TestLoader() + run_unittest(test_loader.discover(start_dir, top_level_dir=top_dir)) + reap_children() + + +if __name__ == '__main__': + test_main() diff --git a/Lib/packaging/tests/fake_dists/babar-0.1.dist-info/INSTALLER b/Lib/packaging/tests/fake_dists/babar-0.1.dist-info/INSTALLER new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Lib/packaging/tests/fake_dists/babar-0.1.dist-info/METADATA b/Lib/packaging/tests/fake_dists/babar-0.1.dist-info/METADATA new file mode 100644 index 000000000000..65e839a01fae --- /dev/null +++ b/Lib/packaging/tests/fake_dists/babar-0.1.dist-info/METADATA @@ -0,0 +1,4 @@ +Metadata-version: 1.2 +Name: babar +Version: 0.1 +Author: FELD Boris \ No newline at end of file diff --git a/Lib/packaging/tests/fake_dists/babar-0.1.dist-info/RECORD b/Lib/packaging/tests/fake_dists/babar-0.1.dist-info/RECORD new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Lib/packaging/tests/fake_dists/babar-0.1.dist-info/REQUESTED b/Lib/packaging/tests/fake_dists/babar-0.1.dist-info/REQUESTED new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Lib/packaging/tests/fake_dists/babar-0.1.dist-info/RESOURCES b/Lib/packaging/tests/fake_dists/babar-0.1.dist-info/RESOURCES new file mode 100644 index 000000000000..5d0da494a5e2 --- /dev/null +++ b/Lib/packaging/tests/fake_dists/babar-0.1.dist-info/RESOURCES @@ -0,0 +1,2 @@ +babar.png,babar.png +babar.cfg,babar.cfg \ No newline at end of file diff --git a/Lib/packaging/tests/fake_dists/babar.cfg b/Lib/packaging/tests/fake_dists/babar.cfg new file mode 100644 index 000000000000..ecd6efe9a691 --- /dev/null +++ b/Lib/packaging/tests/fake_dists/babar.cfg @@ -0,0 +1 @@ +Config \ No newline at end of file diff --git a/Lib/packaging/tests/fake_dists/babar.png b/Lib/packaging/tests/fake_dists/babar.png new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Lib/packaging/tests/fake_dists/bacon-0.1.egg-info/PKG-INFO b/Lib/packaging/tests/fake_dists/bacon-0.1.egg-info/PKG-INFO new file mode 100644 index 000000000000..a176dfdfde16 --- /dev/null +++ b/Lib/packaging/tests/fake_dists/bacon-0.1.egg-info/PKG-INFO @@ -0,0 +1,6 @@ +Metadata-Version: 1.2 +Name: bacon +Version: 0.1 +Provides-Dist: truffles (2.0) +Provides-Dist: bacon (0.1) +Obsoletes-Dist: truffles (>=0.9,<=1.5) diff --git a/Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/PKG-INFO b/Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/PKG-INFO new file mode 100644 index 000000000000..a7e118a88f3b --- /dev/null +++ b/Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/PKG-INFO @@ -0,0 +1,18 @@ +Metadata-Version: 1.0 +Name: banana +Version: 0.4 +Summary: A yellow fruit +Home-page: http://en.wikipedia.org/wiki/Banana +Author: Josip Djolonga +Author-email: foo@nbar.com +License: BSD +Description: A fruit +Keywords: foo bar +Platform: UNKNOWN +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Science/Research +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Scientific/Engineering :: GIS diff --git a/Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/SOURCES.txt b/Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/SOURCES.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/dependency_links.txt b/Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/dependency_links.txt new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/entry_points.txt b/Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/entry_points.txt new file mode 100644 index 000000000000..5d3e5f68c785 --- /dev/null +++ b/Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/entry_points.txt @@ -0,0 +1,3 @@ + + # -*- Entry points: -*- + \ No newline at end of file diff --git a/Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/not-zip-safe b/Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/not-zip-safe new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/not-zip-safe @@ -0,0 +1 @@ + diff --git a/Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/requires.txt b/Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/requires.txt new file mode 100644 index 000000000000..4354305659c9 --- /dev/null +++ b/Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/requires.txt @@ -0,0 +1,6 @@ +# this should be ignored + +strawberry >=0.5 + +[section ignored] +foo ==0.5 diff --git a/Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/top_level.txt b/Lib/packaging/tests/fake_dists/banana-0.4.egg/EGG-INFO/top_level.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Lib/packaging/tests/fake_dists/cheese-2.0.2.egg-info b/Lib/packaging/tests/fake_dists/cheese-2.0.2.egg-info new file mode 100644 index 000000000000..27cbe3014dd0 --- /dev/null +++ b/Lib/packaging/tests/fake_dists/cheese-2.0.2.egg-info @@ -0,0 +1,5 @@ +Metadata-Version: 1.2 +Name: cheese +Version: 2.0.2 +Provides-Dist: truffles (1.0.2) +Obsoletes-Dist: truffles (!=1.2,<=2.0) diff --git a/Lib/packaging/tests/fake_dists/choxie-2.0.0.9.dist-info/INSTALLER b/Lib/packaging/tests/fake_dists/choxie-2.0.0.9.dist-info/INSTALLER new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Lib/packaging/tests/fake_dists/choxie-2.0.0.9.dist-info/METADATA b/Lib/packaging/tests/fake_dists/choxie-2.0.0.9.dist-info/METADATA new file mode 100644 index 000000000000..418929eccae1 --- /dev/null +++ b/Lib/packaging/tests/fake_dists/choxie-2.0.0.9.dist-info/METADATA @@ -0,0 +1,9 @@ +Metadata-Version: 1.2 +Name: choxie +Version: 2.0.0.9 +Summary: Chocolate with a kick! +Requires-Dist: towel-stuff (0.1) +Requires-Dist: nut +Provides-Dist: truffles (1.0) +Obsoletes-Dist: truffles (<=0.8,>=0.5) +Obsoletes-Dist: truffles (<=0.9,>=0.6) diff --git a/Lib/packaging/tests/fake_dists/choxie-2.0.0.9.dist-info/RECORD b/Lib/packaging/tests/fake_dists/choxie-2.0.0.9.dist-info/RECORD new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Lib/packaging/tests/fake_dists/choxie-2.0.0.9.dist-info/REQUESTED b/Lib/packaging/tests/fake_dists/choxie-2.0.0.9.dist-info/REQUESTED new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Lib/packaging/tests/fake_dists/choxie-2.0.0.9/choxie/__init__.py b/Lib/packaging/tests/fake_dists/choxie-2.0.0.9/choxie/__init__.py new file mode 100644 index 000000000000..40a96afc6ff0 --- /dev/null +++ b/Lib/packaging/tests/fake_dists/choxie-2.0.0.9/choxie/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/Lib/packaging/tests/fake_dists/choxie-2.0.0.9/choxie/chocolate.py b/Lib/packaging/tests/fake_dists/choxie-2.0.0.9/choxie/chocolate.py new file mode 100644 index 000000000000..c4027f36c1ad --- /dev/null +++ b/Lib/packaging/tests/fake_dists/choxie-2.0.0.9/choxie/chocolate.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +from towel_stuff import Towel + +class Chocolate(object): + """A piece of chocolate.""" + + def wrap_with_towel(self): + towel = Towel() + towel.wrap(self) + return towel diff --git a/Lib/packaging/tests/fake_dists/choxie-2.0.0.9/truffles.py b/Lib/packaging/tests/fake_dists/choxie-2.0.0.9/truffles.py new file mode 100644 index 000000000000..342b8ea851df --- /dev/null +++ b/Lib/packaging/tests/fake_dists/choxie-2.0.0.9/truffles.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +from choxie.chocolate import Chocolate + +class Truffle(Chocolate): + """A truffle.""" diff --git a/Lib/packaging/tests/fake_dists/coconuts-aster-10.3.egg-info/PKG-INFO b/Lib/packaging/tests/fake_dists/coconuts-aster-10.3.egg-info/PKG-INFO new file mode 100644 index 000000000000..499a083e40fa --- /dev/null +++ b/Lib/packaging/tests/fake_dists/coconuts-aster-10.3.egg-info/PKG-INFO @@ -0,0 +1,5 @@ +Metadata-Version: 1.2 +Name: coconuts-aster +Version: 10.3 +Provides-Dist: strawberry (0.6) +Provides-Dist: banana (0.4) diff --git a/Lib/packaging/tests/fake_dists/grammar-1.0a4.dist-info/INSTALLER b/Lib/packaging/tests/fake_dists/grammar-1.0a4.dist-info/INSTALLER new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Lib/packaging/tests/fake_dists/grammar-1.0a4.dist-info/METADATA b/Lib/packaging/tests/fake_dists/grammar-1.0a4.dist-info/METADATA new file mode 100644 index 000000000000..0b99f5249a1d --- /dev/null +++ b/Lib/packaging/tests/fake_dists/grammar-1.0a4.dist-info/METADATA @@ -0,0 +1,5 @@ +Metadata-Version: 1.2 +Name: grammar +Version: 1.0a4 +Requires-Dist: truffles (>=1.2) +Author: Sherlock Holmes diff --git a/Lib/packaging/tests/fake_dists/grammar-1.0a4.dist-info/RECORD b/Lib/packaging/tests/fake_dists/grammar-1.0a4.dist-info/RECORD new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Lib/packaging/tests/fake_dists/grammar-1.0a4.dist-info/REQUESTED b/Lib/packaging/tests/fake_dists/grammar-1.0a4.dist-info/REQUESTED new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Lib/packaging/tests/fake_dists/grammar-1.0a4/grammar/__init__.py b/Lib/packaging/tests/fake_dists/grammar-1.0a4/grammar/__init__.py new file mode 100644 index 000000000000..40a96afc6ff0 --- /dev/null +++ b/Lib/packaging/tests/fake_dists/grammar-1.0a4/grammar/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/Lib/packaging/tests/fake_dists/grammar-1.0a4/grammar/utils.py b/Lib/packaging/tests/fake_dists/grammar-1.0a4/grammar/utils.py new file mode 100644 index 000000000000..66ba796c3d14 --- /dev/null +++ b/Lib/packaging/tests/fake_dists/grammar-1.0a4/grammar/utils.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from random import randint + +def is_valid_grammar(sentence): + if randint(0, 10) < 2: + return False + else: + return True diff --git a/Lib/packaging/tests/fake_dists/nut-funkyversion.egg-info b/Lib/packaging/tests/fake_dists/nut-funkyversion.egg-info new file mode 100644 index 000000000000..0c58ec1ce92b --- /dev/null +++ b/Lib/packaging/tests/fake_dists/nut-funkyversion.egg-info @@ -0,0 +1,3 @@ +Metadata-Version: 1.2 +Name: nut +Version: funkyversion diff --git a/Lib/packaging/tests/fake_dists/strawberry-0.6.egg b/Lib/packaging/tests/fake_dists/strawberry-0.6.egg new file mode 100644 index 0000000000000000000000000000000000000000..6d160e8b161031ae52638514843592187925b757 GIT binary patch literal 1402 zc-m7|)KALH(=X28%1l#;R!B%nEKbc!%uQ8LF-TCbRZuEUEh#N1$r9UQWv|0ty0Ufu2)H%gjmDgJ}xL0otCbPy`8@OrXVy z$=M1e`3iV~M*-+)g_5F5g~as4%sjABpm0h{1UV)xlPkcRnT3l11j?rGw_!j6Vhl12 zuI}!-o_=or`X%`V@j0nwsX2Nj6(yk|oD9qiubF*7xU_i9RnLxBqqeU~_GC)B*8Q%^m+PITr6Z&8t31F+o4>xK^{d-p7?^ zx~J#&ZkWuS9 isinstance(x, T) + type(x) is T -> isinstance(x, T) + type(x) != T -> not isinstance(x, T) + type(x) is not T -> not isinstance(x, T) + +* Change "while 1:" into "while True:". + +* Change both + + v = list(EXPR) + v.sort() + foo(v) + +and the more general + + v = EXPR + v.sort() + foo(v) + +into + + v = sorted(EXPR) + foo(v) +""" +# Author: Jacques Frechet, Collin Winter + +# Local imports +from lib2to3 import fixer_base +from lib2to3.fixer_util import Call, Comma, Name, Node, syms + +CMP = "(n='!=' | '==' | 'is' | n=comp_op< 'is' 'not' >)" +TYPE = "power< 'type' trailer< '(' x=any ')' > >" + +class FixIdioms(fixer_base.BaseFix): + + explicit = False # The user must ask for this fixer + + PATTERN = r""" + isinstance=comparison< %s %s T=any > + | + isinstance=comparison< T=any %s %s > + | + while_stmt< 'while' while='1' ':' any+ > + | + sorted=any< + any* + simple_stmt< + expr_stmt< id1=any '=' + power< list='list' trailer< '(' (not arglist) any ')' > > + > + '\n' + > + sort= + simple_stmt< + power< id2=any + trailer< '.' 'sort' > trailer< '(' ')' > + > + '\n' + > + next=any* + > + | + sorted=any< + any* + simple_stmt< expr_stmt< id1=any '=' expr=any > '\n' > + sort= + simple_stmt< + power< id2=any + trailer< '.' 'sort' > trailer< '(' ')' > + > + '\n' + > + next=any* + > + """ % (TYPE, CMP, CMP, TYPE) + + def match(self, node): + r = super(FixIdioms, self).match(node) + # If we've matched one of the sort/sorted subpatterns above, we + # want to reject matches where the initial assignment and the + # subsequent .sort() call involve different identifiers. + if r and "sorted" in r: + if r["id1"] == r["id2"]: + return r + return None + return r + + def transform(self, node, results): + if "isinstance" in results: + return self.transform_isinstance(node, results) + elif "while" in results: + return self.transform_while(node, results) + elif "sorted" in results: + return self.transform_sort(node, results) + else: + raise RuntimeError("Invalid match") + + def transform_isinstance(self, node, results): + x = results["x"].clone() # The thing inside of type() + T = results["T"].clone() # The type being compared against + x.prefix = "" + T.prefix = " " + test = Call(Name("isinstance"), [x, Comma(), T]) + if "n" in results: + test.prefix = " " + test = Node(syms.not_test, [Name("not"), test]) + test.prefix = node.prefix + return test + + def transform_while(self, node, results): + one = results["while"] + one.replace(Name("True", prefix=one.prefix)) + + def transform_sort(self, node, results): + sort_stmt = results["sort"] + next_stmt = results["next"] + list_call = results.get("list") + simple_expr = results.get("expr") + + if list_call: + list_call.replace(Name("sorted", prefix=list_call.prefix)) + elif simple_expr: + new = simple_expr.clone() + new.prefix = "" + simple_expr.replace(Call(Name("sorted"), [new], + prefix=simple_expr.prefix)) + else: + raise RuntimeError("should not have reached here") + sort_stmt.remove() + if next_stmt: + next_stmt[0].prefix = sort_stmt._prefix diff --git a/Lib/packaging/tests/pypi_server.py b/Lib/packaging/tests/pypi_server.py new file mode 100644 index 000000000000..cc5fcca05eff --- /dev/null +++ b/Lib/packaging/tests/pypi_server.py @@ -0,0 +1,444 @@ +"""Mock PyPI Server implementation, to use in tests. + +This module also provides a simple test case to extend if you need to use +the PyPIServer all along your test case. Be sure to read the documentation +before any use. + +XXX TODO: + +The mock server can handle simple HTTP request (to simulate a simple index) or +XMLRPC requests, over HTTP. Both does not have the same intergface to deal +with, and I think it's a pain. + +A good idea could be to re-think a bit the way dstributions are handled in the +mock server. As it should return malformed HTML pages, we need to keep the +static behavior. + +I think of something like that: + + >>> server = PyPIMockServer() + >>> server.startHTTP() + >>> server.startXMLRPC() + +Then, the server must have only one port to rely on, eg. + + >>> server.fulladress() + "http://ip:port/" + +It could be simple to have one HTTP server, relaying the requests to the two +implementations (static HTTP and XMLRPC over HTTP). +""" + +import os +import queue +import select +import socket +import threading +import socketserver +from functools import wraps +from http.server import HTTPServer, SimpleHTTPRequestHandler +from xmlrpc.server import SimpleXMLRPCServer + +from packaging.tests import unittest + +PYPI_DEFAULT_STATIC_PATH = os.path.join( + os.path.dirname(os.path.abspath(__file__)), 'pypiserver') + + +def use_xmlrpc_server(*server_args, **server_kwargs): + server_kwargs['serve_xmlrpc'] = True + return use_pypi_server(*server_args, **server_kwargs) + + +def use_http_server(*server_args, **server_kwargs): + server_kwargs['serve_xmlrpc'] = False + return use_pypi_server(*server_args, **server_kwargs) + + +def use_pypi_server(*server_args, **server_kwargs): + """Decorator to make use of the PyPIServer for test methods, + just when needed, and not for the entire duration of the testcase. + """ + def wrapper(func): + @wraps(func) + def wrapped(*args, **kwargs): + server = PyPIServer(*server_args, **server_kwargs) + server.start() + try: + func(server=server, *args, **kwargs) + finally: + server.stop() + return wrapped + return wrapper + + +class PyPIServerTestCase(unittest.TestCase): + + def setUp(self): + super(PyPIServerTestCase, self).setUp() + self.pypi = PyPIServer() + self.pypi.start() + self.addCleanup(self.pypi.stop) + + +class PyPIServer(threading.Thread): + """PyPI Mocked server. + Provides a mocked version of the PyPI API's, to ease tests. + + Support serving static content and serving previously given text. + """ + + def __init__(self, test_static_path=None, + static_filesystem_paths=["default"], + static_uri_paths=["simple", "packages"], serve_xmlrpc=False): + """Initialize the server. + + Default behavior is to start the HTTP server. You can either start the + xmlrpc server by setting xmlrpc to True. Caution: Only one server will + be started. + + static_uri_paths and static_base_path are parameters used to provides + respectively the http_paths to serve statically, and where to find the + matching files on the filesystem. + """ + # we want to launch the server in a new dedicated thread, to not freeze + # tests. + threading.Thread.__init__(self) + self._run = True + self._serve_xmlrpc = serve_xmlrpc + + #TODO allow to serve XMLRPC and HTTP static files at the same time. + if not self._serve_xmlrpc: + self.server = HTTPServer(('127.0.0.1', 0), PyPIRequestHandler) + self.server.RequestHandlerClass.pypi_server = self + + self.request_queue = queue.Queue() + self._requests = [] + self.default_response_status = 404 + self.default_response_headers = [('Content-type', 'text/plain')] + self.default_response_data = "The page does not exists" + + # initialize static paths / filesystems + self.static_uri_paths = static_uri_paths + + # append the static paths defined locally + if test_static_path is not None: + static_filesystem_paths.append(test_static_path) + self.static_filesystem_paths = [ + PYPI_DEFAULT_STATIC_PATH + "/" + path + for path in static_filesystem_paths] + else: + # XMLRPC server + self.server = PyPIXMLRPCServer(('127.0.0.1', 0)) + self.xmlrpc = XMLRPCMockIndex() + # register the xmlrpc methods + self.server.register_introspection_functions() + self.server.register_instance(self.xmlrpc) + + self.address = (self.server.server_name, self.server.server_port) + # to not have unwanted outputs. + self.server.RequestHandlerClass.log_request = lambda *_: None + + def run(self): + # loop because we can't stop it otherwise, for python < 2.6 + while self._run: + r, w, e = select.select([self.server], [], [], 0.5) + if r: + self.server.handle_request() + + def stop(self): + """self shutdown is not supported for python < 2.6""" + self._run = False + + def get_next_response(self): + return (self.default_response_status, + self.default_response_headers, + self.default_response_data) + + @property + def requests(self): + """Use this property to get all requests that have been made + to the server + """ + while True: + try: + self._requests.append(self.request_queue.get_nowait()) + except queue.Empty: + break + return self._requests + + @property + def full_address(self): + return "http://%s:%s" % self.address + + +class PyPIRequestHandler(SimpleHTTPRequestHandler): + # we need to access the pypi server while serving the content + pypi_server = None + + def serve_request(self): + """Serve the content. + + Also record the requests to be accessed later. If trying to access an + url matching a static uri, serve static content, otherwise serve + what is provided by the `get_next_response` method. + + If nothing is defined there, return a 404 header. + """ + # record the request. Read the input only on PUT or POST requests + if self.command in ("PUT", "POST"): + if 'content-length' in self.headers: + request_data = self.rfile.read( + int(self.headers['content-length'])) + else: + request_data = self.rfile.read() + + elif self.command in ("GET", "DELETE"): + request_data = '' + + self.pypi_server.request_queue.put((self, request_data)) + + # serve the content from local disc if we request an URL beginning + # by a pattern defined in `static_paths` + url_parts = self.path.split("/") + if (len(url_parts) > 1 and + url_parts[1] in self.pypi_server.static_uri_paths): + data = None + # always take the last first. + fs_paths = [] + fs_paths.extend(self.pypi_server.static_filesystem_paths) + fs_paths.reverse() + relative_path = self.path + for fs_path in fs_paths: + try: + if self.path.endswith("/"): + relative_path += "index.html" + + if relative_path.endswith('.tar.gz'): + with open(fs_path + relative_path, 'br') as file: + data = file.read() + headers = [('Content-type', 'application/x-gtar')] + else: + with open(fs_path + relative_path) as file: + data = file.read().encode() + headers = [('Content-type', 'text/html')] + + self.make_response(data, headers=headers) + + except IOError: + pass + + if data is None: + self.make_response("Not found", 404) + + # otherwise serve the content from get_next_response + else: + # send back a response + status, headers, data = self.pypi_server.get_next_response() + self.make_response(data, status, headers) + + do_POST = do_GET = do_DELETE = do_PUT = serve_request + + def make_response(self, data, status=200, + headers=[('Content-type', 'text/html')]): + """Send the response to the HTTP client""" + if not isinstance(status, int): + try: + status = int(status) + except ValueError: + # we probably got something like YYY Codename. + # Just get the first 3 digits + status = int(status[:3]) + + self.send_response(status) + for header, value in headers: + self.send_header(header, value) + self.end_headers() + + if type(data) is str: + data = data.encode() + + self.wfile.write(data) + + +class PyPIXMLRPCServer(SimpleXMLRPCServer): + def server_bind(self): + """Override server_bind to store the server name.""" + socketserver.TCPServer.server_bind(self) + host, port = self.socket.getsockname()[:2] + self.server_name = socket.getfqdn(host) + self.server_port = port + + +class MockDist: + """Fake distribution, used in the Mock PyPI Server""" + + def __init__(self, name, version="1.0", hidden=False, url="http://url/", + type="sdist", filename="", size=10000, + digest="123456", downloads=7, has_sig=False, + python_version="source", comment="comment", + author="John Doe", author_email="john@doe.name", + maintainer="Main Tayner", maintainer_email="maintainer_mail", + project_url="http://project_url/", homepage="http://homepage/", + keywords="", platform="UNKNOWN", classifiers=[], licence="", + description="Description", summary="Summary", stable_version="", + ordering="", documentation_id="", code_kwalitee_id="", + installability_id="", obsoletes=[], obsoletes_dist=[], + provides=[], provides_dist=[], requires=[], requires_dist=[], + requires_external=[], requires_python=""): + + # basic fields + self.name = name + self.version = version + self.hidden = hidden + + # URL infos + self.url = url + self.digest = digest + self.downloads = downloads + self.has_sig = has_sig + self.python_version = python_version + self.comment = comment + self.type = type + + # metadata + self.author = author + self.author_email = author_email + self.maintainer = maintainer + self.maintainer_email = maintainer_email + self.project_url = project_url + self.homepage = homepage + self.keywords = keywords + self.platform = platform + self.classifiers = classifiers + self.licence = licence + self.description = description + self.summary = summary + self.stable_version = stable_version + self.ordering = ordering + self.cheesecake_documentation_id = documentation_id + self.cheesecake_code_kwalitee_id = code_kwalitee_id + self.cheesecake_installability_id = installability_id + + self.obsoletes = obsoletes + self.obsoletes_dist = obsoletes_dist + self.provides = provides + self.provides_dist = provides_dist + self.requires = requires + self.requires_dist = requires_dist + self.requires_external = requires_external + self.requires_python = requires_python + + def url_infos(self): + return { + 'url': self.url, + 'packagetype': self.type, + 'filename': 'filename.tar.gz', + 'size': '6000', + 'md5_digest': self.digest, + 'downloads': self.downloads, + 'has_sig': self.has_sig, + 'python_version': self.python_version, + 'comment_text': self.comment, + } + + def metadata(self): + return { + 'maintainer': self.maintainer, + 'project_url': [self.project_url], + 'maintainer_email': self.maintainer_email, + 'cheesecake_code_kwalitee_id': self.cheesecake_code_kwalitee_id, + 'keywords': self.keywords, + 'obsoletes_dist': self.obsoletes_dist, + 'requires_external': self.requires_external, + 'author': self.author, + 'author_email': self.author_email, + 'download_url': self.url, + 'platform': self.platform, + 'version': self.version, + 'obsoletes': self.obsoletes, + 'provides': self.provides, + 'cheesecake_documentation_id': self.cheesecake_documentation_id, + '_pypi_hidden': self.hidden, + 'description': self.description, + '_pypi_ordering': 19, + 'requires_dist': self.requires_dist, + 'requires_python': self.requires_python, + 'classifiers': [], + 'name': self.name, + 'licence': self.licence, + 'summary': self.summary, + 'home_page': self.homepage, + 'stable_version': self.stable_version, + 'provides_dist': self.provides_dist or "%s (%s)" % (self.name, + self.version), + 'requires': self.requires, + 'cheesecake_installability_id': self.cheesecake_installability_id, + } + + def search_result(self): + return { + '_pypi_ordering': 0, + 'version': self.version, + 'name': self.name, + 'summary': self.summary, + } + + +class XMLRPCMockIndex: + """Mock XMLRPC server""" + + def __init__(self, dists=[]): + self._dists = dists + self._search_result = [] + + def add_distributions(self, dists): + for dist in dists: + self._dists.append(MockDist(**dist)) + + def set_distributions(self, dists): + self._dists = [] + self.add_distributions(dists) + + def set_search_result(self, result): + """set a predefined search result""" + self._search_result = result + + def _get_search_results(self): + results = [] + for name in self._search_result: + found_dist = [d for d in self._dists if d.name == name] + if found_dist: + results.append(found_dist[0]) + else: + dist = MockDist(name) + results.append(dist) + self._dists.append(dist) + return [r.search_result() for r in results] + + def list_packages(self): + return [d.name for d in self._dists] + + def package_releases(self, package_name, show_hidden=False): + if show_hidden: + # return all + return [d.version for d in self._dists if d.name == package_name] + else: + # return only un-hidden + return [d.version for d in self._dists if d.name == package_name + and not d.hidden] + + def release_urls(self, package_name, version): + return [d.url_infos() for d in self._dists + if d.name == package_name and d.version == version] + + def release_data(self, package_name, version): + release = [d for d in self._dists + if d.name == package_name and d.version == version] + if release: + return release[0].metadata() + else: + return {} + + def search(self, spec, operator="and"): + return self._get_search_results() diff --git a/Lib/packaging/tests/pypi_test_server.py b/Lib/packaging/tests/pypi_test_server.py new file mode 100644 index 000000000000..8c8c641eb0d4 --- /dev/null +++ b/Lib/packaging/tests/pypi_test_server.py @@ -0,0 +1,59 @@ +"""Test PyPI Server implementation at testpypi.python.org, to use in tests. + +This is a drop-in replacement for the mock pypi server for testing against a +real pypi server hosted by python.org especially for testing against. +""" + +import unittest + +PYPI_DEFAULT_STATIC_PATH = None + + +def use_xmlrpc_server(*server_args, **server_kwargs): + server_kwargs['serve_xmlrpc'] = True + return use_pypi_server(*server_args, **server_kwargs) + + +def use_http_server(*server_args, **server_kwargs): + server_kwargs['serve_xmlrpc'] = False + return use_pypi_server(*server_args, **server_kwargs) + + +def use_pypi_server(*server_args, **server_kwargs): + """Decorator to make use of the PyPIServer for test methods, + just when needed, and not for the entire duration of the testcase. + """ + def wrapper(func): + def wrapped(*args, **kwargs): + server = PyPIServer(*server_args, **server_kwargs) + func(server=server, *args, **kwargs) + return wrapped + return wrapper + + +class PyPIServerTestCase(unittest.TestCase): + + def setUp(self): + super(PyPIServerTestCase, self).setUp() + self.pypi = PyPIServer() + self.pypi.start() + self.addCleanup(self.pypi.stop) + + +class PyPIServer: + """Shim to access testpypi.python.org, for testing a real server.""" + + def __init__(self, test_static_path=None, + static_filesystem_paths=["default"], + static_uri_paths=["simple"], serve_xmlrpc=False): + self.address = ('testpypi.python.org', '80') + + def start(self): + pass + + def stop(self): + pass + + @property + def full_address(self): + return "http://%s:%s" % self.address diff --git a/Lib/packaging/tests/pypiserver/downloads_with_md5/packages/source/f/foobar/foobar-0.1.tar.gz b/Lib/packaging/tests/pypiserver/downloads_with_md5/packages/source/f/foobar/foobar-0.1.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..333961eb18a6e7db80fefd41c339ab218d5180c4 GIT binary patch literal 110 zc-oWi=3uy!>FUeC{PvtR-ysJc)&sVu?9yZ7`(A1Di)P(6s!I71JWZ;--fWND`LA)=lAmk-7Jbj=XMlnFEsQ#U Kd|Vkc7#IK&xGYxy literal 0 Hc-jL100001 diff --git a/Lib/packaging/tests/pypiserver/downloads_with_md5/simple/badmd5/badmd5-0.1.tar.gz b/Lib/packaging/tests/pypiserver/downloads_with_md5/simple/badmd5/badmd5-0.1.tar.gz new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Lib/packaging/tests/pypiserver/downloads_with_md5/simple/badmd5/index.html b/Lib/packaging/tests/pypiserver/downloads_with_md5/simple/badmd5/index.html new file mode 100644 index 000000000000..b89f1bdb8468 --- /dev/null +++ b/Lib/packaging/tests/pypiserver/downloads_with_md5/simple/badmd5/index.html @@ -0,0 +1,3 @@ + +badmd5-0.1.tar.gz
+ diff --git a/Lib/packaging/tests/pypiserver/downloads_with_md5/simple/foobar/index.html b/Lib/packaging/tests/pypiserver/downloads_with_md5/simple/foobar/index.html new file mode 100644 index 000000000000..9e42b16d23c7 --- /dev/null +++ b/Lib/packaging/tests/pypiserver/downloads_with_md5/simple/foobar/index.html @@ -0,0 +1,3 @@ + +foobar-0.1.tar.gz
+ diff --git a/Lib/packaging/tests/pypiserver/downloads_with_md5/simple/index.html b/Lib/packaging/tests/pypiserver/downloads_with_md5/simple/index.html new file mode 100644 index 000000000000..9baee0479e9b --- /dev/null +++ b/Lib/packaging/tests/pypiserver/downloads_with_md5/simple/index.html @@ -0,0 +1,2 @@ +foobar/ +badmd5/ diff --git a/Lib/packaging/tests/pypiserver/foo_bar_baz/simple/bar/index.html b/Lib/packaging/tests/pypiserver/foo_bar_baz/simple/bar/index.html new file mode 100644 index 000000000000..c3d42c56925b --- /dev/null +++ b/Lib/packaging/tests/pypiserver/foo_bar_baz/simple/bar/index.html @@ -0,0 +1,6 @@ +Links for bar

Links for bar

+bar-1.0.tar.gz
+bar-1.0.1.tar.gz
+bar-2.0.tar.gz
+bar-2.0.1.tar.gz
+ diff --git a/Lib/packaging/tests/pypiserver/foo_bar_baz/simple/baz/index.html b/Lib/packaging/tests/pypiserver/foo_bar_baz/simple/baz/index.html new file mode 100644 index 000000000000..4f34312a30c9 --- /dev/null +++ b/Lib/packaging/tests/pypiserver/foo_bar_baz/simple/baz/index.html @@ -0,0 +1,6 @@ +Links for baz

Links for baz

+baz-1.0.tar.gz
+baz-1.0.1.tar.gz
+baz-2.0.tar.gz
+baz-2.0.1.tar.gz
+ diff --git a/Lib/packaging/tests/pypiserver/foo_bar_baz/simple/foo/index.html b/Lib/packaging/tests/pypiserver/foo_bar_baz/simple/foo/index.html new file mode 100644 index 000000000000..0565e11bddcf --- /dev/null +++ b/Lib/packaging/tests/pypiserver/foo_bar_baz/simple/foo/index.html @@ -0,0 +1,6 @@ +Links for foo

Links for foo

+foo-1.0.tar.gz
+foo-1.0.1.tar.gz
+foo-2.0.tar.gz
+foo-2.0.1.tar.gz
+ diff --git a/Lib/packaging/tests/pypiserver/foo_bar_baz/simple/index.html b/Lib/packaging/tests/pypiserver/foo_bar_baz/simple/index.html new file mode 100644 index 000000000000..a70cfd345fbc --- /dev/null +++ b/Lib/packaging/tests/pypiserver/foo_bar_baz/simple/index.html @@ -0,0 +1,3 @@ +foo/ +bar/ +baz/ diff --git a/Lib/packaging/tests/pypiserver/project_list/simple/index.html b/Lib/packaging/tests/pypiserver/project_list/simple/index.html new file mode 100644 index 000000000000..b36d728a0bbd --- /dev/null +++ b/Lib/packaging/tests/pypiserver/project_list/simple/index.html @@ -0,0 +1,5 @@ +FooBar-bar +Foobar-baz +Baz-FooBar +Baz +Foo diff --git a/Lib/packaging/tests/pypiserver/test_found_links/simple/foobar/index.html b/Lib/packaging/tests/pypiserver/test_found_links/simple/foobar/index.html new file mode 100644 index 000000000000..a282a4ea3108 --- /dev/null +++ b/Lib/packaging/tests/pypiserver/test_found_links/simple/foobar/index.html @@ -0,0 +1,6 @@ +Links for Foobar

Links for Foobar

+Foobar-1.0.tar.gz
+Foobar-1.0.1.tar.gz
+Foobar-2.0.tar.gz
+Foobar-2.0.1.tar.gz
+ diff --git a/Lib/packaging/tests/pypiserver/test_found_links/simple/index.html b/Lib/packaging/tests/pypiserver/test_found_links/simple/index.html new file mode 100644 index 000000000000..a1a7bb72825d --- /dev/null +++ b/Lib/packaging/tests/pypiserver/test_found_links/simple/index.html @@ -0,0 +1 @@ +foobar/ diff --git a/Lib/packaging/tests/pypiserver/test_pypi_server/external/index.html b/Lib/packaging/tests/pypiserver/test_pypi_server/external/index.html new file mode 100644 index 000000000000..265ee0af9509 --- /dev/null +++ b/Lib/packaging/tests/pypiserver/test_pypi_server/external/index.html @@ -0,0 +1 @@ +index.html from external server diff --git a/Lib/packaging/tests/pypiserver/test_pypi_server/simple/index.html b/Lib/packaging/tests/pypiserver/test_pypi_server/simple/index.html new file mode 100644 index 000000000000..6f976676f559 --- /dev/null +++ b/Lib/packaging/tests/pypiserver/test_pypi_server/simple/index.html @@ -0,0 +1 @@ +Yeah diff --git a/Lib/packaging/tests/pypiserver/with_externals/external/external.html b/Lib/packaging/tests/pypiserver/with_externals/external/external.html new file mode 100644 index 000000000000..92e4702f634d --- /dev/null +++ b/Lib/packaging/tests/pypiserver/with_externals/external/external.html @@ -0,0 +1,3 @@ + +bad old link + diff --git a/Lib/packaging/tests/pypiserver/with_externals/simple/foobar/index.html b/Lib/packaging/tests/pypiserver/with_externals/simple/foobar/index.html new file mode 100644 index 000000000000..b100a26542ea --- /dev/null +++ b/Lib/packaging/tests/pypiserver/with_externals/simple/foobar/index.html @@ -0,0 +1,4 @@ + +foobar-0.1.tar.gz
+external homepage
+ diff --git a/Lib/packaging/tests/pypiserver/with_externals/simple/index.html b/Lib/packaging/tests/pypiserver/with_externals/simple/index.html new file mode 100644 index 000000000000..a1a7bb72825d --- /dev/null +++ b/Lib/packaging/tests/pypiserver/with_externals/simple/index.html @@ -0,0 +1 @@ +foobar/ diff --git a/Lib/packaging/tests/pypiserver/with_norel_links/external/homepage.html b/Lib/packaging/tests/pypiserver/with_norel_links/external/homepage.html new file mode 100644 index 000000000000..1cc0c32f1bb8 --- /dev/null +++ b/Lib/packaging/tests/pypiserver/with_norel_links/external/homepage.html @@ -0,0 +1,7 @@ + + +

a rel=homepage HTML page

+foobar 2.0 + + + diff --git a/Lib/packaging/tests/pypiserver/with_norel_links/external/nonrel.html b/Lib/packaging/tests/pypiserver/with_norel_links/external/nonrel.html new file mode 100644 index 000000000000..f6ace2205488 --- /dev/null +++ b/Lib/packaging/tests/pypiserver/with_norel_links/external/nonrel.html @@ -0,0 +1 @@ +A page linked without rel="download" or rel="homepage" link. diff --git a/Lib/packaging/tests/pypiserver/with_norel_links/simple/foobar/index.html b/Lib/packaging/tests/pypiserver/with_norel_links/simple/foobar/index.html new file mode 100644 index 000000000000..171df9360cef --- /dev/null +++ b/Lib/packaging/tests/pypiserver/with_norel_links/simple/foobar/index.html @@ -0,0 +1,6 @@ + +foobar-0.1.tar.gz
+external homepage
+unrelated link
+unrelated download
+ diff --git a/Lib/packaging/tests/pypiserver/with_norel_links/simple/index.html b/Lib/packaging/tests/pypiserver/with_norel_links/simple/index.html new file mode 100644 index 000000000000..a1a7bb72825d --- /dev/null +++ b/Lib/packaging/tests/pypiserver/with_norel_links/simple/index.html @@ -0,0 +1 @@ +foobar/ diff --git a/Lib/packaging/tests/pypiserver/with_real_externals/simple/foobar/index.html b/Lib/packaging/tests/pypiserver/with_real_externals/simple/foobar/index.html new file mode 100644 index 000000000000..b2885ae384a2 --- /dev/null +++ b/Lib/packaging/tests/pypiserver/with_real_externals/simple/foobar/index.html @@ -0,0 +1,4 @@ + +foobar-0.1.tar.gz
+external homepage
+ diff --git a/Lib/packaging/tests/pypiserver/with_real_externals/simple/index.html b/Lib/packaging/tests/pypiserver/with_real_externals/simple/index.html new file mode 100644 index 000000000000..a1a7bb72825d --- /dev/null +++ b/Lib/packaging/tests/pypiserver/with_real_externals/simple/index.html @@ -0,0 +1 @@ +foobar/ diff --git a/Lib/packaging/tests/support.py b/Lib/packaging/tests/support.py new file mode 100644 index 000000000000..cf5d7883a923 --- /dev/null +++ b/Lib/packaging/tests/support.py @@ -0,0 +1,259 @@ +"""Support code for packaging test cases. + +A few helper classes are provided: LoggingCatcher, TempdirManager and +EnvironRestorer. They are written to be used as mixins:: + + from packaging.tests import unittest + from packaging.tests.support import LoggingCatcher + + class SomeTestCase(LoggingCatcher, unittest.TestCase): + +If you need to define a setUp method on your test class, you have to +call the mixin class' setUp method or it won't work (same thing for +tearDown): + + def setUp(self): + super(SomeTestCase, self).setUp() + ... # other setup code + +Also provided is a DummyCommand class, useful to mock commands in the +tests of another command that needs them, a create_distribution function +and a skip_unless_symlink decorator. + +Also provided is a DummyCommand class, useful to mock commands in the +tests of another command that needs them, a create_distribution function +and a skip_unless_symlink decorator. + +Each class or function has a docstring to explain its purpose and usage. +""" + +import os +import errno +import shutil +import logging +import weakref +import tempfile + +from packaging import logger +from packaging.dist import Distribution +from packaging.tests import unittest + +__all__ = ['LoggingCatcher', 'TempdirManager', 'EnvironRestorer', + 'DummyCommand', 'unittest', 'create_distribution', + 'skip_unless_symlink'] + + +class _TestHandler(logging.handlers.BufferingHandler): + # stolen and adapted from test.support + + def __init__(self): + logging.handlers.BufferingHandler.__init__(self, 0) + self.setLevel(logging.DEBUG) + + def shouldFlush(self): + return False + + def emit(self, record): + self.buffer.append(record) + + +class LoggingCatcher: + """TestCase-compatible mixin to receive logging calls. + + Upon setUp, instances of this classes get a BufferingHandler that's + configured to record all messages logged to the 'packaging' logger. + + Use get_logs to retrieve messages and self.loghandler.flush to discard + them. + """ + + def setUp(self): + super(LoggingCatcher, self).setUp() + self.loghandler = handler = _TestHandler() + logger.addHandler(handler) + self.addCleanup(logger.setLevel, logger.level) + logger.setLevel(logging.DEBUG) # we want all messages + + def tearDown(self): + handler = self.loghandler + # All this is necessary to properly shut down the logging system and + # avoid a regrtest complaint. Thanks to Vinay Sajip for the help. + handler.close() + logger.removeHandler(handler) + for ref in weakref.getweakrefs(handler): + logging._removeHandlerRef(ref) + del self.loghandler + super(LoggingCatcher, self).tearDown() + + def get_logs(self, *levels): + """Return all log messages with level in *levels*. + + Without explicit levels given, returns all messages. + *levels* defaults to all levels. For log calls with arguments (i.e. + logger.info('bla bla %s', arg)), the messages + Returns a list. + + Example: self.get_logs(logging.WARN, logging.DEBUG). + """ + if not levels: + return [log.getMessage() for log in self.loghandler.buffer] + return [log.getMessage() for log in self.loghandler.buffer + if log.levelno in levels] + + +class TempdirManager: + """TestCase-compatible mixin to create temporary directories and files. + + Directories and files created in a test_* method will be removed after it + has run. + """ + + def setUp(self): + super(TempdirManager, self).setUp() + self._basetempdir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self._basetempdir, os.name in ('nt', 'cygwin')) + super(TempdirManager, self).tearDown() + + def mktempfile(self): + """Create a read-write temporary file and return it.""" + + def _delete_file(filename): + try: + os.remove(filename) + except OSError as exc: + if exc.errno != errno.ENOENT: + raise + + fd, fn = tempfile.mkstemp(dir=self._basetempdir) + os.close(fd) + fp = open(fn, 'w+') + self.addCleanup(fp.close) + self.addCleanup(_delete_file, fn) + return fp + + def mkdtemp(self): + """Create a temporary directory and return its path.""" + d = tempfile.mkdtemp(dir=self._basetempdir) + return d + + def write_file(self, path, content='xxx'): + """Write a file at the given path. + + path can be a string, a tuple or a list; if it's a tuple or list, + os.path.join will be used to produce a path. + """ + if isinstance(path, (list, tuple)): + path = os.path.join(*path) + f = open(path, 'w') + try: + f.write(content) + finally: + f.close() + + def create_dist(self, **kw): + """Create a stub distribution object and files. + + This function creates a Distribution instance (use keyword arguments + to customize it) and a temporary directory with a project structure + (currently an empty directory). + + It returns the path to the directory and the Distribution instance. + You can use self.write_file to write any file in that + directory, e.g. setup scripts or Python modules. + """ + if 'name' not in kw: + kw['name'] = 'foo' + tmp_dir = self.mkdtemp() + project_dir = os.path.join(tmp_dir, kw['name']) + os.mkdir(project_dir) + dist = Distribution(attrs=kw) + return project_dir, dist + + def assertIsFile(self, *args): + path = os.path.join(*args) + dirname = os.path.dirname(path) + file = os.path.basename(path) + if os.path.isdir(dirname): + files = os.listdir(dirname) + msg = "%s not found in %s: %s" % (file, dirname, files) + assert os.path.isfile(path), msg + else: + raise AssertionError( + '%s not found. %s does not exist' % (file, dirname)) + + def assertIsNotFile(self, *args): + path = os.path.join(*args) + self.assertFalse(os.path.isfile(path), "%r exists" % path) + + +class EnvironRestorer: + """TestCase-compatible mixin to restore or delete environment variables. + + The variables to restore (or delete if they were not originally present) + must be explicitly listed in self.restore_environ. It's better to be + aware of what we're modifying instead of saving and restoring the whole + environment. + """ + + def setUp(self): + super(EnvironRestorer, self).setUp() + self._saved = [] + self._added = [] + for key in self.restore_environ: + if key in os.environ: + self._saved.append((key, os.environ[key])) + else: + self._added.append(key) + + def tearDown(self): + for key, value in self._saved: + os.environ[key] = value + for key in self._added: + os.environ.pop(key, None) + super(EnvironRestorer, self).tearDown() + + +class DummyCommand: + """Class to store options for retrieval via set_undefined_options(). + + Useful for mocking one dependency command in the tests for another + command, see e.g. the dummy build command in test_build_scripts. + """ + + def __init__(self, **kwargs): + for kw, val in kwargs.items(): + setattr(self, kw, val) + + def ensure_finalized(self): + pass + + +class TestDistribution(Distribution): + """Distribution subclasses that avoids the default search for + configuration files. + + The ._config_files attribute must be set before + .parse_config_files() is called. + """ + + def find_config_files(self): + return self._config_files + + +def create_distribution(configfiles=()): + """Prepares a distribution with given config files parsed.""" + d = TestDistribution() + d.config.find_config_files = d.find_config_files + d._config_files = configfiles + d.parse_config_files() + d.parse_command_line() + return d + + +try: + from test.support import skip_unless_symlink +except ImportError: + skip_unless_symlink = unittest.skip( + 'requires test.support.skip_unless_symlink') diff --git a/Lib/packaging/tests/test_ccompiler.py b/Lib/packaging/tests/test_ccompiler.py new file mode 100644 index 000000000000..dd4bdd9d95b6 --- /dev/null +++ b/Lib/packaging/tests/test_ccompiler.py @@ -0,0 +1,15 @@ +"""Tests for distutils.compiler.ccompiler.""" + +from packaging.compiler import ccompiler +from packaging.tests import unittest, support + + +class CCompilerTestCase(unittest.TestCase): + pass # XXX need some tests on CCompiler + + +def test_suite(): + return unittest.makeSuite(CCompilerTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_command_bdist.py b/Lib/packaging/tests/test_command_bdist.py new file mode 100644 index 000000000000..1522b7e2142b --- /dev/null +++ b/Lib/packaging/tests/test_command_bdist.py @@ -0,0 +1,77 @@ +"""Tests for distutils.command.bdist.""" + +from packaging import util +from packaging.command.bdist import bdist, show_formats + +from packaging.tests import unittest, support, captured_stdout + + +class BuildTestCase(support.TempdirManager, + support.LoggingCatcher, + unittest.TestCase): + + def _mock_get_platform(self): + self._get_platform_called = True + return self._get_platform() + + def setUp(self): + super(BuildTestCase, self).setUp() + + # mock util.get_platform + self._get_platform_called = False + self._get_platform = util.get_platform + util.get_platform = self._mock_get_platform + + def tearDown(self): + super(BuildTestCase, self).tearDown() + util.get_platform = self._get_platform + + def test_formats(self): + + # let's create a command and make sure + # we can fix the format + pkg_pth, dist = self.create_dist() + cmd = bdist(dist) + cmd.formats = ['msi'] + cmd.ensure_finalized() + self.assertEqual(cmd.formats, ['msi']) + + # what format bdist offers ? + # XXX an explicit list in bdist is + # not the best way to bdist_* commands + # we should add a registry + formats = sorted(('zip', 'gztar', 'bztar', 'ztar', + 'tar', 'wininst', 'msi')) + found = sorted(cmd.format_command) + self.assertEqual(found, formats) + + def test_skip_build(self): + pkg_pth, dist = self.create_dist() + cmd = bdist(dist) + cmd.skip_build = False + cmd.formats = ['ztar'] + cmd.ensure_finalized() + self.assertFalse(self._get_platform_called) + + pkg_pth, dist = self.create_dist() + cmd = bdist(dist) + cmd.skip_build = True + cmd.formats = ['ztar'] + cmd.ensure_finalized() + self.assertTrue(self._get_platform_called) + + def test_show_formats(self): + __, stdout = captured_stdout(show_formats) + + # the output should be a header line + one line per format + num_formats = len(bdist.format_commands) + output = [line for line in stdout.split('\n') + if line.strip().startswith('--formats=')] + self.assertEqual(len(output), num_formats) + + +def test_suite(): + return unittest.makeSuite(BuildTestCase) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/Lib/packaging/tests/test_command_bdist_dumb.py b/Lib/packaging/tests/test_command_bdist_dumb.py new file mode 100644 index 000000000000..ce1563ff7722 --- /dev/null +++ b/Lib/packaging/tests/test_command_bdist_dumb.py @@ -0,0 +1,103 @@ +"""Tests for distutils.command.bdist_dumb.""" + +import sys +import os + +# zlib is not used here, but if it's not available +# test_simple_built will fail +try: + import zlib +except ImportError: + zlib = None + +from packaging.dist import Distribution +from packaging.command.bdist_dumb import bdist_dumb +from packaging.tests import unittest, support + + +SETUP_PY = """\ +from distutils.run import setup +import foo + +setup(name='foo', version='0.1', py_modules=['foo'], + url='xxx', author='xxx', author_email='xxx') +""" + + +class BuildDumbTestCase(support.TempdirManager, + support.LoggingCatcher, + unittest.TestCase): + + def setUp(self): + super(BuildDumbTestCase, self).setUp() + self.old_location = os.getcwd() + self.old_sys_argv = sys.argv, sys.argv[:] + + def tearDown(self): + os.chdir(self.old_location) + sys.argv = self.old_sys_argv[0] + sys.argv[:] = self.old_sys_argv[1] + super(BuildDumbTestCase, self).tearDown() + + @unittest.skipUnless(zlib, "requires zlib") + def test_simple_built(self): + + # let's create a simple package + tmp_dir = self.mkdtemp() + pkg_dir = os.path.join(tmp_dir, 'foo') + os.mkdir(pkg_dir) + self.write_file((pkg_dir, 'setup.py'), SETUP_PY) + self.write_file((pkg_dir, 'foo.py'), '#') + self.write_file((pkg_dir, 'MANIFEST.in'), 'include foo.py') + self.write_file((pkg_dir, 'README'), '') + + dist = Distribution({'name': 'foo', 'version': '0.1', + 'py_modules': ['foo'], + 'url': 'xxx', 'author': 'xxx', + 'author_email': 'xxx'}) + dist.script_name = 'setup.py' + os.chdir(pkg_dir) + + sys.argv[:] = ['setup.py'] + cmd = bdist_dumb(dist) + + # so the output is the same no matter + # what is the platform + cmd.format = 'zip' + + cmd.ensure_finalized() + cmd.run() + + # see what we have + dist_created = os.listdir(os.path.join(pkg_dir, 'dist')) + base = "%s.%s" % (dist.get_fullname(), cmd.plat_name) + if os.name == 'os2': + base = base.replace(':', '-') + + wanted = ['%s.zip' % base] + self.assertEqual(dist_created, wanted) + + # now let's check what we have in the zip file + # XXX to be done + + def test_finalize_options(self): + pkg_dir, dist = self.create_dist() + os.chdir(pkg_dir) + cmd = bdist_dumb(dist) + self.assertEqual(cmd.bdist_dir, None) + cmd.finalize_options() + + # bdist_dir is initialized to bdist_base/dumb if not set + base = cmd.get_finalized_command('bdist').bdist_base + self.assertEqual(cmd.bdist_dir, os.path.join(base, 'dumb')) + + # the format is set to a default value depending on the os.name + default = cmd.default_format[os.name] + self.assertEqual(cmd.format, default) + + +def test_suite(): + return unittest.makeSuite(BuildDumbTestCase) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/Lib/packaging/tests/test_command_bdist_msi.py b/Lib/packaging/tests/test_command_bdist_msi.py new file mode 100644 index 000000000000..fded962dbf68 --- /dev/null +++ b/Lib/packaging/tests/test_command_bdist_msi.py @@ -0,0 +1,25 @@ +"""Tests for distutils.command.bdist_msi.""" +import sys + +from packaging.tests import unittest, support + + +class BDistMSITestCase(support.TempdirManager, + support.LoggingCatcher, + unittest.TestCase): + + @unittest.skipUnless(sys.platform == "win32", "runs only on win32") + def test_minimal(self): + # minimal test XXX need more tests + from packaging.command.bdist_msi import bdist_msi + pkg_pth, dist = self.create_dist() + cmd = bdist_msi(dist) + cmd.ensure_finalized() + + +def test_suite(): + return unittest.makeSuite(BDistMSITestCase) + + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/Lib/packaging/tests/test_command_bdist_wininst.py b/Lib/packaging/tests/test_command_bdist_wininst.py new file mode 100644 index 000000000000..09bdaadfc903 --- /dev/null +++ b/Lib/packaging/tests/test_command_bdist_wininst.py @@ -0,0 +1,32 @@ +"""Tests for distutils.command.bdist_wininst.""" + +from packaging.command.bdist_wininst import bdist_wininst +from packaging.tests import unittest, support + + +class BuildWinInstTestCase(support.TempdirManager, + support.LoggingCatcher, + unittest.TestCase): + + def test_get_exe_bytes(self): + + # issue5731: command was broken on non-windows platforms + # this test makes sure it works now for every platform + # let's create a command + pkg_pth, dist = self.create_dist() + cmd = bdist_wininst(dist) + cmd.ensure_finalized() + + # let's run the code that finds the right wininst*.exe file + # and make sure it finds it and returns its content + # no matter what platform we have + exe_file = cmd.get_exe_bytes() + self.assertGreater(len(exe_file), 10) + + +def test_suite(): + return unittest.makeSuite(BuildWinInstTestCase) + + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/Lib/packaging/tests/test_command_build.py b/Lib/packaging/tests/test_command_build.py new file mode 100644 index 000000000000..91fbe42a0d85 --- /dev/null +++ b/Lib/packaging/tests/test_command_build.py @@ -0,0 +1,55 @@ +"""Tests for distutils.command.build.""" +import os +import sys + +from packaging.command.build import build +from sysconfig import get_platform +from packaging.tests import unittest, support + + +class BuildTestCase(support.TempdirManager, + support.LoggingCatcher, + unittest.TestCase): + + def test_finalize_options(self): + pkg_dir, dist = self.create_dist() + cmd = build(dist) + cmd.finalize_options() + + # if not specified, plat_name gets the current platform + self.assertEqual(cmd.plat_name, get_platform()) + + # build_purelib is build + lib + wanted = os.path.join(cmd.build_base, 'lib') + self.assertEqual(cmd.build_purelib, wanted) + + # build_platlib is 'build/lib.platform-x.x[-pydebug]' + # examples: + # build/lib.macosx-10.3-i386-2.7 + plat_spec = '.%s-%s' % (cmd.plat_name, sys.version[0:3]) + if hasattr(sys, 'gettotalrefcount'): + self.assertTrue(cmd.build_platlib.endswith('-pydebug')) + plat_spec += '-pydebug' + wanted = os.path.join(cmd.build_base, 'lib' + plat_spec) + self.assertEqual(cmd.build_platlib, wanted) + + # by default, build_lib = build_purelib + self.assertEqual(cmd.build_lib, cmd.build_purelib) + + # build_temp is build/temp. + wanted = os.path.join(cmd.build_base, 'temp' + plat_spec) + self.assertEqual(cmd.build_temp, wanted) + + # build_scripts is build/scripts-x.x + wanted = os.path.join(cmd.build_base, 'scripts-' + sys.version[0:3]) + self.assertEqual(cmd.build_scripts, wanted) + + # executable is os.path.normpath(sys.executable) + self.assertEqual(cmd.executable, os.path.normpath(sys.executable)) + + +def test_suite(): + return unittest.makeSuite(BuildTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_command_build_clib.py b/Lib/packaging/tests/test_command_build_clib.py new file mode 100644 index 000000000000..a2a8583b0fe0 --- /dev/null +++ b/Lib/packaging/tests/test_command_build_clib.py @@ -0,0 +1,141 @@ +"""Tests for distutils.command.build_clib.""" +import os +import sys + +from packaging.util import find_executable +from packaging.command.build_clib import build_clib +from packaging.errors import PackagingSetupError +from packaging.tests import unittest, support + + +class BuildCLibTestCase(support.TempdirManager, + support.LoggingCatcher, + unittest.TestCase): + + def test_check_library_dist(self): + pkg_dir, dist = self.create_dist() + cmd = build_clib(dist) + + # 'libraries' option must be a list + self.assertRaises(PackagingSetupError, cmd.check_library_list, 'foo') + + # each element of 'libraries' must a 2-tuple + self.assertRaises(PackagingSetupError, cmd.check_library_list, + ['foo1', 'foo2']) + + # first element of each tuple in 'libraries' + # must be a string (the library name) + self.assertRaises(PackagingSetupError, cmd.check_library_list, + [(1, 'foo1'), ('name', 'foo2')]) + + # library name may not contain directory separators + self.assertRaises(PackagingSetupError, cmd.check_library_list, + [('name', 'foo1'), + ('another/name', 'foo2')]) + + # second element of each tuple must be a dictionary (build info) + self.assertRaises(PackagingSetupError, cmd.check_library_list, + [('name', {}), + ('another', 'foo2')]) + + # those work + libs = [('name', {}), ('name', {'ok': 'good'})] + cmd.check_library_list(libs) + + def test_get_source_files(self): + pkg_dir, dist = self.create_dist() + cmd = build_clib(dist) + + # "in 'libraries' option 'sources' must be present and must be + # a list of source filenames + cmd.libraries = [('name', {})] + self.assertRaises(PackagingSetupError, cmd.get_source_files) + + cmd.libraries = [('name', {'sources': 1})] + self.assertRaises(PackagingSetupError, cmd.get_source_files) + + cmd.libraries = [('name', {'sources': ['a', 'b']})] + self.assertEqual(cmd.get_source_files(), ['a', 'b']) + + cmd.libraries = [('name', {'sources': ('a', 'b')})] + self.assertEqual(cmd.get_source_files(), ['a', 'b']) + + cmd.libraries = [('name', {'sources': ('a', 'b')}), + ('name2', {'sources': ['c', 'd']})] + self.assertEqual(cmd.get_source_files(), ['a', 'b', 'c', 'd']) + + def test_build_libraries(self): + pkg_dir, dist = self.create_dist() + cmd = build_clib(dist) + + class FakeCompiler: + def compile(*args, **kw): + pass + create_static_lib = compile + + cmd.compiler = FakeCompiler() + + # build_libraries is also doing a bit of type checking + lib = [('name', {'sources': 'notvalid'})] + self.assertRaises(PackagingSetupError, cmd.build_libraries, lib) + + lib = [('name', {'sources': []})] + cmd.build_libraries(lib) + + lib = [('name', {'sources': ()})] + cmd.build_libraries(lib) + + def test_finalize_options(self): + pkg_dir, dist = self.create_dist() + cmd = build_clib(dist) + + cmd.include_dirs = 'one-dir' + cmd.finalize_options() + self.assertEqual(cmd.include_dirs, ['one-dir']) + + cmd.include_dirs = None + cmd.finalize_options() + self.assertEqual(cmd.include_dirs, []) + + cmd.distribution.libraries = 'WONTWORK' + self.assertRaises(PackagingSetupError, cmd.finalize_options) + + @unittest.skipIf(sys.platform == 'win32', 'disabled on win32') + def test_run(self): + pkg_dir, dist = self.create_dist() + cmd = build_clib(dist) + + foo_c = os.path.join(pkg_dir, 'foo.c') + self.write_file(foo_c, 'int main(void) { return 1;}\n') + cmd.libraries = [('foo', {'sources': [foo_c]})] + + build_temp = os.path.join(pkg_dir, 'build') + os.mkdir(build_temp) + cmd.build_temp = build_temp + cmd.build_clib = build_temp + + # before we run the command, we want to make sure + # all commands are present on the system + # by creating a compiler and checking its executables + from packaging.compiler import new_compiler, customize_compiler + + compiler = new_compiler() + customize_compiler(compiler) + for ccmd in compiler.executables.values(): + if ccmd is None: + continue + if find_executable(ccmd[0]) is None: + raise unittest.SkipTest("can't test") + + # this should work + cmd.run() + + # let's check the result + self.assertIn('libfoo.a', os.listdir(build_temp)) + + +def test_suite(): + return unittest.makeSuite(BuildCLibTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_command_build_ext.py b/Lib/packaging/tests/test_command_build_ext.py new file mode 100644 index 000000000000..2d798422b2d6 --- /dev/null +++ b/Lib/packaging/tests/test_command_build_ext.py @@ -0,0 +1,353 @@ +import os +import sys +import site +import shutil +import sysconfig +from io import StringIO +from packaging.dist import Distribution +from packaging.errors import UnknownFileError, CompileError +from packaging.command.build_ext import build_ext +from packaging.compiler.extension import Extension + +from packaging.tests import support, unittest, verbose, unload + +# http://bugs.python.org/issue4373 +# Don't load the xx module more than once. +ALREADY_TESTED = False + + +def _get_source_filename(): + srcdir = sysconfig.get_config_var('srcdir') + return os.path.join(srcdir, 'Modules', 'xxmodule.c') + + +class BuildExtTestCase(support.TempdirManager, + support.LoggingCatcher, + unittest.TestCase): + def setUp(self): + # Create a simple test environment + # Note that we're making changes to sys.path + super(BuildExtTestCase, self).setUp() + self.tmp_dir = self.mkdtemp() + self.sys_path = sys.path, sys.path[:] + sys.path.append(self.tmp_dir) + shutil.copy(_get_source_filename(), self.tmp_dir) + self.old_user_base = site.USER_BASE + site.USER_BASE = self.mkdtemp() + build_ext.USER_BASE = site.USER_BASE + + def test_build_ext(self): + global ALREADY_TESTED + xx_c = os.path.join(self.tmp_dir, 'xxmodule.c') + xx_ext = Extension('xx', [xx_c]) + dist = Distribution({'name': 'xx', 'ext_modules': [xx_ext]}) + dist.package_dir = self.tmp_dir + cmd = build_ext(dist) + if os.name == "nt": + # On Windows, we must build a debug version iff running + # a debug build of Python + cmd.debug = sys.executable.endswith("_d.exe") + cmd.build_lib = self.tmp_dir + cmd.build_temp = self.tmp_dir + + old_stdout = sys.stdout + if not verbose: + # silence compiler output + sys.stdout = StringIO() + try: + cmd.ensure_finalized() + cmd.run() + finally: + sys.stdout = old_stdout + + if ALREADY_TESTED: + return + else: + ALREADY_TESTED = True + + import xx + + for attr in ('error', 'foo', 'new', 'roj'): + self.assertTrue(hasattr(xx, attr)) + + self.assertEqual(xx.foo(2, 5), 7) + self.assertEqual(xx.foo(13, 15), 28) + self.assertEqual(xx.new().demo(), None) + doc = 'This is a template module just for instruction.' + self.assertEqual(xx.__doc__, doc) + self.assertTrue(isinstance(xx.Null(), xx.Null)) + self.assertTrue(isinstance(xx.Str(), xx.Str)) + + def tearDown(self): + # Get everything back to normal + unload('xx') + sys.path = self.sys_path[0] + sys.path[:] = self.sys_path[1] + if sys.version > "2.6": + site.USER_BASE = self.old_user_base + build_ext.USER_BASE = self.old_user_base + + super(BuildExtTestCase, self).tearDown() + + def test_solaris_enable_shared(self): + dist = Distribution({'name': 'xx'}) + cmd = build_ext(dist) + old = sys.platform + + sys.platform = 'sunos' # fooling finalize_options + from sysconfig import _CONFIG_VARS + + old_var = _CONFIG_VARS.get('Py_ENABLE_SHARED') + _CONFIG_VARS['Py_ENABLE_SHARED'] = 1 + try: + cmd.ensure_finalized() + finally: + sys.platform = old + if old_var is None: + del _CONFIG_VARS['Py_ENABLE_SHARED'] + else: + _CONFIG_VARS['Py_ENABLE_SHARED'] = old_var + + # make sure we get some library dirs under solaris + self.assertGreater(len(cmd.library_dirs), 0) + + @unittest.skipIf(sys.version < '2.6', 'requires Python 2.6 or higher') + def test_user_site(self): + dist = Distribution({'name': 'xx'}) + cmd = build_ext(dist) + + # making sure the user option is there + options = [name for name, short, label in + cmd.user_options] + self.assertIn('user', options) + + # setting a value + cmd.user = True + + # setting user based lib and include + lib = os.path.join(site.USER_BASE, 'lib') + incl = os.path.join(site.USER_BASE, 'include') + os.mkdir(lib) + os.mkdir(incl) + + # let's run finalize + cmd.ensure_finalized() + + # see if include_dirs and library_dirs + # were set + self.assertIn(lib, cmd.library_dirs) + self.assertIn(lib, cmd.rpath) + self.assertIn(incl, cmd.include_dirs) + + def test_optional_extension(self): + + # this extension will fail, but let's ignore this failure + # with the optional argument. + modules = [Extension('foo', ['xxx'], optional=False)] + dist = Distribution({'name': 'xx', 'ext_modules': modules}) + cmd = build_ext(dist) + cmd.ensure_finalized() + self.assertRaises((UnknownFileError, CompileError), + cmd.run) # should raise an error + + modules = [Extension('foo', ['xxx'], optional=True)] + dist = Distribution({'name': 'xx', 'ext_modules': modules}) + cmd = build_ext(dist) + cmd.ensure_finalized() + cmd.run() # should pass + + def test_finalize_options(self): + # Make sure Python's include directories (for Python.h, pyconfig.h, + # etc.) are in the include search path. + modules = [Extension('foo', ['xxx'], optional=False)] + dist = Distribution({'name': 'xx', 'ext_modules': modules}) + cmd = build_ext(dist) + cmd.finalize_options() + + py_include = sysconfig.get_path('include') + self.assertIn(py_include, cmd.include_dirs) + + plat_py_include = sysconfig.get_path('platinclude') + self.assertIn(plat_py_include, cmd.include_dirs) + + # make sure cmd.libraries is turned into a list + # if it's a string + cmd = build_ext(dist) + cmd.libraries = 'my_lib' + cmd.finalize_options() + self.assertEqual(cmd.libraries, ['my_lib']) + + # make sure cmd.library_dirs is turned into a list + # if it's a string + cmd = build_ext(dist) + cmd.library_dirs = 'my_lib_dir' + cmd.finalize_options() + self.assertIn('my_lib_dir', cmd.library_dirs) + + # make sure rpath is turned into a list + # if it's a list of os.pathsep's paths + cmd = build_ext(dist) + cmd.rpath = os.pathsep.join(['one', 'two']) + cmd.finalize_options() + self.assertEqual(cmd.rpath, ['one', 'two']) + + # XXX more tests to perform for win32 + + # make sure define is turned into 2-tuples + # strings if they are ','-separated strings + cmd = build_ext(dist) + cmd.define = 'one,two' + cmd.finalize_options() + self.assertEqual(cmd.define, [('one', '1'), ('two', '1')]) + + # make sure undef is turned into a list of + # strings if they are ','-separated strings + cmd = build_ext(dist) + cmd.undef = 'one,two' + cmd.finalize_options() + self.assertEqual(cmd.undef, ['one', 'two']) + + # make sure swig_opts is turned into a list + cmd = build_ext(dist) + cmd.swig_opts = None + cmd.finalize_options() + self.assertEqual(cmd.swig_opts, []) + + cmd = build_ext(dist) + cmd.swig_opts = '1 2' + cmd.finalize_options() + self.assertEqual(cmd.swig_opts, ['1', '2']) + + def test_get_source_files(self): + modules = [Extension('foo', ['xxx'], optional=False)] + dist = Distribution({'name': 'xx', 'ext_modules': modules}) + cmd = build_ext(dist) + cmd.ensure_finalized() + self.assertEqual(cmd.get_source_files(), ['xxx']) + + def test_compiler_option(self): + # cmd.compiler is an option and + # should not be overriden by a compiler instance + # when the command is run + dist = Distribution() + cmd = build_ext(dist) + cmd.compiler = 'unix' + cmd.ensure_finalized() + cmd.run() + self.assertEqual(cmd.compiler, 'unix') + + def test_get_outputs(self): + tmp_dir = self.mkdtemp() + c_file = os.path.join(tmp_dir, 'foo.c') + self.write_file(c_file, 'void initfoo(void) {};\n') + ext = Extension('foo', [c_file], optional=False) + dist = Distribution({'name': 'xx', + 'ext_modules': [ext]}) + cmd = build_ext(dist) + cmd.ensure_finalized() + self.assertEqual(len(cmd.get_outputs()), 1) + + if os.name == "nt": + cmd.debug = sys.executable.endswith("_d.exe") + + cmd.build_lib = os.path.join(self.tmp_dir, 'build') + cmd.build_temp = os.path.join(self.tmp_dir, 'tempt') + + # issue #5977 : distutils build_ext.get_outputs + # returns wrong result with --inplace + other_tmp_dir = os.path.realpath(self.mkdtemp()) + old_wd = os.getcwd() + os.chdir(other_tmp_dir) + try: + cmd.inplace = True + cmd.run() + so_file = cmd.get_outputs()[0] + finally: + os.chdir(old_wd) + self.assertTrue(os.path.exists(so_file)) + so_ext = sysconfig.get_config_var('SO') + self.assertTrue(so_file.endswith(so_ext)) + so_dir = os.path.dirname(so_file) + self.assertEqual(so_dir, other_tmp_dir) + + cmd.inplace = False + cmd.run() + so_file = cmd.get_outputs()[0] + self.assertTrue(os.path.exists(so_file)) + self.assertTrue(so_file.endswith(so_ext)) + so_dir = os.path.dirname(so_file) + self.assertEqual(so_dir, cmd.build_lib) + + # inplace = False, cmd.package = 'bar' + build_py = cmd.get_finalized_command('build_py') + build_py.package_dir = 'bar' + path = cmd.get_ext_fullpath('foo') + # checking that the last directory is the build_dir + path = os.path.split(path)[0] + self.assertEqual(path, cmd.build_lib) + + # inplace = True, cmd.package = 'bar' + cmd.inplace = True + other_tmp_dir = os.path.realpath(self.mkdtemp()) + old_wd = os.getcwd() + os.chdir(other_tmp_dir) + try: + path = cmd.get_ext_fullpath('foo') + finally: + os.chdir(old_wd) + # checking that the last directory is bar + path = os.path.split(path)[0] + lastdir = os.path.split(path)[-1] + self.assertEqual(lastdir, 'bar') + + def test_ext_fullpath(self): + ext = sysconfig.get_config_vars()['SO'] + # building lxml.etree inplace + #etree_c = os.path.join(self.tmp_dir, 'lxml.etree.c') + #etree_ext = Extension('lxml.etree', [etree_c]) + #dist = Distribution({'name': 'lxml', 'ext_modules': [etree_ext]}) + dist = Distribution() + cmd = build_ext(dist) + cmd.inplace = True + cmd.distribution.package_dir = 'src' + cmd.distribution.packages = ['lxml', 'lxml.html'] + curdir = os.getcwd() + wanted = os.path.join(curdir, 'src', 'lxml', 'etree' + ext) + path = cmd.get_ext_fullpath('lxml.etree') + self.assertEqual(wanted, path) + + # building lxml.etree not inplace + cmd.inplace = False + cmd.build_lib = os.path.join(curdir, 'tmpdir') + wanted = os.path.join(curdir, 'tmpdir', 'lxml', 'etree' + ext) + path = cmd.get_ext_fullpath('lxml.etree') + self.assertEqual(wanted, path) + + # building twisted.runner.portmap not inplace + build_py = cmd.get_finalized_command('build_py') + build_py.package_dir = None + cmd.distribution.packages = ['twisted', 'twisted.runner.portmap'] + path = cmd.get_ext_fullpath('twisted.runner.portmap') + wanted = os.path.join(curdir, 'tmpdir', 'twisted', 'runner', + 'portmap' + ext) + self.assertEqual(wanted, path) + + # building twisted.runner.portmap inplace + cmd.inplace = True + path = cmd.get_ext_fullpath('twisted.runner.portmap') + wanted = os.path.join(curdir, 'twisted', 'runner', 'portmap' + ext) + self.assertEqual(wanted, path) + + +def test_suite(): + src = _get_source_filename() + if not os.path.exists(src): + if verbose: + print ('test_build_ext: Cannot find source code (test' + ' must run in python build dir)') + return unittest.TestSuite() + else: + return unittest.makeSuite(BuildExtTestCase) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/Lib/packaging/tests/test_command_build_py.py b/Lib/packaging/tests/test_command_build_py.py new file mode 100644 index 000000000000..9b40e6d9b23c --- /dev/null +++ b/Lib/packaging/tests/test_command_build_py.py @@ -0,0 +1,124 @@ +"""Tests for distutils.command.build_py.""" + +import os +import sys + +from packaging.command.build_py import build_py +from packaging.dist import Distribution +from packaging.errors import PackagingFileError + +from packaging.tests import unittest, support + + +class BuildPyTestCase(support.TempdirManager, + support.LoggingCatcher, + unittest.TestCase): + + def test_package_data(self): + sources = self.mkdtemp() + pkg_dir = os.path.join(sources, 'pkg') + os.mkdir(pkg_dir) + f = open(os.path.join(pkg_dir, "__init__.py"), "w") + try: + f.write("# Pretend this is a package.") + finally: + f.close() + f = open(os.path.join(pkg_dir, "README.txt"), "w") + try: + f.write("Info about this package") + finally: + f.close() + + destination = self.mkdtemp() + + dist = Distribution({"packages": ["pkg"], + "package_dir": sources}) + # script_name need not exist, it just need to be initialized + + dist.script_name = os.path.join(sources, "setup.py") + dist.command_obj["build"] = support.DummyCommand( + force=False, + build_lib=destination, + use_2to3_fixers=None, + convert_2to3_doctests=None, + use_2to3=False) + dist.packages = ["pkg"] + dist.package_data = {"pkg": ["README.txt"]} + dist.package_dir = sources + + cmd = build_py(dist) + cmd.compile = True + cmd.ensure_finalized() + self.assertEqual(cmd.package_data, dist.package_data) + + cmd.run() + + # This makes sure the list of outputs includes byte-compiled + # files for Python modules but not for package data files + # (there shouldn't *be* byte-code files for those!). + # + self.assertEqual(len(cmd.get_outputs()), 3) + pkgdest = os.path.join(destination, "pkg") + files = os.listdir(pkgdest) + self.assertIn("__init__.py", files) + self.assertIn("__init__.pyc", files) + self.assertIn("README.txt", files) + + def test_empty_package_dir(self): + # See SF 1668596/1720897. + cwd = os.getcwd() + + # create the distribution files. + sources = self.mkdtemp() + pkg = os.path.join(sources, 'pkg') + os.mkdir(pkg) + open(os.path.join(pkg, "__init__.py"), "w").close() + testdir = os.path.join(pkg, "doc") + os.mkdir(testdir) + open(os.path.join(testdir, "testfile"), "w").close() + + os.chdir(sources) + old_stdout = sys.stdout + #sys.stdout = StringIO.StringIO() + + try: + dist = Distribution({"packages": ["pkg"], + "package_dir": sources, + "package_data": {"pkg": ["doc/*"]}}) + # script_name need not exist, it just need to be initialized + dist.script_name = os.path.join(sources, "setup.py") + dist.script_args = ["build"] + dist.parse_command_line() + + try: + dist.run_commands() + except PackagingFileError as e: + self.fail("failed package_data test when package_dir is ''") + finally: + # Restore state. + os.chdir(cwd) + sys.stdout = old_stdout + + @unittest.skipUnless(hasattr(sys, 'dont_write_bytecode'), + 'sys.dont_write_bytecode not supported') + def test_dont_write_bytecode(self): + # makes sure byte_compile is not used + pkg_dir, dist = self.create_dist() + cmd = build_py(dist) + cmd.compile = True + cmd.optimize = 1 + + old_dont_write_bytecode = sys.dont_write_bytecode + sys.dont_write_bytecode = True + try: + cmd.byte_compile([]) + finally: + sys.dont_write_bytecode = old_dont_write_bytecode + + self.assertIn('byte-compiling is disabled', self.get_logs()[0]) + +def test_suite(): + return unittest.makeSuite(BuildPyTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_command_build_scripts.py b/Lib/packaging/tests/test_command_build_scripts.py new file mode 100644 index 000000000000..60d8b68d80cc --- /dev/null +++ b/Lib/packaging/tests/test_command_build_scripts.py @@ -0,0 +1,112 @@ +"""Tests for distutils.command.build_scripts.""" + +import os +import sys +import sysconfig +from packaging.dist import Distribution +from packaging.command.build_scripts import build_scripts + +from packaging.tests import unittest, support + + +class BuildScriptsTestCase(support.TempdirManager, + support.LoggingCatcher, + unittest.TestCase): + + def test_default_settings(self): + cmd = self.get_build_scripts_cmd("/foo/bar", []) + self.assertFalse(cmd.force) + self.assertIs(cmd.build_dir, None) + + cmd.finalize_options() + + self.assertTrue(cmd.force) + self.assertEqual(cmd.build_dir, "/foo/bar") + + def test_build(self): + source = self.mkdtemp() + target = self.mkdtemp() + expected = self.write_sample_scripts(source) + + cmd = self.get_build_scripts_cmd(target, + [os.path.join(source, fn) + for fn in expected]) + cmd.finalize_options() + cmd.run() + + built = os.listdir(target) + for name in expected: + self.assertIn(name, built) + + def get_build_scripts_cmd(self, target, scripts): + dist = Distribution() + dist.scripts = scripts + dist.command_obj["build"] = support.DummyCommand( + build_scripts=target, + force=True, + executable=sys.executable, + use_2to3=False, + use_2to3_fixers=None, + convert_2to3_doctests=None + ) + return build_scripts(dist) + + def write_sample_scripts(self, dir): + expected = [] + expected.append("script1.py") + self.write_script(dir, "script1.py", + ("#! /usr/bin/env python2.3\n" + "# bogus script w/ Python sh-bang\n" + "pass\n")) + expected.append("script2.py") + self.write_script(dir, "script2.py", + ("#!/usr/bin/python\n" + "# bogus script w/ Python sh-bang\n" + "pass\n")) + expected.append("shell.sh") + self.write_script(dir, "shell.sh", + ("#!/bin/sh\n" + "# bogus shell script w/ sh-bang\n" + "exit 0\n")) + return expected + + def write_script(self, dir, name, text): + f = open(os.path.join(dir, name), "w") + try: + f.write(text) + finally: + f.close() + + def test_version_int(self): + source = self.mkdtemp() + target = self.mkdtemp() + expected = self.write_sample_scripts(source) + + + cmd = self.get_build_scripts_cmd(target, + [os.path.join(source, fn) + for fn in expected]) + cmd.finalize_options() + + # http://bugs.python.org/issue4524 + # + # On linux-g++-32 with command line `./configure --enable-ipv6 + # --with-suffix=3`, python is compiled okay but the build scripts + # failed when writing the name of the executable + old = sysconfig.get_config_vars().get('VERSION') + sysconfig._CONFIG_VARS['VERSION'] = 4 + try: + cmd.run() + finally: + if old is not None: + sysconfig._CONFIG_VARS['VERSION'] = old + + built = os.listdir(target) + for name in expected: + self.assertIn(name, built) + +def test_suite(): + return unittest.makeSuite(BuildScriptsTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_command_check.py b/Lib/packaging/tests/test_command_check.py new file mode 100644 index 000000000000..8b32673c1e73 --- /dev/null +++ b/Lib/packaging/tests/test_command_check.py @@ -0,0 +1,131 @@ +"""Tests for distutils.command.check.""" + +import logging +from packaging.command.check import check +from packaging.metadata import _HAS_DOCUTILS +from packaging.errors import PackagingSetupError, MetadataMissingError +from packaging.tests import unittest, support + + +class CheckTestCase(support.LoggingCatcher, + support.TempdirManager, + unittest.TestCase): + + def _run(self, metadata=None, **options): + if metadata is None: + metadata = {'name': 'xxx', 'version': '1.2'} + pkg_info, dist = self.create_dist(**metadata) + cmd = check(dist) + cmd.initialize_options() + for name, value in options.items(): + setattr(cmd, name, value) + cmd.ensure_finalized() + cmd.run() + return cmd + + def test_check_metadata(self): + # let's run the command with no metadata at all + # by default, check is checking the metadata + # should have some warnings + cmd = self._run() + # trick: using assertNotEqual with an empty list will give us a more + # useful error message than assertGreater(.., 0) when the code change + # and the test fails + self.assertNotEqual([], self.get_logs(logging.WARNING)) + + # now let's add the required fields + # and run it again, to make sure we don't get + # any warning anymore + self.loghandler.flush() + metadata = {'home_page': 'xxx', 'author': 'xxx', + 'author_email': 'xxx', + 'name': 'xxx', 'version': '4.2', + } + cmd = self._run(metadata) + self.assertEqual([], self.get_logs(logging.WARNING)) + + # now with the strict mode, we should + # get an error if there are missing metadata + self.assertRaises(MetadataMissingError, self._run, {}, **{'strict': 1}) + self.assertRaises(PackagingSetupError, self._run, + {'name': 'xxx', 'version': 'xxx'}, **{'strict': 1}) + + # and of course, no error when all metadata fields are present + self.loghandler.flush() + cmd = self._run(metadata, strict=True) + self.assertEqual([], self.get_logs(logging.WARNING)) + + def test_check_metadata_1_2(self): + # let's run the command with no metadata at all + # by default, check is checking the metadata + # should have some warnings + cmd = self._run() + self.assertNotEqual([], self.get_logs(logging.WARNING)) + + # now let's add the required fields and run it again, to make sure we + # don't get any warning anymore let's use requires_python as a marker + # to enforce Metadata-Version 1.2 + metadata = {'home_page': 'xxx', 'author': 'xxx', + 'author_email': 'xxx', + 'name': 'xxx', 'version': '4.2', + 'requires_python': '2.4', + } + self.loghandler.flush() + cmd = self._run(metadata) + self.assertEqual([], self.get_logs(logging.WARNING)) + + # now with the strict mode, we should + # get an error if there are missing metadata + self.assertRaises(MetadataMissingError, self._run, {}, **{'strict': 1}) + self.assertRaises(PackagingSetupError, self._run, + {'name': 'xxx', 'version': 'xxx'}, **{'strict': 1}) + + # complain about version format + metadata['version'] = 'xxx' + self.assertRaises(PackagingSetupError, self._run, metadata, + **{'strict': 1}) + + # now with correct version format again + metadata['version'] = '4.2' + self.loghandler.flush() + cmd = self._run(metadata, strict=True) + self.assertEqual([], self.get_logs(logging.WARNING)) + + @unittest.skipUnless(_HAS_DOCUTILS, "requires docutils") + def test_check_restructuredtext(self): + # let's see if it detects broken rest in long_description + broken_rest = 'title\n===\n\ntest' + pkg_info, dist = self.create_dist(description=broken_rest) + cmd = check(dist) + cmd.check_restructuredtext() + self.assertEqual(len(self.get_logs(logging.WARNING)), 1) + + self.loghandler.flush() + pkg_info, dist = self.create_dist(description='title\n=====\n\ntest') + cmd = check(dist) + cmd.check_restructuredtext() + self.assertEqual([], self.get_logs(logging.WARNING)) + + def test_check_all(self): + self.assertRaises(PackagingSetupError, self._run, + {'name': 'xxx', 'version': 'xxx'}, **{'strict': 1, + 'all': 1}) + self.assertRaises(MetadataMissingError, self._run, + {}, **{'strict': 1, + 'all': 1}) + + def test_check_hooks(self): + pkg_info, dist = self.create_dist() + dist.command_options['install_dist'] = { + 'pre_hook': ('file', {"a": 'some.nonextistant.hook.ghrrraarrhll'}), + } + cmd = check(dist) + cmd.check_hooks_resolvable() + self.assertEqual(len(self.get_logs(logging.WARNING)), 1) + + +def test_suite(): + return unittest.makeSuite(CheckTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_command_clean.py b/Lib/packaging/tests/test_command_clean.py new file mode 100644 index 000000000000..8d29e4dcdcb8 --- /dev/null +++ b/Lib/packaging/tests/test_command_clean.py @@ -0,0 +1,48 @@ +"""Tests for distutils.command.clean.""" +import os + +from packaging.command.clean import clean +from packaging.tests import unittest, support + + +class cleanTestCase(support.TempdirManager, support.LoggingCatcher, + unittest.TestCase): + + def test_simple_run(self): + pkg_dir, dist = self.create_dist() + cmd = clean(dist) + + # let's add some elements clean should remove + dirs = [(d, os.path.join(pkg_dir, d)) + for d in ('build_temp', 'build_lib', 'bdist_base', + 'build_scripts', 'build_base')] + + for name, path in dirs: + os.mkdir(path) + setattr(cmd, name, path) + if name == 'build_base': + continue + for f in ('one', 'two', 'three'): + self.write_file(os.path.join(path, f)) + + # let's run the command + cmd.all = True + cmd.ensure_finalized() + cmd.run() + + # make sure the files where removed + for name, path in dirs: + self.assertFalse(os.path.exists(path), + '%r was not removed' % path) + + # let's run the command again (should spit warnings but succeed) + cmd.all = True + cmd.ensure_finalized() + cmd.run() + + +def test_suite(): + return unittest.makeSuite(cleanTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_command_cmd.py b/Lib/packaging/tests/test_command_cmd.py new file mode 100644 index 000000000000..8ac9dce66d1e --- /dev/null +++ b/Lib/packaging/tests/test_command_cmd.py @@ -0,0 +1,101 @@ +"""Tests for distutils.cmd.""" +import os + +from packaging.command.cmd import Command +from packaging.dist import Distribution +from packaging.errors import PackagingOptionError +from packaging.tests import support, unittest + + +class MyCmd(Command): + def initialize_options(self): + pass + + +class CommandTestCase(support.LoggingCatcher, + unittest.TestCase): + + def setUp(self): + super(CommandTestCase, self).setUp() + dist = Distribution() + self.cmd = MyCmd(dist) + + def test_make_file(self): + cmd = self.cmd + + # making sure it raises when infiles is not a string or a list/tuple + self.assertRaises(TypeError, cmd.make_file, + infiles=1, outfile='', func='func', args=()) + + # making sure execute gets called properly + def _execute(func, args, exec_msg, level): + self.assertEqual(exec_msg, 'generating out from in') + cmd.force = True + cmd.execute = _execute + cmd.make_file(infiles='in', outfile='out', func='func', args=()) + + def test_dump_options(self): + cmd = self.cmd + cmd.option1 = 1 + cmd.option2 = 1 + cmd.user_options = [('option1', '', ''), ('option2', '', '')] + cmd.dump_options() + + wanted = ["command options for 'MyCmd':", ' option1 = 1', + ' option2 = 1'] + msgs = self.get_logs() + self.assertEqual(msgs, wanted) + + def test_ensure_string(self): + cmd = self.cmd + cmd.option1 = 'ok' + cmd.ensure_string('option1') + + cmd.option2 = None + cmd.ensure_string('option2', 'xxx') + self.assertTrue(hasattr(cmd, 'option2')) + + cmd.option3 = 1 + self.assertRaises(PackagingOptionError, cmd.ensure_string, 'option3') + + def test_ensure_string_list(self): + cmd = self.cmd + cmd.option1 = 'ok,dok' + cmd.ensure_string_list('option1') + self.assertEqual(cmd.option1, ['ok', 'dok']) + + cmd.yes_string_list = ['one', 'two', 'three'] + cmd.yes_string_list2 = 'ok' + cmd.ensure_string_list('yes_string_list') + cmd.ensure_string_list('yes_string_list2') + self.assertEqual(cmd.yes_string_list, ['one', 'two', 'three']) + self.assertEqual(cmd.yes_string_list2, ['ok']) + + cmd.not_string_list = ['one', 2, 'three'] + cmd.not_string_list2 = object() + self.assertRaises(PackagingOptionError, + cmd.ensure_string_list, 'not_string_list') + + self.assertRaises(PackagingOptionError, + cmd.ensure_string_list, 'not_string_list2') + + def test_ensure_filename(self): + cmd = self.cmd + cmd.option1 = __file__ + cmd.ensure_filename('option1') + cmd.option2 = 'xxx' + self.assertRaises(PackagingOptionError, cmd.ensure_filename, 'option2') + + def test_ensure_dirname(self): + cmd = self.cmd + cmd.option1 = os.path.dirname(__file__) or os.curdir + cmd.ensure_dirname('option1') + cmd.option2 = 'xxx' + self.assertRaises(PackagingOptionError, cmd.ensure_dirname, 'option2') + + +def test_suite(): + return unittest.makeSuite(CommandTestCase) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/Lib/packaging/tests/test_command_config.py b/Lib/packaging/tests/test_command_config.py new file mode 100644 index 000000000000..6d780c5fd5aa --- /dev/null +++ b/Lib/packaging/tests/test_command_config.py @@ -0,0 +1,76 @@ +"""Tests for distutils.command.config.""" +import os +import sys +import logging + +from packaging.command.config import dump_file, config +from packaging.tests import unittest, support + + +class ConfigTestCase(support.LoggingCatcher, + support.TempdirManager, + unittest.TestCase): + + def test_dump_file(self): + this_file = __file__.rstrip('co') + with open(this_file) as f: + numlines = len(f.readlines()) + + dump_file(this_file, 'I am the header') + + logs = [] + for log in self.get_logs(logging.INFO): + logs.extend(line for line in log.split('\n')) + self.assertEqual(len(logs), numlines + 2) + + @unittest.skipIf(sys.platform == 'win32', 'disabled on win32') + def test_search_cpp(self): + pkg_dir, dist = self.create_dist() + cmd = config(dist) + + # simple pattern searches + match = cmd.search_cpp(pattern='xxx', body='// xxx') + self.assertEqual(match, 0) + + match = cmd.search_cpp(pattern='_configtest', body='// xxx') + self.assertEqual(match, 1) + + def test_finalize_options(self): + # finalize_options does a bit of transformation + # on options + pkg_dir, dist = self.create_dist() + cmd = config(dist) + cmd.include_dirs = 'one%stwo' % os.pathsep + cmd.libraries = 'one' + cmd.library_dirs = 'three%sfour' % os.pathsep + cmd.ensure_finalized() + + self.assertEqual(cmd.include_dirs, ['one', 'two']) + self.assertEqual(cmd.libraries, ['one']) + self.assertEqual(cmd.library_dirs, ['three', 'four']) + + def test_clean(self): + # _clean removes files + tmp_dir = self.mkdtemp() + f1 = os.path.join(tmp_dir, 'one') + f2 = os.path.join(tmp_dir, 'two') + + self.write_file(f1, 'xxx') + self.write_file(f2, 'xxx') + + for f in (f1, f2): + self.assertTrue(os.path.exists(f)) + + pkg_dir, dist = self.create_dist() + cmd = config(dist) + cmd._clean(f1, f2) + + for f in (f1, f2): + self.assertFalse(os.path.exists(f)) + + +def test_suite(): + return unittest.makeSuite(ConfigTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_command_install_data.py b/Lib/packaging/tests/test_command_install_data.py new file mode 100644 index 000000000000..8b8bbac774b6 --- /dev/null +++ b/Lib/packaging/tests/test_command_install_data.py @@ -0,0 +1,80 @@ +"""Tests for packaging.command.install_data.""" +import os +import sysconfig +from sysconfig import _get_default_scheme +from packaging.tests import unittest, support +from packaging.command.install_data import install_data + + +class InstallDataTestCase(support.TempdirManager, + support.LoggingCatcher, + unittest.TestCase): + + def test_simple_run(self): + self.addCleanup(setattr, sysconfig, '_SCHEMES', sysconfig._SCHEMES) + + pkg_dir, dist = self.create_dist() + cmd = install_data(dist) + cmd.install_dir = inst = os.path.join(pkg_dir, 'inst') + + sysconfig._SCHEMES.set(_get_default_scheme(), 'inst', + os.path.join(pkg_dir, 'inst')) + sysconfig._SCHEMES.set(_get_default_scheme(), 'inst2', + os.path.join(pkg_dir, 'inst2')) + + one = os.path.join(pkg_dir, 'one') + self.write_file(one, 'xxx') + inst2 = os.path.join(pkg_dir, 'inst2') + two = os.path.join(pkg_dir, 'two') + self.write_file(two, 'xxx') + + cmd.data_files = {one: '{inst}/one', two: '{inst2}/two'} + self.assertCountEqual(cmd.get_inputs(), [one, two]) + + # let's run the command + cmd.ensure_finalized() + cmd.run() + + # let's check the result + self.assertEqual(len(cmd.get_outputs()), 2) + rtwo = os.path.split(two)[-1] + self.assertTrue(os.path.exists(os.path.join(inst2, rtwo))) + rone = os.path.split(one)[-1] + self.assertTrue(os.path.exists(os.path.join(inst, rone))) + cmd.outfiles = [] + + # let's try with warn_dir one + cmd.warn_dir = True + cmd.ensure_finalized() + cmd.run() + + # let's check the result + self.assertEqual(len(cmd.get_outputs()), 2) + self.assertTrue(os.path.exists(os.path.join(inst2, rtwo))) + self.assertTrue(os.path.exists(os.path.join(inst, rone))) + cmd.outfiles = [] + + # now using root and empty dir + cmd.root = os.path.join(pkg_dir, 'root') + three = os.path.join(cmd.install_dir, 'three') + self.write_file(three, 'xx') + + sysconfig._SCHEMES.set(_get_default_scheme(), 'inst3', + cmd.install_dir) + + cmd.data_files = {one: '{inst}/one', two: '{inst2}/two', + three: '{inst3}/three'} + cmd.ensure_finalized() + cmd.run() + + # let's check the result + self.assertEqual(len(cmd.get_outputs()), 3) + self.assertTrue(os.path.exists(os.path.join(inst2, rtwo))) + self.assertTrue(os.path.exists(os.path.join(inst, rone))) + + +def test_suite(): + return unittest.makeSuite(InstallDataTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_command_install_dist.py b/Lib/packaging/tests/test_command_install_dist.py new file mode 100644 index 000000000000..a06d1f65a49b --- /dev/null +++ b/Lib/packaging/tests/test_command_install_dist.py @@ -0,0 +1,210 @@ +"""Tests for packaging.command.install.""" + +import os +import sys + +from sysconfig import (get_scheme_names, get_config_vars, + _SCHEMES, get_config_var, get_path) + +_CONFIG_VARS = get_config_vars() + +from packaging.tests import captured_stdout + +from packaging.command.install_dist import install_dist +from packaging.command import install_dist as install_module +from packaging.dist import Distribution +from packaging.errors import PackagingOptionError + +from packaging.tests import unittest, support + + +class InstallTestCase(support.TempdirManager, + support.LoggingCatcher, + unittest.TestCase): + + def test_home_installation_scheme(self): + # This ensure two things: + # - that --home generates the desired set of directory names + # - test --home is supported on all platforms + builddir = self.mkdtemp() + destination = os.path.join(builddir, "installation") + + dist = Distribution({"name": "foopkg"}) + # script_name need not exist, it just need to be initialized + dist.script_name = os.path.join(builddir, "setup.py") + dist.command_obj["build"] = support.DummyCommand( + build_base=builddir, + build_lib=os.path.join(builddir, "lib"), + ) + + old_posix_prefix = _SCHEMES.get('posix_prefix', 'platinclude') + old_posix_home = _SCHEMES.get('posix_home', 'platinclude') + + new_path = '{platbase}/include/python{py_version_short}' + _SCHEMES.set('posix_prefix', 'platinclude', new_path) + _SCHEMES.set('posix_home', 'platinclude', '{platbase}/include/python') + + try: + cmd = install_dist(dist) + cmd.home = destination + cmd.ensure_finalized() + finally: + _SCHEMES.set('posix_prefix', 'platinclude', old_posix_prefix) + _SCHEMES.set('posix_home', 'platinclude', old_posix_home) + + self.assertEqual(cmd.install_base, destination) + self.assertEqual(cmd.install_platbase, destination) + + def check_path(got, expected): + got = os.path.normpath(got) + expected = os.path.normpath(expected) + self.assertEqual(got, expected) + + libdir = os.path.join(destination, "lib", "python") + check_path(cmd.install_lib, libdir) + check_path(cmd.install_platlib, libdir) + check_path(cmd.install_purelib, libdir) + check_path(cmd.install_headers, + os.path.join(destination, "include", "python", "foopkg")) + check_path(cmd.install_scripts, os.path.join(destination, "bin")) + check_path(cmd.install_data, destination) + + @unittest.skipIf(sys.version < '2.6', 'requires Python 2.6 or higher') + def test_user_site(self): + # test install with --user + # preparing the environment for the test + self.old_user_base = get_config_var('userbase') + self.old_user_site = get_path('purelib', '%s_user' % os.name) + self.tmpdir = self.mkdtemp() + self.user_base = os.path.join(self.tmpdir, 'B') + self.user_site = os.path.join(self.tmpdir, 'S') + _CONFIG_VARS['userbase'] = self.user_base + scheme = '%s_user' % os.name + _SCHEMES.set(scheme, 'purelib', self.user_site) + + def _expanduser(path): + if path[0] == '~': + path = os.path.normpath(self.tmpdir) + path[1:] + return path + + self.old_expand = os.path.expanduser + os.path.expanduser = _expanduser + + try: + # this is the actual test + self._test_user_site() + finally: + _CONFIG_VARS['userbase'] = self.old_user_base + _SCHEMES.set(scheme, 'purelib', self.old_user_site) + os.path.expanduser = self.old_expand + + def _test_user_site(self): + schemes = get_scheme_names() + for key in ('nt_user', 'posix_user', 'os2_home'): + self.assertIn(key, schemes) + + dist = Distribution({'name': 'xx'}) + cmd = install_dist(dist) + # making sure the user option is there + options = [name for name, short, lable in + cmd.user_options] + self.assertIn('user', options) + + # setting a value + cmd.user = True + + # user base and site shouldn't be created yet + self.assertFalse(os.path.exists(self.user_base)) + self.assertFalse(os.path.exists(self.user_site)) + + # let's run finalize + cmd.ensure_finalized() + + # now they should + self.assertTrue(os.path.exists(self.user_base)) + self.assertTrue(os.path.exists(self.user_site)) + + self.assertIn('userbase', cmd.config_vars) + self.assertIn('usersite', cmd.config_vars) + + def test_handle_extra_path(self): + dist = Distribution({'name': 'xx', 'extra_path': 'path,dirs'}) + cmd = install_dist(dist) + + # two elements + cmd.handle_extra_path() + self.assertEqual(cmd.extra_path, ['path', 'dirs']) + self.assertEqual(cmd.extra_dirs, 'dirs') + self.assertEqual(cmd.path_file, 'path') + + # one element + cmd.extra_path = ['path'] + cmd.handle_extra_path() + self.assertEqual(cmd.extra_path, ['path']) + self.assertEqual(cmd.extra_dirs, 'path') + self.assertEqual(cmd.path_file, 'path') + + # none + dist.extra_path = cmd.extra_path = None + cmd.handle_extra_path() + self.assertEqual(cmd.extra_path, None) + self.assertEqual(cmd.extra_dirs, '') + self.assertEqual(cmd.path_file, None) + + # three elements (no way !) + cmd.extra_path = 'path,dirs,again' + self.assertRaises(PackagingOptionError, cmd.handle_extra_path) + + def test_finalize_options(self): + dist = Distribution({'name': 'xx'}) + cmd = install_dist(dist) + + # must supply either prefix/exec-prefix/home or + # install-base/install-platbase -- not both + cmd.prefix = 'prefix' + cmd.install_base = 'base' + self.assertRaises(PackagingOptionError, cmd.finalize_options) + + # must supply either home or prefix/exec-prefix -- not both + cmd.install_base = None + cmd.home = 'home' + self.assertRaises(PackagingOptionError, cmd.finalize_options) + + if sys.version >= '2.6': + # can't combine user with with prefix/exec_prefix/home or + # install_(plat)base + cmd.prefix = None + cmd.user = 'user' + self.assertRaises(PackagingOptionError, cmd.finalize_options) + + def test_old_record(self): + # test pre-PEP 376 --record option (outside dist-info dir) + install_dir = self.mkdtemp() + pkgdir, dist = self.create_dist() + + dist = Distribution() + cmd = install_dist(dist) + dist.command_obj['install_dist'] = cmd + cmd.root = install_dir + cmd.record = os.path.join(pkgdir, 'filelist') + cmd.ensure_finalized() + cmd.run() + + # let's check the record file was created with four + # lines, one for each .dist-info entry: METADATA, + # INSTALLER, REQUSTED, RECORD + f = open(cmd.record) + try: + self.assertEqual(len(f.readlines()), 4) + finally: + f.close() + + # XXX test that fancy_getopt is okay with options named + # record and no-record but unrelated + + +def test_suite(): + return unittest.makeSuite(InstallTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_command_install_distinfo.py b/Lib/packaging/tests/test_command_install_distinfo.py new file mode 100644 index 000000000000..3d33691de692 --- /dev/null +++ b/Lib/packaging/tests/test_command_install_distinfo.py @@ -0,0 +1,192 @@ +"""Tests for ``packaging.command.install_distinfo``. """ + +import os +import csv +import hashlib +import sys + +from packaging.command.install_distinfo import install_distinfo +from packaging.command.cmd import Command +from packaging.metadata import Metadata +from packaging.tests import unittest, support + + +class DummyInstallCmd(Command): + + def __init__(self, dist=None): + self.outputs = [] + self.distribution = dist + + def __getattr__(self, name): + return None + + def ensure_finalized(self): + pass + + def get_outputs(self): + return (self.outputs + + self.get_finalized_command('install_distinfo').get_outputs()) + + +class InstallDistinfoTestCase(support.TempdirManager, + support.LoggingCatcher, + unittest.TestCase): + + checkLists = lambda self, x, y: self.assertListEqual(sorted(x), sorted(y)) + + def test_empty_install(self): + pkg_dir, dist = self.create_dist(name='foo', + version='1.0') + install_dir = self.mkdtemp() + + install = DummyInstallCmd(dist) + dist.command_obj['install_dist'] = install + + cmd = install_distinfo(dist) + dist.command_obj['install_distinfo'] = cmd + + cmd.initialize_options() + cmd.distinfo_dir = install_dir + cmd.ensure_finalized() + cmd.run() + + self.checkLists(os.listdir(install_dir), ['foo-1.0.dist-info']) + + dist_info = os.path.join(install_dir, 'foo-1.0.dist-info') + self.checkLists(os.listdir(dist_info), + ['METADATA', 'RECORD', 'REQUESTED', 'INSTALLER']) + with open(os.path.join(dist_info, 'INSTALLER')) as fp: + self.assertEqual(fp.read(), 'distutils') + with open(os.path.join(dist_info, 'REQUESTED')) as fp: + self.assertEqual(fp.read(), '') + meta_path = os.path.join(dist_info, 'METADATA') + self.assertTrue(Metadata(path=meta_path).check()) + + def test_installer(self): + pkg_dir, dist = self.create_dist(name='foo', + version='1.0') + install_dir = self.mkdtemp() + + install = DummyInstallCmd(dist) + dist.command_obj['install_dist'] = install + + cmd = install_distinfo(dist) + dist.command_obj['install_distinfo'] = cmd + + cmd.initialize_options() + cmd.distinfo_dir = install_dir + cmd.installer = 'bacon-python' + cmd.ensure_finalized() + cmd.run() + + dist_info = os.path.join(install_dir, 'foo-1.0.dist-info') + with open(os.path.join(dist_info, 'INSTALLER')) as fp: + self.assertEqual(fp.read(), 'bacon-python') + + def test_requested(self): + pkg_dir, dist = self.create_dist(name='foo', + version='1.0') + install_dir = self.mkdtemp() + + install = DummyInstallCmd(dist) + dist.command_obj['install_dist'] = install + + cmd = install_distinfo(dist) + dist.command_obj['install_distinfo'] = cmd + + cmd.initialize_options() + cmd.distinfo_dir = install_dir + cmd.requested = False + cmd.ensure_finalized() + cmd.run() + + dist_info = os.path.join(install_dir, 'foo-1.0.dist-info') + self.checkLists(os.listdir(dist_info), + ['METADATA', 'RECORD', 'INSTALLER']) + + def test_no_record(self): + pkg_dir, dist = self.create_dist(name='foo', + version='1.0') + install_dir = self.mkdtemp() + + install = DummyInstallCmd(dist) + dist.command_obj['install_dist'] = install + + cmd = install_distinfo(dist) + dist.command_obj['install_distinfo'] = cmd + + cmd.initialize_options() + cmd.distinfo_dir = install_dir + cmd.no_record = True + cmd.ensure_finalized() + cmd.run() + + dist_info = os.path.join(install_dir, 'foo-1.0.dist-info') + self.checkLists(os.listdir(dist_info), + ['METADATA', 'REQUESTED', 'INSTALLER']) + + def test_record(self): + pkg_dir, dist = self.create_dist(name='foo', + version='1.0') + install_dir = self.mkdtemp() + + install = DummyInstallCmd(dist) + dist.command_obj['install_dist'] = install + + fake_dists = os.path.join(os.path.dirname(__file__), 'fake_dists') + fake_dists = os.path.realpath(fake_dists) + + # for testing, we simply add all files from _backport's fake_dists + dirs = [] + for dir in os.listdir(fake_dists): + full_path = os.path.join(fake_dists, dir) + if (not dir.endswith('.egg') or dir.endswith('.egg-info') or + dir.endswith('.dist-info')) and os.path.isdir(full_path): + dirs.append(full_path) + + for dir in dirs: + for path, subdirs, files in os.walk(dir): + install.outputs += [os.path.join(path, f) for f in files] + install.outputs += [os.path.join('path', f + 'c') + for f in files if f.endswith('.py')] + + cmd = install_distinfo(dist) + dist.command_obj['install_distinfo'] = cmd + + cmd.initialize_options() + cmd.distinfo_dir = install_dir + cmd.ensure_finalized() + cmd.run() + + dist_info = os.path.join(install_dir, 'foo-1.0.dist-info') + + expected = [] + for f in install.get_outputs(): + if (f.endswith('.pyc') or f == os.path.join( + install_dir, 'foo-1.0.dist-info', 'RECORD')): + expected.append([f, '', '']) + else: + size = os.path.getsize(f) + md5 = hashlib.md5() + with open(f) as fp: + md5.update(fp.read().encode()) + hash = md5.hexdigest() + expected.append([f, hash, str(size)]) + + parsed = [] + with open(os.path.join(dist_info, 'RECORD'), 'r') as f: + reader = csv.reader(f, delimiter=',', + lineterminator=os.linesep, + quotechar='"') + parsed = list(reader) + + self.maxDiff = None + self.checkLists(parsed, expected) + + +def test_suite(): + return unittest.makeSuite(InstallDistinfoTestCase) + + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_command_install_headers.py b/Lib/packaging/tests/test_command_install_headers.py new file mode 100644 index 000000000000..f2906a7e1802 --- /dev/null +++ b/Lib/packaging/tests/test_command_install_headers.py @@ -0,0 +1,38 @@ +"""Tests for packaging.command.install_headers.""" +import os + +from packaging.command.install_headers import install_headers +from packaging.tests import unittest, support + + +class InstallHeadersTestCase(support.TempdirManager, + support.LoggingCatcher, + unittest.TestCase): + + def test_simple_run(self): + # we have two headers + header_list = self.mkdtemp() + header1 = os.path.join(header_list, 'header1') + header2 = os.path.join(header_list, 'header2') + self.write_file(header1) + self.write_file(header2) + headers = [header1, header2] + + pkg_dir, dist = self.create_dist(headers=headers) + cmd = install_headers(dist) + self.assertEqual(cmd.get_inputs(), headers) + + # let's run the command + cmd.install_dir = os.path.join(pkg_dir, 'inst') + cmd.ensure_finalized() + cmd.run() + + # let's check the results + self.assertEqual(len(cmd.get_outputs()), 2) + + +def test_suite(): + return unittest.makeSuite(InstallHeadersTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_command_install_lib.py b/Lib/packaging/tests/test_command_install_lib.py new file mode 100644 index 000000000000..99d47dd621ef --- /dev/null +++ b/Lib/packaging/tests/test_command_install_lib.py @@ -0,0 +1,111 @@ +"""Tests for packaging.command.install_data.""" +import sys +import os + +from packaging.tests import unittest, support +from packaging.command.install_lib import install_lib +from packaging.compiler.extension import Extension +from packaging.errors import PackagingOptionError + +try: + no_bytecode = sys.dont_write_bytecode + bytecode_support = True +except AttributeError: + no_bytecode = False + bytecode_support = False + + +class InstallLibTestCase(support.TempdirManager, + support.LoggingCatcher, + support.EnvironRestorer, + unittest.TestCase): + + restore_environ = ['PYTHONPATH'] + + def test_finalize_options(self): + pkg_dir, dist = self.create_dist() + cmd = install_lib(dist) + + cmd.finalize_options() + self.assertTrue(cmd.compile) + self.assertEqual(cmd.optimize, 0) + + # optimize must be 0, 1, or 2 + cmd.optimize = 'foo' + self.assertRaises(PackagingOptionError, cmd.finalize_options) + cmd.optimize = '4' + self.assertRaises(PackagingOptionError, cmd.finalize_options) + + cmd.optimize = '2' + cmd.finalize_options() + self.assertEqual(cmd.optimize, 2) + + @unittest.skipIf(no_bytecode, 'byte-compile not supported') + def test_byte_compile(self): + pkg_dir, dist = self.create_dist() + cmd = install_lib(dist) + cmd.compile = True + cmd.optimize = 1 + + f = os.path.join(pkg_dir, 'foo.py') + self.write_file(f, '# python file') + cmd.byte_compile([f]) + self.assertTrue(os.path.exists(os.path.join(pkg_dir, 'foo.pyc'))) + self.assertTrue(os.path.exists(os.path.join(pkg_dir, 'foo.pyo'))) + + def test_get_outputs(self): + pkg_dir, dist = self.create_dist() + cmd = install_lib(dist) + + # setting up a dist environment + cmd.compile = True + cmd.optimize = 1 + cmd.install_dir = pkg_dir + f = os.path.join(pkg_dir, '__init__.py') + self.write_file(f, '# python package') + cmd.distribution.ext_modules = [Extension('foo', ['xxx'])] + cmd.distribution.packages = [pkg_dir] + cmd.distribution.script_name = 'setup.py' + + # get_output should return 4 elements + self.assertEqual(len(cmd.get_outputs()), 4) + + def test_get_inputs(self): + pkg_dir, dist = self.create_dist() + cmd = install_lib(dist) + + # setting up a dist environment + cmd.compile = True + cmd.optimize = 1 + cmd.install_dir = pkg_dir + f = os.path.join(pkg_dir, '__init__.py') + self.write_file(f, '# python package') + cmd.distribution.ext_modules = [Extension('foo', ['xxx'])] + cmd.distribution.packages = [pkg_dir] + cmd.distribution.script_name = 'setup.py' + + # get_input should return 2 elements + self.assertEqual(len(cmd.get_inputs()), 2) + + @unittest.skipUnless(bytecode_support, + 'sys.dont_write_bytecode not supported') + def test_dont_write_bytecode(self): + # makes sure byte_compile is not used + pkg_dir, dist = self.create_dist() + cmd = install_lib(dist) + cmd.compile = True + cmd.optimize = 1 + + self.addCleanup(setattr, sys, 'dont_write_bytecode', + sys.dont_write_bytecode) + sys.dont_write_bytecode = True + cmd.byte_compile([]) + + self.assertIn('byte-compiling is disabled', self.get_logs()[0]) + + +def test_suite(): + return unittest.makeSuite(InstallLibTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_command_install_scripts.py b/Lib/packaging/tests/test_command_install_scripts.py new file mode 100644 index 000000000000..08c7338e34a9 --- /dev/null +++ b/Lib/packaging/tests/test_command_install_scripts.py @@ -0,0 +1,78 @@ +"""Tests for packaging.command.install_scripts.""" +import os + +from packaging.tests import unittest, support +from packaging.command.install_scripts import install_scripts +from packaging.dist import Distribution + + +class InstallScriptsTestCase(support.TempdirManager, + support.LoggingCatcher, + unittest.TestCase): + + def test_default_settings(self): + dist = Distribution() + dist.command_obj["build"] = support.DummyCommand( + build_scripts="/foo/bar") + dist.command_obj["install_dist"] = support.DummyCommand( + install_scripts="/splat/funk", + force=True, + skip_build=True, + ) + cmd = install_scripts(dist) + self.assertFalse(cmd.force) + self.assertFalse(cmd.skip_build) + self.assertIs(cmd.build_dir, None) + self.assertIs(cmd.install_dir, None) + + cmd.finalize_options() + + self.assertTrue(cmd.force) + self.assertTrue(cmd.skip_build) + self.assertEqual(cmd.build_dir, "/foo/bar") + self.assertEqual(cmd.install_dir, "/splat/funk") + + def test_installation(self): + source = self.mkdtemp() + expected = [] + + def write_script(name, text): + expected.append(name) + f = open(os.path.join(source, name), "w") + try: + f.write(text) + finally: + f.close() + + write_script("script1.py", ("#! /usr/bin/env python2.3\n" + "# bogus script w/ Python sh-bang\n" + "pass\n")) + write_script("script2.py", ("#!/usr/bin/python\n" + "# bogus script w/ Python sh-bang\n" + "pass\n")) + write_script("shell.sh", ("#!/bin/sh\n" + "# bogus shell script w/ sh-bang\n" + "exit 0\n")) + + target = self.mkdtemp() + dist = Distribution() + dist.command_obj["build"] = support.DummyCommand(build_scripts=source) + dist.command_obj["install_dist"] = support.DummyCommand( + install_scripts=target, + force=True, + skip_build=True, + ) + cmd = install_scripts(dist) + cmd.finalize_options() + cmd.run() + + installed = os.listdir(target) + for name in expected: + self.assertIn(name, installed) + + +def test_suite(): + return unittest.makeSuite(InstallScriptsTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_command_register.py b/Lib/packaging/tests/test_command_register.py new file mode 100644 index 000000000000..7aa487ae5bc9 --- /dev/null +++ b/Lib/packaging/tests/test_command_register.py @@ -0,0 +1,259 @@ +"""Tests for packaging.command.register.""" +import os +import getpass +import urllib.request +import urllib.error +import urllib.parse + +try: + import docutils + DOCUTILS_SUPPORT = True +except ImportError: + DOCUTILS_SUPPORT = False + +from packaging.tests import unittest, support +from packaging.command import register as register_module +from packaging.command.register import register +from packaging.errors import PackagingSetupError + + +PYPIRC_NOPASSWORD = """\ +[distutils] + +index-servers = + server1 + +[server1] +username:me +""" + +WANTED_PYPIRC = """\ +[distutils] +index-servers = + pypi + +[pypi] +username:tarek +password:password +""" + + +class Inputs: + """Fakes user inputs.""" + def __init__(self, *answers): + self.answers = answers + self.index = 0 + + def __call__(self, prompt=''): + try: + return self.answers[self.index] + finally: + self.index += 1 + + +class FakeOpener: + """Fakes a PyPI server""" + def __init__(self): + self.reqs = [] + + def __call__(self, *args): + return self + + def open(self, req): + self.reqs.append(req) + return self + + def read(self): + return 'xxx' + + +class RegisterTestCase(support.TempdirManager, + support.EnvironRestorer, + support.LoggingCatcher, + unittest.TestCase): + + restore_environ = ['HOME'] + + def setUp(self): + super(RegisterTestCase, self).setUp() + self.tmp_dir = self.mkdtemp() + self.rc = os.path.join(self.tmp_dir, '.pypirc') + os.environ['HOME'] = self.tmp_dir + + # patching the password prompt + self._old_getpass = getpass.getpass + + def _getpass(prompt): + return 'password' + + getpass.getpass = _getpass + self.old_opener = urllib.request.build_opener + self.conn = urllib.request.build_opener = FakeOpener() + + def tearDown(self): + getpass.getpass = self._old_getpass + urllib.request.build_opener = self.old_opener + if hasattr(register_module, 'input'): + del register_module.input + super(RegisterTestCase, self).tearDown() + + def _get_cmd(self, metadata=None): + if metadata is None: + metadata = {'url': 'xxx', 'author': 'xxx', + 'author_email': 'xxx', + 'name': 'xxx', 'version': 'xxx'} + pkg_info, dist = self.create_dist(**metadata) + return register(dist) + + def test_create_pypirc(self): + # this test makes sure a .pypirc file + # is created when requested. + + # let's create a register instance + cmd = self._get_cmd() + + # we shouldn't have a .pypirc file yet + self.assertFalse(os.path.exists(self.rc)) + + # patching input and getpass.getpass + # so register gets happy + # Here's what we are faking : + # use your existing login (choice 1.) + # Username : 'tarek' + # Password : 'password' + # Save your login (y/N)? : 'y' + inputs = Inputs('1', 'tarek', 'y') + register_module.input = inputs + cmd.ensure_finalized() + cmd.run() + + # we should have a brand new .pypirc file + self.assertTrue(os.path.exists(self.rc)) + + # with the content similar to WANTED_PYPIRC + with open(self.rc) as fp: + content = fp.read() + self.assertEqual(content, WANTED_PYPIRC) + + # now let's make sure the .pypirc file generated + # really works : we shouldn't be asked anything + # if we run the command again + def _no_way(prompt=''): + raise AssertionError(prompt) + + register_module.input = _no_way + cmd.show_response = True + cmd.ensure_finalized() + cmd.run() + + # let's see what the server received : we should + # have 2 similar requests + self.assertEqual(len(self.conn.reqs), 2) + req1 = dict(self.conn.reqs[0].headers) + req2 = dict(self.conn.reqs[1].headers) + self.assertEqual(req2['Content-length'], req1['Content-length']) + self.assertIn('xxx', self.conn.reqs[1].data) + + def test_password_not_in_file(self): + + self.write_file(self.rc, PYPIRC_NOPASSWORD) + cmd = self._get_cmd() + cmd.finalize_options() + cmd._set_config() + cmd.send_metadata() + + # dist.password should be set + # therefore used afterwards by other commands + self.assertEqual(cmd.distribution.password, 'password') + + def test_registration(self): + # this test runs choice 2 + cmd = self._get_cmd() + inputs = Inputs('2', 'tarek', 'tarek@ziade.org') + register_module.input = inputs + # let's run the command + # FIXME does this send a real request? use a mock server + cmd.ensure_finalized() + cmd.run() + + # we should have send a request + self.assertEqual(len(self.conn.reqs), 1) + req = self.conn.reqs[0] + headers = dict(req.headers) + self.assertEqual(headers['Content-length'], '608') + self.assertIn('tarek', req.data) + + def test_password_reset(self): + # this test runs choice 3 + cmd = self._get_cmd() + inputs = Inputs('3', 'tarek@ziade.org') + register_module.input = inputs + cmd.ensure_finalized() + cmd.run() + + # we should have send a request + self.assertEqual(len(self.conn.reqs), 1) + req = self.conn.reqs[0] + headers = dict(req.headers) + self.assertEqual(headers['Content-length'], '290') + self.assertIn('tarek', req.data) + + @unittest.skipUnless(DOCUTILS_SUPPORT, 'needs docutils') + def test_strict(self): + # testing the script option + # when on, the register command stops if + # the metadata is incomplete or if + # long_description is not reSt compliant + + # empty metadata + cmd = self._get_cmd({'name': 'xxx', 'version': 'xxx'}) + cmd.ensure_finalized() + cmd.strict = True + inputs = Inputs('1', 'tarek', 'y') + register_module.input = inputs + self.assertRaises(PackagingSetupError, cmd.run) + + # metadata is OK but long_description is broken + metadata = {'home_page': 'xxx', 'author': 'xxx', + 'author_email': 'éxéxé', + 'name': 'xxx', 'version': 'xxx', + 'description': 'title\n==\n\ntext'} + + cmd = self._get_cmd(metadata) + cmd.ensure_finalized() + cmd.strict = True + + self.assertRaises(PackagingSetupError, cmd.run) + + # now something that works + metadata['description'] = 'title\n=====\n\ntext' + cmd = self._get_cmd(metadata) + cmd.ensure_finalized() + cmd.strict = True + inputs = Inputs('1', 'tarek', 'y') + register_module.input = inputs + cmd.ensure_finalized() + cmd.run() + + # strict is not by default + cmd = self._get_cmd() + cmd.ensure_finalized() + inputs = Inputs('1', 'tarek', 'y') + register_module.input = inputs + cmd.ensure_finalized() + cmd.run() + + def test_register_pep345(self): + cmd = self._get_cmd({}) + cmd.ensure_finalized() + cmd.distribution.metadata['Requires-Dist'] = ['lxml'] + data = cmd.build_post_data('submit') + self.assertEqual(data['metadata_version'], '1.2') + self.assertEqual(data['requires_dist'], ['lxml']) + + +def test_suite(): + return unittest.makeSuite(RegisterTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_command_sdist.py b/Lib/packaging/tests/test_command_sdist.py new file mode 100644 index 000000000000..956e2583662d --- /dev/null +++ b/Lib/packaging/tests/test_command_sdist.py @@ -0,0 +1,407 @@ +"""Tests for packaging.command.sdist.""" +import os +import zipfile +import tarfile +import logging + +# zlib is not used here, but if it's not available +# the tests that use zipfile may fail +try: + import zlib +except ImportError: + zlib = None + +try: + import grp + import pwd + UID_GID_SUPPORT = True +except ImportError: + UID_GID_SUPPORT = False + +from os.path import join +from packaging.tests import captured_stdout +from packaging.command.sdist import sdist +from packaging.command.sdist import show_formats +from packaging.dist import Distribution +from packaging.tests import unittest +from packaging.errors import PackagingOptionError +from packaging.util import find_executable +from packaging.tests import support +from shutil import get_archive_formats + +SETUP_PY = """ +from packaging.core import setup +import somecode + +setup(name='fake') +""" + +MANIFEST = """\ +# file GENERATED by packaging, do NOT edit +README +inroot.txt +data%(sep)sdata.dt +scripts%(sep)sscript.py +some%(sep)sfile.txt +some%(sep)sother_file.txt +somecode%(sep)s__init__.py +somecode%(sep)sdoc.dat +somecode%(sep)sdoc.txt +""" + + +def builder(dist, filelist): + filelist.append('bah') + + +class SDistTestCase(support.TempdirManager, + support.LoggingCatcher, + support.EnvironRestorer, + unittest.TestCase): + + restore_environ = ['HOME'] + + def setUp(self): + # PyPIRCCommandTestCase creates a temp dir already + # and put it in self.tmp_dir + super(SDistTestCase, self).setUp() + self.tmp_dir = self.mkdtemp() + os.environ['HOME'] = self.tmp_dir + # setting up an environment + self.old_path = os.getcwd() + os.mkdir(join(self.tmp_dir, 'somecode')) + os.mkdir(join(self.tmp_dir, 'dist')) + # a package, and a README + self.write_file((self.tmp_dir, 'README'), 'xxx') + self.write_file((self.tmp_dir, 'somecode', '__init__.py'), '#') + self.write_file((self.tmp_dir, 'setup.py'), SETUP_PY) + os.chdir(self.tmp_dir) + + def tearDown(self): + # back to normal + os.chdir(self.old_path) + super(SDistTestCase, self).tearDown() + + def get_cmd(self, metadata=None): + """Returns a cmd""" + if metadata is None: + metadata = {'name': 'fake', 'version': '1.0', + 'url': 'xxx', 'author': 'xxx', + 'author_email': 'xxx'} + dist = Distribution(metadata) + dist.script_name = 'setup.py' + dist.packages = ['somecode'] + dist.include_package_data = True + cmd = sdist(dist) + cmd.dist_dir = 'dist' + return dist, cmd + + @unittest.skipUnless(zlib, "requires zlib") + def test_prune_file_list(self): + # this test creates a package with some vcs dirs in it + # and launch sdist to make sure they get pruned + # on all systems + + # creating VCS directories with some files in them + os.mkdir(join(self.tmp_dir, 'somecode', '.svn')) + + self.write_file((self.tmp_dir, 'somecode', '.svn', 'ok.py'), 'xxx') + + os.mkdir(join(self.tmp_dir, 'somecode', '.hg')) + self.write_file((self.tmp_dir, 'somecode', '.hg', + 'ok'), 'xxx') + + os.mkdir(join(self.tmp_dir, 'somecode', '.git')) + self.write_file((self.tmp_dir, 'somecode', '.git', + 'ok'), 'xxx') + + # now building a sdist + dist, cmd = self.get_cmd() + + # zip is available universally + # (tar might not be installed under win32) + cmd.formats = ['zip'] + + cmd.ensure_finalized() + cmd.run() + + # now let's check what we have + dist_folder = join(self.tmp_dir, 'dist') + files = os.listdir(dist_folder) + self.assertEqual(files, ['fake-1.0.zip']) + + with zipfile.ZipFile(join(dist_folder, 'fake-1.0.zip')) as zip_file: + content = zip_file.namelist() + + # making sure everything has been pruned correctly + self.assertEqual(len(content), 3) + + @unittest.skipUnless(zlib, "requires zlib") + @unittest.skipIf(find_executable('tar') is None or + find_executable('gzip') is None, + 'requires tar and gzip programs') + def test_make_distribution(self): + # building a sdist + dist, cmd = self.get_cmd() + + # creating a gztar then a tar + cmd.formats = ['gztar', 'tar'] + cmd.ensure_finalized() + cmd.run() + + # making sure we have two files + dist_folder = join(self.tmp_dir, 'dist') + result = sorted(os.listdir(dist_folder)) + self.assertEqual(result, ['fake-1.0.tar', 'fake-1.0.tar.gz']) + + os.remove(join(dist_folder, 'fake-1.0.tar')) + os.remove(join(dist_folder, 'fake-1.0.tar.gz')) + + # now trying a tar then a gztar + cmd.formats = ['tar', 'gztar'] + + cmd.ensure_finalized() + cmd.run() + + result = sorted(os.listdir(dist_folder)) + self.assertEqual(result, ['fake-1.0.tar', 'fake-1.0.tar.gz']) + + @unittest.skipUnless(zlib, "requires zlib") + def test_add_defaults(self): + + # http://bugs.python.org/issue2279 + + # add_default should also include + # data_files and package_data + dist, cmd = self.get_cmd() + + # filling data_files by pointing files + # in package_data + dist.package_data = {'': ['*.cfg', '*.dat'], + 'somecode': ['*.txt']} + self.write_file((self.tmp_dir, 'somecode', 'doc.txt'), '#') + self.write_file((self.tmp_dir, 'somecode', 'doc.dat'), '#') + + # adding some data in data_files + data_dir = join(self.tmp_dir, 'data') + os.mkdir(data_dir) + self.write_file((data_dir, 'data.dt'), '#') + some_dir = join(self.tmp_dir, 'some') + os.mkdir(some_dir) + self.write_file((self.tmp_dir, 'inroot.txt'), '#') + self.write_file((some_dir, 'file.txt'), '#') + self.write_file((some_dir, 'other_file.txt'), '#') + + dist.data_files = {'data/data.dt': '{appdata}/data.dt', + 'inroot.txt': '{appdata}/inroot.txt', + 'some/file.txt': '{appdata}/file.txt', + 'some/other_file.txt': '{appdata}/other_file.txt'} + + # adding a script + script_dir = join(self.tmp_dir, 'scripts') + os.mkdir(script_dir) + self.write_file((script_dir, 'script.py'), '#') + dist.scripts = [join('scripts', 'script.py')] + + cmd.formats = ['zip'] + cmd.use_defaults = True + + cmd.ensure_finalized() + cmd.run() + + # now let's check what we have + dist_folder = join(self.tmp_dir, 'dist') + files = os.listdir(dist_folder) + self.assertEqual(files, ['fake-1.0.zip']) + + with zipfile.ZipFile(join(dist_folder, 'fake-1.0.zip')) as zip_file: + content = zip_file.namelist() + + # Making sure everything was added. This includes 9 code and data + # files in addition to PKG-INFO. + self.assertEqual(len(content), 10) + + # Checking the MANIFEST + with open(join(self.tmp_dir, 'MANIFEST')) as fp: + manifest = fp.read() + self.assertEqual(manifest, MANIFEST % {'sep': os.sep}) + + @unittest.skipUnless(zlib, "requires zlib") + def test_metadata_check_option(self): + # testing the `check-metadata` option + dist, cmd = self.get_cmd(metadata={'name': 'xxx', 'version': 'xxx'}) + + # this should raise some warnings + # with the check subcommand + cmd.ensure_finalized() + cmd.run() + warnings = self.get_logs(logging.WARN) + self.assertEqual(len(warnings), 3) + + # trying with a complete set of metadata + self.loghandler.flush() + dist, cmd = self.get_cmd() + cmd.ensure_finalized() + cmd.metadata_check = False + cmd.run() + warnings = self.get_logs(logging.WARN) + # removing manifest generated warnings + warnings = [warn for warn in warnings if + not warn.endswith('-- skipping')] + # the remaining warning is about the use of the default file list + self.assertEqual(len(warnings), 1) + + def test_show_formats(self): + __, stdout = captured_stdout(show_formats) + + # the output should be a header line + one line per format + num_formats = len(get_archive_formats()) + output = [line for line in stdout.split('\n') + if line.strip().startswith('--formats=')] + self.assertEqual(len(output), num_formats) + + def test_finalize_options(self): + + dist, cmd = self.get_cmd() + cmd.finalize_options() + + # default options set by finalize + self.assertEqual(cmd.manifest, 'MANIFEST') + self.assertEqual(cmd.dist_dir, 'dist') + + # formats has to be a string splitable on (' ', ',') or + # a stringlist + cmd.formats = 1 + self.assertRaises(PackagingOptionError, cmd.finalize_options) + cmd.formats = ['zip'] + cmd.finalize_options() + + # formats has to be known + cmd.formats = 'supazipa' + self.assertRaises(PackagingOptionError, cmd.finalize_options) + + @unittest.skipUnless(zlib, "requires zlib") + @unittest.skipUnless(UID_GID_SUPPORT, "requires grp and pwd support") + @unittest.skipIf(find_executable('tar') is None or + find_executable('gzip') is None, + 'requires tar and gzip programs') + def test_make_distribution_owner_group(self): + # building a sdist + dist, cmd = self.get_cmd() + + # creating a gztar and specifying the owner+group + cmd.formats = ['gztar'] + cmd.owner = pwd.getpwuid(0)[0] + cmd.group = grp.getgrgid(0)[0] + cmd.ensure_finalized() + cmd.run() + + # making sure we have the good rights + archive_name = join(self.tmp_dir, 'dist', 'fake-1.0.tar.gz') + with tarfile.open(archive_name) as archive: + for member in archive.getmembers(): + self.assertEqual(member.uid, 0) + self.assertEqual(member.gid, 0) + + # building a sdist again + dist, cmd = self.get_cmd() + + # creating a gztar + cmd.formats = ['gztar'] + cmd.ensure_finalized() + cmd.run() + + # making sure we have the good rights + archive_name = join(self.tmp_dir, 'dist', 'fake-1.0.tar.gz') + with tarfile.open(archive_name) as archive: + + # note that we are not testing the group ownership here + # because, depending on the platforms and the container + # rights (see #7408) + for member in archive.getmembers(): + self.assertEqual(member.uid, os.getuid()) + + def test_get_file_list(self): + # make sure MANIFEST is recalculated + dist, cmd = self.get_cmd() + # filling data_files by pointing files in package_data + dist.package_data = {'somecode': ['*.txt']} + self.write_file((self.tmp_dir, 'somecode', 'doc.txt'), '#') + cmd.ensure_finalized() + cmd.run() + + # Should produce four lines. Those lines are one comment, one default + # (README) and two package files. + with open(cmd.manifest) as f: + manifest = [line.strip() for line in f.read().split('\n') + if line.strip() != ''] + self.assertEqual(len(manifest), 4) + + # Adding a file + self.write_file((self.tmp_dir, 'somecode', 'doc2.txt'), '#') + + # make sure build_py is reinitialized, like a fresh run + build_py = dist.get_command_obj('build_py') + build_py.finalized = False + build_py.ensure_finalized() + + cmd.run() + + with open(cmd.manifest) as f: + manifest2 = [line.strip() for line in f.read().split('\n') + if line.strip() != ''] + + # Do we have the new file in MANIFEST? + self.assertEqual(len(manifest2), 5) + self.assertIn('doc2.txt', manifest2[-1]) + + def test_manifest_marker(self): + # check that autogenerated MANIFESTs have a marker + dist, cmd = self.get_cmd() + cmd.ensure_finalized() + cmd.run() + + with open(cmd.manifest) as f: + manifest = [line.strip() for line in f.read().split('\n') + if line.strip() != ''] + + self.assertEqual(manifest[0], + '# file GENERATED by packaging, do NOT edit') + + def test_manual_manifest(self): + # check that a MANIFEST without a marker is left alone + dist, cmd = self.get_cmd() + cmd.ensure_finalized() + self.write_file((self.tmp_dir, cmd.manifest), 'README.manual') + cmd.run() + + with open(cmd.manifest) as f: + manifest = [line.strip() for line in f.read().split('\n') + if line.strip() != ''] + + self.assertEqual(manifest, ['README.manual']) + + def test_template(self): + dist, cmd = self.get_cmd() + dist.extra_files = ['include yeah'] + cmd.ensure_finalized() + self.write_file((self.tmp_dir, 'yeah'), 'xxx') + cmd.run() + with open(cmd.manifest) as f: + content = f.read() + + self.assertIn('yeah', content) + + def test_manifest_builder(self): + dist, cmd = self.get_cmd() + cmd.manifest_builders = 'packaging.tests.test_command_sdist.builder' + cmd.ensure_finalized() + cmd.run() + self.assertIn('bah', cmd.filelist.files) + + +def test_suite(): + return unittest.makeSuite(SDistTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_command_test.py b/Lib/packaging/tests/test_command_test.py new file mode 100644 index 000000000000..4fd8452683d0 --- /dev/null +++ b/Lib/packaging/tests/test_command_test.py @@ -0,0 +1,225 @@ +import os +import re +import sys +import shutil +import logging +import unittest as ut1 +import packaging.database + +from os.path import join +from operator import getitem, setitem, delitem +from packaging.command.build import build +from packaging.tests import unittest +from packaging.tests.support import (TempdirManager, EnvironRestorer, + LoggingCatcher) +from packaging.command.test import test +from packaging.command import set_command +from packaging.dist import Distribution + + +EXPECTED_OUTPUT_RE = r'''FAIL: test_blah \(myowntestmodule.SomeTest\) +---------------------------------------------------------------------- +Traceback \(most recent call last\): + File ".+/myowntestmodule.py", line \d+, in test_blah + self.fail\("horribly"\) +AssertionError: horribly +''' + +here = os.path.dirname(os.path.abspath(__file__)) + + +class MockBuildCmd(build): + build_lib = "mock build lib" + command_name = 'build' + plat_name = 'whatever' + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + self._record.append("build has run") + + +class TestTest(TempdirManager, + EnvironRestorer, + LoggingCatcher, + unittest.TestCase): + + restore_environ = ['PYTHONPATH'] + + def setUp(self): + super(TestTest, self).setUp() + self.addCleanup(packaging.database.clear_cache) + new_pythonpath = os.path.dirname(os.path.dirname(here)) + pythonpath = os.environ.get('PYTHONPATH') + if pythonpath is not None: + new_pythonpath = os.pathsep.join((new_pythonpath, pythonpath)) + os.environ['PYTHONPATH'] = new_pythonpath + + def assert_re_match(self, pattern, string): + def quote(s): + lines = ['## ' + line for line in s.split('\n')] + sep = ["#" * 60] + return [''] + sep + lines + sep + msg = quote(pattern) + ["didn't match"] + quote(string) + msg = "\n".join(msg) + if not re.search(pattern, string): + self.fail(msg) + + def prepare_dist(self, dist_name): + pkg_dir = join(os.path.dirname(__file__), "dists", dist_name) + temp_pkg_dir = join(self.mkdtemp(), dist_name) + shutil.copytree(pkg_dir, temp_pkg_dir) + return temp_pkg_dir + + def safely_replace(self, obj, attr, + new_val=None, delete=False, dictionary=False): + """Replace a object's attribute returning to its original state at the + end of the test run. Creates the attribute if not present before + (deleting afterwards). When delete=True, makes sure the value is del'd + for the test run. If dictionary is set to True, operates of its items + rather than attributes.""" + if dictionary: + _setattr, _getattr, _delattr = setitem, getitem, delitem + + def _hasattr(_dict, value): + return value in _dict + else: + _setattr, _getattr, _delattr, _hasattr = (setattr, getattr, + delattr, hasattr) + + orig_has_attr = _hasattr(obj, attr) + if orig_has_attr: + orig_val = _getattr(obj, attr) + + if delete is False: + _setattr(obj, attr, new_val) + elif orig_has_attr: + _delattr(obj, attr) + + def do_cleanup(): + if orig_has_attr: + _setattr(obj, attr, orig_val) + elif _hasattr(obj, attr): + _delattr(obj, attr) + + self.addCleanup(do_cleanup) + + def test_runs_unittest(self): + module_name, a_module = self.prepare_a_module() + record = [] + a_module.recorder = lambda *args: record.append("suite") + + class MockTextTestRunner: + def __init__(*_, **__): + pass + + def run(_self, suite): + record.append("run") + + self.safely_replace(ut1, "TextTestRunner", MockTextTestRunner) + + dist = Distribution() + cmd = test(dist) + cmd.suite = "%s.recorder" % module_name + cmd.run() + self.assertEqual(record, ["suite", "run"]) + + def test_builds_before_running_tests(self): + self.addCleanup(set_command, 'packaging.command.build.build') + set_command('packaging.tests.test_command_test.MockBuildCmd') + + dist = Distribution() + dist.get_command_obj('build')._record = record = [] + cmd = test(dist) + cmd.runner = self.prepare_named_function(lambda: None) + cmd.ensure_finalized() + cmd.run() + self.assertEqual(['build has run'], record) + + def _test_works_with_2to3(self): + pass + + def test_checks_requires(self): + dist = Distribution() + cmd = test(dist) + phony_project = 'ohno_ohno-impossible_1234-name_stop-that!' + cmd.tests_require = [phony_project] + cmd.ensure_finalized() + logs = self.get_logs(logging.WARNING) + self.assertEqual(1, len(logs)) + self.assertIn(phony_project, logs[0]) + + def prepare_a_module(self): + tmp_dir = self.mkdtemp() + sys.path.append(tmp_dir) + self.addCleanup(sys.path.remove, tmp_dir) + + self.write_file((tmp_dir, 'packaging_tests_a.py'), '') + import packaging_tests_a as a_module + return "packaging_tests_a", a_module + + def prepare_named_function(self, func): + module_name, a_module = self.prepare_a_module() + a_module.recorder = func + return "%s.recorder" % module_name + + def test_custom_runner(self): + dist = Distribution() + cmd = test(dist) + record = [] + cmd.runner = self.prepare_named_function( + lambda: record.append("runner called")) + cmd.ensure_finalized() + cmd.run() + self.assertEqual(["runner called"], record) + + def prepare_mock_ut2(self): + class MockUTClass: + def __init__(*_, **__): + pass + + def discover(self): + pass + + def run(self, _): + pass + + class MockUTModule: + TestLoader = MockUTClass + TextTestRunner = MockUTClass + + mock_ut2 = MockUTModule() + self.safely_replace(sys.modules, "unittest2", + mock_ut2, dictionary=True) + return mock_ut2 + + def test_gets_unittest_discovery(self): + mock_ut2 = self.prepare_mock_ut2() + dist = Distribution() + cmd = test(dist) + self.safely_replace(ut1.TestLoader, "discover", lambda: None) + self.assertEqual(cmd.get_ut_with_discovery(), ut1) + + del ut1.TestLoader.discover + self.assertEqual(cmd.get_ut_with_discovery(), mock_ut2) + + def test_calls_discover(self): + self.safely_replace(ut1.TestLoader, "discover", delete=True) + mock_ut2 = self.prepare_mock_ut2() + record = [] + mock_ut2.TestLoader.discover = lambda self, path: record.append(path) + dist = Distribution() + cmd = test(dist) + cmd.run() + self.assertEqual([os.curdir], record) + + +def test_suite(): + return unittest.makeSuite(TestTest) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_command_upload.py b/Lib/packaging/tests/test_command_upload.py new file mode 100644 index 000000000000..f2e338b9dd62 --- /dev/null +++ b/Lib/packaging/tests/test_command_upload.py @@ -0,0 +1,157 @@ +"""Tests for packaging.command.upload.""" +import os +import sys + +from packaging.command.upload import upload +from packaging.dist import Distribution +from packaging.errors import PackagingOptionError + +from packaging.tests import unittest, support +from packaging.tests.pypi_server import PyPIServer, PyPIServerTestCase + + +PYPIRC_NOPASSWORD = """\ +[distutils] + +index-servers = + server1 + +[server1] +username:me +""" + +PYPIRC = """\ +[distutils] + +index-servers = + server1 + server2 + +[server1] +username:me +password:secret + +[server2] +username:meagain +password: secret +realm:acme +repository:http://another.pypi/ +""" + + +class UploadTestCase(support.TempdirManager, support.EnvironRestorer, + support.LoggingCatcher, PyPIServerTestCase): + + restore_environ = ['HOME'] + + def setUp(self): + super(UploadTestCase, self).setUp() + self.tmp_dir = self.mkdtemp() + self.rc = os.path.join(self.tmp_dir, '.pypirc') + os.environ['HOME'] = self.tmp_dir + + def test_finalize_options(self): + # new format + self.write_file(self.rc, PYPIRC) + dist = Distribution() + cmd = upload(dist) + cmd.finalize_options() + for attr, expected in (('username', 'me'), ('password', 'secret'), + ('realm', 'pypi'), + ('repository', 'http://pypi.python.org/pypi')): + self.assertEqual(getattr(cmd, attr), expected) + + def test_finalize_options_unsigned_identity_raises_exception(self): + self.write_file(self.rc, PYPIRC) + dist = Distribution() + cmd = upload(dist) + cmd.identity = True + cmd.sign = False + self.assertRaises(PackagingOptionError, cmd.finalize_options) + + def test_saved_password(self): + # file with no password + self.write_file(self.rc, PYPIRC_NOPASSWORD) + + # make sure it passes + dist = Distribution() + cmd = upload(dist) + cmd.ensure_finalized() + self.assertEqual(cmd.password, None) + + # make sure we get it as well, if another command + # initialized it at the dist level + dist.password = 'xxx' + cmd = upload(dist) + cmd.finalize_options() + self.assertEqual(cmd.password, 'xxx') + + def test_upload_without_files_raises_exception(self): + dist = Distribution() + cmd = upload(dist) + self.assertRaises(PackagingOptionError, cmd.run) + + def test_upload(self): + path = os.path.join(self.tmp_dir, 'xxx') + self.write_file(path) + command, pyversion, filename = 'xxx', '3.3', path + dist_files = [(command, pyversion, filename)] + + # lets run it + pkg_dir, dist = self.create_dist(dist_files=dist_files, author='dédé') + cmd = upload(dist) + cmd.ensure_finalized() + cmd.repository = self.pypi.full_address + cmd.run() + + # what did we send ? + handler, request_data = self.pypi.requests[-1] + headers = handler.headers + #self.assertIn('dédé', str(request_data)) + self.assertIn(b'xxx', request_data) + + self.assertEqual(int(headers['content-length']), len(request_data)) + self.assertLess(int(headers['content-length']), 2500) + self.assertTrue(headers['content-type'].startswith('multipart/form-data')) + self.assertEqual(handler.command, 'POST') + self.assertNotIn('\n', headers['authorization']) + + def test_upload_docs(self): + path = os.path.join(self.tmp_dir, 'xxx') + self.write_file(path) + command, pyversion, filename = 'xxx', '3.3', path + dist_files = [(command, pyversion, filename)] + docs_path = os.path.join(self.tmp_dir, "build", "docs") + os.makedirs(docs_path) + self.write_file(os.path.join(docs_path, "index.html"), "yellow") + self.write_file(self.rc, PYPIRC) + + # lets run it + pkg_dir, dist = self.create_dist(dist_files=dist_files, author='dédé') + + cmd = upload(dist) + cmd.get_finalized_command("build").run() + cmd.upload_docs = True + cmd.ensure_finalized() + cmd.repository = self.pypi.full_address + try: + prev_dir = os.getcwd() + os.chdir(self.tmp_dir) + cmd.run() + finally: + os.chdir(prev_dir) + + handler, request_data = self.pypi.requests[-1] + action, name, content = request_data.split( + "----------------GHSKFJDLGDS7543FJKLFHRE75642756743254" + .encode())[1:4] + + self.assertIn(b'name=":action"', action) + self.assertIn(b'doc_upload', action) + + +def test_suite(): + return unittest.makeSuite(UploadTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_command_upload_docs.py b/Lib/packaging/tests/test_command_upload_docs.py new file mode 100644 index 000000000000..b103894374b9 --- /dev/null +++ b/Lib/packaging/tests/test_command_upload_docs.py @@ -0,0 +1,205 @@ +"""Tests for packaging.command.upload_docs.""" +import os +import sys +import shutil +import zipfile + +from packaging.command import upload_docs as upload_docs_mod +from packaging.command.upload_docs import (upload_docs, zip_dir, + encode_multipart) +from packaging.dist import Distribution +from packaging.errors import PackagingFileError, PackagingOptionError + +from packaging.tests import unittest, support +from packaging.tests.pypi_server import PyPIServerTestCase + + +EXPECTED_MULTIPART_OUTPUT = [ + b'---x', + b'Content-Disposition: form-data; name="username"', + b'', + b'wok', + b'---x', + b'Content-Disposition: form-data; name="password"', + b'', + b'secret', + b'---x', + b'Content-Disposition: form-data; name="picture"; filename="wok.png"', + b'', + b'PNG89', + b'---x--', + b'', +] + +PYPIRC = """\ +[distutils] +index-servers = server1 + +[server1] +repository = %s +username = real_slim_shady +password = long_island +""" + +class UploadDocsTestCase(support.TempdirManager, + support.EnvironRestorer, + support.LoggingCatcher, + PyPIServerTestCase): + + restore_environ = ['HOME'] + + def setUp(self): + super(UploadDocsTestCase, self).setUp() + self.tmp_dir = self.mkdtemp() + self.rc = os.path.join(self.tmp_dir, '.pypirc') + os.environ['HOME'] = self.tmp_dir + self.dist = Distribution() + self.dist.metadata['Name'] = "distr-name" + self.cmd = upload_docs(self.dist) + + def test_default_uploaddir(self): + sandbox = self.mkdtemp() + previous = os.getcwd() + os.chdir(sandbox) + try: + os.mkdir("build") + self.prepare_sample_dir("build") + self.cmd.ensure_finalized() + self.assertEqual(self.cmd.upload_dir, os.path.join("build", "docs")) + finally: + os.chdir(previous) + + def test_default_uploaddir_looks_for_doc_also(self): + sandbox = self.mkdtemp() + previous = os.getcwd() + os.chdir(sandbox) + try: + os.mkdir("build") + self.prepare_sample_dir("build") + os.rename(os.path.join("build", "docs"), os.path.join("build", "doc")) + self.cmd.ensure_finalized() + self.assertEqual(self.cmd.upload_dir, os.path.join("build", "doc")) + finally: + os.chdir(previous) + + def prepare_sample_dir(self, sample_dir=None): + if sample_dir is None: + sample_dir = self.mkdtemp() + os.mkdir(os.path.join(sample_dir, "docs")) + self.write_file(os.path.join(sample_dir, "docs", "index.html"), "Ce mortel ennui") + self.write_file(os.path.join(sample_dir, "index.html"), "Oh la la") + return sample_dir + + def test_zip_dir(self): + source_dir = self.prepare_sample_dir() + compressed = zip_dir(source_dir) + + zip_f = zipfile.ZipFile(compressed) + self.assertEqual(zip_f.namelist(), ['index.html', 'docs/index.html']) + + def test_encode_multipart(self): + fields = [('username', 'wok'), ('password', 'secret')] + files = [('picture', 'wok.png', b'PNG89')] + content_type, body = encode_multipart(fields, files, b'-x') + self.assertEqual(b'multipart/form-data; boundary=-x', content_type) + self.assertEqual(EXPECTED_MULTIPART_OUTPUT, body.split(b'\r\n')) + + def prepare_command(self): + self.cmd.upload_dir = self.prepare_sample_dir() + self.cmd.ensure_finalized() + self.cmd.repository = self.pypi.full_address + self.cmd.username = "username" + self.cmd.password = "password" + + def test_upload(self): + self.prepare_command() + self.cmd.run() + + self.assertEqual(len(self.pypi.requests), 1) + handler, request_data = self.pypi.requests[-1] + self.assertIn(b"content", request_data) + self.assertIn("Basic", handler.headers['authorization']) + self.assertTrue(handler.headers['content-type'] + .startswith('multipart/form-data;')) + + action, name, version, content =\ + request_data.split("----------------GHSKFJDLGDS7543FJKLFHRE75642756743254".encode())[1:5] + + + # check that we picked the right chunks + self.assertIn(b'name=":action"', action) + self.assertIn(b'name="name"', name) + self.assertIn(b'name="version"', version) + self.assertIn(b'name="content"', content) + + # check their contents + self.assertIn(b'doc_upload', action) + self.assertIn(b'distr-name', name) + self.assertIn(b'docs/index.html', content) + self.assertIn(b'Ce mortel ennui', content) + + def test_https_connection(self): + https_called = False + + orig_https = upload_docs_mod.http.client.HTTPConnection + + def https_conn_wrapper(*args): + nonlocal https_called + https_called = True + # the testing server is http + return upload_docs_mod.http.client.HTTPConnection(*args) + + upload_docs_mod.http.client.HTTPSConnection = https_conn_wrapper + try: + self.prepare_command() + self.cmd.run() + self.assertFalse(https_called) + + self.cmd.repository = self.cmd.repository.replace("http", "https") + self.cmd.run() + self.assertTrue(https_called) + finally: + upload_docs_mod.http.client.HTTPConnection = orig_https + + def test_handling_response(self): + self.pypi.default_response_status = '403 Forbidden' + self.prepare_command() + self.cmd.run() + self.assertIn('Upload failed (403): Forbidden', self.get_logs()[-1]) + + self.pypi.default_response_status = '301 Moved Permanently' + self.pypi.default_response_headers.append(("Location", "brand_new_location")) + self.cmd.run() + self.assertIn('brand_new_location', self.get_logs()[-1]) + + def test_reads_pypirc_data(self): + self.write_file(self.rc, PYPIRC % self.pypi.full_address) + self.cmd.repository = self.pypi.full_address + self.cmd.upload_dir = self.prepare_sample_dir() + self.cmd.ensure_finalized() + self.assertEqual(self.cmd.username, "real_slim_shady") + self.assertEqual(self.cmd.password, "long_island") + + def test_checks_index_html_presence(self): + self.cmd.upload_dir = self.prepare_sample_dir() + os.remove(os.path.join(self.cmd.upload_dir, "index.html")) + self.assertRaises(PackagingFileError, self.cmd.ensure_finalized) + + def test_checks_upload_dir(self): + self.cmd.upload_dir = self.prepare_sample_dir() + shutil.rmtree(os.path.join(self.cmd.upload_dir)) + self.assertRaises(PackagingOptionError, self.cmd.ensure_finalized) + + def test_show_response(self): + self.prepare_command() + self.cmd.show_response = True + self.cmd.run() + record = self.get_logs()[-1] + self.assertTrue(record, "should report the response") + self.assertIn(self.pypi.default_response_data, record) + +def test_suite(): + return unittest.makeSuite(UploadDocsTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_compiler.py b/Lib/packaging/tests/test_compiler.py new file mode 100644 index 000000000000..2c620cb513ad --- /dev/null +++ b/Lib/packaging/tests/test_compiler.py @@ -0,0 +1,66 @@ +"""Tests for distutils.compiler.""" +import os + +from packaging.compiler import (get_default_compiler, customize_compiler, + gen_lib_options) +from packaging.tests import unittest, support + + +class FakeCompiler: + + name = 'fake' + description = 'Fake' + + def library_dir_option(self, dir): + return "-L" + dir + + def runtime_library_dir_option(self, dir): + return ["-cool", "-R" + dir] + + def find_library_file(self, dirs, lib, debug=False): + return 'found' + + def library_option(self, lib): + return "-l" + lib + + +class CompilerTestCase(support.EnvironRestorer, unittest.TestCase): + + restore_environ = ['AR', 'ARFLAGS'] + + @unittest.skipUnless(get_default_compiler() == 'unix', + 'irrelevant if default compiler is not unix') + def test_customize_compiler(self): + + os.environ['AR'] = 'my_ar' + os.environ['ARFLAGS'] = '-arflags' + + # make sure AR gets caught + class compiler: + name = 'unix' + + def set_executables(self, **kw): + self.exes = kw + + comp = compiler() + customize_compiler(comp) + self.assertEqual(comp.exes['archiver'], 'my_ar -arflags') + + def test_gen_lib_options(self): + compiler = FakeCompiler() + libdirs = ['lib1', 'lib2'] + runlibdirs = ['runlib1'] + libs = [os.path.join('dir', 'name'), 'name2'] + + opts = gen_lib_options(compiler, libdirs, runlibdirs, libs) + wanted = ['-Llib1', '-Llib2', '-cool', '-Rrunlib1', 'found', + '-lname2'] + self.assertEqual(opts, wanted) + + +def test_suite(): + return unittest.makeSuite(CompilerTestCase) + + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_config.py b/Lib/packaging/tests/test_config.py new file mode 100644 index 000000000000..8908c4fa4c22 --- /dev/null +++ b/Lib/packaging/tests/test_config.py @@ -0,0 +1,424 @@ +"""Tests for packaging.config.""" +import os +import sys +import logging +from io import StringIO + +from packaging import command +from packaging.dist import Distribution +from packaging.errors import PackagingFileError +from packaging.compiler import new_compiler, _COMPILERS +from packaging.command.sdist import sdist + +from packaging.tests import unittest, support + + +SETUP_CFG = """ +[metadata] +name = RestingParrot +version = 0.6.4 +author = Carl Meyer +author_email = carl@oddbird.net +maintainer = Éric Araujo +maintainer_email = merwok@netwok.org +summary = A sample project demonstrating packaging +description-file = %(description-file)s +keywords = packaging, sample project + +classifier = + Development Status :: 4 - Beta + Environment :: Console (Text Based) + Environment :: X11 Applications :: GTK; python_version < '3' + License :: OSI Approved :: MIT License + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 3 + +requires_python = >=2.4, <3.2 + +requires_dist = + PetShoppe + MichaelPalin (> 1.1) + pywin32; sys.platform == 'win32' + pysqlite2; python_version < '2.5' + inotify (0.0.1); sys.platform == 'linux2' + +requires_external = libxml2 + +provides_dist = packaging-sample-project (0.2) + unittest2-sample-project + +project_url = + Main repository, http://bitbucket.org/carljm/sample-distutils2-project + Fork in progress, http://bitbucket.org/Merwok/sample-distutils2-project + +[files] +packages_root = src + +packages = one + two + three + +modules = haven + +scripts = + script1.py + scripts/find-coconuts + bin/taunt + +package_data = + cheese = data/templates/* + +extra_files = %(extra-files)s + +# Replaces MANIFEST.in +sdist_extra = + include THANKS HACKING + recursive-include examples *.txt *.py + prune examples/sample?/build + +resources= + bm/ {b1,b2}.gif = {icon} + Cf*/ *.CFG = {config}/baBar/ + init_script = {script}/JunGle/ + +[global] +commands = + packaging.tests.test_config.FooBarBazTest + +compilers = + packaging.tests.test_config.DCompiler + +setup_hook = %(setup-hook)s + + + +[install_dist] +sub_commands = foo +""" + +# Can not be merged with SETUP_CFG else install_dist +# command will fail when trying to compile C sources +EXT_SETUP_CFG = """ +[files] +packages = one + two + +[extension=speed_coconuts] +name = one.speed_coconuts +sources = c_src/speed_coconuts.c +extra_link_args = "`gcc -print-file-name=libgcc.a`" -shared +define_macros = HAVE_CAIRO HAVE_GTK2 +libraries = gecodeint gecodekernel -- sys.platform != 'win32' + GecodeInt GecodeKernel -- sys.platform == 'win32' + +[extension=fast_taunt] +name = three.fast_taunt +sources = cxx_src/utils_taunt.cxx + cxx_src/python_module.cxx +include_dirs = /usr/include/gecode + /usr/include/blitz +extra_compile_args = -fPIC -O2 + -DGECODE_VERSION=$(./gecode_version) -- sys.platform != 'win32' + /DGECODE_VERSION='win32' -- sys.platform == 'win32' +language = cxx + +""" + + +class DCompiler: + name = 'd' + description = 'D Compiler' + + def __init__(self, *args): + pass + + +def hook(content): + content['metadata']['version'] += '.dev1' + + +class FooBarBazTest: + + def __init__(self, dist): + self.distribution = dist + + @classmethod + def get_command_name(cls): + return 'foo' + + def run(self): + self.distribution.foo_was_here = True + + def nothing(self): + pass + + def get_source_files(self): + return [] + + ensure_finalized = finalize_options = initialize_options = nothing + + +class ConfigTestCase(support.TempdirManager, + support.EnvironRestorer, + support.LoggingCatcher, + unittest.TestCase): + + restore_environ = ['PLAT'] + + def setUp(self): + super(ConfigTestCase, self).setUp() + self.addCleanup(setattr, sys, 'stdout', sys.stdout) + self.addCleanup(setattr, sys, 'stderr', sys.stderr) + sys.stdout = StringIO() + sys.stderr = StringIO() + + self.addCleanup(os.chdir, os.getcwd()) + tempdir = self.mkdtemp() + os.chdir(tempdir) + self.tempdir = tempdir + + def write_setup(self, kwargs=None): + opts = {'description-file': 'README', 'extra-files': '', + 'setup-hook': 'packaging.tests.test_config.hook'} + if kwargs: + opts.update(kwargs) + self.write_file('setup.cfg', SETUP_CFG % opts) + + def get_dist(self): + dist = Distribution() + dist.parse_config_files() + return dist + + def test_config(self): + self.write_setup() + self.write_file('README', 'yeah') + os.mkdir('bm') + self.write_file(('bm', 'b1.gif'), '') + self.write_file(('bm', 'b2.gif'), '') + os.mkdir('Cfg') + self.write_file(('Cfg', 'data.CFG'), '') + self.write_file('init_script', '') + + # try to load the metadata now + dist = self.get_dist() + + # check what was done + self.assertEqual(dist.metadata['Author'], 'Carl Meyer') + self.assertEqual(dist.metadata['Author-Email'], 'carl@oddbird.net') + + # the hook adds .dev1 + self.assertEqual(dist.metadata['Version'], '0.6.4.dev1') + + wanted = [ + 'Development Status :: 4 - Beta', + 'Environment :: Console (Text Based)', + "Environment :: X11 Applications :: GTK; python_version < '3'", + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 3'] + self.assertEqual(dist.metadata['Classifier'], wanted) + + wanted = ['packaging', 'sample project'] + self.assertEqual(dist.metadata['Keywords'], wanted) + + self.assertEqual(dist.metadata['Requires-Python'], '>=2.4, <3.2') + + wanted = ['PetShoppe', + 'MichaelPalin (> 1.1)', + "pywin32; sys.platform == 'win32'", + "pysqlite2; python_version < '2.5'", + "inotify (0.0.1); sys.platform == 'linux2'"] + + self.assertEqual(dist.metadata['Requires-Dist'], wanted) + urls = [('Main repository', + 'http://bitbucket.org/carljm/sample-distutils2-project'), + ('Fork in progress', + 'http://bitbucket.org/Merwok/sample-distutils2-project')] + self.assertEqual(dist.metadata['Project-Url'], urls) + + self.assertEqual(dist.packages, ['one', 'two', 'three']) + self.assertEqual(dist.py_modules, ['haven']) + self.assertEqual(dist.package_data, {'cheese': 'data/templates/*'}) + self.assertEqual( + {'bm/b1.gif': '{icon}/b1.gif', + 'bm/b2.gif': '{icon}/b2.gif', + 'Cfg/data.CFG': '{config}/baBar/data.CFG', + 'init_script': '{script}/JunGle/init_script'}, + dist.data_files) + + self.assertEqual(dist.package_dir, 'src') + + # Make sure we get the foo command loaded. We use a string comparison + # instead of assertIsInstance because the class is not the same when + # this test is run directly: foo is packaging.tests.test_config.Foo + # because get_command_class uses the full name, but a bare "Foo" in + # this file would be __main__.Foo when run as "python test_config.py". + # The name FooBarBazTest should be unique enough to prevent + # collisions. + self.assertEqual('FooBarBazTest', + dist.get_command_obj('foo').__class__.__name__) + + # did the README got loaded ? + self.assertEqual(dist.metadata['description'], 'yeah') + + # do we have the D Compiler enabled ? + self.assertIn('d', _COMPILERS) + d = new_compiler(compiler='d') + self.assertEqual(d.description, 'D Compiler') + + def test_multiple_description_file(self): + self.write_setup({'description-file': 'README CHANGES'}) + self.write_file('README', 'yeah') + self.write_file('CHANGES', 'changelog2') + dist = self.get_dist() + self.assertEqual(dist.metadata.requires_files, ['README', 'CHANGES']) + + def test_multiline_description_file(self): + self.write_setup({'description-file': 'README\n CHANGES'}) + self.write_file('README', 'yeah') + self.write_file('CHANGES', 'changelog') + dist = self.get_dist() + self.assertEqual(dist.metadata['description'], 'yeah\nchangelog') + self.assertEqual(dist.metadata.requires_files, ['README', 'CHANGES']) + + def test_parse_extensions_in_config(self): + self.write_file('setup.cfg', EXT_SETUP_CFG) + dist = self.get_dist() + + ext_modules = dict((mod.name, mod) for mod in dist.ext_modules) + self.assertEqual(len(ext_modules), 2) + ext = ext_modules.get('one.speed_coconuts') + self.assertEqual(ext.sources, ['c_src/speed_coconuts.c']) + self.assertEqual(ext.define_macros, ['HAVE_CAIRO', 'HAVE_GTK2']) + libs = ['gecodeint', 'gecodekernel'] + if sys.platform == 'win32': + libs = ['GecodeInt', 'GecodeKernel'] + self.assertEqual(ext.libraries, libs) + self.assertEqual(ext.extra_link_args, + ['`gcc -print-file-name=libgcc.a`', '-shared']) + + ext = ext_modules.get('three.fast_taunt') + self.assertEqual(ext.sources, + ['cxx_src/utils_taunt.cxx', 'cxx_src/python_module.cxx']) + self.assertEqual(ext.include_dirs, + ['/usr/include/gecode', '/usr/include/blitz']) + cargs = ['-fPIC', '-O2'] + if sys.platform == 'win32': + cargs.append("/DGECODE_VERSION='win32'") + else: + cargs.append('-DGECODE_VERSION=$(./gecode_version)') + self.assertEqual(ext.extra_compile_args, cargs) + self.assertEqual(ext.language, 'cxx') + + def test_missing_setuphook_warns(self): + self.write_setup({'setup-hook': 'this.does._not.exist'}) + self.write_file('README', 'yeah') + dist = self.get_dist() + logs = self.get_logs(logging.WARNING) + self.assertEqual(1, len(logs)) + self.assertIn('could not import setup_hook', logs[0]) + + def test_metadata_requires_description_files_missing(self): + self.write_setup({'description-file': 'README\n README2'}) + self.write_file('README', 'yeah') + self.write_file('README2', 'yeah') + os.mkdir('src') + self.write_file(('src', 'haven.py'), '#') + self.write_file('script1.py', '#') + os.mkdir('scripts') + self.write_file(('scripts', 'find-coconuts'), '#') + os.mkdir('bin') + self.write_file(('bin', 'taunt'), '#') + + for pkg in ('one', 'two', 'three'): + pkg = os.path.join('src', pkg) + os.mkdir(pkg) + self.write_file((pkg, '__init__.py'), '#') + + dist = self.get_dist() + cmd = sdist(dist) + cmd.finalize_options() + cmd.get_file_list() + self.assertRaises(PackagingFileError, cmd.make_distribution) + + def test_metadata_requires_description_files(self): + # Create the following file structure: + # README + # README2 + # script1.py + # scripts/ + # find-coconuts + # bin/ + # taunt + # src/ + # haven.py + # one/__init__.py + # two/__init__.py + # three/__init__.py + + self.write_setup({'description-file': 'README\n README2', + 'extra-files': '\n README3'}) + self.write_file('README', 'yeah 1') + self.write_file('README2', 'yeah 2') + self.write_file('README3', 'yeah 3') + os.mkdir('src') + self.write_file(('src', 'haven.py'), '#') + self.write_file('script1.py', '#') + os.mkdir('scripts') + self.write_file(('scripts', 'find-coconuts'), '#') + os.mkdir('bin') + self.write_file(('bin', 'taunt'), '#') + + for pkg in ('one', 'two', 'three'): + pkg = os.path.join('src', pkg) + os.mkdir(pkg) + self.write_file((pkg, '__init__.py'), '#') + + dist = self.get_dist() + self.assertIn('yeah 1\nyeah 2', dist.metadata['description']) + + cmd = sdist(dist) + cmd.finalize_options() + cmd.get_file_list() + self.assertRaises(PackagingFileError, cmd.make_distribution) + + self.write_setup({'description-file': 'README\n README2', + 'extra-files': '\n README2\n README'}) + dist = self.get_dist() + cmd = sdist(dist) + cmd.finalize_options() + cmd.get_file_list() + cmd.make_distribution() + with open('MANIFEST') as fp: + self.assertIn('README\nREADME2\n', fp.read()) + + def test_sub_commands(self): + self.write_setup() + self.write_file('README', 'yeah') + os.mkdir('src') + self.write_file(('src', 'haven.py'), '#') + self.write_file('script1.py', '#') + os.mkdir('scripts') + self.write_file(('scripts', 'find-coconuts'), '#') + os.mkdir('bin') + self.write_file(('bin', 'taunt'), '#') + + for pkg in ('one', 'two', 'three'): + pkg = os.path.join('src', pkg) + os.mkdir(pkg) + self.write_file((pkg, '__init__.py'), '#') + + # try to run the install command to see if foo is called + dist = self.get_dist() + self.assertIn('foo', command.get_command_names()) + self.assertEqual('FooBarBazTest', + dist.get_command_obj('foo').__class__.__name__) + + +def test_suite(): + return unittest.makeSuite(ConfigTestCase) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/Lib/packaging/tests/test_create.py b/Lib/packaging/tests/test_create.py new file mode 100644 index 000000000000..99ab0633d38f --- /dev/null +++ b/Lib/packaging/tests/test_create.py @@ -0,0 +1,235 @@ +"""Tests for packaging.create.""" +import io +import os +import sys +import sysconfig +from textwrap import dedent +from packaging.create import MainProgram, ask_yn, ask, main + +from packaging.tests import support, unittest + + +class CreateTestCase(support.TempdirManager, + support.EnvironRestorer, + unittest.TestCase): + + restore_environ = ['PLAT'] + + def setUp(self): + super(CreateTestCase, self).setUp() + self._stdin = sys.stdin # TODO use Inputs + self._stdout = sys.stdout + sys.stdin = io.StringIO() + sys.stdout = io.StringIO() + self._cwd = os.getcwd() + self.wdir = self.mkdtemp() + os.chdir(self.wdir) + # patch sysconfig + self._old_get_paths = sysconfig.get_paths + sysconfig.get_paths = lambda *args, **kwargs: { + 'man': sys.prefix + '/share/man', + 'doc': sys.prefix + '/share/doc/pyxfoil', } + + def tearDown(self): + super(CreateTestCase, self).tearDown() + sys.stdin = self._stdin + sys.stdout = self._stdout + os.chdir(self._cwd) + sysconfig.get_paths = self._old_get_paths + + def test_ask_yn(self): + sys.stdin.write('y\n') + sys.stdin.seek(0) + self.assertEqual('y', ask_yn('is this a test')) + + def test_ask(self): + sys.stdin.write('a\n') + sys.stdin.write('b\n') + sys.stdin.seek(0) + self.assertEqual('a', ask('is this a test')) + self.assertEqual('b', ask(str(list(range(0, 70))), default='c', + lengthy=True)) + + def test_set_multi(self): + mainprogram = MainProgram() + sys.stdin.write('aaaaa\n') + sys.stdin.seek(0) + mainprogram.data['author'] = [] + mainprogram._set_multi('_set_multi test', 'author') + self.assertEqual(['aaaaa'], mainprogram.data['author']) + + def test_find_files(self): + # making sure we scan a project dir correctly + mainprogram = MainProgram() + + # building the structure + tempdir = self.wdir + dirs = ['pkg1', 'data', 'pkg2', 'pkg2/sub'] + files = ['README', 'setup.cfg', 'foo.py', + 'pkg1/__init__.py', 'pkg1/bar.py', + 'data/data1', 'pkg2/__init__.py', + 'pkg2/sub/__init__.py'] + + for dir_ in dirs: + os.mkdir(os.path.join(tempdir, dir_)) + + for file_ in files: + path = os.path.join(tempdir, file_) + self.write_file(path, 'xxx') + + mainprogram._find_files() + mainprogram.data['packages'].sort() + + # do we have what we want? + self.assertEqual(mainprogram.data['packages'], + ['pkg1', 'pkg2', 'pkg2.sub']) + self.assertEqual(mainprogram.data['modules'], ['foo']) + data_fn = os.path.join('data', 'data1') + self.assertEqual(set(mainprogram.data['extra_files']), + set(['setup.cfg', 'README', data_fn])) + + def test_convert_setup_py_to_cfg(self): + self.write_file((self.wdir, 'setup.py'), + dedent(""" + # -*- coding: utf-8 -*- + from distutils.core import setup + + long_description = '''My super Death-scription + barbar is now on the public domain, + ho, baby !''' + + setup(name='pyxfoil', + version='0.2', + description='Python bindings for the Xfoil engine', + long_description=long_description, + maintainer='André Espaze', + maintainer_email='andre.espaze@logilab.fr', + url='http://www.python-science.org/project/pyxfoil', + license='GPLv2', + packages=['pyxfoil', 'babar', 'me'], + data_files=[ + ('share/doc/pyxfoil', ['README.rst']), + ('share/man', ['pyxfoil.1']), + ], + py_modules=['my_lib', 'mymodule'], + package_dir={ + 'babar': '', + 'me': 'Martinique/Lamentin', + }, + package_data={ + 'babar': ['Pom', 'Flora', 'Alexander'], + 'me': ['dady', 'mumy', 'sys', 'bro'], + '': ['setup.py', 'README'], + 'pyxfoil': ['fengine.so'], + }, + scripts=['my_script', 'bin/run'], + ) + """)) + sys.stdin.write('y\n') + sys.stdin.seek(0) + main() + + with open(os.path.join(self.wdir, 'setup.cfg')) as fp: + lines = set(line.rstrip() for line in fp) + + # FIXME don't use sets + self.assertEqual(lines, set(['', + '[metadata]', + 'version = 0.2', + 'name = pyxfoil', + 'maintainer = André Espaze', + 'description = My super Death-scription', + ' |barbar is now on the public domain,', + ' |ho, baby !', + 'maintainer_email = andre.espaze@logilab.fr', + 'home_page = http://www.python-science.org/project/pyxfoil', + 'download_url = UNKNOWN', + 'summary = Python bindings for the Xfoil engine', + '[files]', + 'modules = my_lib', + ' mymodule', + 'packages = pyxfoil', + ' babar', + ' me', + 'extra_files = Martinique/Lamentin/dady', + ' Martinique/Lamentin/mumy', + ' Martinique/Lamentin/sys', + ' Martinique/Lamentin/bro', + ' Pom', + ' Flora', + ' Alexander', + ' setup.py', + ' README', + ' pyxfoil/fengine.so', + 'scripts = my_script', + ' bin/run', + 'resources =', + ' README.rst = {doc}', + ' pyxfoil.1 = {man}', + ])) + + def test_convert_setup_py_to_cfg_with_description_in_readme(self): + self.write_file((self.wdir, 'setup.py'), + dedent(""" + # -*- coding: utf-8 -*- + from distutils.core import setup + fp = open('README.txt') + try: + long_description = fp.read() + finally: + fp.close() + + setup(name='pyxfoil', + version='0.2', + description='Python bindings for the Xfoil engine', + long_description=long_description, + maintainer='André Espaze', + maintainer_email='andre.espaze@logilab.fr', + url='http://www.python-science.org/project/pyxfoil', + license='GPLv2', + packages=['pyxfoil'], + package_data={'pyxfoil': ['fengine.so', 'babar.so']}, + data_files=[ + ('share/doc/pyxfoil', ['README.rst']), + ('share/man', ['pyxfoil.1']), + ], + ) + """)) + self.write_file((self.wdir, 'README.txt'), + dedent(''' +My super Death-scription +barbar is now in the public domain, +ho, baby! + ''')) + sys.stdin.write('y\n') + sys.stdin.seek(0) + # FIXME Out of memory error. + main() + with open(os.path.join(self.wdir, 'setup.cfg')) as fp: + lines = set(line.rstrip() for line in fp) + + self.assertEqual(lines, set(['', + '[metadata]', + 'version = 0.2', + 'name = pyxfoil', + 'maintainer = André Espaze', + 'maintainer_email = andre.espaze@logilab.fr', + 'home_page = http://www.python-science.org/project/pyxfoil', + 'download_url = UNKNOWN', + 'summary = Python bindings for the Xfoil engine', + 'description-file = README.txt', + '[files]', + 'packages = pyxfoil', + 'extra_files = pyxfoil/fengine.so', + ' pyxfoil/babar.so', + 'resources =', + ' README.rst = {doc}', + ' pyxfoil.1 = {man}', + ])) + + +def test_suite(): + return unittest.makeSuite(CreateTestCase) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/Lib/packaging/tests/test_cygwinccompiler.py b/Lib/packaging/tests/test_cygwinccompiler.py new file mode 100644 index 000000000000..17c43cd28aea --- /dev/null +++ b/Lib/packaging/tests/test_cygwinccompiler.py @@ -0,0 +1,88 @@ +"""Tests for packaging.cygwinccompiler.""" +import os +import sys +import sysconfig +from packaging.compiler.cygwinccompiler import ( + check_config_h, get_msvcr, + CONFIG_H_OK, CONFIG_H_NOTOK, CONFIG_H_UNCERTAIN) + +from packaging.tests import unittest, support + + +class CygwinCCompilerTestCase(support.TempdirManager, + unittest.TestCase): + + def setUp(self): + super(CygwinCCompilerTestCase, self).setUp() + self.version = sys.version + self.python_h = os.path.join(self.mkdtemp(), 'python.h') + self.old_get_config_h_filename = sysconfig.get_config_h_filename + sysconfig.get_config_h_filename = self._get_config_h_filename + + def tearDown(self): + sys.version = self.version + sysconfig.get_config_h_filename = self.old_get_config_h_filename + super(CygwinCCompilerTestCase, self).tearDown() + + def _get_config_h_filename(self): + return self.python_h + + def test_check_config_h(self): + # check_config_h looks for "GCC" in sys.version first + # returns CONFIG_H_OK if found + sys.version = ('2.6.1 (r261:67515, Dec 6 2008, 16:42:21) \n[GCC ' + '4.0.1 (Apple Computer, Inc. build 5370)]') + + self.assertEqual(check_config_h()[0], CONFIG_H_OK) + + # then it tries to see if it can find "__GNUC__" in pyconfig.h + sys.version = 'something without the *CC word' + + # if the file doesn't exist it returns CONFIG_H_UNCERTAIN + self.assertEqual(check_config_h()[0], CONFIG_H_UNCERTAIN) + + # if it exists but does not contain __GNUC__, it returns CONFIG_H_NOTOK + self.write_file(self.python_h, 'xxx') + self.assertEqual(check_config_h()[0], CONFIG_H_NOTOK) + + # and CONFIG_H_OK if __GNUC__ is found + self.write_file(self.python_h, 'xxx __GNUC__ xxx') + self.assertEqual(check_config_h()[0], CONFIG_H_OK) + + def test_get_msvcr(self): + # none + sys.version = ('2.6.1 (r261:67515, Dec 6 2008, 16:42:21) ' + '\n[GCC 4.0.1 (Apple Computer, Inc. build 5370)]') + self.assertEqual(get_msvcr(), None) + + # MSVC 7.0 + sys.version = ('2.5.1 (r251:54863, Apr 18 2007, 08:51:08) ' + '[MSC v.1300 32 bits (Intel)]') + self.assertEqual(get_msvcr(), ['msvcr70']) + + # MSVC 7.1 + sys.version = ('2.5.1 (r251:54863, Apr 18 2007, 08:51:08) ' + '[MSC v.1310 32 bits (Intel)]') + self.assertEqual(get_msvcr(), ['msvcr71']) + + # VS2005 / MSVC 8.0 + sys.version = ('2.5.1 (r251:54863, Apr 18 2007, 08:51:08) ' + '[MSC v.1400 32 bits (Intel)]') + self.assertEqual(get_msvcr(), ['msvcr80']) + + # VS2008 / MSVC 9.0 + sys.version = ('2.5.1 (r251:54863, Apr 18 2007, 08:51:08) ' + '[MSC v.1500 32 bits (Intel)]') + self.assertEqual(get_msvcr(), ['msvcr90']) + + # unknown + sys.version = ('2.5.1 (r251:54863, Apr 18 2007, 08:51:08) ' + '[MSC v.1999 32 bits (Intel)]') + self.assertRaises(ValueError, get_msvcr) + + +def test_suite(): + return unittest.makeSuite(CygwinCCompilerTestCase) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/Lib/packaging/tests/test_database.py b/Lib/packaging/tests/test_database.py new file mode 100644 index 000000000000..c8d941527eaa --- /dev/null +++ b/Lib/packaging/tests/test_database.py @@ -0,0 +1,506 @@ +import os +import io +import csv +import imp +import sys +import shutil +import zipfile +import tempfile +from os.path import relpath # separate import for backport concerns +from hashlib import md5 + +from packaging.errors import PackagingError +from packaging.metadata import Metadata +from packaging.tests import unittest, run_unittest, support, TESTFN + +from packaging.database import ( + Distribution, EggInfoDistribution, get_distribution, get_distributions, + provides_distribution, obsoletes_distribution, get_file_users, + enable_cache, disable_cache, distinfo_dirname, _yield_distributions) + +# TODO Add a test for getting a distribution provided by another distribution +# TODO Add a test for absolute pathed RECORD items (e.g. /etc/myapp/config.ini) +# TODO Add tests from the former pep376 project (zipped site-packages, etc.) + + +def get_hexdigest(filename): + with open(filename, 'rb') as file: + checksum = md5(file.read()) + return checksum.hexdigest() + + +def record_pieces(file): + path = relpath(file, sys.prefix) + digest = get_hexdigest(file) + size = os.path.getsize(file) + return [path, digest, size] + + +class CommonDistributionTests: + """Mixin used to test the interface common to both Distribution classes. + + Derived classes define cls, sample_dist, dirs and records. These + attributes are used in test methods. See source code for details. + """ + + def setUp(self): + super(CommonDistributionTests, self).setUp() + self.addCleanup(enable_cache) + disable_cache() + self.fake_dists_path = os.path.abspath( + os.path.join(os.path.dirname(__file__), 'fake_dists')) + + def test_instantiation(self): + # check that useful attributes are here + name, version, distdir = self.sample_dist + here = os.path.abspath(os.path.dirname(__file__)) + dist_path = os.path.join(here, 'fake_dists', distdir) + + dist = self.dist = self.cls(dist_path) + self.assertEqual(dist.path, dist_path) + self.assertEqual(dist.name, name) + self.assertEqual(dist.metadata['Name'], name) + self.assertIsInstance(dist.metadata, Metadata) + self.assertEqual(dist.version, version) + self.assertEqual(dist.metadata['Version'], version) + + def test_repr(self): + dist = self.cls(self.dirs[0]) + # just check that the class name is in the repr + self.assertIn(self.cls.__name__, repr(dist)) + + def test_comparison(self): + # tests for __eq__ and __hash__ + dist = self.cls(self.dirs[0]) + dist2 = self.cls(self.dirs[0]) + dist3 = self.cls(self.dirs[1]) + self.assertIn(dist, {dist: True}) + self.assertEqual(dist, dist) + + self.assertIsNot(dist, dist2) + self.assertEqual(dist, dist2) + self.assertNotEqual(dist, dist3) + self.assertNotEqual(dist, ()) + + def test_list_installed_files(self): + for dir_ in self.dirs: + dist = self.cls(dir_) + for path, md5_, size in dist.list_installed_files(): + record_data = self.records[dist.path] + self.assertIn(path, record_data) + self.assertEqual(md5_, record_data[path][0]) + self.assertEqual(size, record_data[path][1]) + + +class TestDistribution(CommonDistributionTests, unittest.TestCase): + + cls = Distribution + sample_dist = 'choxie', '2.0.0.9', 'choxie-2.0.0.9.dist-info' + + def setUp(self): + super(TestDistribution, self).setUp() + self.dirs = [os.path.join(self.fake_dists_path, f) + for f in os.listdir(self.fake_dists_path) + if f.endswith('.dist-info')] + + self.records = {} + for distinfo_dir in self.dirs: + record_file = os.path.join(distinfo_dir, 'RECORD') + with open(record_file, 'w') as file: + record_writer = csv.writer( + file, delimiter=',', quoting=csv.QUOTE_NONE) + + dist_location = distinfo_dir.replace('.dist-info', '') + + for path, dirs, files in os.walk(dist_location): + for f in files: + record_writer.writerow(record_pieces( + os.path.join(path, f))) + for file in ('INSTALLER', 'METADATA', 'REQUESTED'): + record_writer.writerow(record_pieces( + os.path.join(distinfo_dir, file))) + record_writer.writerow([relpath(record_file, sys.prefix)]) + + with open(record_file) as file: + record_reader = csv.reader(file) + record_data = {} + for row in record_reader: + path, md5_, size = (row[:] + + [None for i in range(len(row), 3)]) + record_data[path] = md5_, size + self.records[distinfo_dir] = record_data + + def tearDown(self): + for distinfo_dir in self.dirs: + record_file = os.path.join(distinfo_dir, 'RECORD') + open(record_file, 'w').close() + super(TestDistribution, self).tearDown() + + def test_instantiation(self): + super(TestDistribution, self).test_instantiation() + self.assertIsInstance(self.dist.requested, bool) + + def test_uses(self): + # Test to determine if a distribution uses a specified file. + # Criteria to test against + distinfo_name = 'grammar-1.0a4' + distinfo_dir = os.path.join(self.fake_dists_path, + distinfo_name + '.dist-info') + true_path = [self.fake_dists_path, distinfo_name, + 'grammar', 'utils.py'] + true_path = relpath(os.path.join(*true_path), sys.prefix) + false_path = [self.fake_dists_path, 'towel_stuff-0.1', 'towel_stuff', + '__init__.py'] + false_path = relpath(os.path.join(*false_path), sys.prefix) + + # Test if the distribution uses the file in question + dist = Distribution(distinfo_dir) + self.assertTrue(dist.uses(true_path)) + self.assertFalse(dist.uses(false_path)) + + def test_get_distinfo_file(self): + # Test the retrieval of dist-info file objects. + distinfo_name = 'choxie-2.0.0.9' + other_distinfo_name = 'grammar-1.0a4' + distinfo_dir = os.path.join(self.fake_dists_path, + distinfo_name + '.dist-info') + dist = Distribution(distinfo_dir) + # Test for known good file matches + distinfo_files = [ + # Relative paths + 'INSTALLER', 'METADATA', + # Absolute paths + os.path.join(distinfo_dir, 'RECORD'), + os.path.join(distinfo_dir, 'REQUESTED'), + ] + + for distfile in distinfo_files: + with dist.get_distinfo_file(distfile) as value: + self.assertIsInstance(value, io.TextIOWrapper) + # Is it the correct file? + self.assertEqual(value.name, + os.path.join(distinfo_dir, distfile)) + + # Test an absolute path that is part of another distributions dist-info + other_distinfo_file = os.path.join( + self.fake_dists_path, other_distinfo_name + '.dist-info', + 'REQUESTED') + self.assertRaises(PackagingError, dist.get_distinfo_file, + other_distinfo_file) + # Test for a file that should not exist + self.assertRaises(PackagingError, dist.get_distinfo_file, + 'MAGICFILE') + + def test_list_distinfo_files(self): + # Test for the iteration of RECORD path entries. + distinfo_name = 'towel_stuff-0.1' + distinfo_dir = os.path.join(self.fake_dists_path, + distinfo_name + '.dist-info') + dist = Distribution(distinfo_dir) + # Test for the iteration of the raw path + distinfo_record_paths = self.records[distinfo_dir].keys() + found = dist.list_distinfo_files() + self.assertEqual(sorted(found), sorted(distinfo_record_paths)) + # Test for the iteration of local absolute paths + distinfo_record_paths = [os.path.join(sys.prefix, path) + for path in self.records[distinfo_dir]] + found = dist.list_distinfo_files(local=True) + self.assertEqual(sorted(found), sorted(distinfo_record_paths)) + + def test_get_resources_path(self): + distinfo_name = 'babar-0.1' + distinfo_dir = os.path.join(self.fake_dists_path, + distinfo_name + '.dist-info') + dist = Distribution(distinfo_dir) + resource_path = dist.get_resource_path('babar.png') + self.assertEqual(resource_path, 'babar.png') + self.assertRaises(KeyError, dist.get_resource_path, 'notexist') + + +class TestEggInfoDistribution(CommonDistributionTests, + support.LoggingCatcher, + unittest.TestCase): + + cls = EggInfoDistribution + sample_dist = 'bacon', '0.1', 'bacon-0.1.egg-info' + + def setUp(self): + super(TestEggInfoDistribution, self).setUp() + + self.dirs = [os.path.join(self.fake_dists_path, f) + for f in os.listdir(self.fake_dists_path) + if f.endswith('.egg') or f.endswith('.egg-info')] + + self.records = {} + + @unittest.skip('not implemented yet') + def test_list_installed_files(self): + # EggInfoDistribution defines list_installed_files but there is no + # test for it yet; someone with setuptools expertise needs to add a + # file with the list of installed files for one of the egg fake dists + # and write the support code to populate self.records (and then delete + # this method) + pass + + +class TestDatabase(support.LoggingCatcher, + unittest.TestCase): + + def setUp(self): + super(TestDatabase, self).setUp() + disable_cache() + # Setup the path environment with our fake distributions + current_path = os.path.abspath(os.path.dirname(__file__)) + self.sys_path = sys.path[:] + self.fake_dists_path = os.path.join(current_path, 'fake_dists') + sys.path.insert(0, self.fake_dists_path) + + def tearDown(self): + sys.path[:] = self.sys_path + enable_cache() + super(TestDatabase, self).tearDown() + + def test_distinfo_dirname(self): + # Given a name and a version, we expect the distinfo_dirname function + # to return a standard distribution information directory name. + + items = [ + # (name, version, standard_dirname) + # Test for a very simple single word name and decimal version + # number + ('docutils', '0.5', 'docutils-0.5.dist-info'), + # Test for another except this time with a '-' in the name, which + # needs to be transformed during the name lookup + ('python-ldap', '2.5', 'python_ldap-2.5.dist-info'), + # Test for both '-' in the name and a funky version number + ('python-ldap', '2.5 a---5', 'python_ldap-2.5 a---5.dist-info'), + ] + + # Loop through the items to validate the results + for name, version, standard_dirname in items: + dirname = distinfo_dirname(name, version) + self.assertEqual(dirname, standard_dirname) + + def test_get_distributions(self): + # Lookup all distributions found in the ``sys.path``. + # This test could potentially pick up other installed distributions + fake_dists = [('grammar', '1.0a4'), ('choxie', '2.0.0.9'), + ('towel-stuff', '0.1'), ('babar', '0.1')] + found_dists = [] + + # Verify the fake dists have been found. + dists = [dist for dist in get_distributions()] + for dist in dists: + self.assertIsInstance(dist, Distribution) + if (dist.name in dict(fake_dists) and + dist.path.startswith(self.fake_dists_path)): + found_dists.append((dist.name, dist.metadata['version'], )) + else: + # check that it doesn't find anything more than this + self.assertFalse(dist.path.startswith(self.fake_dists_path)) + # otherwise we don't care what other distributions are found + + # Finally, test that we found all that we were looking for + self.assertEqual(sorted(found_dists), sorted(fake_dists)) + + # Now, test if the egg-info distributions are found correctly as well + fake_dists += [('bacon', '0.1'), ('cheese', '2.0.2'), + ('coconuts-aster', '10.3'), + ('banana', '0.4'), ('strawberry', '0.6'), + ('truffles', '5.0'), ('nut', 'funkyversion')] + found_dists = [] + + dists = [dist for dist in get_distributions(use_egg_info=True)] + for dist in dists: + self.assertIsInstance(dist, (Distribution, EggInfoDistribution)) + if (dist.name in dict(fake_dists) and + dist.path.startswith(self.fake_dists_path)): + found_dists.append((dist.name, dist.metadata['version'])) + else: + self.assertFalse(dist.path.startswith(self.fake_dists_path)) + + self.assertEqual(sorted(fake_dists), sorted(found_dists)) + + def test_get_distribution(self): + # Test for looking up a distribution by name. + # Test the lookup of the towel-stuff distribution + name = 'towel-stuff' # Note: This is different from the directory name + + # Lookup the distribution + dist = get_distribution(name) + self.assertIsInstance(dist, Distribution) + self.assertEqual(dist.name, name) + + # Verify that an unknown distribution returns None + self.assertIsNone(get_distribution('bogus')) + + # Verify partial name matching doesn't work + self.assertIsNone(get_distribution('towel')) + + # Verify that it does not find egg-info distributions, when not + # instructed to + self.assertIsNone(get_distribution('bacon')) + self.assertIsNone(get_distribution('cheese')) + self.assertIsNone(get_distribution('strawberry')) + self.assertIsNone(get_distribution('banana')) + + # Now check that it works well in both situations, when egg-info + # is a file and directory respectively. + dist = get_distribution('cheese', use_egg_info=True) + self.assertIsInstance(dist, EggInfoDistribution) + self.assertEqual(dist.name, 'cheese') + + dist = get_distribution('bacon', use_egg_info=True) + self.assertIsInstance(dist, EggInfoDistribution) + self.assertEqual(dist.name, 'bacon') + + dist = get_distribution('banana', use_egg_info=True) + self.assertIsInstance(dist, EggInfoDistribution) + self.assertEqual(dist.name, 'banana') + + dist = get_distribution('strawberry', use_egg_info=True) + self.assertIsInstance(dist, EggInfoDistribution) + self.assertEqual(dist.name, 'strawberry') + + def test_get_file_users(self): + # Test the iteration of distributions that use a file. + name = 'towel_stuff-0.1' + path = os.path.join(self.fake_dists_path, name, + 'towel_stuff', '__init__.py') + for dist in get_file_users(path): + self.assertIsInstance(dist, Distribution) + self.assertEqual(dist.name, name) + + def test_provides(self): + # Test for looking up distributions by what they provide + checkLists = lambda x, y: self.assertEqual(sorted(x), sorted(y)) + + l = [dist.name for dist in provides_distribution('truffles')] + checkLists(l, ['choxie', 'towel-stuff']) + + l = [dist.name for dist in provides_distribution('truffles', '1.0')] + checkLists(l, ['choxie']) + + l = [dist.name for dist in provides_distribution('truffles', '1.0', + use_egg_info=True)] + checkLists(l, ['choxie', 'cheese']) + + l = [dist.name for dist in provides_distribution('truffles', '1.1.2')] + checkLists(l, ['towel-stuff']) + + l = [dist.name for dist in provides_distribution('truffles', '1.1')] + checkLists(l, ['towel-stuff']) + + l = [dist.name for dist in provides_distribution('truffles', + '!=1.1,<=2.0')] + checkLists(l, ['choxie']) + + l = [dist.name for dist in provides_distribution('truffles', + '!=1.1,<=2.0', + use_egg_info=True)] + checkLists(l, ['choxie', 'bacon', 'cheese']) + + l = [dist.name for dist in provides_distribution('truffles', '>1.0')] + checkLists(l, ['towel-stuff']) + + l = [dist.name for dist in provides_distribution('truffles', '>1.5')] + checkLists(l, []) + + l = [dist.name for dist in provides_distribution('truffles', '>1.5', + use_egg_info=True)] + checkLists(l, ['bacon']) + + l = [dist.name for dist in provides_distribution('truffles', '>=1.0')] + checkLists(l, ['choxie', 'towel-stuff']) + + l = [dist.name for dist in provides_distribution('strawberry', '0.6', + use_egg_info=True)] + checkLists(l, ['coconuts-aster']) + + l = [dist.name for dist in provides_distribution('strawberry', '>=0.5', + use_egg_info=True)] + checkLists(l, ['coconuts-aster']) + + l = [dist.name for dist in provides_distribution('strawberry', '>0.6', + use_egg_info=True)] + checkLists(l, []) + + l = [dist.name for dist in provides_distribution('banana', '0.4', + use_egg_info=True)] + checkLists(l, ['coconuts-aster']) + + l = [dist.name for dist in provides_distribution('banana', '>=0.3', + use_egg_info=True)] + checkLists(l, ['coconuts-aster']) + + l = [dist.name for dist in provides_distribution('banana', '!=0.4', + use_egg_info=True)] + checkLists(l, []) + + def test_obsoletes(self): + # Test looking for distributions based on what they obsolete + checkLists = lambda x, y: self.assertEqual(sorted(x), sorted(y)) + + l = [dist.name for dist in obsoletes_distribution('truffles', '1.0')] + checkLists(l, []) + + l = [dist.name for dist in obsoletes_distribution('truffles', '1.0', + use_egg_info=True)] + checkLists(l, ['cheese', 'bacon']) + + l = [dist.name for dist in obsoletes_distribution('truffles', '0.8')] + checkLists(l, ['choxie']) + + l = [dist.name for dist in obsoletes_distribution('truffles', '0.8', + use_egg_info=True)] + checkLists(l, ['choxie', 'cheese']) + + l = [dist.name for dist in obsoletes_distribution('truffles', '0.9.6')] + checkLists(l, ['choxie', 'towel-stuff']) + + l = [dist.name for dist in obsoletes_distribution('truffles', + '0.5.2.3')] + checkLists(l, ['choxie', 'towel-stuff']) + + l = [dist.name for dist in obsoletes_distribution('truffles', '0.2')] + checkLists(l, ['towel-stuff']) + + def test_yield_distribution(self): + # tests the internal function _yield_distributions + checkLists = lambda x, y: self.assertEqual(sorted(x), sorted(y)) + + eggs = [('bacon', '0.1'), ('banana', '0.4'), ('strawberry', '0.6'), + ('truffles', '5.0'), ('cheese', '2.0.2'), + ('coconuts-aster', '10.3'), ('nut', 'funkyversion')] + dists = [('choxie', '2.0.0.9'), ('grammar', '1.0a4'), + ('towel-stuff', '0.1'), ('babar', '0.1')] + + checkLists([], _yield_distributions(False, False)) + + found = [(dist.name, dist.metadata['Version']) + for dist in _yield_distributions(False, True) + if dist.path.startswith(self.fake_dists_path)] + checkLists(eggs, found) + + found = [(dist.name, dist.metadata['Version']) + for dist in _yield_distributions(True, False) + if dist.path.startswith(self.fake_dists_path)] + checkLists(dists, found) + + found = [(dist.name, dist.metadata['Version']) + for dist in _yield_distributions(True, True) + if dist.path.startswith(self.fake_dists_path)] + checkLists(dists + eggs, found) + + +def test_suite(): + suite = unittest.TestSuite() + load = unittest.defaultTestLoader.loadTestsFromTestCase + suite.addTest(load(TestDistribution)) + suite.addTest(load(TestEggInfoDistribution)) + suite.addTest(load(TestDatabase)) + return suite + + +if __name__ == "__main__": + unittest.main(defaultTest='test_suite') diff --git a/Lib/packaging/tests/test_depgraph.py b/Lib/packaging/tests/test_depgraph.py new file mode 100644 index 000000000000..9271a7ba6861 --- /dev/null +++ b/Lib/packaging/tests/test_depgraph.py @@ -0,0 +1,301 @@ +"""Tests for packaging.depgraph """ +import io +import os +import re +import sys +import packaging.database +from packaging import depgraph + +from packaging.tests import unittest, support + + +class DepGraphTestCase(support.LoggingCatcher, + unittest.TestCase): + + DISTROS_DIST = ('choxie', 'grammar', 'towel-stuff') + DISTROS_EGG = ('bacon', 'banana', 'strawberry', 'cheese') + BAD_EGGS = ('nut',) + + EDGE = re.compile( + r'"(?P.*)" -> "(?P.*)" \[label="(?P