diff options
| author | Konrad Delong <konryd@gmail.com> | 2010-07-27 15:07:46 +0200 |
|---|---|---|
| committer | Konrad Delong <konryd@gmail.com> | 2010-07-27 15:07:46 +0200 |
| commit | 9da67de95ddac89371ae85e609003fee0331723f (patch) | |
| tree | 21def38a20233c7673fcb5734a17ef473ffafc5d | |
| parent | 6b554ce70cd2e6b7a6e0633100de6cb533e46d46 (diff) | |
| parent | 64aa303aca57a5f5a1e09b43f598eb866cfc8483 (diff) | |
| download | disutils2-9da67de95ddac89371ae85e609003fee0331723f.tar.gz | |
merged upstream
140 files changed, 7833 insertions, 4819 deletions
@@ -1,5 +1,9 @@ -.*\.pyc$ -.*\.pyo$ -^src/build -^docs/build -.*\.swp$ +syntax: glob +*.py[co] +__pycache__/ +configure.cache +build/ +MANIFEST +dist/ +*.swp +.coverage
\ No newline at end of file diff --git a/docs/design/pep-0376.txt b/docs/design/pep-0376.txt index 505d354..19cac33 100644 --- a/docs/design/pep-0376.txt +++ b/docs/design/pep-0376.txt @@ -402,7 +402,7 @@ The new functions added in the ``pkgutil`` are : ``.egg-info`` directory that contains a PKG-INFO that matches `name` for the `name` metadata. - Notice that there should be at most one result. The first result founded + Notice that there should be at most one result. The first result found is returned. If the directory is not found, returns None. - ``get_file_users(path)`` -> iterator of ``Distribution`` instances. diff --git a/docs/design/wiki.rst b/docs/design/wiki.rst index 278565f..11b3058 100644 --- a/docs/design/wiki.rst +++ b/docs/design/wiki.rst @@ -399,7 +399,7 @@ Open issues the folder hierarchy from the module until we find a setup.cfg? A setup.cfg is necessary if you use distutils2, is it not? - -> information founded in setup.cfg will be put in the *FILES* file upon + -> information found in setup.cfg will be put in the *FILES* file upon installation in the egg-info directory. IOW in the unbuit-egg case, we would need to create that dir, then use pkgutil APIs. diff --git a/docs/source/_static/depgraph_big.png b/docs/source/_static/depgraph_big.png Binary files differnew file mode 100644 index 0000000..8b8a27c --- /dev/null +++ b/docs/source/_static/depgraph_big.png diff --git a/docs/source/conf.py b/docs/source/conf.py index 225d793..9ffd9f3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,12 +17,13 @@ import sys, os # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.append(os.path.abspath('.')) +sys.path.append(os.path.abspath(os.path.join(__file__, '..', '..', '..', 'src'))) # -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [] +extensions = ['sphinx.ext.autodoc'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/source/depgraph.rst b/docs/source/depgraph.rst new file mode 100644 index 0000000..8e1ec1e --- /dev/null +++ b/docs/source/depgraph.rst @@ -0,0 +1,125 @@ +=============================== +Dependency Graph Builder Module +=============================== + +Introduction +------------ + +This module provides the means to analyse the dependencies between various +distributions and furthermore to create a graph representing the relationships +from a list of :class:`distutils2._backport.pkgutil.Distribution` and +:class:`distutils2._backport.pkgutil.EggInfoDistribution` instances. The graph +is represented by the :class:`distutils2.depgraph.DependencyGraph` class that +keeps internally an adjacency list. Several functions are provided that +generate a graph in different manners. First, all of them are documented and +then several use case examples are provided along with +`graphviz <http://www.graphviz.org/>`_ illustrations +of the generated graphs. + +API +--- + +.. automodule:: distutils2.depgraph + :members: + +Example Usage +------------- + +Depict all Dependenciess in the System +++++++++++++++++++++++++++++++++++++++ + +First, we shall generate a graph of all the distributions on the system +and then create an image out of it using the tools provided by +`graphviz <http://www.graphviz.org/>`_. For obtaining the list of +installed distributions, we will use the functions provided by the module +:mod:`distutils2._backport.pkgutil`:: + + from distutils2._backport.pkgutil import get_distributions + from distutils2.depgraph import generate_graph + + dists = list(pkgutil.get_distributions()) + graph = generate_graph(dists) + +Now, it would be of interest to print out the missing requirements. This +can be done as follows:: + + for dist, reqs in graph.missing.iteritems(): + if len(reqs) > 0: + reqs_s = " ,".join(reqs) + print("Missing dependencies for %s: %s" % (dist.name, reqs_s)) + +Example output on my configuration is: + +.. code-block:: none + + Missing dependencies for TurboCheetah: Cheetah + Missing dependencies for TurboGears: ConfigObj, DecoratorTools, RuleDispatch + Missing dependencies for jockey: PyKDE4.kdecore, PyKDE4.kdeui, PyQt4.QtCore, PyQt4.QtGui + Missing dependencies for TurboKid: kid + Missing dependencies for TurboJson: DecoratorTools, RuleDispatch + +Now, we proceed with generating a graphical representation of the graph. First +we write it to a file, and then we generate a PNG image using the ``dot`` +command line tool:: + + from distutils2.depgraph import graph_to_dot + f = open('output.dot', 'w') + # we only show the interesting distributions, skipping the disconnected ones + graph_to_dot(graph, f, skip_disconnected=True) + +Now, we can create the actual picture using: + +.. code-block:: bash + + $ dot -Tpng output.dot > output.png + +An example output image is: + +.. figure:: images/depgraph_output.png + :alt: An example dot output + +If you want to include ``.egg`` and ``.egg-info`` distributions as well, then +the code requires only one change, namely the line:: + + dists = list(pkgutil.get_distributions()) + +has to be replaced with:: + + dists = list(pkgutil.get_distributions(use_egg_info=True)) + +Then, on most platforms, a richer graph is obtained because at the moment most +distributions are provided in the ``.egg``/``.egg-info`` rather than the +``.dist-info`` format. An example of a more involved graph for illustrative +reasons can be seen `here <_static/depgraph_big.png>`_. + + +List all Dependent Distributions +++++++++++++++++++++++++++++++++ + +We will list all distributions that are dependent on some given distibution. +This time, ``.egg``/``.egg-info`` distributions will be considered as well:: + + from distutils2._backport.pkgutil import get_distributions + from distutils2.depgraph import dependent_dists + import sys + + dists = list(get_distributions(use_egg_info=True)) + dist = None + for d in dists: + if d.name == 'bacon': + dist = d + break + if dist is None: + print('No such distribution in the system') + sys.exit(1) + deps = dependent_dists(dists, dist) + deps_s = ", ".join([x.name for x in deps]) + print("The following distributions depend on %s: %s" % (dist.name, deps_s)) + +And this is example output with the dependency relationships as in the +`previous section <_static/depgraph_big.png>`_: + +.. code-block:: none + + The following distributions depend on bacon: towel-stuff, choxie, grammar + diff --git a/docs/source/images/depgraph_output.png b/docs/source/images/depgraph_output.png Binary files differnew file mode 100644 index 0000000..960bb1b --- /dev/null +++ b/docs/source/images/depgraph_output.png diff --git a/docs/source/index.rst b/docs/source/index.rst index c58e1ba..eb11721 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,8 +12,12 @@ Contents: :maxdepth: 2 metadata + pkgutil + depgraph new_commands test_framework + pypi + version Indices and tables ================== diff --git a/docs/source/metadata.rst b/docs/source/metadata.rst index 40a6235..1b7fc39 100644 --- a/docs/source/metadata.rst +++ b/docs/source/metadata.rst @@ -5,19 +5,19 @@ Metadata Distutils2 provides a :class:`DistributionMetadata` class that can read and write Metadata files. This class is compatible with all versions of Metadata: -- 1.0 : PEP 241 -- 1.1 : PEP 314 -- 1.2 : PEP 345 +* 1.0 : :pep:`241` +* 1.1 : :pep:`314` +* 1.2 : :pep:`345` -The PEP 345 implementation supports the micro-language for the environment +The :pep:`345` implementation supports the micro-language for the environment markers, and displays warnings when versions that are supposed to be -PEP 386 are violating the scheme. +:pep:`386` are violating the scheme. Reading metadata ================ -The :class:`DistributionMetadata` class can be instanciated with the path of +The :class:`DistributionMetadata` class can be instantiated with the path of the metadata file, and provides a dict-like interface to the values:: >>> from distutils2.metadata import DistributionMetadata @@ -32,14 +32,14 @@ the metadata file, and provides a dict-like interface to the values:: ["pywin32; sys.platform == 'win32'", "Sphinx"] The fields that supports environment markers can be automatically ignored if -the object is instanciated using the ``platform_dependant`` option. +the object is instantiated using the ``platform_dependent`` option. :class:`DistributionMetadata` will interpret in the case the markers and will automatically remove the fields that are not compliant with the running environment. Here's an example under Mac OS X. The win32 dependency we saw earlier is ignored:: >>> from distutils2.metadata import DistributionMetadata - >>> metadata = DistributionMetadata('PKG-INFO', platform_dependant=True) + >>> metadata = DistributionMetadata('PKG-INFO', platform_dependent=True) >>> metadata['Requires-Dist'] ['bar'] @@ -53,7 +53,7 @@ Here's an example, simulating a win32 environment:: >>> from distutils2.metadata import DistributionMetadata >>> context = {'sys.platform': 'win32'} - >>> metadata = DistributionMetadata('PKG-INFO', platform_dependant=True, + >>> metadata = DistributionMetadata('PKG-INFO', platform_dependent=True, ... execution_context=context) ... >>> metadata['Requires-Dist'] = ["pywin32; sys.platform == 'win32'", @@ -71,15 +71,15 @@ Writing metadata can be done using the ``write`` API:: >>> metadata.write('/to/my/PKG-INFO') The class will pick the best version for the metadata, depending on the values -provided. If all the values provided exists in all versions, the class will +provided. If all the values provided exist in all versions, the class will use :attr:`metadata.PKG_INFO_PREFERRED_VERSION`. It is set by default to 1.0. Conflict checking and best version ================================== -Some fields in PEP 345 have to follow a version scheme in their versions -predicate. When the scheme is violated, a warning is emited:: +Some fields in :pep:`345` have to follow a version scheme in their versions +predicate. When the scheme is violated, a warning is emitted:: >>> from distutils2.metadata import DistributionMetadata >>> metadata = DistributionMetadata() @@ -89,7 +89,4 @@ predicate. When the scheme is violated, a warning is emited:: -XXX talk about check() - - - +.. TODO talk about check() diff --git a/docs/source/pkgutil.rst b/docs/source/pkgutil.rst new file mode 100644 index 0000000..65a8871 --- /dev/null +++ b/docs/source/pkgutil.rst @@ -0,0 +1,143 @@ +======= +pkgutil +======= + +Introduction +============ + +This module provides the necessary functions to provide support for +the "Importer Protocol" as described in :pep:`302` and for working with +the database of installed Python distributions which is specified in +:pep:`376`. In addition to the functions required in :pep:`376`, back support +for older ``.egg`` and ``.egg-info`` distributions is provided as well. These +distributions are represented by the class +:class:`distutils2._backport.pkgutil.EggInfoDistribution` and +most functions provide an extra argument ``use_egg_info`` which indicates if +they should consider these old styled distributions. In this document, +first a complete documentation of the functions and classes +is provided and then several use cases are presented. + +API Reference +============= + +.. automodule:: distutils2._backport.pkgutil + :members: + +Example Usage +============= + +Print All Information About a Distribution +++++++++++++++++++++++++++++++++++++++++++ + +Given a path to a ``.dist-info`` distribution, we shall print out all +information that can be obtained using functions provided in this module:: + + from distutils2._backport import pkgutil + import sys + + path = raw_input() # read the path from the keyboard + # first create the Distribution instance + try: + dist = pkgutil.Distribution(path) + except IOError: + print('No such distribution') + sys.exit(1) + + print('Information about %s' % dist.name) + print('Files') + print('=====') + for (path, md5, size) in dist.get_installed_files(): + print('* Path: %s' % path) + print(' Hash %s, Size: %s bytes' % (md5, size)) + print('Metadata') + print('========') + for key, value in dist.metadata.items(): + print('%20s: %s' % (key, value)) + print('Extra') + print('=====') + if dist.requested: + print('* It was installed by user request') + else: + print('* It was installed as a dependency') + +If we save the script above as ``print_info.py`` and we are intested in the +distribution located at +``/home/josip/dev/distutils2/src/distutils2/_backport/tests/fake_dists/choxie-2.0.0.9`` +then by typing in the console: + +.. code-block:: bash + + $ echo /home/josip/dev/distutils2/src/distutils2/_backport/tests/fake_dists/choxie-2.0.0.9.dist-info | python print_info.py + +we get the following output: + +.. code-block:: none + + Information about choxie + Files + ===== + * Path: ../home/josip/dev/distutils2/src/distutils2/_backport/tests/fake_dists/choxie-2.0.0.9/truffles.py + Hash 5e052db6a478d06bad9ae033e6bc08af, Size: 111 bytes + * Path: ../home/josip/dev/distutils2/src/distutils2/_backport/tests/fake_dists/choxie-2.0.0.9/choxie/chocolate.py + Hash ac56bf496d8d1d26f866235b95f31030, Size: 214 bytes + * Path: ../home/josip/dev/distutils2/src/distutils2/_backport/tests/fake_dists/choxie-2.0.0.9/choxie/__init__.py + Hash 416aab08dfa846f473129e89a7625bbc, Size: 25 bytes + * Path: ../home/josip/dev/distutils2/src/distutils2/_backport/tests/fake_dists/choxie-2.0.0.9.dist-info/INSTALLER + Hash d41d8cd98f00b204e9800998ecf8427e, Size: 0 bytes + * Path: ../home/josip/dev/distutils2/src/distutils2/_backport/tests/fake_dists/choxie-2.0.0.9.dist-info/METADATA + Hash 696a209967fef3c8b8f5a7bb10386385, Size: 225 bytes + * Path: ../home/josip/dev/distutils2/src/distutils2/_backport/tests/fake_dists/choxie-2.0.0.9.dist-info/REQUESTED + Hash d41d8cd98f00b204e9800998ecf8427e, Size: 0 bytes + * Path: ../home/josip/dev/distutils2/src/distutils2/_backport/tests/fake_dists/choxie-2.0.0.9.dist-info/RECORD + Hash None, Size: None bytes + Metadata + ======== + Metadata-Version: 1.2 + Name: choxie + Version: 2.0.0.9 + Platform: [] + Supported-Platform: UNKNOWN + Summary: Chocolate with a kick! + Description: UNKNOWN + Keywords: [] + Home-page: UNKNOWN + Author: UNKNOWN + Author-email: UNKNOWN + Maintainer: UNKNOWN + Maintainer-email: UNKNOWN + License: UNKNOWN + Classifier: [] + Download-URL: UNKNOWN + Obsoletes-Dist: ['truffles (<=0.8,>=0.5)', 'truffles (<=0.9,>=0.6)'] + Project-URL: [] + Provides-Dist: ['truffles (1.0)'] + Requires-Dist: ['towel-stuff (0.1)'] + Requires-Python: UNKNOWN + Requires-External: [] + Extra + ===== + * It was installed as a dependency + +Find Out Obsoleted Distributions +++++++++++++++++++++++++++++++++ + +Now, we take tackle a different problem, we are interested in finding out +which distributions have been obsoleted. This can be easily done as follows:: + + from distutils2._backport import pkgutil + + # iterate over all distributions in the system + for dist in pkgutil.get_distributions(): + name = dist.name + version = dist.metadata['Version'] + # find out which distributions obsolete this name/version combination + for obsoleted_by in pkgutil.obsoletes_distribution(name, version): + print('%s(%s) is obsoleted by %s' % (name, version, obsoleted_by.name)) + +This is how the output might look like: + +.. code-block:: none + + strawberry(0.6) is obsoleted by choxie + grammar(1.0a4) is obsoleted by towel-stuff + diff --git a/docs/source/pypi.rst b/docs/source/pypi.rst new file mode 100644 index 0000000..d46c08e --- /dev/null +++ b/docs/source/pypi.rst @@ -0,0 +1,195 @@ +========================================= +Tools to query PyPI: the PyPI package +========================================= + +Distutils2 comes with a module (eg. `distutils2.pypi`) which contains +facilities to access the Python Package Index (named "pypi", and avalaible on +the url `http://pypi.python.org`. + +There is two ways to retrieve data from pypi: using the *simple* API, and using +*XML-RPC*. The first one is in fact a set of HTML pages avalaible at +`http://pypi.python.org/simple/`, and the second one contains a set of XML-RPC +methods. In order to reduce the overload caused by running distant methods on +the pypi server (by using the XML-RPC methods), the best way to retrieve +informations is by using the simple API, when it contains the information you +need. + +Distutils2 provides two python modules to ease the work with those two APIs: +`distutils2.pypi.simple` and `distutils2.pypi.xmlrpc`. Both of them depends on +another python module: `distutils2.pypi.dist`. + + +Requesting information via the "simple" API `distutils2.pypi.simple` +==================================================================== + +`distutils2.pypi.simple` can process the Python Package Index and return and +download urls of distributions, for specific versions or latests, but it also +can process external html pages, with the goal to find *pypi unhosted* versions +of python distributions. + +You should use `distutils2.pypi.simple` for: + + * Search distributions by name and versions. + * Process pypi external pages. + * Download distributions by name and versions. + +And should not be used to: + + * Things that will end up in too long index processing (like "finding all + distributions with a specific version, no matters the name") + +API +---- + +Here is a complete overview of the APIs of the SimpleIndex class. + +.. autoclass:: distutils2.pypi.simple.SimpleIndex + :members: + +Usage Exemples +--------------- + +To help you understand how using the `SimpleIndex` class, here are some basic +usages. + +Request PyPI to get a specific distribution +++++++++++++++++++++++++++++++++++++++++++++ + +Supposing you want to scan the PyPI index to get a list of distributions for +the "foobar" project. You can use the "find" method for that:: + + >>> from distutils2.pypi import SimpleIndex + >>> client = SimpleIndex() + >>> client.find("foobar") + [<PyPIDistribution "Foobar 1.1">, <PyPIDistribution "Foobar 1.2">] + +Note that you also can request the client about specific versions, using version +specifiers (described in `PEP 345 +<http://www.python.org/dev/peps/pep-0345/#version-specifiers>`_):: + + >>> client.find("foobar < 1.2") + [<PyPIDistribution "foobar 1.1">, ] + +`find` returns a list of distributions, but you also can get the last +distribution (the more up to date) that fullfil your requirements, like this:: + + >>> client.get("foobar < 1.2") + <PyPIDistribution "foobar 1.1"> + +Download distributions ++++++++++++++++++++++++ + +As it can get the urls of distributions provided by PyPI, the `SimpleIndex` +client also can download the distributions and put it for you in a temporary +destination:: + + >>> client.download("foobar") + /tmp/temp_dir/foobar-1.2.tar.gz + +You also can specify the directory you want to download to:: + + >>> client.download("foobar", "/path/to/my/dir") + /path/to/my/dir/foobar-1.2.tar.gz + +While downloading, the md5 of the archive will be checked, if not matches, it +will try another time, then if fails again, raise `MD5HashDoesNotMatchError`. + +Internally, that's not the SimpleIndex which download the distributions, but the +`PyPIDistribution` class. Please refer to this documentation for more details. + +Following PyPI external links +++++++++++++++++++++++++++++++ + +The default behavior for distutils2 is to *not* follow the links provided +by HTML pages in the "simple index", to find distributions related +downloads. + +It's possible to tell the PyPIClient to follow external links by setting the +`follow_externals` attribute, on instanciation or after:: + + >>> client = SimpleIndex(follow_externals=True) + +or :: + + >>> client = SimpleIndex() + >>> client.follow_externals = True + +Working with external indexes, and mirrors ++++++++++++++++++++++++++++++++++++++++++++ + +The default `SimpleIndex` behavior is to rely on the Python Package index stored +on PyPI (http://pypi.python.org/simple). + +As you can need to work with a local index, or private indexes, you can specify +it using the index_url parameter:: + + >>> client = SimpleIndex(index_url="file://filesystem/path/") + +or :: + + >>> client = SimpleIndex(index_url="http://some.specific.url/") + +You also can specify mirrors to fallback on in case the first index_url you +provided doesnt respond, or not correctly. The default behavior for +`SimpleIndex` is to use the list provided by Python.org DNS records, as +described in the :pep:`381` about mirroring infrastructure. + +If you don't want to rely on these, you could specify the list of mirrors you +want to try by specifying the `mirrors` attribute. It's a simple iterable:: + + >>> mirrors = ["http://first.mirror","http://second.mirror"] + >>> client = SimpleIndex(mirrors=mirrors) + + +Requesting informations via XML-RPC (`distutils2.pypi.XmlRpcIndex`) +========================================================================== + +The other method to request the Python package index, is using the XML-RPC +methods. Distutils2 provides a simple wrapper around `xmlrpclib +<http://docs.python.org/library/xmlrpclib.html>`_, that can return you +`PyPIDistribution` objects. + +:: + >>> from distutils2.pypi import XmlRpcIndex() + >>> client = XmlRpcIndex() + + +PyPI Distributions +================== + +Both `SimpleIndex` and `XmlRpcIndex` classes works with the classes provided +in the `pypi.dist` package. + +`PyPIDistribution` +------------------ + +`PyPIDistribution` is a simple class that defines the following attributes: + +:name: + The name of the package. `foobar` in our exemples here +:version: + The version of the package +:location: + If the files from the archive has been downloaded, here is the path where + you can find them. +:url: + The url of the distribution + +.. autoclass:: distutils2.pypi.dist.PyPIDistribution + :members: + +`PyPIDistributions` +------------------- + +The `dist` module also provides another class, to work with lists of +`PyPIDistribution` classes. It allow to filter results and is used as a +container of + +.. autoclass:: distutils2.pypi.dist.PyPIDistributions + :members: + +At a higher level +================= + +XXX : A description about a wraper around PyPI simple and XmlRpc Indexes +(PyPIIndex ?) diff --git a/docs/source/version.rst b/docs/source/version.rst new file mode 100644 index 0000000..6f72729 --- /dev/null +++ b/docs/source/version.rst @@ -0,0 +1,64 @@ +====================== +Working with versions +====================== + +Distutils2 ships with a python package capable to work with version numbers. +It's an implementation of version specifiers `as defined in PEP 345 +<http://www.python.org/dev/peps/pep-0345/#version-specifiers>`_ about +Metadata. + +`distutils2.version.NormalizedVersion` +====================================== + +A Normalized version corresponds to a specific version of a distribution, as +described in the PEP 345. So, you can work with the `NormalizedVersion` like +this:: + + >>> NormalizedVersion("1.2b1") + NormalizedVersion('1.2b1') + +If you try to use irrational version specifiers, an `IrrationalVersionError` +will be raised:: + + >>> NormalizedVersion("irrational_version_number") + ... + IrrationalVersionError: irrational_version_number + +You can compare NormalizedVersion objects, like this:: + + >>> NormalizedVersion("1.2b1") < NormalizedVersion("1.2") + True + +NormalizedVersion is used internally by `VersionPredicate` to do his stuff. + +`distutils2.version.suggest_normalized_version` +----------------------------------------------- + +You also can let the normalized version be suggested to you, using the +`suggest_normalized_version` function:: + + >>> suggest_normalized_version('2.1-rc1') + 2.1c1 + +If `suggest_normalized_version` can't actually suggest you a version, it will +return `None`:: + + >>> print suggest_normalized_version('not a version') + None + +`distutils2.version.VersionPredicate` +===================================== + +`VersionPredicate` knows how to parse stuff like "ProjectName (>=version)", the +class also provides a `match` method to test if a version number is the version +predicate:: + + >>> version = VersionPredicate("ProjectName (<1.2,>1.0") + >>> version.match("1.2.1") + False + >>> version.match("1.1.1") + True + +`is_valid_predicate` +-------------------- + diff --git a/src/CONTRIBUTORS.txt b/src/CONTRIBUTORS.txt index 887dffa..80594d1 100644 --- a/src/CONTRIBUTORS.txt +++ b/src/CONTRIBUTORS.txt @@ -5,7 +5,7 @@ Contributors Distutils2 is a project that was started and that is maintained by Tarek Ziadé, and many people are contributing to the project. -If you did, please add your name below in alphabetical order ! +If you did, please add your name below in alphabetical order! Thanks to: @@ -13,14 +13,18 @@ Thanks to: - Pior Bastida - Titus Brown - Nicolas Cadou +- Konrad Delong - Josip Djolonga - Yannick Gringas +- Jeremy Kloth +- Martin von Löwis - Carl Meyer +- Alexis Métaireau +- Zubin Mithra - Michael Mulich -- George Peris +- George Peristerakis - Sean Reifschneider +- Luis Rojas - Erik Rose - Brian Rosner - Alexandre Vassalotti -- Martin von Löwis - diff --git a/src/DEVNOTES.txt b/src/DEVNOTES.txt index f8a8e33..4e506f8 100644 --- a/src/DEVNOTES.txt +++ b/src/DEVNOTES.txt @@ -3,8 +3,8 @@ Notes for developers - Distutils2 runs from 2.4 to 3.2 (3.x not implemented yet), so make sure you don't use a syntax that doesn't work under - a specific Python version. + one of these Python versions. - Always run tests.sh before you push a change. This implies - that you have all Python versions installed. + that you have all Python versions installed from 2.4 to 2.6. diff --git a/src/Modules/_hashopenssl.c b/src/Modules/_hashopenssl.c new file mode 100644 index 0000000..fdcae5d --- /dev/null +++ b/src/Modules/_hashopenssl.c @@ -0,0 +1,524 @@ +/* Module that wraps all OpenSSL hash algorithms */ + +/* + * Copyright (C) 2005 Gregory P. Smith (greg@krypto.org) + * Licensed to PSF under a Contributor Agreement. + * + * Derived from a skeleton of shamodule.c containing work performed by: + * + * Andrew Kuchling (amk@amk.ca) + * Greg Stein (gstein@lyra.org) + * + */ + +#define PY_SSIZE_T_CLEAN + +#include "Python.h" +#include "structmember.h" + +#if (PY_VERSION_HEX < 0x02050000) +#define Py_ssize_t int +#endif + +/* EVP is the preferred interface to hashing in OpenSSL */ +#include <openssl/evp.h> + +#define MUNCH_SIZE INT_MAX + + +#ifndef HASH_OBJ_CONSTRUCTOR +#define HASH_OBJ_CONSTRUCTOR 0 +#endif + +typedef struct { + PyObject_HEAD + PyObject *name; /* name of this hash algorithm */ + EVP_MD_CTX ctx; /* OpenSSL message digest context */ +} EVPobject; + + +static PyTypeObject EVPtype; + + +#define DEFINE_CONSTS_FOR_NEW(Name) \ + static PyObject *CONST_ ## Name ## _name_obj; \ + static EVP_MD_CTX CONST_new_ ## Name ## _ctx; \ + static EVP_MD_CTX *CONST_new_ ## Name ## _ctx_p = NULL; + +DEFINE_CONSTS_FOR_NEW(md5) +DEFINE_CONSTS_FOR_NEW(sha1) +DEFINE_CONSTS_FOR_NEW(sha224) +DEFINE_CONSTS_FOR_NEW(sha256) +DEFINE_CONSTS_FOR_NEW(sha384) +DEFINE_CONSTS_FOR_NEW(sha512) + + +static EVPobject * +newEVPobject(PyObject *name) +{ + EVPobject *retval = (EVPobject *)PyObject_New(EVPobject, &EVPtype); + + /* save the name for .name to return */ + if (retval != NULL) { + Py_INCREF(name); + retval->name = name; + } + + return retval; +} + +/* Internal methods for a hash object */ + +static void +EVP_dealloc(PyObject *ptr) +{ + EVP_MD_CTX_cleanup(&((EVPobject *)ptr)->ctx); + Py_XDECREF(((EVPobject *)ptr)->name); + PyObject_Del(ptr); +} + + +/* External methods for a hash object */ + +PyDoc_STRVAR(EVP_copy__doc__, "Return a copy of the hash object."); + +static PyObject * +EVP_copy(EVPobject *self, PyObject *unused) +{ + EVPobject *newobj; + + if ( (newobj = newEVPobject(self->name))==NULL) + return NULL; + + EVP_MD_CTX_copy(&newobj->ctx, &self->ctx); + return (PyObject *)newobj; +} + +PyDoc_STRVAR(EVP_digest__doc__, +"Return the digest value as a string of binary data."); + +static PyObject * +EVP_digest(EVPobject *self, PyObject *unused) +{ + unsigned char digest[EVP_MAX_MD_SIZE]; + EVP_MD_CTX temp_ctx; + PyObject *retval; + unsigned int digest_size; + + EVP_MD_CTX_copy(&temp_ctx, &self->ctx); + digest_size = EVP_MD_CTX_size(&temp_ctx); + EVP_DigestFinal(&temp_ctx, digest, NULL); + + retval = PyString_FromStringAndSize((const char *)digest, digest_size); + EVP_MD_CTX_cleanup(&temp_ctx); + return retval; +} + +PyDoc_STRVAR(EVP_hexdigest__doc__, +"Return the digest value as a string of hexadecimal digits."); + +static PyObject * +EVP_hexdigest(EVPobject *self, PyObject *unused) +{ + unsigned char digest[EVP_MAX_MD_SIZE]; + EVP_MD_CTX temp_ctx; + PyObject *retval; + char *hex_digest; + unsigned int i, j, digest_size; + + /* Get the raw (binary) digest value */ + EVP_MD_CTX_copy(&temp_ctx, &self->ctx); + digest_size = EVP_MD_CTX_size(&temp_ctx); + EVP_DigestFinal(&temp_ctx, digest, NULL); + + EVP_MD_CTX_cleanup(&temp_ctx); + + /* Create a new string */ + /* NOTE: not thread safe! modifying an already created string object */ + /* (not a problem because we hold the GIL by default) */ + retval = PyString_FromStringAndSize(NULL, digest_size * 2); + if (!retval) + return NULL; + hex_digest = PyString_AsString(retval); + if (!hex_digest) { + Py_DECREF(retval); + return NULL; + } + + /* Make hex version of the digest */ + for(i=j=0; i<digest_size; i++) { + char c; + c = (digest[i] >> 4) & 0xf; + c = (c>9) ? c+'a'-10 : c + '0'; + hex_digest[j++] = c; + c = (digest[i] & 0xf); + c = (c>9) ? c+'a'-10 : c + '0'; + hex_digest[j++] = c; + } + return retval; +} + +PyDoc_STRVAR(EVP_update__doc__, +"Update this hash object's state with the provided string."); + +static PyObject * +EVP_update(EVPobject *self, PyObject *args) +{ + unsigned char *cp; + Py_ssize_t len; + + if (!PyArg_ParseTuple(args, "s#:update", &cp, &len)) + return NULL; + + if (len > 0 && len <= MUNCH_SIZE) { + EVP_DigestUpdate(&self->ctx, cp, Py_SAFE_DOWNCAST(len, Py_ssize_t, + unsigned int)); + } else { + Py_ssize_t offset = 0; + while (len) { + unsigned int process = len > MUNCH_SIZE ? MUNCH_SIZE : len; + EVP_DigestUpdate(&self->ctx, cp + offset, process); + len -= process; + offset += process; + } + } + Py_INCREF(Py_None); + return Py_None; +} + +static PyMethodDef EVP_methods[] = { + {"update", (PyCFunction)EVP_update, METH_VARARGS, EVP_update__doc__}, + {"digest", (PyCFunction)EVP_digest, METH_NOARGS, EVP_digest__doc__}, + {"hexdigest", (PyCFunction)EVP_hexdigest, METH_NOARGS, EVP_hexdigest__doc__}, + {"copy", (PyCFunction)EVP_copy, METH_NOARGS, EVP_copy__doc__}, + {NULL, NULL} /* sentinel */ +}; + +static PyObject * +EVP_get_block_size(EVPobject *self, void *closure) +{ + return PyInt_FromLong(EVP_MD_CTX_block_size(&((EVPobject *)self)->ctx)); +} + +static PyObject * +EVP_get_digest_size(EVPobject *self, void *closure) +{ + return PyInt_FromLong(EVP_MD_CTX_size(&((EVPobject *)self)->ctx)); +} + +static PyMemberDef EVP_members[] = { + {"name", T_OBJECT, offsetof(EVPobject, name), READONLY, PyDoc_STR("algorithm name.")}, + {NULL} /* Sentinel */ +}; + +static PyGetSetDef EVP_getseters[] = { + {"digest_size", + (getter)EVP_get_digest_size, NULL, + NULL, + NULL}, + {"block_size", + (getter)EVP_get_block_size, NULL, + NULL, + NULL}, + /* the old md5 and sha modules support 'digest_size' as in PEP 247. + * the old sha module also supported 'digestsize'. ugh. */ + {"digestsize", + (getter)EVP_get_digest_size, NULL, + NULL, + NULL}, + {NULL} /* Sentinel */ +}; + + +static PyObject * +EVP_repr(PyObject *self) +{ + char buf[100]; + PyOS_snprintf(buf, sizeof(buf), "<%s HASH object @ %p>", + PyString_AsString(((EVPobject *)self)->name), self); + return PyString_FromString(buf); +} + +#if HASH_OBJ_CONSTRUCTOR +static int +EVP_tp_init(EVPobject *self, PyObject *args, PyObject *kwds) +{ + static char *kwlist[] = {"name", "string", NULL}; + PyObject *name_obj = NULL; + char *nameStr; + unsigned char *cp = NULL; + Py_ssize_t len = 0; + const EVP_MD *digest; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|s#:HASH", kwlist, + &name_obj, &cp, &len)) { + return -1; + } + + if (!PyArg_Parse(name_obj, "s", &nameStr)) { + PyErr_SetString(PyExc_TypeError, "name must be a string"); + return -1; + } + + digest = EVP_get_digestbyname(nameStr); + if (!digest) { + PyErr_SetString(PyExc_ValueError, "unknown hash function"); + return -1; + } + EVP_DigestInit(&self->ctx, digest); + + self->name = name_obj; + Py_INCREF(self->name); + + if (cp && len) { + if (len > 0 && len <= MUNCH_SIZE) { + EVP_DigestUpdate(&self->ctx, cp, Py_SAFE_DOWNCAST(len, Py_ssize_t, + unsigned int)); + } else { + Py_ssize_t offset = 0; + while (len) { + unsigned int process = len > MUNCH_SIZE ? MUNCH_SIZE : len; + EVP_DigestUpdate(&self->ctx, cp + offset, process); + len -= process; + offset += process; + } + } + } + + return 0; +} +#endif + + +PyDoc_STRVAR(hashtype_doc, +"A hash represents the object used to calculate a checksum of a\n\ +string of information.\n\ +\n\ +Methods:\n\ +\n\ +update() -- updates the current digest with an additional string\n\ +digest() -- return the current digest value\n\ +hexdigest() -- return the current digest as a string of hexadecimal digits\n\ +copy() -- return a copy of the current hash object\n\ +\n\ +Attributes:\n\ +\n\ +name -- the hash algorithm being used by this object\n\ +digest_size -- number of bytes in this hashes output\n"); + +static PyTypeObject EVPtype = { + PyObject_HEAD_INIT(NULL) + 0, /*ob_size*/ + "_hashlib.HASH", /*tp_name*/ + sizeof(EVPobject), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + /* methods */ + EVP_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + EVP_repr, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ + hashtype_doc, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + EVP_methods, /* tp_methods */ + EVP_members, /* tp_members */ + EVP_getseters, /* tp_getset */ +#if 1 + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ +#endif +#if HASH_OBJ_CONSTRUCTOR + (initproc)EVP_tp_init, /* tp_init */ +#endif +}; + +static PyObject * +EVPnew(PyObject *name_obj, + const EVP_MD *digest, const EVP_MD_CTX *initial_ctx, + const unsigned char *cp, Py_ssize_t len) +{ + EVPobject *self; + + if (!digest && !initial_ctx) { + PyErr_SetString(PyExc_ValueError, "unsupported hash type"); + return NULL; + } + + if ((self = newEVPobject(name_obj)) == NULL) + return NULL; + + if (initial_ctx) { + EVP_MD_CTX_copy(&self->ctx, initial_ctx); + } else { + EVP_DigestInit(&self->ctx, digest); + } + + if (cp && len) { + if (len > 0 && len <= MUNCH_SIZE) { + EVP_DigestUpdate(&self->ctx, cp, Py_SAFE_DOWNCAST(len, Py_ssize_t, + unsigned int)); + } else { + Py_ssize_t offset = 0; + while (len) { + unsigned int process = len > MUNCH_SIZE ? MUNCH_SIZE : len; + EVP_DigestUpdate(&self->ctx, cp + offset, process); + len -= process; + offset += process; + } + } + } + + return (PyObject *)self; +} + + +/* The module-level function: new() */ + +PyDoc_STRVAR(EVP_new__doc__, +"Return a new hash object using the named algorithm.\n\ +An optional string argument may be provided and will be\n\ +automatically hashed.\n\ +\n\ +The MD5 and SHA1 algorithms are always supported.\n"); + +static PyObject * +EVP_new(PyObject *self, PyObject *args, PyObject *kwdict) +{ + static char *kwlist[] = {"name", "string", NULL}; + PyObject *name_obj = NULL; + char *name; + const EVP_MD *digest; + unsigned char *cp = NULL; + Py_ssize_t len = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwdict, "O|s#:new", kwlist, + &name_obj, &cp, &len)) { + return NULL; + } + + if (!PyArg_Parse(name_obj, "s", &name)) { + PyErr_SetString(PyExc_TypeError, "name must be a string"); + return NULL; + } + + digest = EVP_get_digestbyname(name); + + return EVPnew(name_obj, digest, NULL, cp, len); +} + +/* + * This macro generates constructor function definitions for specific + * hash algorithms. These constructors are much faster than calling + * the generic one passing it a python string and are noticably + * faster than calling a python new() wrapper. Thats important for + * code that wants to make hashes of a bunch of small strings. + */ +#define GEN_CONSTRUCTOR(NAME) \ + static PyObject * \ + EVP_new_ ## NAME (PyObject *self, PyObject *args) \ + { \ + unsigned char *cp = NULL; \ + Py_ssize_t len = 0; \ + \ + if (!PyArg_ParseTuple(args, "|s#:" #NAME , &cp, &len)) { \ + return NULL; \ + } \ + \ + return EVPnew( \ + CONST_ ## NAME ## _name_obj, \ + NULL, \ + CONST_new_ ## NAME ## _ctx_p, \ + cp, len); \ + } + +/* a PyMethodDef structure for the constructor */ +#define CONSTRUCTOR_METH_DEF(NAME) \ + {"openssl_" #NAME, (PyCFunction)EVP_new_ ## NAME, METH_VARARGS, \ + PyDoc_STR("Returns a " #NAME \ + " hash object; optionally initialized with a string") \ + } + +/* used in the init function to setup a constructor */ +#define INIT_CONSTRUCTOR_CONSTANTS(NAME) do { \ + CONST_ ## NAME ## _name_obj = PyString_FromString(#NAME); \ + if (EVP_get_digestbyname(#NAME)) { \ + CONST_new_ ## NAME ## _ctx_p = &CONST_new_ ## NAME ## _ctx; \ + EVP_DigestInit(CONST_new_ ## NAME ## _ctx_p, EVP_get_digestbyname(#NAME)); \ + } \ +} while (0); + +GEN_CONSTRUCTOR(md5) +GEN_CONSTRUCTOR(sha1) +GEN_CONSTRUCTOR(sha224) +GEN_CONSTRUCTOR(sha256) +GEN_CONSTRUCTOR(sha384) +GEN_CONSTRUCTOR(sha512) + +/* List of functions exported by this module */ + +static struct PyMethodDef EVP_functions[] = { + {"new", (PyCFunction)EVP_new, METH_VARARGS|METH_KEYWORDS, EVP_new__doc__}, + CONSTRUCTOR_METH_DEF(md5), + CONSTRUCTOR_METH_DEF(sha1), + CONSTRUCTOR_METH_DEF(sha224), + CONSTRUCTOR_METH_DEF(sha256), + CONSTRUCTOR_METH_DEF(sha384), + CONSTRUCTOR_METH_DEF(sha512), + {NULL, NULL} /* Sentinel */ +}; + + +/* Initialize this module. */ + +PyMODINIT_FUNC +init_hashlib(void) +{ + PyObject *m; + + OpenSSL_add_all_digests(); + + /* TODO build EVP_functions openssl_* entries dynamically based + * on what hashes are supported rather than listing many + * but having some be unsupported. Only init appropriate + * constants. */ + + EVPtype.ob_type = &PyType_Type; + if (PyType_Ready(&EVPtype) < 0) + return; + + m = Py_InitModule("_hashlib", EVP_functions); + if (m == NULL) + return; + +#if HASH_OBJ_CONSTRUCTOR + Py_INCREF(&EVPtype); + PyModule_AddObject(m, "HASH", (PyObject *)&EVPtype); +#endif + + /* these constants are used by the convenience constructors */ + INIT_CONSTRUCTOR_CONSTANTS(md5); + INIT_CONSTRUCTOR_CONSTANTS(sha1); + INIT_CONSTRUCTOR_CONSTANTS(sha224); + INIT_CONSTRUCTOR_CONSTANTS(sha256); + INIT_CONSTRUCTOR_CONSTANTS(sha384); + INIT_CONSTRUCTOR_CONSTANTS(sha512); +} diff --git a/src/Modules/md5.c b/src/Modules/md5.c new file mode 100644 index 0000000..c35d96c --- /dev/null +++ b/src/Modules/md5.c @@ -0,0 +1,381 @@ +/* + Copyright (C) 1999, 2000, 2002 Aladdin Enterprises. All rights reserved. + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + L. Peter Deutsch + ghost@aladdin.com + + */ +/* $Id: md5.c,v 1.6 2002/04/13 19:20:28 lpd Exp $ */ +/* + Independent implementation of MD5 (RFC 1321). + + This code implements the MD5 Algorithm defined in RFC 1321, whose + text is available at + http://www.ietf.org/rfc/rfc1321.txt + The code is derived from the text of the RFC, including the test suite + (section A.5) but excluding the rest of Appendix A. It does not include + any code or documentation that is identified in the RFC as being + copyrighted. + + The original and principal author of md5.c is L. Peter Deutsch + <ghost@aladdin.com>. Other authors are noted in the change history + that follows (in reverse chronological order): + + 2002-04-13 lpd Clarified derivation from RFC 1321; now handles byte order + either statically or dynamically; added missing #include <string.h> + in library. + 2002-03-11 lpd Corrected argument list for main(), and added int return + type, in test program and T value program. + 2002-02-21 lpd Added missing #include <stdio.h> in test program. + 2000-07-03 lpd Patched to eliminate warnings about "constant is + unsigned in ANSI C, signed in traditional"; made test program + self-checking. + 1999-11-04 lpd Edited comments slightly for automatic TOC extraction. + 1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5). + 1999-05-03 lpd Original version. + */ + +#include "md5.h" +#include <string.h> + +#undef BYTE_ORDER /* 1 = big-endian, -1 = little-endian, 0 = unknown */ +#ifdef ARCH_IS_BIG_ENDIAN +# define BYTE_ORDER (ARCH_IS_BIG_ENDIAN ? 1 : -1) +#else +# define BYTE_ORDER 0 +#endif + +#define T_MASK ((md5_word_t)~0) +#define T1 /* 0xd76aa478 */ (T_MASK ^ 0x28955b87) +#define T2 /* 0xe8c7b756 */ (T_MASK ^ 0x173848a9) +#define T3 0x242070db +#define T4 /* 0xc1bdceee */ (T_MASK ^ 0x3e423111) +#define T5 /* 0xf57c0faf */ (T_MASK ^ 0x0a83f050) +#define T6 0x4787c62a +#define T7 /* 0xa8304613 */ (T_MASK ^ 0x57cfb9ec) +#define T8 /* 0xfd469501 */ (T_MASK ^ 0x02b96afe) +#define T9 0x698098d8 +#define T10 /* 0x8b44f7af */ (T_MASK ^ 0x74bb0850) +#define T11 /* 0xffff5bb1 */ (T_MASK ^ 0x0000a44e) +#define T12 /* 0x895cd7be */ (T_MASK ^ 0x76a32841) +#define T13 0x6b901122 +#define T14 /* 0xfd987193 */ (T_MASK ^ 0x02678e6c) +#define T15 /* 0xa679438e */ (T_MASK ^ 0x5986bc71) +#define T16 0x49b40821 +#define T17 /* 0xf61e2562 */ (T_MASK ^ 0x09e1da9d) +#define T18 /* 0xc040b340 */ (T_MASK ^ 0x3fbf4cbf) +#define T19 0x265e5a51 +#define T20 /* 0xe9b6c7aa */ (T_MASK ^ 0x16493855) +#define T21 /* 0xd62f105d */ (T_MASK ^ 0x29d0efa2) +#define T22 0x02441453 +#define T23 /* 0xd8a1e681 */ (T_MASK ^ 0x275e197e) +#define T24 /* 0xe7d3fbc8 */ (T_MASK ^ 0x182c0437) +#define T25 0x21e1cde6 +#define T26 /* 0xc33707d6 */ (T_MASK ^ 0x3cc8f829) +#define T27 /* 0xf4d50d87 */ (T_MASK ^ 0x0b2af278) +#define T28 0x455a14ed +#define T29 /* 0xa9e3e905 */ (T_MASK ^ 0x561c16fa) +#define T30 /* 0xfcefa3f8 */ (T_MASK ^ 0x03105c07) +#define T31 0x676f02d9 +#define T32 /* 0x8d2a4c8a */ (T_MASK ^ 0x72d5b375) +#define T33 /* 0xfffa3942 */ (T_MASK ^ 0x0005c6bd) +#define T34 /* 0x8771f681 */ (T_MASK ^ 0x788e097e) +#define T35 0x6d9d6122 +#define T36 /* 0xfde5380c */ (T_MASK ^ 0x021ac7f3) +#define T37 /* 0xa4beea44 */ (T_MASK ^ 0x5b4115bb) +#define T38 0x4bdecfa9 +#define T39 /* 0xf6bb4b60 */ (T_MASK ^ 0x0944b49f) +#define T40 /* 0xbebfbc70 */ (T_MASK ^ 0x4140438f) +#define T41 0x289b7ec6 +#define T42 /* 0xeaa127fa */ (T_MASK ^ 0x155ed805) +#define T43 /* 0xd4ef3085 */ (T_MASK ^ 0x2b10cf7a) +#define T44 0x04881d05 +#define T45 /* 0xd9d4d039 */ (T_MASK ^ 0x262b2fc6) +#define T46 /* 0xe6db99e5 */ (T_MASK ^ 0x1924661a) +#define T47 0x1fa27cf8 +#define T48 /* 0xc4ac5665 */ (T_MASK ^ 0x3b53a99a) +#define T49 /* 0xf4292244 */ (T_MASK ^ 0x0bd6ddbb) +#define T50 0x432aff97 +#define T51 /* 0xab9423a7 */ (T_MASK ^ 0x546bdc58) +#define T52 /* 0xfc93a039 */ (T_MASK ^ 0x036c5fc6) +#define T53 0x655b59c3 +#define T54 /* 0x8f0ccc92 */ (T_MASK ^ 0x70f3336d) +#define T55 /* 0xffeff47d */ (T_MASK ^ 0x00100b82) +#define T56 /* 0x85845dd1 */ (T_MASK ^ 0x7a7ba22e) +#define T57 0x6fa87e4f +#define T58 /* 0xfe2ce6e0 */ (T_MASK ^ 0x01d3191f) +#define T59 /* 0xa3014314 */ (T_MASK ^ 0x5cfebceb) +#define T60 0x4e0811a1 +#define T61 /* 0xf7537e82 */ (T_MASK ^ 0x08ac817d) +#define T62 /* 0xbd3af235 */ (T_MASK ^ 0x42c50dca) +#define T63 0x2ad7d2bb +#define T64 /* 0xeb86d391 */ (T_MASK ^ 0x14792c6e) + + +static void +md5_process(md5_state_t *pms, const md5_byte_t *data /*[64]*/) +{ + md5_word_t + a = pms->abcd[0], b = pms->abcd[1], + c = pms->abcd[2], d = pms->abcd[3]; + md5_word_t t; +#if BYTE_ORDER > 0 + /* Define storage only for big-endian CPUs. */ + md5_word_t X[16]; +#else + /* Define storage for little-endian or both types of CPUs. */ + md5_word_t xbuf[16]; + const md5_word_t *X; +#endif + + { +#if BYTE_ORDER == 0 + /* + * Determine dynamically whether this is a big-endian or + * little-endian machine, since we can use a more efficient + * algorithm on the latter. + */ + static const int w = 1; + + if (*((const md5_byte_t *)&w)) /* dynamic little-endian */ +#endif +#if BYTE_ORDER <= 0 /* little-endian */ + { + /* + * On little-endian machines, we can process properly aligned + * data without copying it. + */ + if (!((data - (const md5_byte_t *)0) & 3)) { + /* data are properly aligned */ + X = (const md5_word_t *)data; + } else { + /* not aligned */ + memcpy(xbuf, data, 64); + X = xbuf; + } + } +#endif +#if BYTE_ORDER == 0 + else /* dynamic big-endian */ +#endif +#if BYTE_ORDER >= 0 /* big-endian */ + { + /* + * On big-endian machines, we must arrange the bytes in the + * right order. + */ + const md5_byte_t *xp = data; + int i; + +# if BYTE_ORDER == 0 + X = xbuf; /* (dynamic only) */ +# else +# define xbuf X /* (static only) */ +# endif + for (i = 0; i < 16; ++i, xp += 4) + xbuf[i] = xp[0] + (xp[1] << 8) + (xp[2] << 16) + (xp[3] << 24); + } +#endif + } + +#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32 - (n)))) + + /* Round 1. */ + /* Let [abcd k s i] denote the operation + a = b + ((a + F(b,c,d) + X[k] + T[i]) <<< s). */ +#define F(x, y, z) (((x) & (y)) | (~(x) & (z))) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + F(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 0, 7, T1); + SET(d, a, b, c, 1, 12, T2); + SET(c, d, a, b, 2, 17, T3); + SET(b, c, d, a, 3, 22, T4); + SET(a, b, c, d, 4, 7, T5); + SET(d, a, b, c, 5, 12, T6); + SET(c, d, a, b, 6, 17, T7); + SET(b, c, d, a, 7, 22, T8); + SET(a, b, c, d, 8, 7, T9); + SET(d, a, b, c, 9, 12, T10); + SET(c, d, a, b, 10, 17, T11); + SET(b, c, d, a, 11, 22, T12); + SET(a, b, c, d, 12, 7, T13); + SET(d, a, b, c, 13, 12, T14); + SET(c, d, a, b, 14, 17, T15); + SET(b, c, d, a, 15, 22, T16); +#undef SET + + /* Round 2. */ + /* Let [abcd k s i] denote the operation + a = b + ((a + G(b,c,d) + X[k] + T[i]) <<< s). */ +#define G(x, y, z) (((x) & (z)) | ((y) & ~(z))) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + G(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 1, 5, T17); + SET(d, a, b, c, 6, 9, T18); + SET(c, d, a, b, 11, 14, T19); + SET(b, c, d, a, 0, 20, T20); + SET(a, b, c, d, 5, 5, T21); + SET(d, a, b, c, 10, 9, T22); + SET(c, d, a, b, 15, 14, T23); + SET(b, c, d, a, 4, 20, T24); + SET(a, b, c, d, 9, 5, T25); + SET(d, a, b, c, 14, 9, T26); + SET(c, d, a, b, 3, 14, T27); + SET(b, c, d, a, 8, 20, T28); + SET(a, b, c, d, 13, 5, T29); + SET(d, a, b, c, 2, 9, T30); + SET(c, d, a, b, 7, 14, T31); + SET(b, c, d, a, 12, 20, T32); +#undef SET + + /* Round 3. */ + /* Let [abcd k s t] denote the operation + a = b + ((a + H(b,c,d) + X[k] + T[i]) <<< s). */ +#define H(x, y, z) ((x) ^ (y) ^ (z)) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + H(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 5, 4, T33); + SET(d, a, b, c, 8, 11, T34); + SET(c, d, a, b, 11, 16, T35); + SET(b, c, d, a, 14, 23, T36); + SET(a, b, c, d, 1, 4, T37); + SET(d, a, b, c, 4, 11, T38); + SET(c, d, a, b, 7, 16, T39); + SET(b, c, d, a, 10, 23, T40); + SET(a, b, c, d, 13, 4, T41); + SET(d, a, b, c, 0, 11, T42); + SET(c, d, a, b, 3, 16, T43); + SET(b, c, d, a, 6, 23, T44); + SET(a, b, c, d, 9, 4, T45); + SET(d, a, b, c, 12, 11, T46); + SET(c, d, a, b, 15, 16, T47); + SET(b, c, d, a, 2, 23, T48); +#undef SET + + /* Round 4. */ + /* Let [abcd k s t] denote the operation + a = b + ((a + I(b,c,d) + X[k] + T[i]) <<< s). */ +#define I(x, y, z) ((y) ^ ((x) | ~(z))) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + I(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 0, 6, T49); + SET(d, a, b, c, 7, 10, T50); + SET(c, d, a, b, 14, 15, T51); + SET(b, c, d, a, 5, 21, T52); + SET(a, b, c, d, 12, 6, T53); + SET(d, a, b, c, 3, 10, T54); + SET(c, d, a, b, 10, 15, T55); + SET(b, c, d, a, 1, 21, T56); + SET(a, b, c, d, 8, 6, T57); + SET(d, a, b, c, 15, 10, T58); + SET(c, d, a, b, 6, 15, T59); + SET(b, c, d, a, 13, 21, T60); + SET(a, b, c, d, 4, 6, T61); + SET(d, a, b, c, 11, 10, T62); + SET(c, d, a, b, 2, 15, T63); + SET(b, c, d, a, 9, 21, T64); +#undef SET + + /* Then perform the following additions. (That is increment each + of the four registers by the value it had before this block + was started.) */ + pms->abcd[0] += a; + pms->abcd[1] += b; + pms->abcd[2] += c; + pms->abcd[3] += d; +} + +void +md5_init(md5_state_t *pms) +{ + pms->count[0] = pms->count[1] = 0; + pms->abcd[0] = 0x67452301; + pms->abcd[1] = /*0xefcdab89*/ T_MASK ^ 0x10325476; + pms->abcd[2] = /*0x98badcfe*/ T_MASK ^ 0x67452301; + pms->abcd[3] = 0x10325476; +} + +void +md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes) +{ + const md5_byte_t *p = data; + int left = nbytes; + int offset = (pms->count[0] >> 3) & 63; + md5_word_t nbits = (md5_word_t)(nbytes << 3); + + if (nbytes <= 0) + return; + + /* Update the message length. */ + pms->count[1] += nbytes >> 29; + pms->count[0] += nbits; + if (pms->count[0] < nbits) + pms->count[1]++; + + /* Process an initial partial block. */ + if (offset) { + int copy = (offset + nbytes > 64 ? 64 - offset : nbytes); + + memcpy(pms->buf + offset, p, copy); + if (offset + copy < 64) + return; + p += copy; + left -= copy; + md5_process(pms, pms->buf); + } + + /* Process full blocks. */ + for (; left >= 64; p += 64, left -= 64) + md5_process(pms, p); + + /* Process a final partial block. */ + if (left) + memcpy(pms->buf, p, left); +} + +void +md5_finish(md5_state_t *pms, md5_byte_t digest[16]) +{ + static const md5_byte_t pad[64] = { + 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + md5_byte_t data[8]; + int i; + + /* Save the length before padding. */ + for (i = 0; i < 8; ++i) + data[i] = (md5_byte_t)(pms->count[i >> 2] >> ((i & 3) << 3)); + /* Pad to 56 bytes mod 64. */ + md5_append(pms, pad, ((55 - (pms->count[0] >> 3)) & 63) + 1); + /* Append the length. */ + md5_append(pms, data, 8); + for (i = 0; i < 16; ++i) + digest[i] = (md5_byte_t)(pms->abcd[i >> 2] >> ((i & 3) << 3)); +} diff --git a/src/Modules/md5.h b/src/Modules/md5.h new file mode 100644 index 0000000..1718401 --- /dev/null +++ b/src/Modules/md5.h @@ -0,0 +1,91 @@ +/* + Copyright (C) 1999, 2002 Aladdin Enterprises. All rights reserved. + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + L. Peter Deutsch + ghost@aladdin.com + + */ +/* $Id: md5.h 43594 2006-04-03 16:27:50Z matthias.klose $ */ +/* + Independent implementation of MD5 (RFC 1321). + + This code implements the MD5 Algorithm defined in RFC 1321, whose + text is available at + http://www.ietf.org/rfc/rfc1321.txt + The code is derived from the text of the RFC, including the test suite + (section A.5) but excluding the rest of Appendix A. It does not include + any code or documentation that is identified in the RFC as being + copyrighted. + + The original and principal author of md5.h is L. Peter Deutsch + <ghost@aladdin.com>. Other authors are noted in the change history + that follows (in reverse chronological order): + + 2002-04-13 lpd Removed support for non-ANSI compilers; removed + references to Ghostscript; clarified derivation from RFC 1321; + now handles byte order either statically or dynamically. + 1999-11-04 lpd Edited comments slightly for automatic TOC extraction. + 1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5); + added conditionalization for C++ compilation from Martin + Purschke <purschke@bnl.gov>. + 1999-05-03 lpd Original version. + */ + +#ifndef md5_INCLUDED +# define md5_INCLUDED + +/* + * This package supports both compile-time and run-time determination of CPU + * byte order. If ARCH_IS_BIG_ENDIAN is defined as 0, the code will be + * compiled to run only on little-endian CPUs; if ARCH_IS_BIG_ENDIAN is + * defined as non-zero, the code will be compiled to run only on big-endian + * CPUs; if ARCH_IS_BIG_ENDIAN is not defined, the code will be compiled to + * run on either big- or little-endian CPUs, but will run slightly less + * efficiently on either one than if ARCH_IS_BIG_ENDIAN is defined. + */ + +typedef unsigned char md5_byte_t; /* 8-bit byte */ +typedef unsigned int md5_word_t; /* 32-bit word */ + +/* Define the state of the MD5 Algorithm. */ +typedef struct md5_state_s { + md5_word_t count[2]; /* message length in bits, lsw first */ + md5_word_t abcd[4]; /* digest buffer */ + md5_byte_t buf[64]; /* accumulate block */ +} md5_state_t; + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* Initialize the algorithm. */ +void md5_init(md5_state_t *pms); + +/* Append a string to the message. */ +void md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes); + +/* Finish the message and return the digest. */ +void md5_finish(md5_state_t *pms, md5_byte_t digest[16]); + +#ifdef __cplusplus +} /* end extern "C" */ +#endif + +#endif /* md5_INCLUDED */ diff --git a/src/Modules/md5module.c b/src/Modules/md5module.c new file mode 100644 index 0000000..5e4f116 --- /dev/null +++ b/src/Modules/md5module.c @@ -0,0 +1,312 @@ + +/* MD5 module */ + +/* This module provides an interface to the RSA Data Security, + Inc. MD5 Message-Digest Algorithm, described in RFC 1321. + It requires the files md5c.c and md5.h (which are slightly changed + from the versions in the RFC to avoid the "global.h" file.) */ + + +/* MD5 objects */ + +#include "Python.h" +#include "structmember.h" +#include "md5.h" + +typedef struct { + PyObject_HEAD + md5_state_t md5; /* the context holder */ +} md5object; + +static PyTypeObject MD5type; + +#define is_md5object(v) ((v)->ob_type == &MD5type) + +static md5object * +newmd5object(void) +{ + md5object *md5p; + + md5p = PyObject_New(md5object, &MD5type); + if (md5p == NULL) + return NULL; + + md5_init(&md5p->md5); /* actual initialisation */ + return md5p; +} + + +/* MD5 methods */ + +static void +md5_dealloc(md5object *md5p) +{ + PyObject_Del(md5p); +} + + +/* MD5 methods-as-attributes */ + +static PyObject * +md5_update(md5object *self, PyObject *args) +{ + unsigned char *cp; + int len; + + if (!PyArg_ParseTuple(args, "s#:update", &cp, &len)) + return NULL; + + md5_append(&self->md5, cp, len); + + Py_INCREF(Py_None); + return Py_None; +} + +PyDoc_STRVAR(update_doc, +"update (arg)\n\ +\n\ +Update the md5 object with the string arg. Repeated calls are\n\ +equivalent to a single call with the concatenation of all the\n\ +arguments."); + + +static PyObject * +md5_digest(md5object *self) +{ + md5_state_t mdContext; + unsigned char aDigest[16]; + + /* make a temporary copy, and perform the final */ + mdContext = self->md5; + md5_finish(&mdContext, aDigest); + + return PyString_FromStringAndSize((char *)aDigest, 16); +} + +PyDoc_STRVAR(digest_doc, +"digest() -> string\n\ +\n\ +Return the digest of the strings passed to the update() method so\n\ +far. This is a 16-byte string which may contain non-ASCII characters,\n\ +including null bytes."); + + +static PyObject * +md5_hexdigest(md5object *self) +{ + md5_state_t mdContext; + unsigned char digest[16]; + unsigned char hexdigest[32]; + int i, j; + + /* make a temporary copy, and perform the final */ + mdContext = self->md5; + md5_finish(&mdContext, digest); + + /* Make hex version of the digest */ + for(i=j=0; i<16; i++) { + char c; + c = (digest[i] >> 4) & 0xf; + c = (c>9) ? c+'a'-10 : c + '0'; + hexdigest[j++] = c; + c = (digest[i] & 0xf); + c = (c>9) ? c+'a'-10 : c + '0'; + hexdigest[j++] = c; + } + return PyString_FromStringAndSize((char*)hexdigest, 32); +} + + +PyDoc_STRVAR(hexdigest_doc, +"hexdigest() -> string\n\ +\n\ +Like digest(), but returns the digest as a string of hexadecimal digits."); + + +static PyObject * +md5_copy(md5object *self) +{ + md5object *md5p; + + if ((md5p = newmd5object()) == NULL) + return NULL; + + md5p->md5 = self->md5; + + return (PyObject *)md5p; +} + +PyDoc_STRVAR(copy_doc, +"copy() -> md5 object\n\ +\n\ +Return a copy (``clone'') of the md5 object."); + + +static PyMethodDef md5_methods[] = { + {"update", (PyCFunction)md5_update, METH_VARARGS, update_doc}, + {"digest", (PyCFunction)md5_digest, METH_NOARGS, digest_doc}, + {"hexdigest", (PyCFunction)md5_hexdigest, METH_NOARGS, hexdigest_doc}, + {"copy", (PyCFunction)md5_copy, METH_NOARGS, copy_doc}, + {NULL, NULL} /* sentinel */ +}; + +static PyObject * +md5_get_block_size(PyObject *self, void *closure) +{ + return PyInt_FromLong(64); +} + +static PyObject * +md5_get_digest_size(PyObject *self, void *closure) +{ + return PyInt_FromLong(16); +} + +static PyObject * +md5_get_name(PyObject *self, void *closure) +{ + return PyString_FromStringAndSize("MD5", 3); +} + +static PyGetSetDef md5_getseters[] = { + {"digest_size", + (getter)md5_get_digest_size, NULL, + NULL, + NULL}, + {"block_size", + (getter)md5_get_block_size, NULL, + NULL, + NULL}, + {"name", + (getter)md5_get_name, NULL, + NULL, + NULL}, + /* the old md5 and sha modules support 'digest_size' as in PEP 247. + * the old sha module also supported 'digestsize'. ugh. */ + {"digestsize", + (getter)md5_get_digest_size, NULL, + NULL, + NULL}, + {NULL} /* Sentinel */ +}; + + +PyDoc_STRVAR(module_doc, +"This module implements the interface to RSA's MD5 message digest\n\ +algorithm (see also Internet RFC 1321). Its use is quite\n\ +straightforward: use the new() to create an md5 object. You can now\n\ +feed this object with arbitrary strings using the update() method, and\n\ +at any point you can ask it for the digest (a strong kind of 128-bit\n\ +checksum, a.k.a. ``fingerprint'') of the concatenation of the strings\n\ +fed to it so far using the digest() method.\n\ +\n\ +Functions:\n\ +\n\ +new([arg]) -- return a new md5 object, initialized with arg if provided\n\ +md5([arg]) -- DEPRECATED, same as new, but for compatibility\n\ +\n\ +Special Objects:\n\ +\n\ +MD5Type -- type object for md5 objects"); + +PyDoc_STRVAR(md5type_doc, +"An md5 represents the object used to calculate the MD5 checksum of a\n\ +string of information.\n\ +\n\ +Methods:\n\ +\n\ +update() -- updates the current digest with an additional string\n\ +digest() -- return the current digest value\n\ +hexdigest() -- return the current digest as a string of hexadecimal digits\n\ +copy() -- return a copy of the current md5 object"); + +static PyTypeObject MD5type = { + PyObject_HEAD_INIT(NULL) + 0, /*ob_size*/ + "_md5.md5", /*tp_name*/ + sizeof(md5object), /*tp_size*/ + 0, /*tp_itemsize*/ + /* methods */ + (destructor)md5_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + md5type_doc, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + md5_methods, /*tp_methods*/ + 0, /*tp_members*/ + md5_getseters, /*tp_getset*/ +}; + + +/* MD5 functions */ + +static PyObject * +MD5_new(PyObject *self, PyObject *args) +{ + md5object *md5p; + unsigned char *cp = NULL; + int len = 0; + + if (!PyArg_ParseTuple(args, "|s#:new", &cp, &len)) + return NULL; + + if ((md5p = newmd5object()) == NULL) + return NULL; + + if (cp) + md5_append(&md5p->md5, cp, len); + + return (PyObject *)md5p; +} + +PyDoc_STRVAR(new_doc, +"new([arg]) -> md5 object\n\ +\n\ +Return a new md5 object. If arg is present, the method call update(arg)\n\ +is made."); + + +/* List of functions exported by this module */ + +static PyMethodDef md5_functions[] = { + {"new", (PyCFunction)MD5_new, METH_VARARGS, new_doc}, + {NULL, NULL} /* Sentinel */ +}; + + +/* Initialize this module. */ + +PyMODINIT_FUNC +init_md5(void) +{ + PyObject *m, *d; + + MD5type.ob_type = &PyType_Type; + if (PyType_Ready(&MD5type) < 0) + return; + m = Py_InitModule3("_md5", md5_functions, module_doc); + if (m == NULL) + return; + d = PyModule_GetDict(m); + PyDict_SetItemString(d, "MD5Type", (PyObject *)&MD5type); + PyModule_AddIntConstant(m, "digest_size", 16); + /* No need to check the error here, the caller will do that */ +} diff --git a/src/Modules/sha256module.c b/src/Modules/sha256module.c new file mode 100644 index 0000000..96cbd10 --- /dev/null +++ b/src/Modules/sha256module.c @@ -0,0 +1,701 @@ +/* SHA256 module */ + +/* This module provides an interface to NIST's SHA-256 and SHA-224 Algorithms */ + +/* See below for information about the original code this module was + based upon. Additional work performed by: + + Andrew Kuchling (amk@amk.ca) + Greg Stein (gstein@lyra.org) + Trevor Perrin (trevp@trevp.net) + + Copyright (C) 2005 Gregory P. Smith (greg@krypto.org) + Licensed to PSF under a Contributor Agreement. + +*/ + +/* SHA objects */ + +#include "Python.h" +#include "structmember.h" + + +/* Endianness testing and definitions */ +#define TestEndianness(variable) {int i=1; variable=PCT_BIG_ENDIAN;\ + if (*((char*)&i)==1) variable=PCT_LITTLE_ENDIAN;} + +#define PCT_LITTLE_ENDIAN 1 +#define PCT_BIG_ENDIAN 0 + +/* Some useful types */ + +typedef unsigned char SHA_BYTE; + +#if SIZEOF_INT == 4 +typedef unsigned int SHA_INT32; /* 32-bit integer */ +#else +/* not defined. compilation will die. */ +#endif + +/* The SHA block size and message digest sizes, in bytes */ + +#define SHA_BLOCKSIZE 64 +#define SHA_DIGESTSIZE 32 + +/* The structure for storing SHA info */ + +typedef struct { + PyObject_HEAD + SHA_INT32 digest[8]; /* Message digest */ + SHA_INT32 count_lo, count_hi; /* 64-bit bit count */ + SHA_BYTE data[SHA_BLOCKSIZE]; /* SHA data buffer */ + int Endianness; + int local; /* unprocessed amount in data */ + int digestsize; +} SHAobject; + +/* When run on a little-endian CPU we need to perform byte reversal on an + array of longwords. */ + +static void longReverse(SHA_INT32 *buffer, int byteCount, int Endianness) +{ + SHA_INT32 value; + + if ( Endianness == PCT_BIG_ENDIAN ) + return; + + byteCount /= sizeof(*buffer); + while (byteCount--) { + value = *buffer; + value = ( ( value & 0xFF00FF00L ) >> 8 ) | \ + ( ( value & 0x00FF00FFL ) << 8 ); + *buffer++ = ( value << 16 ) | ( value >> 16 ); + } +} + +static void SHAcopy(SHAobject *src, SHAobject *dest) +{ + dest->Endianness = src->Endianness; + dest->local = src->local; + dest->digestsize = src->digestsize; + dest->count_lo = src->count_lo; + dest->count_hi = src->count_hi; + memcpy(dest->digest, src->digest, sizeof(src->digest)); + memcpy(dest->data, src->data, sizeof(src->data)); +} + + +/* ------------------------------------------------------------------------ + * + * This code for the SHA-256 algorithm was noted as public domain. The + * original headers are pasted below. + * + * Several changes have been made to make it more compatible with the + * Python environment and desired interface. + * + */ + +/* LibTomCrypt, modular cryptographic library -- Tom St Denis + * + * LibTomCrypt is a library that provides various cryptographic + * algorithms in a highly modular and flexible manner. + * + * The library is free for all purposes without any express + * gurantee it works. + * + * Tom St Denis, tomstdenis@iahu.ca, http://libtomcrypt.org + */ + + +/* SHA256 by Tom St Denis */ + +/* Various logical functions */ +#define ROR(x, y)\ +( ((((unsigned long)(x)&0xFFFFFFFFUL)>>(unsigned long)((y)&31)) | \ +((unsigned long)(x)<<(unsigned long)(32-((y)&31)))) & 0xFFFFFFFFUL) +#define Ch(x,y,z) (z ^ (x & (y ^ z))) +#define Maj(x,y,z) (((x | y) & z) | (x & y)) +#define S(x, n) ROR((x),(n)) +#define R(x, n) (((x)&0xFFFFFFFFUL)>>(n)) +#define Sigma0(x) (S(x, 2) ^ S(x, 13) ^ S(x, 22)) +#define Sigma1(x) (S(x, 6) ^ S(x, 11) ^ S(x, 25)) +#define Gamma0(x) (S(x, 7) ^ S(x, 18) ^ R(x, 3)) +#define Gamma1(x) (S(x, 17) ^ S(x, 19) ^ R(x, 10)) + + +static void +sha_transform(SHAobject *sha_info) +{ + int i; + SHA_INT32 S[8], W[64], t0, t1; + + memcpy(W, sha_info->data, sizeof(sha_info->data)); + longReverse(W, (int)sizeof(sha_info->data), sha_info->Endianness); + + for (i = 16; i < 64; ++i) { + W[i] = Gamma1(W[i - 2]) + W[i - 7] + Gamma0(W[i - 15]) + W[i - 16]; + } + for (i = 0; i < 8; ++i) { + S[i] = sha_info->digest[i]; + } + + /* Compress */ +#define RND(a,b,c,d,e,f,g,h,i,ki) \ + t0 = h + Sigma1(e) + Ch(e, f, g) + ki + W[i]; \ + t1 = Sigma0(a) + Maj(a, b, c); \ + d += t0; \ + h = t0 + t1; + + RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],0,0x428a2f98); + RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],1,0x71374491); + RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],2,0xb5c0fbcf); + RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],3,0xe9b5dba5); + RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],4,0x3956c25b); + RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],5,0x59f111f1); + RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],6,0x923f82a4); + RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],7,0xab1c5ed5); + RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],8,0xd807aa98); + RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],9,0x12835b01); + RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],10,0x243185be); + RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],11,0x550c7dc3); + RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],12,0x72be5d74); + RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],13,0x80deb1fe); + RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],14,0x9bdc06a7); + RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],15,0xc19bf174); + RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],16,0xe49b69c1); + RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],17,0xefbe4786); + RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],18,0x0fc19dc6); + RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],19,0x240ca1cc); + RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],20,0x2de92c6f); + RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],21,0x4a7484aa); + RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],22,0x5cb0a9dc); + RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],23,0x76f988da); + RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],24,0x983e5152); + RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],25,0xa831c66d); + RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],26,0xb00327c8); + RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],27,0xbf597fc7); + RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],28,0xc6e00bf3); + RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],29,0xd5a79147); + RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],30,0x06ca6351); + RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],31,0x14292967); + RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],32,0x27b70a85); + RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],33,0x2e1b2138); + RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],34,0x4d2c6dfc); + RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],35,0x53380d13); + RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],36,0x650a7354); + RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],37,0x766a0abb); + RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],38,0x81c2c92e); + RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],39,0x92722c85); + RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],40,0xa2bfe8a1); + RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],41,0xa81a664b); + RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],42,0xc24b8b70); + RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],43,0xc76c51a3); + RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],44,0xd192e819); + RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],45,0xd6990624); + RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],46,0xf40e3585); + RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],47,0x106aa070); + RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],48,0x19a4c116); + RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],49,0x1e376c08); + RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],50,0x2748774c); + RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],51,0x34b0bcb5); + RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],52,0x391c0cb3); + RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],53,0x4ed8aa4a); + RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],54,0x5b9cca4f); + RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],55,0x682e6ff3); + RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],56,0x748f82ee); + RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],57,0x78a5636f); + RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],58,0x84c87814); + RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],59,0x8cc70208); + RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],60,0x90befffa); + RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],61,0xa4506ceb); + RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],62,0xbef9a3f7); + RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],63,0xc67178f2); + +#undef RND + + /* feedback */ + for (i = 0; i < 8; i++) { + sha_info->digest[i] = sha_info->digest[i] + S[i]; + } + +} + + + +/* initialize the SHA digest */ + +static void +sha_init(SHAobject *sha_info) +{ + TestEndianness(sha_info->Endianness) + sha_info->digest[0] = 0x6A09E667L; + sha_info->digest[1] = 0xBB67AE85L; + sha_info->digest[2] = 0x3C6EF372L; + sha_info->digest[3] = 0xA54FF53AL; + sha_info->digest[4] = 0x510E527FL; + sha_info->digest[5] = 0x9B05688CL; + sha_info->digest[6] = 0x1F83D9ABL; + sha_info->digest[7] = 0x5BE0CD19L; + sha_info->count_lo = 0L; + sha_info->count_hi = 0L; + sha_info->local = 0; + sha_info->digestsize = 32; +} + +static void +sha224_init(SHAobject *sha_info) +{ + TestEndianness(sha_info->Endianness) + sha_info->digest[0] = 0xc1059ed8L; + sha_info->digest[1] = 0x367cd507L; + sha_info->digest[2] = 0x3070dd17L; + sha_info->digest[3] = 0xf70e5939L; + sha_info->digest[4] = 0xffc00b31L; + sha_info->digest[5] = 0x68581511L; + sha_info->digest[6] = 0x64f98fa7L; + sha_info->digest[7] = 0xbefa4fa4L; + sha_info->count_lo = 0L; + sha_info->count_hi = 0L; + sha_info->local = 0; + sha_info->digestsize = 28; +} + + +/* update the SHA digest */ + +static void +sha_update(SHAobject *sha_info, SHA_BYTE *buffer, int count) +{ + int i; + SHA_INT32 clo; + + clo = sha_info->count_lo + ((SHA_INT32) count << 3); + if (clo < sha_info->count_lo) { + ++sha_info->count_hi; + } + sha_info->count_lo = clo; + sha_info->count_hi += (SHA_INT32) count >> 29; + if (sha_info->local) { + i = SHA_BLOCKSIZE - sha_info->local; + if (i > count) { + i = count; + } + memcpy(((SHA_BYTE *) sha_info->data) + sha_info->local, buffer, i); + count -= i; + buffer += i; + sha_info->local += i; + if (sha_info->local == SHA_BLOCKSIZE) { + sha_transform(sha_info); + } + else { + return; + } + } + while (count >= SHA_BLOCKSIZE) { + memcpy(sha_info->data, buffer, SHA_BLOCKSIZE); + buffer += SHA_BLOCKSIZE; + count -= SHA_BLOCKSIZE; + sha_transform(sha_info); + } + memcpy(sha_info->data, buffer, count); + sha_info->local = count; +} + +/* finish computing the SHA digest */ + +static void +sha_final(unsigned char digest[SHA_DIGESTSIZE], SHAobject *sha_info) +{ + int count; + SHA_INT32 lo_bit_count, hi_bit_count; + + lo_bit_count = sha_info->count_lo; + hi_bit_count = sha_info->count_hi; + count = (int) ((lo_bit_count >> 3) & 0x3f); + ((SHA_BYTE *) sha_info->data)[count++] = 0x80; + if (count > SHA_BLOCKSIZE - 8) { + memset(((SHA_BYTE *) sha_info->data) + count, 0, + SHA_BLOCKSIZE - count); + sha_transform(sha_info); + memset((SHA_BYTE *) sha_info->data, 0, SHA_BLOCKSIZE - 8); + } + else { + memset(((SHA_BYTE *) sha_info->data) + count, 0, + SHA_BLOCKSIZE - 8 - count); + } + + /* GJS: note that we add the hi/lo in big-endian. sha_transform will + swap these values into host-order. */ + sha_info->data[56] = (hi_bit_count >> 24) & 0xff; + sha_info->data[57] = (hi_bit_count >> 16) & 0xff; + sha_info->data[58] = (hi_bit_count >> 8) & 0xff; + sha_info->data[59] = (hi_bit_count >> 0) & 0xff; + sha_info->data[60] = (lo_bit_count >> 24) & 0xff; + sha_info->data[61] = (lo_bit_count >> 16) & 0xff; + sha_info->data[62] = (lo_bit_count >> 8) & 0xff; + sha_info->data[63] = (lo_bit_count >> 0) & 0xff; + sha_transform(sha_info); + digest[ 0] = (unsigned char) ((sha_info->digest[0] >> 24) & 0xff); + digest[ 1] = (unsigned char) ((sha_info->digest[0] >> 16) & 0xff); + digest[ 2] = (unsigned char) ((sha_info->digest[0] >> 8) & 0xff); + digest[ 3] = (unsigned char) ((sha_info->digest[0] ) & 0xff); + digest[ 4] = (unsigned char) ((sha_info->digest[1] >> 24) & 0xff); + digest[ 5] = (unsigned char) ((sha_info->digest[1] >> 16) & 0xff); + digest[ 6] = (unsigned char) ((sha_info->digest[1] >> 8) & 0xff); + digest[ 7] = (unsigned char) ((sha_info->digest[1] ) & 0xff); + digest[ 8] = (unsigned char) ((sha_info->digest[2] >> 24) & 0xff); + digest[ 9] = (unsigned char) ((sha_info->digest[2] >> 16) & 0xff); + digest[10] = (unsigned char) ((sha_info->digest[2] >> 8) & 0xff); + digest[11] = (unsigned char) ((sha_info->digest[2] ) & 0xff); + digest[12] = (unsigned char) ((sha_info->digest[3] >> 24) & 0xff); + digest[13] = (unsigned char) ((sha_info->digest[3] >> 16) & 0xff); + digest[14] = (unsigned char) ((sha_info->digest[3] >> 8) & 0xff); + digest[15] = (unsigned char) ((sha_info->digest[3] ) & 0xff); + digest[16] = (unsigned char) ((sha_info->digest[4] >> 24) & 0xff); + digest[17] = (unsigned char) ((sha_info->digest[4] >> 16) & 0xff); + digest[18] = (unsigned char) ((sha_info->digest[4] >> 8) & 0xff); + digest[19] = (unsigned char) ((sha_info->digest[4] ) & 0xff); + digest[20] = (unsigned char) ((sha_info->digest[5] >> 24) & 0xff); + digest[21] = (unsigned char) ((sha_info->digest[5] >> 16) & 0xff); + digest[22] = (unsigned char) ((sha_info->digest[5] >> 8) & 0xff); + digest[23] = (unsigned char) ((sha_info->digest[5] ) & 0xff); + digest[24] = (unsigned char) ((sha_info->digest[6] >> 24) & 0xff); + digest[25] = (unsigned char) ((sha_info->digest[6] >> 16) & 0xff); + digest[26] = (unsigned char) ((sha_info->digest[6] >> 8) & 0xff); + digest[27] = (unsigned char) ((sha_info->digest[6] ) & 0xff); + digest[28] = (unsigned char) ((sha_info->digest[7] >> 24) & 0xff); + digest[29] = (unsigned char) ((sha_info->digest[7] >> 16) & 0xff); + digest[30] = (unsigned char) ((sha_info->digest[7] >> 8) & 0xff); + digest[31] = (unsigned char) ((sha_info->digest[7] ) & 0xff); +} + +/* + * End of copied SHA code. + * + * ------------------------------------------------------------------------ + */ + +static PyTypeObject SHA224type; +static PyTypeObject SHA256type; + + +static SHAobject * +newSHA224object(void) +{ + return (SHAobject *)PyObject_New(SHAobject, &SHA224type); +} + +static SHAobject * +newSHA256object(void) +{ + return (SHAobject *)PyObject_New(SHAobject, &SHA256type); +} + +/* Internal methods for a hash object */ + +static void +SHA_dealloc(PyObject *ptr) +{ + PyObject_Del(ptr); +} + + +/* External methods for a hash object */ + +PyDoc_STRVAR(SHA256_copy__doc__, "Return a copy of the hash object."); + +static PyObject * +SHA256_copy(SHAobject *self, PyObject *unused) +{ + SHAobject *newobj; + + if (((PyObject*)self)->ob_type == &SHA256type) { + if ( (newobj = newSHA256object())==NULL) + return NULL; + } else { + if ( (newobj = newSHA224object())==NULL) + return NULL; + } + + SHAcopy(self, newobj); + return (PyObject *)newobj; +} + +PyDoc_STRVAR(SHA256_digest__doc__, +"Return the digest value as a string of binary data."); + +static PyObject * +SHA256_digest(SHAobject *self, PyObject *unused) +{ + unsigned char digest[SHA_DIGESTSIZE]; + SHAobject temp; + + SHAcopy(self, &temp); + sha_final(digest, &temp); + return PyString_FromStringAndSize((const char *)digest, self->digestsize); +} + +PyDoc_STRVAR(SHA256_hexdigest__doc__, +"Return the digest value as a string of hexadecimal digits."); + +static PyObject * +SHA256_hexdigest(SHAobject *self, PyObject *unused) +{ + unsigned char digest[SHA_DIGESTSIZE]; + SHAobject temp; + PyObject *retval; + char *hex_digest; + int i, j; + + /* Get the raw (binary) digest value */ + SHAcopy(self, &temp); + sha_final(digest, &temp); + + /* Create a new string */ + retval = PyString_FromStringAndSize(NULL, self->digestsize * 2); + if (!retval) + return NULL; + hex_digest = PyString_AsString(retval); + if (!hex_digest) { + Py_DECREF(retval); + return NULL; + } + + /* Make hex version of the digest */ + for(i=j=0; i<self->digestsize; i++) { + char c; + c = (digest[i] >> 4) & 0xf; + c = (c>9) ? c+'a'-10 : c + '0'; + hex_digest[j++] = c; + c = (digest[i] & 0xf); + c = (c>9) ? c+'a'-10 : c + '0'; + hex_digest[j++] = c; + } + return retval; +} + +PyDoc_STRVAR(SHA256_update__doc__, +"Update this hash object's state with the provided string."); + +static PyObject * +SHA256_update(SHAobject *self, PyObject *args) +{ + unsigned char *cp; + int len; + + if (!PyArg_ParseTuple(args, "s#:update", &cp, &len)) + return NULL; + + sha_update(self, cp, len); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyMethodDef SHA_methods[] = { + {"copy", (PyCFunction)SHA256_copy, METH_NOARGS, SHA256_copy__doc__}, + {"digest", (PyCFunction)SHA256_digest, METH_NOARGS, SHA256_digest__doc__}, + {"hexdigest", (PyCFunction)SHA256_hexdigest, METH_NOARGS, SHA256_hexdigest__doc__}, + {"update", (PyCFunction)SHA256_update, METH_VARARGS, SHA256_update__doc__}, + {NULL, NULL} /* sentinel */ +}; + +static PyObject * +SHA256_get_block_size(PyObject *self, void *closure) +{ + return PyInt_FromLong(SHA_BLOCKSIZE); +} + +static PyObject * +SHA256_get_name(PyObject *self, void *closure) +{ + if (((SHAobject *)self)->digestsize == 32) + return PyString_FromStringAndSize("SHA256", 6); + else + return PyString_FromStringAndSize("SHA224", 6); +} + +static PyGetSetDef SHA_getseters[] = { + {"block_size", + (getter)SHA256_get_block_size, NULL, + NULL, + NULL}, + {"name", + (getter)SHA256_get_name, NULL, + NULL, + NULL}, + {NULL} /* Sentinel */ +}; + +static PyMemberDef SHA_members[] = { + {"digest_size", T_INT, offsetof(SHAobject, digestsize), READONLY, NULL}, + /* the old md5 and sha modules support 'digest_size' as in PEP 247. + * the old sha module also supported 'digestsize'. ugh. */ + {"digestsize", T_INT, offsetof(SHAobject, digestsize), READONLY, NULL}, + {NULL} /* Sentinel */ +}; + +static PyTypeObject SHA224type = { + PyObject_HEAD_INIT(NULL) + 0, /*ob_size*/ + "_sha256.sha224", /*tp_name*/ + sizeof(SHAobject), /*tp_size*/ + 0, /*tp_itemsize*/ + /* methods */ + SHA_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + SHA_methods, /* tp_methods */ + SHA_members, /* tp_members */ + SHA_getseters, /* tp_getset */ +}; + +static PyTypeObject SHA256type = { + PyObject_HEAD_INIT(NULL) + 0, /*ob_size*/ + "_sha256.sha256", /*tp_name*/ + sizeof(SHAobject), /*tp_size*/ + 0, /*tp_itemsize*/ + /* methods */ + SHA_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + SHA_methods, /* tp_methods */ + SHA_members, /* tp_members */ + SHA_getseters, /* tp_getset */ +}; + + +/* The single module-level function: new() */ + +PyDoc_STRVAR(SHA256_new__doc__, +"Return a new SHA-256 hash object; optionally initialized with a string."); + +static PyObject * +SHA256_new(PyObject *self, PyObject *args, PyObject *kwdict) +{ + static char *kwlist[] = {"string", NULL}; + SHAobject *new; + unsigned char *cp = NULL; + int len; + + if (!PyArg_ParseTupleAndKeywords(args, kwdict, "|s#:new", kwlist, + &cp, &len)) { + return NULL; + } + + if ((new = newSHA256object()) == NULL) + return NULL; + + sha_init(new); + + if (PyErr_Occurred()) { + Py_DECREF(new); + return NULL; + } + if (cp) + sha_update(new, cp, len); + + return (PyObject *)new; +} + +PyDoc_STRVAR(SHA224_new__doc__, +"Return a new SHA-224 hash object; optionally initialized with a string."); + +static PyObject * +SHA224_new(PyObject *self, PyObject *args, PyObject *kwdict) +{ + static char *kwlist[] = {"string", NULL}; + SHAobject *new; + unsigned char *cp = NULL; + int len; + + if (!PyArg_ParseTupleAndKeywords(args, kwdict, "|s#:new", kwlist, + &cp, &len)) { + return NULL; + } + + if ((new = newSHA224object()) == NULL) + return NULL; + + sha224_init(new); + + if (PyErr_Occurred()) { + Py_DECREF(new); + return NULL; + } + if (cp) + sha_update(new, cp, len); + + return (PyObject *)new; +} + + +/* List of functions exported by this module */ + +static struct PyMethodDef SHA_functions[] = { + {"sha256", (PyCFunction)SHA256_new, METH_VARARGS|METH_KEYWORDS, SHA256_new__doc__}, + {"sha224", (PyCFunction)SHA224_new, METH_VARARGS|METH_KEYWORDS, SHA224_new__doc__}, + {NULL, NULL} /* Sentinel */ +}; + + +/* Initialize this module. */ + +#define insint(n,v) { PyModule_AddIntConstant(m,n,v); } + +PyMODINIT_FUNC +init_sha256(void) +{ + PyObject *m; + + SHA224type.ob_type = &PyType_Type; + if (PyType_Ready(&SHA224type) < 0) + return; + SHA256type.ob_type = &PyType_Type; + if (PyType_Ready(&SHA256type) < 0) + return; + m = Py_InitModule("_sha256", SHA_functions); + if (m == NULL) + return; +} diff --git a/src/Modules/sha512module.c b/src/Modules/sha512module.c new file mode 100644 index 0000000..dbaec17 --- /dev/null +++ b/src/Modules/sha512module.c @@ -0,0 +1,769 @@ +/* SHA512 module */ + +/* This module provides an interface to NIST's SHA-512 and SHA-384 Algorithms */ + +/* See below for information about the original code this module was + based upon. Additional work performed by: + + Andrew Kuchling (amk@amk.ca) + Greg Stein (gstein@lyra.org) + Trevor Perrin (trevp@trevp.net) + + Copyright (C) 2005 Gregory P. Smith (greg@krypto.org) + Licensed to PSF under a Contributor Agreement. + +*/ + +/* SHA objects */ + +#include "Python.h" +#include "structmember.h" + +#ifdef PY_LONG_LONG /* If no PY_LONG_LONG, don't compile anything! */ + +/* Endianness testing and definitions */ +#define TestEndianness(variable) {int i=1; variable=PCT_BIG_ENDIAN;\ + if (*((char*)&i)==1) variable=PCT_LITTLE_ENDIAN;} + +#define PCT_LITTLE_ENDIAN 1 +#define PCT_BIG_ENDIAN 0 + +/* Some useful types */ + +typedef unsigned char SHA_BYTE; + +#if SIZEOF_INT == 4 +typedef unsigned int SHA_INT32; /* 32-bit integer */ +typedef unsigned PY_LONG_LONG SHA_INT64; /* 64-bit integer */ +#else +/* not defined. compilation will die. */ +#endif + +/* The SHA block size and message digest sizes, in bytes */ + +#define SHA_BLOCKSIZE 128 +#define SHA_DIGESTSIZE 64 + +/* The structure for storing SHA info */ + +typedef struct { + PyObject_HEAD + SHA_INT64 digest[8]; /* Message digest */ + SHA_INT32 count_lo, count_hi; /* 64-bit bit count */ + SHA_BYTE data[SHA_BLOCKSIZE]; /* SHA data buffer */ + int Endianness; + int local; /* unprocessed amount in data */ + int digestsize; +} SHAobject; + +/* When run on a little-endian CPU we need to perform byte reversal on an + array of longwords. */ + +static void longReverse(SHA_INT64 *buffer, int byteCount, int Endianness) +{ + SHA_INT64 value; + + if ( Endianness == PCT_BIG_ENDIAN ) + return; + + byteCount /= sizeof(*buffer); + while (byteCount--) { + value = *buffer; + + ((unsigned char*)buffer)[0] = (unsigned char)(value >> 56) & 0xff; + ((unsigned char*)buffer)[1] = (unsigned char)(value >> 48) & 0xff; + ((unsigned char*)buffer)[2] = (unsigned char)(value >> 40) & 0xff; + ((unsigned char*)buffer)[3] = (unsigned char)(value >> 32) & 0xff; + ((unsigned char*)buffer)[4] = (unsigned char)(value >> 24) & 0xff; + ((unsigned char*)buffer)[5] = (unsigned char)(value >> 16) & 0xff; + ((unsigned char*)buffer)[6] = (unsigned char)(value >> 8) & 0xff; + ((unsigned char*)buffer)[7] = (unsigned char)(value ) & 0xff; + + buffer++; + } +} + +static void SHAcopy(SHAobject *src, SHAobject *dest) +{ + dest->Endianness = src->Endianness; + dest->local = src->local; + dest->digestsize = src->digestsize; + dest->count_lo = src->count_lo; + dest->count_hi = src->count_hi; + memcpy(dest->digest, src->digest, sizeof(src->digest)); + memcpy(dest->data, src->data, sizeof(src->data)); +} + + +/* ------------------------------------------------------------------------ + * + * This code for the SHA-512 algorithm was noted as public domain. The + * original headers are pasted below. + * + * Several changes have been made to make it more compatible with the + * Python environment and desired interface. + * + */ + +/* LibTomCrypt, modular cryptographic library -- Tom St Denis + * + * LibTomCrypt is a library that provides various cryptographic + * algorithms in a highly modular and flexible manner. + * + * The library is free for all purposes without any express + * gurantee it works. + * + * Tom St Denis, tomstdenis@iahu.ca, http://libtomcrypt.org + */ + + +/* SHA512 by Tom St Denis */ + +/* Various logical functions */ +#define ROR64(x, y) \ + ( ((((x) & 0xFFFFFFFFFFFFFFFFULL)>>((unsigned PY_LONG_LONG)(y) & 63)) | \ + ((x)<<((unsigned PY_LONG_LONG)(64-((y) & 63))))) & 0xFFFFFFFFFFFFFFFFULL) +#define Ch(x,y,z) (z ^ (x & (y ^ z))) +#define Maj(x,y,z) (((x | y) & z) | (x & y)) +#define S(x, n) ROR64((x),(n)) +#define R(x, n) (((x) & 0xFFFFFFFFFFFFFFFFULL) >> ((unsigned PY_LONG_LONG)n)) +#define Sigma0(x) (S(x, 28) ^ S(x, 34) ^ S(x, 39)) +#define Sigma1(x) (S(x, 14) ^ S(x, 18) ^ S(x, 41)) +#define Gamma0(x) (S(x, 1) ^ S(x, 8) ^ R(x, 7)) +#define Gamma1(x) (S(x, 19) ^ S(x, 61) ^ R(x, 6)) + + +static void +sha512_transform(SHAobject *sha_info) +{ + int i; + SHA_INT64 S[8], W[80], t0, t1; + + memcpy(W, sha_info->data, sizeof(sha_info->data)); + longReverse(W, (int)sizeof(sha_info->data), sha_info->Endianness); + + for (i = 16; i < 80; ++i) { + W[i] = Gamma1(W[i - 2]) + W[i - 7] + Gamma0(W[i - 15]) + W[i - 16]; + } + for (i = 0; i < 8; ++i) { + S[i] = sha_info->digest[i]; + } + + /* Compress */ +#define RND(a,b,c,d,e,f,g,h,i,ki) \ + t0 = h + Sigma1(e) + Ch(e, f, g) + ki + W[i]; \ + t1 = Sigma0(a) + Maj(a, b, c); \ + d += t0; \ + h = t0 + t1; + + RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],0,0x428a2f98d728ae22ULL); + RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],1,0x7137449123ef65cdULL); + RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],2,0xb5c0fbcfec4d3b2fULL); + RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],3,0xe9b5dba58189dbbcULL); + RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],4,0x3956c25bf348b538ULL); + RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],5,0x59f111f1b605d019ULL); + RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],6,0x923f82a4af194f9bULL); + RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],7,0xab1c5ed5da6d8118ULL); + RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],8,0xd807aa98a3030242ULL); + RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],9,0x12835b0145706fbeULL); + RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],10,0x243185be4ee4b28cULL); + RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],11,0x550c7dc3d5ffb4e2ULL); + RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],12,0x72be5d74f27b896fULL); + RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],13,0x80deb1fe3b1696b1ULL); + RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],14,0x9bdc06a725c71235ULL); + RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],15,0xc19bf174cf692694ULL); + RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],16,0xe49b69c19ef14ad2ULL); + RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],17,0xefbe4786384f25e3ULL); + RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],18,0x0fc19dc68b8cd5b5ULL); + RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],19,0x240ca1cc77ac9c65ULL); + RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],20,0x2de92c6f592b0275ULL); + RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],21,0x4a7484aa6ea6e483ULL); + RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],22,0x5cb0a9dcbd41fbd4ULL); + RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],23,0x76f988da831153b5ULL); + RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],24,0x983e5152ee66dfabULL); + RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],25,0xa831c66d2db43210ULL); + RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],26,0xb00327c898fb213fULL); + RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],27,0xbf597fc7beef0ee4ULL); + RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],28,0xc6e00bf33da88fc2ULL); + RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],29,0xd5a79147930aa725ULL); + RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],30,0x06ca6351e003826fULL); + RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],31,0x142929670a0e6e70ULL); + RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],32,0x27b70a8546d22ffcULL); + RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],33,0x2e1b21385c26c926ULL); + RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],34,0x4d2c6dfc5ac42aedULL); + RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],35,0x53380d139d95b3dfULL); + RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],36,0x650a73548baf63deULL); + RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],37,0x766a0abb3c77b2a8ULL); + RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],38,0x81c2c92e47edaee6ULL); + RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],39,0x92722c851482353bULL); + RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],40,0xa2bfe8a14cf10364ULL); + RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],41,0xa81a664bbc423001ULL); + RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],42,0xc24b8b70d0f89791ULL); + RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],43,0xc76c51a30654be30ULL); + RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],44,0xd192e819d6ef5218ULL); + RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],45,0xd69906245565a910ULL); + RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],46,0xf40e35855771202aULL); + RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],47,0x106aa07032bbd1b8ULL); + RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],48,0x19a4c116b8d2d0c8ULL); + RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],49,0x1e376c085141ab53ULL); + RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],50,0x2748774cdf8eeb99ULL); + RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],51,0x34b0bcb5e19b48a8ULL); + RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],52,0x391c0cb3c5c95a63ULL); + RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],53,0x4ed8aa4ae3418acbULL); + RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],54,0x5b9cca4f7763e373ULL); + RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],55,0x682e6ff3d6b2b8a3ULL); + RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],56,0x748f82ee5defb2fcULL); + RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],57,0x78a5636f43172f60ULL); + RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],58,0x84c87814a1f0ab72ULL); + RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],59,0x8cc702081a6439ecULL); + RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],60,0x90befffa23631e28ULL); + RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],61,0xa4506cebde82bde9ULL); + RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],62,0xbef9a3f7b2c67915ULL); + RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],63,0xc67178f2e372532bULL); + RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],64,0xca273eceea26619cULL); + RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],65,0xd186b8c721c0c207ULL); + RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],66,0xeada7dd6cde0eb1eULL); + RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],67,0xf57d4f7fee6ed178ULL); + RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],68,0x06f067aa72176fbaULL); + RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],69,0x0a637dc5a2c898a6ULL); + RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],70,0x113f9804bef90daeULL); + RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],71,0x1b710b35131c471bULL); + RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],72,0x28db77f523047d84ULL); + RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],73,0x32caab7b40c72493ULL); + RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],74,0x3c9ebe0a15c9bebcULL); + RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],75,0x431d67c49c100d4cULL); + RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],76,0x4cc5d4becb3e42b6ULL); + RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],77,0x597f299cfc657e2aULL); + RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],78,0x5fcb6fab3ad6faecULL); + RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],79,0x6c44198c4a475817ULL); + +#undef RND + + /* feedback */ + for (i = 0; i < 8; i++) { + sha_info->digest[i] = sha_info->digest[i] + S[i]; + } + +} + + + +/* initialize the SHA digest */ + +static void +sha512_init(SHAobject *sha_info) +{ + TestEndianness(sha_info->Endianness) + sha_info->digest[0] = 0x6a09e667f3bcc908ULL; + sha_info->digest[1] = 0xbb67ae8584caa73bULL; + sha_info->digest[2] = 0x3c6ef372fe94f82bULL; + sha_info->digest[3] = 0xa54ff53a5f1d36f1ULL; + sha_info->digest[4] = 0x510e527fade682d1ULL; + sha_info->digest[5] = 0x9b05688c2b3e6c1fULL; + sha_info->digest[6] = 0x1f83d9abfb41bd6bULL; + sha_info->digest[7] = 0x5be0cd19137e2179ULL; + sha_info->count_lo = 0L; + sha_info->count_hi = 0L; + sha_info->local = 0; + sha_info->digestsize = 64; +} + +static void +sha384_init(SHAobject *sha_info) +{ + TestEndianness(sha_info->Endianness) + sha_info->digest[0] = 0xcbbb9d5dc1059ed8ULL; + sha_info->digest[1] = 0x629a292a367cd507ULL; + sha_info->digest[2] = 0x9159015a3070dd17ULL; + sha_info->digest[3] = 0x152fecd8f70e5939ULL; + sha_info->digest[4] = 0x67332667ffc00b31ULL; + sha_info->digest[5] = 0x8eb44a8768581511ULL; + sha_info->digest[6] = 0xdb0c2e0d64f98fa7ULL; + sha_info->digest[7] = 0x47b5481dbefa4fa4ULL; + sha_info->count_lo = 0L; + sha_info->count_hi = 0L; + sha_info->local = 0; + sha_info->digestsize = 48; +} + + +/* update the SHA digest */ + +static void +sha512_update(SHAobject *sha_info, SHA_BYTE *buffer, int count) +{ + int i; + SHA_INT32 clo; + + clo = sha_info->count_lo + ((SHA_INT32) count << 3); + if (clo < sha_info->count_lo) { + ++sha_info->count_hi; + } + sha_info->count_lo = clo; + sha_info->count_hi += (SHA_INT32) count >> 29; + if (sha_info->local) { + i = SHA_BLOCKSIZE - sha_info->local; + if (i > count) { + i = count; + } + memcpy(((SHA_BYTE *) sha_info->data) + sha_info->local, buffer, i); + count -= i; + buffer += i; + sha_info->local += i; + if (sha_info->local == SHA_BLOCKSIZE) { + sha512_transform(sha_info); + } + else { + return; + } + } + while (count >= SHA_BLOCKSIZE) { + memcpy(sha_info->data, buffer, SHA_BLOCKSIZE); + buffer += SHA_BLOCKSIZE; + count -= SHA_BLOCKSIZE; + sha512_transform(sha_info); + } + memcpy(sha_info->data, buffer, count); + sha_info->local = count; +} + +/* finish computing the SHA digest */ + +static void +sha512_final(unsigned char digest[SHA_DIGESTSIZE], SHAobject *sha_info) +{ + int count; + SHA_INT32 lo_bit_count, hi_bit_count; + + lo_bit_count = sha_info->count_lo; + hi_bit_count = sha_info->count_hi; + count = (int) ((lo_bit_count >> 3) & 0x7f); + ((SHA_BYTE *) sha_info->data)[count++] = 0x80; + if (count > SHA_BLOCKSIZE - 16) { + memset(((SHA_BYTE *) sha_info->data) + count, 0, + SHA_BLOCKSIZE - count); + sha512_transform(sha_info); + memset((SHA_BYTE *) sha_info->data, 0, SHA_BLOCKSIZE - 16); + } + else { + memset(((SHA_BYTE *) sha_info->data) + count, 0, + SHA_BLOCKSIZE - 16 - count); + } + + /* GJS: note that we add the hi/lo in big-endian. sha512_transform will + swap these values into host-order. */ + sha_info->data[112] = 0; + sha_info->data[113] = 0; + sha_info->data[114] = 0; + sha_info->data[115] = 0; + sha_info->data[116] = 0; + sha_info->data[117] = 0; + sha_info->data[118] = 0; + sha_info->data[119] = 0; + sha_info->data[120] = (hi_bit_count >> 24) & 0xff; + sha_info->data[121] = (hi_bit_count >> 16) & 0xff; + sha_info->data[122] = (hi_bit_count >> 8) & 0xff; + sha_info->data[123] = (hi_bit_count >> 0) & 0xff; + sha_info->data[124] = (lo_bit_count >> 24) & 0xff; + sha_info->data[125] = (lo_bit_count >> 16) & 0xff; + sha_info->data[126] = (lo_bit_count >> 8) & 0xff; + sha_info->data[127] = (lo_bit_count >> 0) & 0xff; + sha512_transform(sha_info); + digest[ 0] = (unsigned char) ((sha_info->digest[0] >> 56) & 0xff); + digest[ 1] = (unsigned char) ((sha_info->digest[0] >> 48) & 0xff); + digest[ 2] = (unsigned char) ((sha_info->digest[0] >> 40) & 0xff); + digest[ 3] = (unsigned char) ((sha_info->digest[0] >> 32) & 0xff); + digest[ 4] = (unsigned char) ((sha_info->digest[0] >> 24) & 0xff); + digest[ 5] = (unsigned char) ((sha_info->digest[0] >> 16) & 0xff); + digest[ 6] = (unsigned char) ((sha_info->digest[0] >> 8) & 0xff); + digest[ 7] = (unsigned char) ((sha_info->digest[0] ) & 0xff); + digest[ 8] = (unsigned char) ((sha_info->digest[1] >> 56) & 0xff); + digest[ 9] = (unsigned char) ((sha_info->digest[1] >> 48) & 0xff); + digest[10] = (unsigned char) ((sha_info->digest[1] >> 40) & 0xff); + digest[11] = (unsigned char) ((sha_info->digest[1] >> 32) & 0xff); + digest[12] = (unsigned char) ((sha_info->digest[1] >> 24) & 0xff); + digest[13] = (unsigned char) ((sha_info->digest[1] >> 16) & 0xff); + digest[14] = (unsigned char) ((sha_info->digest[1] >> 8) & 0xff); + digest[15] = (unsigned char) ((sha_info->digest[1] ) & 0xff); + digest[16] = (unsigned char) ((sha_info->digest[2] >> 56) & 0xff); + digest[17] = (unsigned char) ((sha_info->digest[2] >> 48) & 0xff); + digest[18] = (unsigned char) ((sha_info->digest[2] >> 40) & 0xff); + digest[19] = (unsigned char) ((sha_info->digest[2] >> 32) & 0xff); + digest[20] = (unsigned char) ((sha_info->digest[2] >> 24) & 0xff); + digest[21] = (unsigned char) ((sha_info->digest[2] >> 16) & 0xff); + digest[22] = (unsigned char) ((sha_info->digest[2] >> 8) & 0xff); + digest[23] = (unsigned char) ((sha_info->digest[2] ) & 0xff); + digest[24] = (unsigned char) ((sha_info->digest[3] >> 56) & 0xff); + digest[25] = (unsigned char) ((sha_info->digest[3] >> 48) & 0xff); + digest[26] = (unsigned char) ((sha_info->digest[3] >> 40) & 0xff); + digest[27] = (unsigned char) ((sha_info->digest[3] >> 32) & 0xff); + digest[28] = (unsigned char) ((sha_info->digest[3] >> 24) & 0xff); + digest[29] = (unsigned char) ((sha_info->digest[3] >> 16) & 0xff); + digest[30] = (unsigned char) ((sha_info->digest[3] >> 8) & 0xff); + digest[31] = (unsigned char) ((sha_info->digest[3] ) & 0xff); + digest[32] = (unsigned char) ((sha_info->digest[4] >> 56) & 0xff); + digest[33] = (unsigned char) ((sha_info->digest[4] >> 48) & 0xff); + digest[34] = (unsigned char) ((sha_info->digest[4] >> 40) & 0xff); + digest[35] = (unsigned char) ((sha_info->digest[4] >> 32) & 0xff); + digest[36] = (unsigned char) ((sha_info->digest[4] >> 24) & 0xff); + digest[37] = (unsigned char) ((sha_info->digest[4] >> 16) & 0xff); + digest[38] = (unsigned char) ((sha_info->digest[4] >> 8) & 0xff); + digest[39] = (unsigned char) ((sha_info->digest[4] ) & 0xff); + digest[40] = (unsigned char) ((sha_info->digest[5] >> 56) & 0xff); + digest[41] = (unsigned char) ((sha_info->digest[5] >> 48) & 0xff); + digest[42] = (unsigned char) ((sha_info->digest[5] >> 40) & 0xff); + digest[43] = (unsigned char) ((sha_info->digest[5] >> 32) & 0xff); + digest[44] = (unsigned char) ((sha_info->digest[5] >> 24) & 0xff); + digest[45] = (unsigned char) ((sha_info->digest[5] >> 16) & 0xff); + digest[46] = (unsigned char) ((sha_info->digest[5] >> 8) & 0xff); + digest[47] = (unsigned char) ((sha_info->digest[5] ) & 0xff); + digest[48] = (unsigned char) ((sha_info->digest[6] >> 56) & 0xff); + digest[49] = (unsigned char) ((sha_info->digest[6] >> 48) & 0xff); + digest[50] = (unsigned char) ((sha_info->digest[6] >> 40) & 0xff); + digest[51] = (unsigned char) ((sha_info->digest[6] >> 32) & 0xff); + digest[52] = (unsigned char) ((sha_info->digest[6] >> 24) & 0xff); + digest[53] = (unsigned char) ((sha_info->digest[6] >> 16) & 0xff); + digest[54] = (unsigned char) ((sha_info->digest[6] >> 8) & 0xff); + digest[55] = (unsigned char) ((sha_info->digest[6] ) & 0xff); + digest[56] = (unsigned char) ((sha_info->digest[7] >> 56) & 0xff); + digest[57] = (unsigned char) ((sha_info->digest[7] >> 48) & 0xff); + digest[58] = (unsigned char) ((sha_info->digest[7] >> 40) & 0xff); + digest[59] = (unsigned char) ((sha_info->digest[7] >> 32) & 0xff); + digest[60] = (unsigned char) ((sha_info->digest[7] >> 24) & 0xff); + digest[61] = (unsigned char) ((sha_info->digest[7] >> 16) & 0xff); + digest[62] = (unsigned char) ((sha_info->digest[7] >> 8) & 0xff); + digest[63] = (unsigned char) ((sha_info->digest[7] ) & 0xff); +} + +/* + * End of copied SHA code. + * + * ------------------------------------------------------------------------ + */ + +static PyTypeObject SHA384type; +static PyTypeObject SHA512type; + + +static SHAobject * +newSHA384object(void) +{ + return (SHAobject *)PyObject_New(SHAobject, &SHA384type); +} + +static SHAobject * +newSHA512object(void) +{ + return (SHAobject *)PyObject_New(SHAobject, &SHA512type); +} + +/* Internal methods for a hash object */ + +static void +SHA512_dealloc(PyObject *ptr) +{ + PyObject_Del(ptr); +} + + +/* External methods for a hash object */ + +PyDoc_STRVAR(SHA512_copy__doc__, "Return a copy of the hash object."); + +static PyObject * +SHA512_copy(SHAobject *self, PyObject *unused) +{ + SHAobject *newobj; + + if (((PyObject*)self)->ob_type == &SHA512type) { + if ( (newobj = newSHA512object())==NULL) + return NULL; + } else { + if ( (newobj = newSHA384object())==NULL) + return NULL; + } + + SHAcopy(self, newobj); + return (PyObject *)newobj; +} + +PyDoc_STRVAR(SHA512_digest__doc__, +"Return the digest value as a string of binary data."); + +static PyObject * +SHA512_digest(SHAobject *self, PyObject *unused) +{ + unsigned char digest[SHA_DIGESTSIZE]; + SHAobject temp; + + SHAcopy(self, &temp); + sha512_final(digest, &temp); + return PyString_FromStringAndSize((const char *)digest, self->digestsize); +} + +PyDoc_STRVAR(SHA512_hexdigest__doc__, +"Return the digest value as a string of hexadecimal digits."); + +static PyObject * +SHA512_hexdigest(SHAobject *self, PyObject *unused) +{ + unsigned char digest[SHA_DIGESTSIZE]; + SHAobject temp; + PyObject *retval; + char *hex_digest; + int i, j; + + /* Get the raw (binary) digest value */ + SHAcopy(self, &temp); + sha512_final(digest, &temp); + + /* Create a new string */ + retval = PyString_FromStringAndSize(NULL, self->digestsize * 2); + if (!retval) + return NULL; + hex_digest = PyString_AsString(retval); + if (!hex_digest) { + Py_DECREF(retval); + return NULL; + } + + /* Make hex version of the digest */ + for (i=j=0; i<self->digestsize; i++) { + char c; + c = (digest[i] >> 4) & 0xf; + c = (c>9) ? c+'a'-10 : c + '0'; + hex_digest[j++] = c; + c = (digest[i] & 0xf); + c = (c>9) ? c+'a'-10 : c + '0'; + hex_digest[j++] = c; + } + return retval; +} + +PyDoc_STRVAR(SHA512_update__doc__, +"Update this hash object's state with the provided string."); + +static PyObject * +SHA512_update(SHAobject *self, PyObject *args) +{ + unsigned char *cp; + int len; + + if (!PyArg_ParseTuple(args, "s#:update", &cp, &len)) + return NULL; + + sha512_update(self, cp, len); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyMethodDef SHA_methods[] = { + {"copy", (PyCFunction)SHA512_copy, METH_NOARGS, SHA512_copy__doc__}, + {"digest", (PyCFunction)SHA512_digest, METH_NOARGS, SHA512_digest__doc__}, + {"hexdigest", (PyCFunction)SHA512_hexdigest, METH_NOARGS, SHA512_hexdigest__doc__}, + {"update", (PyCFunction)SHA512_update, METH_VARARGS, SHA512_update__doc__}, + {NULL, NULL} /* sentinel */ +}; + +static PyObject * +SHA512_get_block_size(PyObject *self, void *closure) +{ + return PyInt_FromLong(SHA_BLOCKSIZE); +} + +static PyObject * +SHA512_get_name(PyObject *self, void *closure) +{ + if (((SHAobject *)self)->digestsize == 64) + return PyString_FromStringAndSize("SHA512", 6); + else + return PyString_FromStringAndSize("SHA384", 6); +} + +static PyGetSetDef SHA_getseters[] = { + {"block_size", + (getter)SHA512_get_block_size, NULL, + NULL, + NULL}, + {"name", + (getter)SHA512_get_name, NULL, + NULL, + NULL}, + {NULL} /* Sentinel */ +}; + +static PyMemberDef SHA_members[] = { + {"digest_size", T_INT, offsetof(SHAobject, digestsize), READONLY, NULL}, + /* the old md5 and sha modules support 'digest_size' as in PEP 247. + * the old sha module also supported 'digestsize'. ugh. */ + {"digestsize", T_INT, offsetof(SHAobject, digestsize), READONLY, NULL}, + {NULL} /* Sentinel */ +}; + +static PyTypeObject SHA384type = { + PyObject_HEAD_INIT(NULL) + 0, /*ob_size*/ + "_sha512.sha384", /*tp_name*/ + sizeof(SHAobject), /*tp_size*/ + 0, /*tp_itemsize*/ + /* methods */ + SHA512_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + SHA_methods, /* tp_methods */ + SHA_members, /* tp_members */ + SHA_getseters, /* tp_getset */ +}; + +static PyTypeObject SHA512type = { + PyObject_HEAD_INIT(NULL) + 0, /*ob_size*/ + "_sha512.sha512", /*tp_name*/ + sizeof(SHAobject), /*tp_size*/ + 0, /*tp_itemsize*/ + /* methods */ + SHA512_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + SHA_methods, /* tp_methods */ + SHA_members, /* tp_members */ + SHA_getseters, /* tp_getset */ +}; + + +/* The single module-level function: new() */ + +PyDoc_STRVAR(SHA512_new__doc__, +"Return a new SHA-512 hash object; optionally initialized with a string."); + +static PyObject * +SHA512_new(PyObject *self, PyObject *args, PyObject *kwdict) +{ + static char *kwlist[] = {"string", NULL}; + SHAobject *new; + unsigned char *cp = NULL; + int len; + + if (!PyArg_ParseTupleAndKeywords(args, kwdict, "|s#:new", kwlist, + &cp, &len)) { + return NULL; + } + + if ((new = newSHA512object()) == NULL) + return NULL; + + sha512_init(new); + + if (PyErr_Occurred()) { + Py_DECREF(new); + return NULL; + } + if (cp) + sha512_update(new, cp, len); + + return (PyObject *)new; +} + +PyDoc_STRVAR(SHA384_new__doc__, +"Return a new SHA-384 hash object; optionally initialized with a string."); + +static PyObject * +SHA384_new(PyObject *self, PyObject *args, PyObject *kwdict) +{ + static char *kwlist[] = {"string", NULL}; + SHAobject *new; + unsigned char *cp = NULL; + int len; + + if (!PyArg_ParseTupleAndKeywords(args, kwdict, "|s#:new", kwlist, + &cp, &len)) { + return NULL; + } + + if ((new = newSHA384object()) == NULL) + return NULL; + + sha384_init(new); + + if (PyErr_Occurred()) { + Py_DECREF(new); + return NULL; + } + if (cp) + sha512_update(new, cp, len); + + return (PyObject *)new; +} + + +/* List of functions exported by this module */ + +static struct PyMethodDef SHA_functions[] = { + {"sha512", (PyCFunction)SHA512_new, METH_VARARGS|METH_KEYWORDS, SHA512_new__doc__}, + {"sha384", (PyCFunction)SHA384_new, METH_VARARGS|METH_KEYWORDS, SHA384_new__doc__}, + {NULL, NULL} /* Sentinel */ +}; + + +/* Initialize this module. */ + +#define insint(n,v) { PyModule_AddIntConstant(m,n,v); } + +PyMODINIT_FUNC +init_sha512(void) +{ + PyObject *m; + + SHA384type.ob_type = &PyType_Type; + if (PyType_Ready(&SHA384type) < 0) + return; + SHA512type.ob_type = &PyType_Type; + if (PyType_Ready(&SHA512type) < 0) + return; + m = Py_InitModule("_sha512", SHA_functions); + if (m == NULL) + return; +} + +#endif diff --git a/src/Modules/shamodule.c b/src/Modules/shamodule.c new file mode 100644 index 0000000..171dfa1 --- /dev/null +++ b/src/Modules/shamodule.c @@ -0,0 +1,593 @@ +/* SHA module */ + +/* This module provides an interface to NIST's Secure Hash Algorithm */ + +/* See below for information about the original code this module was + based upon. Additional work performed by: + + Andrew Kuchling (amk@amk.ca) + Greg Stein (gstein@lyra.org) + + Copyright (C) 2005 Gregory P. Smith (greg@krypto.org) + Licensed to PSF under a Contributor Agreement. + +*/ + +/* SHA objects */ + +#include "Python.h" +#include "structmember.h" + + +/* Endianness testing and definitions */ +#define TestEndianness(variable) {int i=1; variable=PCT_BIG_ENDIAN;\ + if (*((char*)&i)==1) variable=PCT_LITTLE_ENDIAN;} + +#define PCT_LITTLE_ENDIAN 1 +#define PCT_BIG_ENDIAN 0 + +/* Some useful types */ + +typedef unsigned char SHA_BYTE; + +#if SIZEOF_INT == 4 +typedef unsigned int SHA_INT32; /* 32-bit integer */ +#else +/* not defined. compilation will die. */ +#endif + +/* The SHA block size and message digest sizes, in bytes */ + +#define SHA_BLOCKSIZE 64 +#define SHA_DIGESTSIZE 20 + +/* The structure for storing SHS info */ + +typedef struct { + PyObject_HEAD + SHA_INT32 digest[5]; /* Message digest */ + SHA_INT32 count_lo, count_hi; /* 64-bit bit count */ + SHA_BYTE data[SHA_BLOCKSIZE]; /* SHA data buffer */ + int Endianness; + int local; /* unprocessed amount in data */ +} SHAobject; + +/* When run on a little-endian CPU we need to perform byte reversal on an + array of longwords. */ + +static void longReverse(SHA_INT32 *buffer, int byteCount, int Endianness) +{ + SHA_INT32 value; + + if ( Endianness == PCT_BIG_ENDIAN ) + return; + + byteCount /= sizeof(*buffer); + while (byteCount--) { + value = *buffer; + value = ( ( value & 0xFF00FF00L ) >> 8 ) | \ + ( ( value & 0x00FF00FFL ) << 8 ); + *buffer++ = ( value << 16 ) | ( value >> 16 ); + } +} + +static void SHAcopy(SHAobject *src, SHAobject *dest) +{ + dest->Endianness = src->Endianness; + dest->local = src->local; + dest->count_lo = src->count_lo; + dest->count_hi = src->count_hi; + memcpy(dest->digest, src->digest, sizeof(src->digest)); + memcpy(dest->data, src->data, sizeof(src->data)); +} + + +/* ------------------------------------------------------------------------ + * + * This code for the SHA algorithm was noted as public domain. The original + * headers are pasted below. + * + * Several changes have been made to make it more compatible with the + * Python environment and desired interface. + * + */ + +/* NIST Secure Hash Algorithm */ +/* heavily modified by Uwe Hollerbach <uh@alumni.caltech edu> */ +/* from Peter C. Gutmann's implementation as found in */ +/* Applied Cryptography by Bruce Schneier */ +/* Further modifications to include the "UNRAVEL" stuff, below */ + +/* This code is in the public domain */ + +/* UNRAVEL should be fastest & biggest */ +/* UNROLL_LOOPS should be just as big, but slightly slower */ +/* both undefined should be smallest and slowest */ + +#define UNRAVEL +/* #define UNROLL_LOOPS */ + +/* The SHA f()-functions. The f1 and f3 functions can be optimized to + save one boolean operation each - thanks to Rich Schroeppel, + rcs@cs.arizona.edu for discovering this */ + +/*#define f1(x,y,z) ((x & y) | (~x & z)) // Rounds 0-19 */ +#define f1(x,y,z) (z ^ (x & (y ^ z))) /* Rounds 0-19 */ +#define f2(x,y,z) (x ^ y ^ z) /* Rounds 20-39 */ +/*#define f3(x,y,z) ((x & y) | (x & z) | (y & z)) // Rounds 40-59 */ +#define f3(x,y,z) ((x & y) | (z & (x | y))) /* Rounds 40-59 */ +#define f4(x,y,z) (x ^ y ^ z) /* Rounds 60-79 */ + +/* SHA constants */ + +#define CONST1 0x5a827999L /* Rounds 0-19 */ +#define CONST2 0x6ed9eba1L /* Rounds 20-39 */ +#define CONST3 0x8f1bbcdcL /* Rounds 40-59 */ +#define CONST4 0xca62c1d6L /* Rounds 60-79 */ + +/* 32-bit rotate */ + +#define R32(x,n) ((x << n) | (x >> (32 - n))) + +/* the generic case, for when the overall rotation is not unraveled */ + +#define FG(n) \ + T = R32(A,5) + f##n(B,C,D) + E + *WP++ + CONST##n; \ + E = D; D = C; C = R32(B,30); B = A; A = T + +/* specific cases, for when the overall rotation is unraveled */ + +#define FA(n) \ + T = R32(A,5) + f##n(B,C,D) + E + *WP++ + CONST##n; B = R32(B,30) + +#define FB(n) \ + E = R32(T,5) + f##n(A,B,C) + D + *WP++ + CONST##n; A = R32(A,30) + +#define FC(n) \ + D = R32(E,5) + f##n(T,A,B) + C + *WP++ + CONST##n; T = R32(T,30) + +#define FD(n) \ + C = R32(D,5) + f##n(E,T,A) + B + *WP++ + CONST##n; E = R32(E,30) + +#define FE(n) \ + B = R32(C,5) + f##n(D,E,T) + A + *WP++ + CONST##n; D = R32(D,30) + +#define FT(n) \ + A = R32(B,5) + f##n(C,D,E) + T + *WP++ + CONST##n; C = R32(C,30) + +/* do SHA transformation */ + +static void +sha_transform(SHAobject *sha_info) +{ + int i; + SHA_INT32 T, A, B, C, D, E, W[80], *WP; + + memcpy(W, sha_info->data, sizeof(sha_info->data)); + longReverse(W, (int)sizeof(sha_info->data), sha_info->Endianness); + + for (i = 16; i < 80; ++i) { + W[i] = W[i-3] ^ W[i-8] ^ W[i-14] ^ W[i-16]; + + /* extra rotation fix */ + W[i] = R32(W[i], 1); + } + A = sha_info->digest[0]; + B = sha_info->digest[1]; + C = sha_info->digest[2]; + D = sha_info->digest[3]; + E = sha_info->digest[4]; + WP = W; +#ifdef UNRAVEL + FA(1); FB(1); FC(1); FD(1); FE(1); FT(1); FA(1); FB(1); FC(1); FD(1); + FE(1); FT(1); FA(1); FB(1); FC(1); FD(1); FE(1); FT(1); FA(1); FB(1); + FC(2); FD(2); FE(2); FT(2); FA(2); FB(2); FC(2); FD(2); FE(2); FT(2); + FA(2); FB(2); FC(2); FD(2); FE(2); FT(2); FA(2); FB(2); FC(2); FD(2); + FE(3); FT(3); FA(3); FB(3); FC(3); FD(3); FE(3); FT(3); FA(3); FB(3); + FC(3); FD(3); FE(3); FT(3); FA(3); FB(3); FC(3); FD(3); FE(3); FT(3); + FA(4); FB(4); FC(4); FD(4); FE(4); FT(4); FA(4); FB(4); FC(4); FD(4); + FE(4); FT(4); FA(4); FB(4); FC(4); FD(4); FE(4); FT(4); FA(4); FB(4); + sha_info->digest[0] += E; + sha_info->digest[1] += T; + sha_info->digest[2] += A; + sha_info->digest[3] += B; + sha_info->digest[4] += C; +#else /* !UNRAVEL */ +#ifdef UNROLL_LOOPS + FG(1); FG(1); FG(1); FG(1); FG(1); FG(1); FG(1); FG(1); FG(1); FG(1); + FG(1); FG(1); FG(1); FG(1); FG(1); FG(1); FG(1); FG(1); FG(1); FG(1); + FG(2); FG(2); FG(2); FG(2); FG(2); FG(2); FG(2); FG(2); FG(2); FG(2); + FG(2); FG(2); FG(2); FG(2); FG(2); FG(2); FG(2); FG(2); FG(2); FG(2); + FG(3); FG(3); FG(3); FG(3); FG(3); FG(3); FG(3); FG(3); FG(3); FG(3); + FG(3); FG(3); FG(3); FG(3); FG(3); FG(3); FG(3); FG(3); FG(3); FG(3); + FG(4); FG(4); FG(4); FG(4); FG(4); FG(4); FG(4); FG(4); FG(4); FG(4); + FG(4); FG(4); FG(4); FG(4); FG(4); FG(4); FG(4); FG(4); FG(4); FG(4); +#else /* !UNROLL_LOOPS */ + for (i = 0; i < 20; ++i) { FG(1); } + for (i = 20; i < 40; ++i) { FG(2); } + for (i = 40; i < 60; ++i) { FG(3); } + for (i = 60; i < 80; ++i) { FG(4); } +#endif /* !UNROLL_LOOPS */ + sha_info->digest[0] += A; + sha_info->digest[1] += B; + sha_info->digest[2] += C; + sha_info->digest[3] += D; + sha_info->digest[4] += E; +#endif /* !UNRAVEL */ +} + +/* initialize the SHA digest */ + +static void +sha_init(SHAobject *sha_info) +{ + TestEndianness(sha_info->Endianness) + + sha_info->digest[0] = 0x67452301L; + sha_info->digest[1] = 0xefcdab89L; + sha_info->digest[2] = 0x98badcfeL; + sha_info->digest[3] = 0x10325476L; + sha_info->digest[4] = 0xc3d2e1f0L; + sha_info->count_lo = 0L; + sha_info->count_hi = 0L; + sha_info->local = 0; +} + +/* update the SHA digest */ + +static void +sha_update(SHAobject *sha_info, SHA_BYTE *buffer, int count) +{ + int i; + SHA_INT32 clo; + + clo = sha_info->count_lo + ((SHA_INT32) count << 3); + if (clo < sha_info->count_lo) { + ++sha_info->count_hi; + } + sha_info->count_lo = clo; + sha_info->count_hi += (SHA_INT32) count >> 29; + if (sha_info->local) { + i = SHA_BLOCKSIZE - sha_info->local; + if (i > count) { + i = count; + } + memcpy(((SHA_BYTE *) sha_info->data) + sha_info->local, buffer, i); + count -= i; + buffer += i; + sha_info->local += i; + if (sha_info->local == SHA_BLOCKSIZE) { + sha_transform(sha_info); + } + else { + return; + } + } + while (count >= SHA_BLOCKSIZE) { + memcpy(sha_info->data, buffer, SHA_BLOCKSIZE); + buffer += SHA_BLOCKSIZE; + count -= SHA_BLOCKSIZE; + sha_transform(sha_info); + } + memcpy(sha_info->data, buffer, count); + sha_info->local = count; +} + +/* finish computing the SHA digest */ + +static void +sha_final(unsigned char digest[20], SHAobject *sha_info) +{ + int count; + SHA_INT32 lo_bit_count, hi_bit_count; + + lo_bit_count = sha_info->count_lo; + hi_bit_count = sha_info->count_hi; + count = (int) ((lo_bit_count >> 3) & 0x3f); + ((SHA_BYTE *) sha_info->data)[count++] = 0x80; + if (count > SHA_BLOCKSIZE - 8) { + memset(((SHA_BYTE *) sha_info->data) + count, 0, + SHA_BLOCKSIZE - count); + sha_transform(sha_info); + memset((SHA_BYTE *) sha_info->data, 0, SHA_BLOCKSIZE - 8); + } + else { + memset(((SHA_BYTE *) sha_info->data) + count, 0, + SHA_BLOCKSIZE - 8 - count); + } + + /* GJS: note that we add the hi/lo in big-endian. sha_transform will + swap these values into host-order. */ + sha_info->data[56] = (hi_bit_count >> 24) & 0xff; + sha_info->data[57] = (hi_bit_count >> 16) & 0xff; + sha_info->data[58] = (hi_bit_count >> 8) & 0xff; + sha_info->data[59] = (hi_bit_count >> 0) & 0xff; + sha_info->data[60] = (lo_bit_count >> 24) & 0xff; + sha_info->data[61] = (lo_bit_count >> 16) & 0xff; + sha_info->data[62] = (lo_bit_count >> 8) & 0xff; + sha_info->data[63] = (lo_bit_count >> 0) & 0xff; + sha_transform(sha_info); + digest[ 0] = (unsigned char) ((sha_info->digest[0] >> 24) & 0xff); + digest[ 1] = (unsigned char) ((sha_info->digest[0] >> 16) & 0xff); + digest[ 2] = (unsigned char) ((sha_info->digest[0] >> 8) & 0xff); + digest[ 3] = (unsigned char) ((sha_info->digest[0] ) & 0xff); + digest[ 4] = (unsigned char) ((sha_info->digest[1] >> 24) & 0xff); + digest[ 5] = (unsigned char) ((sha_info->digest[1] >> 16) & 0xff); + digest[ 6] = (unsigned char) ((sha_info->digest[1] >> 8) & 0xff); + digest[ 7] = (unsigned char) ((sha_info->digest[1] ) & 0xff); + digest[ 8] = (unsigned char) ((sha_info->digest[2] >> 24) & 0xff); + digest[ 9] = (unsigned char) ((sha_info->digest[2] >> 16) & 0xff); + digest[10] = (unsigned char) ((sha_info->digest[2] >> 8) & 0xff); + digest[11] = (unsigned char) ((sha_info->digest[2] ) & 0xff); + digest[12] = (unsigned char) ((sha_info->digest[3] >> 24) & 0xff); + digest[13] = (unsigned char) ((sha_info->digest[3] >> 16) & 0xff); + digest[14] = (unsigned char) ((sha_info->digest[3] >> 8) & 0xff); + digest[15] = (unsigned char) ((sha_info->digest[3] ) & 0xff); + digest[16] = (unsigned char) ((sha_info->digest[4] >> 24) & 0xff); + digest[17] = (unsigned char) ((sha_info->digest[4] >> 16) & 0xff); + digest[18] = (unsigned char) ((sha_info->digest[4] >> 8) & 0xff); + digest[19] = (unsigned char) ((sha_info->digest[4] ) & 0xff); +} + +/* + * End of copied SHA code. + * + * ------------------------------------------------------------------------ + */ + +static PyTypeObject SHAtype; + + +static SHAobject * +newSHAobject(void) +{ + return (SHAobject *)PyObject_New(SHAobject, &SHAtype); +} + +/* Internal methods for a hashing object */ + +static void +SHA_dealloc(PyObject *ptr) +{ + PyObject_Del(ptr); +} + + +/* External methods for a hashing object */ + +PyDoc_STRVAR(SHA_copy__doc__, "Return a copy of the hashing object."); + +static PyObject * +SHA_copy(SHAobject *self, PyObject *unused) +{ + SHAobject *newobj; + + if ( (newobj = newSHAobject())==NULL) + return NULL; + + SHAcopy(self, newobj); + return (PyObject *)newobj; +} + +PyDoc_STRVAR(SHA_digest__doc__, +"Return the digest value as a string of binary data."); + +static PyObject * +SHA_digest(SHAobject *self, PyObject *unused) +{ + unsigned char digest[SHA_DIGESTSIZE]; + SHAobject temp; + + SHAcopy(self, &temp); + sha_final(digest, &temp); + return PyString_FromStringAndSize((const char *)digest, sizeof(digest)); +} + +PyDoc_STRVAR(SHA_hexdigest__doc__, +"Return the digest value as a string of hexadecimal digits."); + +static PyObject * +SHA_hexdigest(SHAobject *self, PyObject *unused) +{ + unsigned char digest[SHA_DIGESTSIZE]; + SHAobject temp; + PyObject *retval; + char *hex_digest; + int i, j; + + /* Get the raw (binary) digest value */ + SHAcopy(self, &temp); + sha_final(digest, &temp); + + /* Create a new string */ + retval = PyString_FromStringAndSize(NULL, sizeof(digest) * 2); + if (!retval) + return NULL; + hex_digest = PyString_AsString(retval); + if (!hex_digest) { + Py_DECREF(retval); + return NULL; + } + + /* Make hex version of the digest */ + for(i=j=0; i<sizeof(digest); i++) { + char c; + c = (digest[i] >> 4) & 0xf; + c = (c>9) ? c+'a'-10 : c + '0'; + hex_digest[j++] = c; + c = (digest[i] & 0xf); + c = (c>9) ? c+'a'-10 : c + '0'; + hex_digest[j++] = c; + } + return retval; +} + +PyDoc_STRVAR(SHA_update__doc__, +"Update this hashing object's state with the provided string."); + +static PyObject * +SHA_update(SHAobject *self, PyObject *args) +{ + unsigned char *cp; + int len; + + if (!PyArg_ParseTuple(args, "s#:update", &cp, &len)) + return NULL; + + sha_update(self, cp, len); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyMethodDef SHA_methods[] = { + {"copy", (PyCFunction)SHA_copy, METH_NOARGS, SHA_copy__doc__}, + {"digest", (PyCFunction)SHA_digest, METH_NOARGS, SHA_digest__doc__}, + {"hexdigest", (PyCFunction)SHA_hexdigest, METH_NOARGS, SHA_hexdigest__doc__}, + {"update", (PyCFunction)SHA_update, METH_VARARGS, SHA_update__doc__}, + {NULL, NULL} /* sentinel */ +}; + +static PyObject * +SHA_get_block_size(PyObject *self, void *closure) +{ + return PyInt_FromLong(SHA_BLOCKSIZE); +} + +static PyObject * +SHA_get_digest_size(PyObject *self, void *closure) +{ + return PyInt_FromLong(SHA_DIGESTSIZE); +} + +static PyObject * +SHA_get_name(PyObject *self, void *closure) +{ + return PyString_FromStringAndSize("SHA1", 4); +} + +static PyGetSetDef SHA_getseters[] = { + {"digest_size", + (getter)SHA_get_digest_size, NULL, + NULL, + NULL}, + {"block_size", + (getter)SHA_get_block_size, NULL, + NULL, + NULL}, + {"name", + (getter)SHA_get_name, NULL, + NULL, + NULL}, + /* the old md5 and sha modules support 'digest_size' as in PEP 247. + * the old sha module also supported 'digestsize'. ugh. */ + {"digestsize", + (getter)SHA_get_digest_size, NULL, + NULL, + NULL}, + {NULL} /* Sentinel */ +}; + +static PyTypeObject SHAtype = { + PyObject_HEAD_INIT(NULL) + 0, /*ob_size*/ + "_sha.sha", /*tp_name*/ + sizeof(SHAobject), /*tp_size*/ + 0, /*tp_itemsize*/ + /* methods */ + SHA_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + SHA_methods, /* tp_methods */ + 0, /* tp_members */ + SHA_getseters, /* tp_getset */ +}; + + +/* The single module-level function: new() */ + +PyDoc_STRVAR(SHA_new__doc__, +"Return a new SHA hashing object. An optional string argument\n\ +may be provided; if present, this string will be automatically\n\ +hashed."); + +static PyObject * +SHA_new(PyObject *self, PyObject *args, PyObject *kwdict) +{ + static char *kwlist[] = {"string", NULL}; + SHAobject *new; + unsigned char *cp = NULL; + int len; + + if (!PyArg_ParseTupleAndKeywords(args, kwdict, "|s#:new", kwlist, + &cp, &len)) { + return NULL; + } + + if ((new = newSHAobject()) == NULL) + return NULL; + + sha_init(new); + + if (PyErr_Occurred()) { + Py_DECREF(new); + return NULL; + } + if (cp) + sha_update(new, cp, len); + + return (PyObject *)new; +} + + +/* List of functions exported by this module */ + +static struct PyMethodDef SHA_functions[] = { + {"new", (PyCFunction)SHA_new, METH_VARARGS|METH_KEYWORDS, SHA_new__doc__}, + {NULL, NULL} /* Sentinel */ +}; + + +/* Initialize this module. */ + +#define insint(n,v) { PyModule_AddIntConstant(m,n,v); } + +PyMODINIT_FUNC +init_sha(void) +{ + PyObject *m; + + SHAtype.ob_type = &PyType_Type; + if (PyType_Ready(&SHAtype) < 0) + return; + m = Py_InitModule("_sha", SHA_functions); + if (m == NULL) + return; + + /* Add some symbolic constants to the module */ + insint("blocksize", 1); /* For future use, in case some hash + functions require an integral number of + blocks */ + insint("digestsize", 20); + insint("digest_size", 20); +} diff --git a/src/README.txt b/src/README.txt index d9af1b0..32f8a7e 100644 --- a/src/README.txt +++ b/src/README.txt @@ -2,7 +2,7 @@ Distutils2 ========== -Welcome to Distutils2 ! +Welcome to Distutils2! Distutils2 is the new version of Distutils. It's not backward compatible with Distutils but provides more features, and implement most new packaging @@ -10,6 +10,6 @@ standards. See the documentation at http://packages.python.org/Distutils2 for more info. -**Beware that Distutils2 is its in early stage and should not be used in +**Beware that Distutils2 is in its early stage and should not be used in production. Its API is subject to changes** diff --git a/src/check.sh b/src/check.sh new file mode 100755 index 0000000..bc77bf9 --- /dev/null +++ b/src/check.sh @@ -0,0 +1,2 @@ +pep8 distutils2 +pyflakes distutils2 diff --git a/src/distutils2/README b/src/distutils2/README index 0416160..4afd700 100644 --- a/src/distutils2/README +++ b/src/distutils2/README @@ -1,4 +1,4 @@ -This directory contains the Distutils package. +This directory contains the Distutils2 package. There's a full documentation available at: @@ -8,6 +8,6 @@ The Distutils-SIG web page is also a good starting point: http://www.python.org/sigs/distutils-sig/ -WARNING: Distutils2 must remain compatible with 2.4 +WARNING: Distutils2 must remain compatible with Python 2.4 $Id: README 70017 2009-02-27 12:53:34Z tarek.ziade $ diff --git a/src/distutils2/__init__.py b/src/distutils2/__init__.py index 3be2421..0514da7 100644 --- a/src/distutils2/__init__.py +++ b/src/distutils2/__init__.py @@ -1,16 +1,26 @@ """distutils -The main package for the Python Module Distribution Utilities. Normally -used from a setup script as +The main package for the Python Distribution Utilities 2. Setup +scripts should import the setup function from distutils2.core: - from distutils.core import setup + from distutils2.core import setup - setup (...) + setup(name=..., version=..., ...) + +Third-party tools can use parts of Distutils2 as building blocks +without causing the other modules to be imported: + + import distutils2.version + import distutils2.pypi.simple + import distutils2.tests.pypi_server """ -__all__ = ['__version__', 'setup'] +__all__ = ['__version__'] __revision__ = "$Id: __init__.py 78020 2010-02-06 16:37:32Z benjamin.peterson $" __version__ = "1.0a2" -from distutils2.core import setup +# when set to True, converts doctests by default too +run_2to3_on_doctests = True +# Standard package names for fixer packages +lib2to3_fixer_packages = ['lib2to3.fixes'] diff --git a/src/distutils2/_backport/hashlib.py b/src/distutils2/_backport/hashlib.py new file mode 100644 index 0000000..48f2cfd --- /dev/null +++ b/src/distutils2/_backport/hashlib.py @@ -0,0 +1,143 @@ +# $Id$ +# +# Copyright (C) 2005 Gregory P. Smith (greg@krypto.org) +# Licensed to PSF under a Contributor Agreement. +# + +__doc__ = """hashlib module - A common interface to many hash functions. + +new(name, string='') - returns a new hash object implementing the + given hash function; initializing the hash + using the given string data. + +Named constructor functions are also available, these are much faster +than using new(): + +md5(), sha1(), sha224(), sha256(), sha384(), and sha512() + +More algorithms may be available on your platform but the above are +guaranteed to exist. + +NOTE: If you want the adler32 or crc32 hash functions they are available in +the zlib module. + +Choose your hash function wisely. Some have known collision weaknesses. +sha384 and sha512 will be slow on 32 bit platforms. + +Hash objects have these methods: + - update(arg): Update the hash object with the string arg. Repeated calls + are equivalent to a single call with the concatenation of all + the arguments. + - digest(): Return the digest of the strings passed to the update() method + so far. This may contain non-ASCII characters, including + NUL bytes. + - hexdigest(): Like digest() except the digest is returned as a string of + double length, containing only hexadecimal digits. + - copy(): Return a copy (clone) of the hash object. This can be used to + efficiently compute the digests of strings that share a common + initial substring. + +For example, to obtain the digest of the string 'Nobody inspects the +spammish repetition': + + >>> import hashlib + >>> m = hashlib.md5() + >>> m.update("Nobody inspects") + >>> m.update(" the spammish repetition") + >>> m.digest() + '\\xbbd\\x9c\\x83\\xdd\\x1e\\xa5\\xc9\\xd9\\xde\\xc9\\xa1\\x8d\\xf0\\xff\\xe9' + +More condensed: + + >>> hashlib.sha224("Nobody inspects the spammish repetition").hexdigest() + 'a4337bc45a8fc544c03f52dc550cd6e1e87021bc896588bd79e901e2' + +""" + +# This tuple and __get_builtin_constructor() must be modified if a new +# always available algorithm is added. +__always_supported = ('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512') + +algorithms = __always_supported + +__all__ = __always_supported + ('new', 'algorithms') + + +def __get_builtin_constructor(name): + if name in ('SHA1', 'sha1'): + import _sha + return _sha.new + elif name in ('MD5', 'md5'): + import _md5 + return _md5.new + elif name in ('SHA256', 'sha256', 'SHA224', 'sha224'): + import _sha256 + bs = name[3:] + if bs == '256': + return _sha256.sha256 + elif bs == '224': + return _sha256.sha224 + elif name in ('SHA512', 'sha512', 'SHA384', 'sha384'): + import _sha512 + bs = name[3:] + if bs == '512': + return _sha512.sha512 + elif bs == '384': + return _sha512.sha384 + + raise ValueError('unsupported hash type %s' % name) + + +def __get_openssl_constructor(name): + try: + f = getattr(_hashlib, 'openssl_' + name) + # Allow the C module to raise ValueError. The function will be + # defined but the hash not actually available thanks to OpenSSL. + f() + # Use the C function directly (very fast) + return f + except (AttributeError, ValueError): + return __get_builtin_constructor(name) + + +def __py_new(name, string=''): + """new(name, string='') - Return a new hashing object using the named algorithm; + optionally initialized with a string. + """ + return __get_builtin_constructor(name)(string) + + +def __hash_new(name, string=''): + """new(name, string='') - Return a new hashing object using the named algorithm; + optionally initialized with a string. + """ + try: + return _hashlib.new(name, string) + except ValueError: + # If the _hashlib module (OpenSSL) doesn't support the named + # hash, try using our builtin implementations. + # This allows for SHA224/256 and SHA384/512 support even though + # the OpenSSL library prior to 0.9.8 doesn't provide them. + return __get_builtin_constructor(name)(string) + + +try: + import _hashlib + new = __hash_new + __get_hash = __get_openssl_constructor +except ImportError: + new = __py_new + __get_hash = __get_builtin_constructor + +for __func_name in __always_supported: + # try them all, some may not work due to the OpenSSL + # version not supporting that algorithm. + try: + globals()[__func_name] = __get_hash(__func_name) + except ValueError: + import logging + logging.exception('code for hash %s was not found.', __func_name) + +# Cleanup locals() +del __always_supported, __func_name, __get_hash +del __py_new, __hash_new, __get_openssl_constructor diff --git a/src/distutils2/_backport/pkgutil.py b/src/distutils2/_backport/pkgutil.py index 8fbe24d..b033082 100644 --- a/src/distutils2/_backport/pkgutil.py +++ b/src/distutils2/_backport/pkgutil.py @@ -12,10 +12,17 @@ from types import ModuleType from distutils2.errors import DistutilsError from distutils2.metadata import DistributionMetadata from distutils2.version import suggest_normalized_version, VersionPredicate +import zipimport +try: + import cStringIO as StringIO +except ImportError: + import StringIO +import re +import warnings __all__ = [ 'get_importer', 'iter_importers', 'get_loader', 'find_loader', - 'walk_packages', 'iter_modules', + 'walk_packages', 'iter_modules', 'get_data', 'ImpImporter', 'ImpLoader', 'read_code', 'extend_path', 'Distribution', 'EggInfoDistribution', 'distinfo_dirname', 'get_distributions', 'get_distribution', 'get_file_users', @@ -24,7 +31,7 @@ __all__ = [ def read_code(stream): - # This helper is needed in order for the PEP 302 emulation to + # This helper is needed in order for the :pep:`302` emulation to # correctly handle compiled files import marshal @@ -79,32 +86,34 @@ def simplegeneric(func): def walk_packages(path=None, prefix='', onerror=None): - """Yields (module_loader, name, ispkg) for all modules recursively - on path, or, if path is None, all accessible modules. + """Yields ``(module_loader, name, ispkg)`` for all modules recursively + on *path*, or, if *path* is ``None``, all accessible modules. - 'path' should be either None or a list of paths to look for - modules in. + :parameter path: should be either ``None`` or a list of paths to look for + modules in. + :parameter prefix: is a string to output on the front of every module name + on output. - 'prefix' is a string to output on the front of every module name - on output. - - Note that this function must import all *packages* (NOT all - modules!) on the given path, in order to access the __path__ + Note that this function must import all packages (NOT all + modules!) on the given path, in order to access the ``__path__`` attribute to find submodules. - 'onerror' is a function which gets called with one argument (the + *onerror* is a function which gets called with one argument (the name of the package which was being imported) if any exception occurs while trying to import a package. If no onerror function is - supplied, ImportErrors are caught and ignored, while all other + supplied, ``ImportErrors`` are caught and ignored, while all other exceptions are propagated, terminating the search. Examples: - # list all modules python can access - walk_packages() + * list all modules python can access:: + + walk_packages() + + * list all submodules of ctypes:: + + walk_packages(ctypes.__path__, ctypes.__name__+'.') - # list all submodules of ctypes - walk_packages(ctypes.__path__, ctypes.__name__+'.') """ def seen(p, m={}): @@ -137,14 +146,14 @@ def walk_packages(path=None, prefix='', onerror=None): def iter_modules(path=None, prefix=''): - """Yields (module_loader, name, ispkg) for all submodules on path, - or, if path is None, all top-level modules on sys.path. + """Yields ``(module_loader, name, ispkg)`` for all submodules on path, + or, if *path* is ``None``, all top-level modules on ``sys.path``. - 'path' should be either None or a list of paths to look for - modules in. + :parameter path: should be either None or a list of paths to look for + modules in. + :parameter prefix: is a string to output on the front of every module name + on output. - 'prefix' is a string to output on the front of every module name - on output. """ if path is None: @@ -162,6 +171,7 @@ def iter_modules(path=None, prefix=''): #@simplegeneric def iter_importer_modules(importer, prefix=''): + "" if not hasattr(importer, 'iter_modules'): return [] return importer.iter_modules(prefix) @@ -169,15 +179,16 @@ def iter_importer_modules(importer, prefix=''): iter_importer_modules = simplegeneric(iter_importer_modules) -class ImpImporter: - """PEP 302 Importer that wraps Python's "classic" import algorithm +class ImpImporter(object): + """:pep:`302` Importer that wraps Python's "classic" import algorithm - ImpImporter(dirname) produces a PEP 302 importer that searches that - directory. ImpImporter(None) produces a PEP 302 importer that searches - the current sys.path, plus any modules that are frozen or built-in. + ``ImpImporter(dirname)`` produces a :pep:`302` importer that searches that + directory. ``ImpImporter(None)`` produces a :pep:`302` importer that + searches the current ``sys.path``, plus any modules that are frozen + or built-in. - Note that ImpImporter does not currently support being used by placement - on sys.meta_path. + Note that :class:`ImpImporter` does not currently support being used by placement + on ``sys.meta_path``. """ def __init__(self, path=None): @@ -231,9 +242,9 @@ class ImpImporter: yield prefix + modname, ispkg -class ImpLoader: - """PEP 302 Loader that wraps Python's "classic" import algorithm - """ +class ImpLoader(object): + """:pep:`302` Loader that wraps Python's "classic" import algorithm """ + code = source = None def __init__(self, fullname, file, filename, etc): @@ -365,17 +376,17 @@ except ImportError: def get_importer(path_item): - """Retrieve a PEP 302 importer for the given path item + """Retrieve a :pep:`302` importer for the given path item - The returned importer is cached in sys.path_importer_cache + The returned importer is cached in ``sys.path_importer_cache`` if it was newly created by a path hook. If there is no importer, a wrapper around the basic import machinery is returned. This wrapper is never inserted into - the importer cache (None is inserted instead). + the importer cache (``None`` is inserted instead). The cache (or part of it) can be cleared manually if a - rescan of sys.path_hooks is necessary. + rescan of ``sys.path_hooks`` is necessary. """ try: importer = sys.path_importer_cache[path_item] @@ -399,7 +410,7 @@ def get_importer(path_item): def iter_importers(fullname=""): - """Yield PEP 302 importers for the given module name + """Yield :pep:`302` importers for the given module name If fullname contains a '.', the importers will be for the package containing fullname, otherwise they will be importers for sys.meta_path, @@ -407,20 +418,20 @@ def iter_importers(fullname=""): the named module is in a package, that package is imported as a side effect of invoking this function. - Non PEP 302 mechanisms (e.g. the Windows registry) used by the + Non :pep:`302` mechanisms (e.g. the Windows registry) used by the standard import machinery to find files in alternative locations - are partially supported, but are searched AFTER sys.path. Normally, - these locations are searched BEFORE sys.path, preventing sys.path + are partially supported, but are searched AFTER ``sys.path``. Normally, + these locations are searched BEFORE sys.path, preventing ``sys.path`` entries from shadowing them. For this to cause a visible difference in behaviour, there must be a module or package name that is accessible via both sys.path - and one of the non PEP 302 file system mechanisms. In this case, + and one of the non :pep:`302` file system mechanisms. In this case, the emulation will find the former version, while the builtin import mechanism will find the latter. Items of the following types can be affected by this discrepancy: - imp.C_EXTENSION, imp.PY_SOURCE, imp.PY_COMPILED, imp.PKG_DIRECTORY + ``imp.C_EXTENSION, imp.PY_SOURCE, imp.PY_COMPILED, imp.PKG_DIRECTORY`` """ if fullname.startswith('.'): raise ImportError("Relative module names not supported") @@ -441,15 +452,15 @@ def iter_importers(fullname=""): def get_loader(module_or_name): - """Get a PEP 302 "loader" object for module_or_name + """Get a :pep:`302` "loader" object for module_or_name If the module or package is accessible via the normal import mechanism, a wrapper around the relevant part of that machinery is returned. Returns None if the module cannot be found or imported. If the named module is not already imported, its containing package - (if any) is imported, in order to establish the package __path__. + (if any) is imported, in order to establish the package ``__path__``. - This function uses iter_importers(), and is thus subject to the same + This function uses :func:`iter_importers`, and is thus subject to the same limitations regarding platform-specific special import locations such as the Windows registry. """ @@ -467,12 +478,13 @@ def get_loader(module_or_name): def find_loader(fullname): - """Find a PEP 302 "loader" object for fullname + """Find a :pep:`302` "loader" object for fullname - If fullname contains dots, path must be the containing package's __path__. - Returns None if the module cannot be found or imported. This function uses - iter_importers(), and is thus subject to the same limitations regarding - platform-specific special import locations such as the Windows registry. + If fullname contains dots, path must be the containing package's + ``__path__``. Returns ``None`` if the module cannot be found or imported. + This function uses :func:`iter_importers`, and is thus subject to the same + limitations regarding platform-specific special import locations such as + the Windows registry. """ for importer in iter_importers(fullname): loader = importer.find_module(fullname) @@ -485,21 +497,22 @@ def find_loader(fullname): def extend_path(path, name): """Extend a package's path. - Intended use is to place the following code in a package's __init__.py: + Intended use is to place the following code in a package's + ``__init__.py``:: from pkgutil import extend_path __path__ = extend_path(__path__, __name__) - This will add to the package's __path__ all subdirectories of - directories on sys.path named after the package. This is useful + This will add to the package's ``__path__`` all subdirectories of + directories on ``sys.path`` named after the package. This is useful if one wants to distribute different parts of a single logical package as multiple directories. - It also looks for *.pkg files beginning where * matches the name - argument. This feature is similar to *.pth files (see site.py), - except that it doesn't special-case lines starting with 'import'. - A *.pkg file is trusted at face value: apart from checking for - duplicates, all entries found in a *.pkg file are added to the + It also looks for ``*.pkg`` files beginning where ``*`` matches the name + argument. This feature is similar to ``*.pth`` files (see ``site.py``), + except that it doesn't special-case lines starting with ``import``. + A ``*.pkg`` file is trusted at face value: apart from checking for + duplicates, all entries found in a ``*.pkg`` file are added to the path, regardless of whether they are exist the filesystem. (This is a feature.) @@ -512,7 +525,7 @@ def extend_path(path, name): are not (unicode or 8-bit) strings referring to existing directories are ignored. Unicode items of sys.path that cause errors when used as filenames may cause this function to raise an - exception (in line with os.path.isdir() behavior). + exception (in line with ``os.path.isdir()`` behavior). """ if not isinstance(path, list): @@ -560,23 +573,23 @@ def extend_path(path, name): def get_data(package, resource): """Get a resource from a package. - This is a wrapper round the PEP 302 loader get_data API. The package + This is a wrapper round the :pep:`302` loader get_data API. The package argument should be the name of a package, in standard module format - (foo.bar). The resource argument should be in the form of a relative - filename, using '/' as the path separator. The parent directory name '..' - is not allowed, and nor is a rooted name (starting with a '/'). + (``foo.bar``). The resource argument should be in the form of a relative + filename, using ``'/'`` as the path separator. The parent directory name + ``'..'`` is not allowed, and nor is a rooted name (starting with a ``'/'``). The function returns a binary string, which is the contents of the specified resource. For packages located in the filesystem, which have already been imported, - this is the rough equivalent of + this is the rough equivalent of:: d = os.path.dirname(sys.modules[package].__file__) data = open(os.path.join(d, resource), 'rb').read() - If the package cannot be located or loaded, or it uses a PEP 302 loader - which does not support get_data(), then None is returned. + If the package cannot be located or loaded, or it uses a :pep:`302` loader + which does not support :func:`get_data`, then ``None`` is returned. """ loader = get_loader(package) @@ -603,7 +616,7 @@ DIST_FILES = ('INSTALLER', 'METADATA', 'RECORD', 'REQUESTED',) class Distribution(object): """Created with the *path* of the ``.dist-info`` directory provided to the - constructor. It reads the metadata contained in METADATA when it is + constructor. It reads the metadata contained in ``METADATA`` when it is instantiated.""" # Attribute documenting for Sphinx style documentation, see for more info: @@ -612,9 +625,9 @@ class Distribution(object): """The name of the distribution.""" metadata = None """A :class:`distutils2.metadata.DistributionMetadata` instance loaded with - the distribution's METADATA file.""" + the distribution's ``METADATA`` file.""" requested = False - """A boolean that indicates whether the REQUESTED metadata file is present + """A boolean that indicates whether the ``REQUESTED`` metadata file is present (in other words, whether the package was installed by user request).""" def __init__(self, path): @@ -635,15 +648,17 @@ class Distribution(object): def get_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. + 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) """ @@ -651,7 +666,7 @@ class Distribution(object): def uses(self, path): """ - Returns ``True`` if path is listed in RECORD. *path* can be a local + Returns ``True`` if path is listed in ``RECORD``. *path* can be a local absolute path or a relative ``'/'``-separated path. :rtype: boolean @@ -673,8 +688,8 @@ class Distribution(object): directory path, a :class:`DistutilsError` 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). + binary mode (``rb``), otherwise opens it in read-only + mode (``r``). :rtype: file object """ open_flags = 'r' @@ -701,37 +716,129 @@ class Distribution(object): def get_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. + 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. + raw value from ``RECORD`` is returned. :type local: boolean :returns: iterator of paths """ for path, md5, 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(object): """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.""" + file ``PKG-INFO`` under that directory.""" name = '' """The name of the distribution.""" metadata = None """A :class:`distutils2.metadata.DistributionMetadata` instance loaded with - the distribution's METADATA file.""" + the distribution's ``METADATA`` file.""" + _REQUIREMENT = re.compile( \ + r'(?P<name>[-A-Za-z0-9_.]+)\s*' \ + r'(?P<first>(?:<|<=|!=|==|>=|>)[-A-Za-z0-9_.]+)?\s*' \ + r'(?P<rest>(?:\s*,\s*(?:<|<=|!=|==|>=|>)[-A-Za-z0-9_.]+)*)\s*' \ + r'(?P<extras>\[.*\])?') def __init__(self, path): - if os.path.isdir(path): - path = os.path.join(path, 'PKG-INFO') - self.metadata = DistributionMetadata(path=path) - self.name = self.metadata['name'] + self.path = path + + # reused from Distribute's pkg_resources + def yield_lines(strs): + """Yield non-empty/non-comment lines of a ``basestring`` or sequence""" + if isinstance(strs, basestring): + for s in strs.splitlines(): + s = s.strip() + if s and not s.startswith('#'): # skip blank lines/comments + 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 = DistributionMetadata(path=meta_path) + try: + req_path = os.path.join(path, 'EGG-INFO', 'requires.txt') + requires = open(req_path, 'r').read() + except IOError: + requires = None + else: + zipf = zipimport.zipimporter(path) + fileobj = StringIO.StringIO(zipf.get_data('EGG-INFO/PKG-INFO')) + self.metadata = DistributionMetadata(fileobj=fileobj) + try: + requires = zipf.get_data('EGG-INFO/requires.txt') + except IOError: + requires = None + self.name = self.metadata['name'] + elif path.endswith('.egg-info'): + if os.path.isdir(path): + path = os.path.join(path, 'PKG-INFO') + try: + req_f = open(os.path.join(path, 'requires.txt'), 'r') + requires = req_f.read() + except IOError: + requires = None + self.metadata = DistributionMetadata(path=path) + self.name = self.metadata['name'] + else: + raise ValueError('The path must end with .egg-info or .egg') + + provides = "%s (%s)" % (self.metadata['name'], + self.metadata['version']) + if self.metadata['Metadata-Version'] == '1.2': + self.metadata['Provides-Dist'] += (provides,) + else: + self.metadata['Provides'] += (provides,) + reqs = [] + if requires is not None: + for line in yield_lines(requires): + if line[0] == '[': + warnings.warn('distutils2 does not support extensions in requires.txt') + break + else: + match = self._REQUIREMENT.match(line.strip()) + if not match: + raise ValueError('Distribution %s has ill formed ' + 'requires.txt file (%s)' % + (self.name, line)) + else: + if match.group('extras'): + s = 'Distribution %s uses extra requirements which'\ + ' are not supported in distutils' % (self.name) + warnings.warn(s) + 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 self.metadata['Metadata-Version'] == '1.2': + self.metadata['Requires-Dist'] += reqs + else: + self.metadata['Requires'] += reqs def get_installed_files(self, local=False): return [] @@ -739,6 +846,13 @@ class EggInfoDistribution(object): 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 _normalize_dist_name(name): """Returns a normalized name from the given *name*. @@ -792,7 +906,8 @@ def get_distributions(use_egg_info=False): if dir.endswith('.dist-info'): dist = Distribution(os.path.join(realpath, dir)) yield dist - elif use_egg_info and dir.endswith('.egg-info'): + elif use_egg_info and (dir.endswith('.egg-info') or + dir.endswith('.egg')): dist = EggInfoDistribution(os.path.join(realpath, dir)) yield dist @@ -801,18 +916,18 @@ def get_distribution(name, use_egg_info=False): """ 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. + ``.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 founded, as no more than one + 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""" + :rtype: :class:`Distribution` or :class:`EggInfoDistribution` or None""" found = None for dist in get_distributions(): if dist.name == name: @@ -867,7 +982,7 @@ def provides_distribution(name, version=None, use_egg_info=False): 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 founded, since no more than + 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 @@ -887,7 +1002,7 @@ def provides_distribution(name, version=None, use_egg_info=False): provided = dist.metadata['Provides-Dist'] + dist.metadata['Provides'] for p in provided: - p_components = p.split(' ', 1) + p_components = p.rsplit(' ', 1) if len(p_components) == 1 or predicate is None: if name == p_components[0]: yield dist @@ -896,7 +1011,8 @@ def provides_distribution(name, version=None, use_egg_info=False): p_name, p_ver = p_components if len(p_ver) < 2 or p_ver[0] != '(' or p_ver[-1] != ')': raise DistutilsError(('Distribution %s has invalid ' + - 'provides field') % (dist.name,)) + 'provides field: %s') \ + % (dist.name, p)) p_ver = p_ver[1:-1] # trim off the parenthesis if p_name == name and predicate.match(p_ver): yield dist diff --git a/src/distutils2/_backport/sysconfig.cfg b/src/distutils2/_backport/sysconfig.cfg index 2a2a996..01328db 100644 --- a/src/distutils2/_backport/sysconfig.cfg +++ b/src/distutils2/_backport/sysconfig.cfg @@ -1,6 +1,6 @@ [globals] # These are the useful categories that are sometimes referenced at runtime, -# using pkgutils.open(): +# using pkgutil.open(): config = {confdir}/{distribution.name} # Configuration files appdata = {datadir}/{distribution.name} # Non-writable data that is independent of architecture (images, many xml/text files) appdata.arch = {libdir}/{distribution.name} # Non-writable data that is architecture-dependent (some binary data formats) diff --git a/src/distutils2/_backport/sysconfig.py b/src/distutils2/_backport/sysconfig.py index 5d082b7..a240fc4 100644 --- a/src/distutils2/_backport/sysconfig.py +++ b/src/distutils2/_backport/sysconfig.py @@ -5,7 +5,7 @@ import sys import os import re from os.path import pardir, abspath -from ConfigParser import ConfigParser +from ConfigParser import RawConfigParser _PREFIX = os.path.normpath(sys.prefix) _EXEC_PREFIX = os.path.normpath(sys.exec_prefix) @@ -14,7 +14,7 @@ _EXEC_PREFIX = os.path.normpath(sys.exec_prefix) # XXX _CONFIG_DIR will be set by the Makefile later _CONFIG_DIR = os.path.normpath(os.path.dirname(__file__)) _CONFIG_FILE = os.path.join(_CONFIG_DIR, 'sysconfig.cfg') -_SCHEMES = ConfigParser() +_SCHEMES = RawConfigParser() _SCHEMES.read(_CONFIG_FILE) _VAR_REPL = re.compile(r'\{([^{]*?)\}') @@ -580,10 +580,11 @@ def get_platform(): # behaviour. pass else: - m = re.search( - r'<key>ProductUserVisibleVersion</key>\s*' + - r'<string>(.*?)</string>', f.read()) - f.close() + try: + m = re.search(r'<key>ProductUserVisibleVersion</key>\s*' + r'<string>(.*?)</string>', f.read()) + finally: + f.close() if m is not None: macrelease = '.'.join(m.group(1).split('.')[:2]) # else: fall back to the default behaviour diff --git a/src/distutils2/_backport/tarfile.py b/src/distutils2/_backport/tarfile.py index 5399846..0d2c49b 100644 --- a/src/distutils2/_backport/tarfile.py +++ b/src/distutils2/_backport/tarfile.py @@ -370,7 +370,7 @@ class SubsequentHeaderError(HeaderError): #--------------------------- # internal stream interface #--------------------------- -class _LowLevelFile: +class _LowLevelFile(object): """Low-level file object. Supports reading and writing. It is used instead of a regular file object for streaming access. @@ -394,7 +394,7 @@ class _LowLevelFile: def write(self, s): os.write(self.fd, s) -class _Stream: +class _Stream(object): """Class that serves as an adapter between TarFile and a stream-like object. The stream-like object only needs to have a read() or write() method and is accessed @@ -2003,8 +2003,10 @@ class TarFile(object): # Append the tar header and data to the archive. if tarinfo.isreg(): f = bltn_open(name, "rb") - self.addfile(tarinfo, f) - f.close() + try: + self.addfile(tarinfo, f) + finally: + f.close() elif tarinfo.isdir(): self.addfile(tarinfo) @@ -2214,9 +2216,11 @@ class TarFile(object): """ source = self.extractfile(tarinfo) target = bltn_open(targetpath, "wb") - copyfileobj(source, target) - source.close() - target.close() + try: + copyfileobj(source, target) + finally: + source.close() + target.close() def makeunknown(self, tarinfo, targetpath): """Make a file from a TarInfo object with an unknown type @@ -2423,7 +2427,7 @@ class TarFile(object): print >> sys.stderr, msg # class TarFile -class TarIter: +class TarIter(object): """Iterator Class. for tarinfo in TarFile(...): @@ -2460,7 +2464,7 @@ class TarIter: return tarinfo # Helper classes for sparse file support -class _section: +class _section(object): """Base class for _data and _hole. """ def __init__(self, offset, size): @@ -2507,7 +2511,7 @@ class _ringbuffer(list): #--------------------------------------------- TAR_PLAIN = 0 # zipfile.ZIP_STORED TAR_GZIPPED = 8 # zipfile.ZIP_DEFLATED -class TarFileCompat: +class TarFileCompat(object): """TarFile class compatible with standard module zipfile's ZipFile class. """ @@ -2564,8 +2568,10 @@ def is_tarfile(name): are able to handle, else return False. """ try: - t = open(name) - t.close() + try: + t = open(name) + finally: + t.close() return True except TarError: return False diff --git a/src/distutils2/_backport/tests/fake_dists/bacon-0.1.egg-info/PKG-INFO b/src/distutils2/_backport/tests/fake_dists/bacon-0.1.egg-info/PKG-INFO index 8914d76..a176dfd 100644 --- a/src/distutils2/_backport/tests/fake_dists/bacon-0.1.egg-info/PKG-INFO +++ b/src/distutils2/_backport/tests/fake_dists/bacon-0.1.egg-info/PKG-INFO @@ -2,4 +2,5 @@ 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/src/distutils2/_backport/tests/fake_dists/banana-0.4.egg/EGG-INFO/PKG-INFO b/src/distutils2/_backport/tests/fake_dists/banana-0.4.egg/EGG-INFO/PKG-INFO new file mode 100644 index 0000000..a7e118a --- /dev/null +++ b/src/distutils2/_backport/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/src/distutils2/_backport/tests/fake_dists/banana-0.4.egg/EGG-INFO/SOURCES.txt b/src/distutils2/_backport/tests/fake_dists/banana-0.4.egg/EGG-INFO/SOURCES.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/distutils2/_backport/tests/fake_dists/banana-0.4.egg/EGG-INFO/SOURCES.txt diff --git a/src/distutils2/_backport/tests/fake_dists/banana-0.4.egg/EGG-INFO/dependency_links.txt b/src/distutils2/_backport/tests/fake_dists/banana-0.4.egg/EGG-INFO/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/distutils2/_backport/tests/fake_dists/banana-0.4.egg/EGG-INFO/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/distutils2/_backport/tests/fake_dists/banana-0.4.egg/EGG-INFO/entry_points.txt b/src/distutils2/_backport/tests/fake_dists/banana-0.4.egg/EGG-INFO/entry_points.txt new file mode 100644 index 0000000..5d3e5f6 --- /dev/null +++ b/src/distutils2/_backport/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/src/distutils2/_backport/tests/fake_dists/banana-0.4.egg/EGG-INFO/not-zip-safe b/src/distutils2/_backport/tests/fake_dists/banana-0.4.egg/EGG-INFO/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/distutils2/_backport/tests/fake_dists/banana-0.4.egg/EGG-INFO/not-zip-safe @@ -0,0 +1 @@ + diff --git a/src/distutils2/_backport/tests/fake_dists/banana-0.4.egg/EGG-INFO/requires.txt b/src/distutils2/_backport/tests/fake_dists/banana-0.4.egg/EGG-INFO/requires.txt new file mode 100644 index 0000000..4354305 --- /dev/null +++ b/src/distutils2/_backport/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/src/distutils2/_backport/tests/fake_dists/banana-0.4.egg/EGG-INFO/top_level.txt b/src/distutils2/_backport/tests/fake_dists/banana-0.4.egg/EGG-INFO/top_level.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/distutils2/_backport/tests/fake_dists/banana-0.4.egg/EGG-INFO/top_level.txt diff --git a/src/distutils2/_backport/tests/fake_dists/strawberry-0.6.egg b/src/distutils2/_backport/tests/fake_dists/strawberry-0.6.egg Binary files differnew file mode 100644 index 0000000..6d160e8 --- /dev/null +++ b/src/distutils2/_backport/tests/fake_dists/strawberry-0.6.egg diff --git a/src/distutils2/_backport/tests/fake_dists/towel_stuff-0.1.dist-info/METADATA b/src/distutils2/_backport/tests/fake_dists/towel_stuff-0.1.dist-info/METADATA index e8b764e..ca46d0a 100644 --- a/src/distutils2/_backport/tests/fake_dists/towel_stuff-0.1.dist-info/METADATA +++ b/src/distutils2/_backport/tests/fake_dists/towel_stuff-0.1.dist-info/METADATA @@ -2,4 +2,6 @@ Metadata-Version: 1.2 Name: towel-stuff Version: 0.1 Provides-Dist: truffles (1.1.2) +Provides-Dist: towel-stuff (0.1) Obsoletes-Dist: truffles (!=0.8,<1.0) +Requires-Dist: bacon (<=0.2) diff --git a/src/distutils2/_backport/tests/test_pkgutil.py b/src/distutils2/_backport/tests/test_pkgutil.py index 817b156..fafb9a8 100644 --- a/src/distutils2/_backport/tests/test_pkgutil.py +++ b/src/distutils2/_backport/tests/test_pkgutil.py @@ -3,40 +3,176 @@ import sys import os import csv +import imp +import tempfile +import shutil +import zipfile try: from hashlib import md5 except ImportError: from md5 import md5 from test.test_support import run_unittest, TESTFN - -import distutils2._backport.pkgutil from distutils2.tests.support import unittest +from distutils2._backport import pkgutil + +try: + from os.path import relpath +except ImportError: + try: + from unittest.compatibility import relpath + except ImportError: + from unittest2.compatibility import relpath + # TODO Add a test for getting a distribution that is provided by another -# distribution. +# distribution. # TODO Add a test for absolute pathed RECORD items (e.g. /etc/myapp/config.ini) +# Adapted from Python 2.7's trunk +class TestPkgUtilData(unittest.TestCase): + + def setUp(self): + super(TestPkgUtilData, self).setUp() + self.dirname = tempfile.mkdtemp() + sys.path.insert(0, self.dirname) + + def tearDown(self): + super(TestPkgUtilData, self).tearDown() + del sys.path[0] + shutil.rmtree(self.dirname) + + def test_getdata_filesys(self): + pkg = 'test_getdata_filesys' + + # Include a LF and a CRLF, to test that binary data is read back + RESOURCE_DATA = 'Hello, world!\nSecond line\r\nThird line' + + # Make a package with some resources + package_dir = os.path.join(self.dirname, pkg) + os.mkdir(package_dir) + # Empty init.py + f = open(os.path.join(package_dir, '__init__.py'), "wb") + try: + pass + finally: + f.close() + # Resource files, res.txt, sub/res.txt + f = open(os.path.join(package_dir, 'res.txt'), "wb") + try: + f.write(RESOURCE_DATA) + finally: + f.close() + os.mkdir(os.path.join(package_dir, 'sub')) + f = open(os.path.join(package_dir, 'sub', 'res.txt'), "wb") + try: + f.write(RESOURCE_DATA) + finally: + f.close() + + # Check we can read the resources + res1 = pkgutil.get_data(pkg, 'res.txt') + self.assertEqual(res1, RESOURCE_DATA) + res2 = pkgutil.get_data(pkg, 'sub/res.txt') + self.assertEqual(res2, RESOURCE_DATA) + + del sys.modules[pkg] + + def test_getdata_zipfile(self): + zip = 'test_getdata_zipfile.zip' + pkg = 'test_getdata_zipfile' + + # Include a LF and a CRLF, to test that binary data is read back + RESOURCE_DATA = 'Hello, world!\nSecond line\r\nThird line' + + # Make a package with some resources + zip_file = os.path.join(self.dirname, zip) + z = zipfile.ZipFile(zip_file, 'w') + try: + # Empty init.py + z.writestr(pkg + '/__init__.py', "") + # Resource files, res.txt, sub/res.txt + z.writestr(pkg + '/res.txt', RESOURCE_DATA) + z.writestr(pkg + '/sub/res.txt', RESOURCE_DATA) + finally: + z.close() + + # Check we can read the resources + sys.path.insert(0, zip_file) + res1 = pkgutil.get_data(pkg, 'res.txt') + self.assertEqual(res1, RESOURCE_DATA) + res2 = pkgutil.get_data(pkg, 'sub/res.txt') + self.assertEqual(res2, RESOURCE_DATA) + del sys.path[0] + + del sys.modules[pkg] + +# Adapted from Python 2.7's trunk +class TestPkgUtilPEP302(unittest.TestCase): + + class MyTestLoader(object): + def load_module(self, fullname): + # Create an empty module + mod = sys.modules.setdefault(fullname, imp.new_module(fullname)) + mod.__file__ = "<%s>" % self.__class__.__name__ + mod.__loader__ = self + # Make it a package + mod.__path__ = [] + # Count how many times the module is reloaded + mod.__dict__['loads'] = mod.__dict__.get('loads',0) + 1 + return mod + + def get_data(self, path): + return "Hello, world!" + + class MyTestImporter(object): + def find_module(self, fullname, path=None): + return TestPkgUtilPEP302.MyTestLoader() + + def setUp(self): + super(TestPkgUtilPEP302, self).setUp() + sys.meta_path.insert(0, self.MyTestImporter()) + + def tearDown(self): + del sys.meta_path[0] + super(TestPkgUtilPEP302, self).tearDown() + + def test_getdata_pep302(self): + # Use a dummy importer/loader + self.assertEqual(pkgutil.get_data('foo', 'dummy'), "Hello, world!") + del sys.modules['foo'] + + def test_alreadyloaded(self): + # Ensure that get_data works without reloading - the "loads" module + # variable in the example loader should count how many times a reload + # occurs. + import foo + self.assertEqual(foo.loads, 1) + self.assertEqual(pkgutil.get_data('foo', 'dummy'), "Hello, world!") + self.assertEqual(foo.loads, 1) + del sys.modules['foo'] + + class TestPkgUtilDistribution(unittest.TestCase): - """Tests the pkgutil.Distribution class""" + # Tests the pkgutil.Distribution class def setUp(self): super(TestPkgUtilDistribution, self).setUp() - self.fake_dists_path = os.path.abspath( os.path.join(os.path.dirname(__file__), 'fake_dists')) - self.distinfo_dirs = [ os.path.join(self.fake_dists_path, dir) + + self.distinfo_dirs = [os.path.join(self.fake_dists_path, dir) for dir in os.listdir(self.fake_dists_path) - if dir.endswith('.dist-info') - ] + if dir.endswith('.dist-info')] def get_hexdigest(file): md5_hash = md5() md5_hash.update(open(file).read()) return md5_hash.hexdigest() + def record_pieces(file): - path = os.path.relpath(file, sys.prefix) + path = relpath(file, sys.prefix) digest = get_hexdigest(file) size = os.path.getsize(file) return [path, digest, size] @@ -51,16 +187,18 @@ class TestPkgUtilDistribution(unittest.TestCase): for path, dirs, files in os.walk(dist_location): for f in files: - record_writer.writerow(record_pieces(os.path.join(path, f))) + 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([os.path.relpath(record_file, sys.prefix)]) + record_writer.writerow([relpath(record_file, sys.prefix)]) del record_writer # causes the RECORD file to close record_reader = csv.reader(open(record_file, 'rb')) record_data = [] for row in record_reader: - path, md5_, size = row[:] + [ None for i in xrange(len(row), 3) ] + path, md5_, size = row[:] + \ + [None for i in xrange(len(row), 3)] record_data.append([path, (md5_, size,)]) self.records[distinfo_dir] = dict(record_data) @@ -72,8 +210,8 @@ class TestPkgUtilDistribution(unittest.TestCase): super(TestPkgUtilDistribution, self).tearDown() def test_instantiation(self): - """Test the Distribution class's instantiation provides us with usable - attributes.""" + # Test the Distribution class's instantiation provides us with usable + # attributes. # Import the Distribution class from distutils2._backport.pkgutil import distinfo_dirname, Distribution @@ -91,7 +229,7 @@ class TestPkgUtilDistribution(unittest.TestCase): self.assertTrue(isinstance(dist.requested, type(bool()))) def test_installed_files(self): - """Test the iteration of installed files.""" + # Test the iteration of installed files. # Test the distribution's installed files from distutils2._backport.pkgutil import Distribution for distinfo_dir in self.distinfo_dirs: @@ -103,16 +241,17 @@ class TestPkgUtilDistribution(unittest.TestCase): self.assertEqual(size, record_data[path][1]) def test_uses(self): - """Test to determine if a distribution uses a specified file.""" + # 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 = os.path.relpath(os.path.join(*true_path), sys.prefix) + 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 = os.path.relpath(os.path.join(*false_path), sys.prefix) + false_path = relpath(os.path.join(*false_path), sys.prefix) # Test if the distribution uses the file in question from distutils2._backport.pkgutil import Distribution @@ -121,7 +260,7 @@ class TestPkgUtilDistribution(unittest.TestCase): self.assertFalse(dist.uses(false_path)) def test_get_distinfo_file(self): - """Test the retrieval of dist-info file objects.""" + # Test the retrieval of dist-info file objects. from distutils2._backport.pkgutil import Distribution distinfo_name = 'choxie-2.0.0.9' other_distinfo_name = 'grammar-1.0a4' @@ -150,10 +289,11 @@ class TestPkgUtilDistribution(unittest.TestCase): self.assertRaises(DistutilsError, dist.get_distinfo_file, other_distinfo_file) # Test for a file that does not exist and should not exist - self.assertRaises(DistutilsError, dist.get_distinfo_file, 'ENTRYPOINTS') + self.assertRaises(DistutilsError, dist.get_distinfo_file, \ + 'ENTRYPOINTS') def test_get_distinfo_files(self): - """Test for the iteration of RECORD path entries.""" + # Test for the iteration of RECORD path entries. from distutils2._backport.pkgutil import Distribution distinfo_name = 'towel_stuff-0.1' distinfo_dir = os.path.join(self.fake_dists_path, @@ -161,21 +301,20 @@ class TestPkgUtilDistribution(unittest.TestCase): dist = Distribution(distinfo_dir) # Test for the iteration of the raw path distinfo_record_paths = self.records[distinfo_dir].keys() - found = [ path for path in dist.get_distinfo_files() ] + found = [path for path in dist.get_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].keys() - ] - found = [ path for path in dist.get_distinfo_files(local=True) ] + distinfo_record_paths = [os.path.join(sys.prefix, path) + for path in self.records[distinfo_dir].keys()] + found = [path for path in dist.get_distinfo_files(local=True)] self.assertEqual(sorted(found), sorted(distinfo_record_paths)) -class TestPkgUtilFunctions(unittest.TestCase): - """Tests for the new functionality added in PEP 376.""" +class TestPkgUtilPEP376(unittest.TestCase): + # Tests for the new functionality added in PEP 376. def setUp(self): - super(TestPkgUtilFunctions, self).setUp() + super(TestPkgUtilPEP376, self).setUp() # Setup the path environment with our fake distributions current_path = os.path.abspath(os.path.dirname(__file__)) self.sys_path = sys.path[:] @@ -183,18 +322,19 @@ class TestPkgUtilFunctions(unittest.TestCase): sys.path[0:0] = [self.fake_dists_path] def tearDown(self): - super(TestPkgUtilFunctions, self).tearDown() sys.path[:] = self.sys_path + super(TestPkgUtilPEP376, 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.""" + # 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 + 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 + # 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'), @@ -209,7 +349,7 @@ class TestPkgUtilFunctions(unittest.TestCase): self.assertEqual(dirname, standard_dirname) def test_get_distributions(self): - """Lookup all distributions found in the ``sys.path``.""" + # 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')] @@ -221,7 +361,7 @@ class TestPkgUtilFunctions(unittest.TestCase): EggInfoDistribution # Verify the fake dists have been found. - dists = [ dist for dist in get_distributions() ] + dists = [dist for dist in get_distributions()] for dist in dists: if not isinstance(dist, Distribution): self.fail("item received was not a Distribution instance: " @@ -234,10 +374,11 @@ class TestPkgUtilFunctions(unittest.TestCase): self.assertListEqual(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')] + fake_dists += [('bacon', '0.1'), ('cheese', '2.0.2'), + ('banana', '0.4'), ('strawberry', '0.6')] found_dists = [] - dists = [ dist for dist in get_distributions(use_egg_info=True) ] + dists = [dist for dist in get_distributions(use_egg_info=True)] for dist in dists: if not (isinstance(dist, Distribution) or \ isinstance(dist, EggInfoDistribution)): @@ -248,9 +389,8 @@ class TestPkgUtilFunctions(unittest.TestCase): self.assertListEqual(sorted(fake_dists), sorted(found_dists)) - def test_get_distribution(self): - """Test for looking up a distribution by name.""" + # 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 @@ -274,6 +414,8 @@ class TestPkgUtilFunctions(unittest.TestCase): # instructed to self.assertEqual(None, get_distribution('bacon')) self.assertEqual(None, get_distribution('cheese')) + self.assertEqual(None, get_distribution('strawberry')) + self.assertEqual(None, get_distribution('banana')) # Now check that it works well in both situations, when egg-info # is a file and directory respectively. @@ -285,8 +427,16 @@ class TestPkgUtilFunctions(unittest.TestCase): self.assertTrue(isinstance(dist, EggInfoDistribution)) self.assertEqual(dist.name, 'bacon') + dist = get_distribution('banana', use_egg_info=True) + self.assertTrue(isinstance(dist, EggInfoDistribution)) + self.assertEqual(dist.name, 'banana') + + dist = get_distribution('strawberry', use_egg_info=True) + self.assertTrue(isinstance(dist, EggInfoDistribution)) + self.assertEqual(dist.name, 'strawberry') + def test_get_file_users(self): - """Test the iteration of distributions that use a file.""" + # Test the iteration of distributions that use a file. from distutils2._backport.pkgutil import get_file_users, Distribution name = 'towel_stuff-0.1' path = os.path.join(self.fake_dists_path, name, @@ -296,11 +446,11 @@ class TestPkgUtilFunctions(unittest.TestCase): self.assertEqual(dist.name, name) def test_provides(self): - """ Test for looking up distributions by what they provide """ + # Test for looking up distributions by what they provide from distutils2._backport.pkgutil import provides_distribution from distutils2.errors import DistutilsError - checkLists = lambda x,y: self.assertListEqual(sorted(x), sorted(y)) + checkLists = lambda x, y: self.assertListEqual(sorted(x), sorted(y)) l = [dist.name for dist in provides_distribution('truffles')] checkLists(l, ['choxie', 'towel-stuff']) @@ -318,10 +468,12 @@ class TestPkgUtilFunctions(unittest.TestCase): 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')] + 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', + l = [dist.name for dist in provides_distribution('truffles', \ + '!=1.1,<=2.0', use_egg_info=True)] checkLists(l, ['choxie', 'bacon', 'cheese']) @@ -338,12 +490,39 @@ class TestPkgUtilFunctions(unittest.TestCase): 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, ['strawberry']) + + l = [dist.name for dist in provides_distribution('strawberry', '>=0.5', + use_egg_info=True)] + checkLists(l, ['strawberry']) + + + 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, ['banana']) + + l = [dist.name for dist in provides_distribution('banana', '>=0.3', + use_egg_info=True)] + checkLists(l, ['banana']) + + + 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 """ + # Test looking for distributions based on what they obsolete from distutils2._backport.pkgutil import obsoletes_distribution from distutils2.errors import DistutilsError - checkLists = lambda x,y: self.assertListEqual(sorted(x), sorted(y)) + checkLists = lambda x, y: self.assertListEqual(sorted(x), sorted(y)) l = [dist.name for dist in obsoletes_distribution('truffles', '1.0')] checkLists(l, []) @@ -363,7 +542,8 @@ class TestPkgUtilFunctions(unittest.TestCase): 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')] + 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')] @@ -372,26 +552,17 @@ class TestPkgUtilFunctions(unittest.TestCase): def test_suite(): suite = unittest.TestSuite() - testcase_loader = unittest.loader.defaultTestLoader.loadTestsFromTestCase - suite.addTest(testcase_loader(TestPkgUtilFunctions)) - suite.addTest(testcase_loader(TestPkgUtilDistribution)) + load = unittest.defaultTestLoader.loadTestsFromTestCase + suite.addTest(load(TestPkgUtilData)) + suite.addTest(load(TestPkgUtilDistribution)) + suite.addTest(load(TestPkgUtilPEP302)) + suite.addTest(load(TestPkgUtilPEP376)) return suite -def test_main(): - run_unittest(test_suite()) - -if __name__ == "__main__": - test_main() - -def test_suite(): - suite = unittest.TestSuite() - testcase_loader = unittest.loader.defaultTestLoader.loadTestsFromTestCase - suite.addTest(testcase_loader(TestPkgUtilFunctions)) - suite.addTest(testcase_loader(TestPkgUtilDistribution)) - return suite def test_main(): run_unittest(test_suite()) + if __name__ == "__main__": test_main() diff --git a/src/distutils2/_backport/tests/test_sysconfig.py b/src/distutils2/_backport/tests/test_sysconfig.py index bb2e6a7..1b79ae9 100644 --- a/src/distutils2/_backport/tests/test_sysconfig.py +++ b/src/distutils2/_backport/tests/test_sysconfig.py @@ -8,7 +8,7 @@ import sys import os import shutil from copy import copy, deepcopy -from ConfigParser import ConfigParser +from ConfigParser import RawConfigParser from test.test_support import run_unittest, TESTFN @@ -88,15 +88,13 @@ class TestSysConfig(unittest.TestCase): shutil.rmtree(path) def test_nested_var_substitution(self): - """Assert that the {curly brace token} expansion pattern will replace - only the inner {something} on nested expressions like {py{something}} on - the first pass. + # Assert that the {curly brace token} expansion pattern will replace + # only the inner {something} on nested expressions like {py{something}} on + # the first pass. - We have no plans to make use of this, but it keeps the option open for - the future, at the cost only of disallowing { itself as a piece of a - substitution key (which would be weird). - - """ + # We have no plans to make use of this, but it keeps the option open for + # the future, at the cost only of disallowing { itself as a piece of a + # substitution key (which would be weird). self.assertEqual(_subst_vars('{py{version}}', {'version': '31'}), '{py31}') def test_get_paths(self): @@ -107,7 +105,7 @@ class TestSysConfig(unittest.TestCase): wanted.sort() scheme = scheme.items() scheme.sort() - self.assertEquals(scheme, wanted) + self.assertEqual(scheme, wanted) def test_get_config_vars(self): cvars = get_config_vars() @@ -120,21 +118,21 @@ class TestSysConfig(unittest.TestCase): sys.version = ('2.4.4 (#71, Oct 18 2006, 08:34:43) ' '[MSC v.1310 32 bit (Intel)]') sys.platform = 'win32' - self.assertEquals(get_platform(), 'win32') + self.assertEqual(get_platform(), 'win32') # windows XP, amd64 os.name = 'nt' sys.version = ('2.4.4 (#71, Oct 18 2006, 08:34:43) ' '[MSC v.1310 32 bit (Amd64)]') sys.platform = 'win32' - self.assertEquals(get_platform(), 'win-amd64') + self.assertEqual(get_platform(), 'win-amd64') # windows XP, itanium os.name = 'nt' sys.version = ('2.4.4 (#71, Oct 18 2006, 08:34:43) ' '[MSC v.1310 32 bit (Itanium)]') sys.platform = 'win32' - self.assertEquals(get_platform(), 'win-ia64') + self.assertEqual(get_platform(), 'win-ia64') # macbook os.name = 'posix' @@ -153,9 +151,9 @@ class TestSysConfig(unittest.TestCase): maxint = sys.maxint try: sys.maxint = 2147483647 - self.assertEquals(get_platform(), 'macosx-10.3-ppc') + self.assertEqual(get_platform(), 'macosx-10.3-ppc') sys.maxint = 9223372036854775807 - self.assertEquals(get_platform(), 'macosx-10.3-ppc64') + self.assertEqual(get_platform(), 'macosx-10.3-ppc64') finally: sys.maxint = maxint @@ -173,9 +171,9 @@ class TestSysConfig(unittest.TestCase): maxint = sys.maxint try: sys.maxint = 2147483647 - self.assertEquals(get_platform(), 'macosx-10.3-i386') + self.assertEqual(get_platform(), 'macosx-10.3-i386') sys.maxint = 9223372036854775807 - self.assertEquals(get_platform(), 'macosx-10.3-x86_64') + self.assertEqual(get_platform(), 'macosx-10.3-x86_64') finally: sys.maxint = maxint @@ -186,33 +184,33 @@ class TestSysConfig(unittest.TestCase): '-fno-strict-aliasing -fno-common ' '-dynamic -DNDEBUG -g -O3') - self.assertEquals(get_platform(), 'macosx-10.4-fat') + self.assertEqual(get_platform(), 'macosx-10.4-fat') get_config_vars()['CFLAGS'] = ('-arch x86_64 -arch i386 -isysroot ' '/Developer/SDKs/MacOSX10.4u.sdk ' '-fno-strict-aliasing -fno-common ' '-dynamic -DNDEBUG -g -O3') - self.assertEquals(get_platform(), 'macosx-10.4-intel') + self.assertEqual(get_platform(), 'macosx-10.4-intel') get_config_vars()['CFLAGS'] = ('-arch x86_64 -arch ppc -arch i386 -isysroot ' '/Developer/SDKs/MacOSX10.4u.sdk ' '-fno-strict-aliasing -fno-common ' '-dynamic -DNDEBUG -g -O3') - self.assertEquals(get_platform(), 'macosx-10.4-fat3') + self.assertEqual(get_platform(), 'macosx-10.4-fat3') get_config_vars()['CFLAGS'] = ('-arch ppc64 -arch x86_64 -arch ppc -arch i386 -isysroot ' '/Developer/SDKs/MacOSX10.4u.sdk ' '-fno-strict-aliasing -fno-common ' '-dynamic -DNDEBUG -g -O3') - self.assertEquals(get_platform(), 'macosx-10.4-universal') + self.assertEqual(get_platform(), 'macosx-10.4-universal') get_config_vars()['CFLAGS'] = ('-arch x86_64 -arch ppc64 -isysroot ' '/Developer/SDKs/MacOSX10.4u.sdk ' '-fno-strict-aliasing -fno-common ' '-dynamic -DNDEBUG -g -O3') - self.assertEquals(get_platform(), 'macosx-10.4-fat64') + self.assertEqual(get_platform(), 'macosx-10.4-fat64') for arch in ('ppc', 'i386', 'x86_64', 'ppc64'): get_config_vars()['CFLAGS'] = ('-arch %s -isysroot ' @@ -220,7 +218,7 @@ class TestSysConfig(unittest.TestCase): '-fno-strict-aliasing -fno-common ' '-dynamic -DNDEBUG -g -O3'%(arch,)) - self.assertEquals(get_platform(), 'macosx-10.4-%s'%(arch,)) + self.assertEqual(get_platform(), 'macosx-10.4-%s'%(arch,)) # linux debian sarge os.name = 'posix' @@ -230,7 +228,7 @@ class TestSysConfig(unittest.TestCase): self._set_uname(('Linux', 'aglae', '2.6.21.1dedibox-r7', '#1 Mon Apr 30 17:25:38 CEST 2007', 'i686')) - self.assertEquals(get_platform(), 'linux-i686') + self.assertEqual(get_platform(), 'linux-i686') # XXX more platforms to tests here @@ -241,11 +239,11 @@ class TestSysConfig(unittest.TestCase): def test_get_scheme_names(self): wanted = ('nt', 'nt_user', 'os2', 'os2_home', 'posix_home', 'posix_prefix', 'posix_user') - self.assertEquals(get_scheme_names(), wanted) + self.assertEqual(get_scheme_names(), wanted) def test_expand_globals(self): - config = ConfigParser() + config = RawConfigParser() config.add_section('globals') config.set('globals', 'foo', 'ok') config.add_section('posix') @@ -254,8 +252,8 @@ class TestSysConfig(unittest.TestCase): _expand_globals(config) - self.assertEquals(config.get('posix', 'foo'), 'ok') - self.assertEquals(config.get('posix', 'more'), '/etc/ok') + self.assertEqual(config.get('posix', 'foo'), 'ok') + self.assertEqual(config.get('posix', 'more'), '/etc/ok') # we might not have globals after all # extending again (==no more globals section) diff --git a/src/distutils2/command/bdist.py b/src/distutils2/command/bdist.py index 27f9ec3..147a31e 100644 --- a/src/distutils2/command/bdist.py +++ b/src/distutils2/command/bdist.py @@ -62,7 +62,7 @@ class bdist(Command): 'os2': 'zip'} # Establish the preferred order (for the --help-formats option). - format_commands = ['rpm', 'gztar', 'bztar', 'ztar', 'tar', + format_commands = ['gztar', 'bztar', 'ztar', 'tar', 'wininst', 'zip', 'msi'] # And the real information. @@ -96,7 +96,7 @@ class bdist(Command): # 'bdist_base' -- parent of per-built-distribution-format # temporary directories (eg. we'll probably have - # "build/bdist.<plat>/dumb", "build/bdist.<plat>/rpm", etc.) + # "build/bdist.<plat>/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, @@ -126,7 +126,7 @@ class bdist(Command): # Reinitialize and run each command. for i in range(len(self.formats)): cmd_name = commands[i] - sub_cmd = self.reinitialize_command(cmd_name) + sub_cmd = self.get_reinitialized_command(cmd_name) # passing the owner and group names for tar archiving if cmd_name == 'bdist_dumb': diff --git a/src/distutils2/command/bdist_dumb.py b/src/distutils2/command/bdist_dumb.py index 429725c..604bcb7 100644 --- a/src/distutils2/command/bdist_dumb.py +++ b/src/distutils2/command/bdist_dumb.py @@ -85,7 +85,7 @@ class bdist_dumb (Command): if not self.skip_build: self.run_command('build') - install = self.reinitialize_command('install', reinit_subcommands=1) + install = self.get_reinitialized_command('install', reinit_subcommands=1) install.root = self.bdist_dir install.skip_build = self.skip_build install.warn_dir = 0 @@ -115,8 +115,9 @@ class bdist_dumb (Command): % (repr(install.install_base), repr(install.install_platbase))) else: - archive_root = os.path.join(self.bdist_dir, - ensure_relative(install.install_base)) + archive_root = os.path.join( + self.bdist_dir, + self._ensure_relative(install.install_base)) # Make the archive filename = self.make_archive(pseudoinstall_root, @@ -135,3 +136,9 @@ class bdist_dumb (Command): 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/src/distutils2/command/bdist_msi.py b/src/distutils2/command/bdist_msi.py index 158065b..bbbcbcc 100644 --- a/src/distutils2/command/bdist_msi.py +++ b/src/distutils2/command/bdist_msi.py @@ -177,12 +177,12 @@ class bdist_msi (Command): if not self.skip_build: self.run_command('build') - install = self.reinitialize_command('install', reinit_subcommands=1) + install = self.get_reinitialized_command('install', reinit_subcommands=1) install.prefix = self.bdist_dir install.skip_build = self.skip_build install.warn_dir = 0 - install_lib = self.reinitialize_command('install_lib') + install_lib = self.get_reinitialized_command('install_lib') # we do not want to include pyc or pyo files install_lib.compile = 0 install_lib.optimize = 0 diff --git a/src/distutils2/command/bdist_wininst.py b/src/distutils2/command/bdist_wininst.py index 10f8bec..7b55de1 100644 --- a/src/distutils2/command/bdist_wininst.py +++ b/src/distutils2/command/bdist_wininst.py @@ -8,6 +8,7 @@ __revision__ = "$Id: bdist_wininst.py 77761 2010-01-26 22:46:15Z tarek.ziade $" import sys import os import string +from shutil import rmtree try: from sysconfig import get_python_version except ImportError: @@ -126,13 +127,13 @@ class bdist_wininst (Command): if not self.skip_build: self.run_command('build') - install = self.reinitialize_command('install', reinit_subcommands=1) + install = self.get_reinitialized_command('install', reinit_subcommands=1) install.root = self.bdist_dir install.skip_build = self.skip_build install.warn_dir = 0 install.plat_name = self.plat_name - install_lib = self.reinitialize_command('install_lib') + install_lib = self.get_reinitialized_command('install_lib') # we do not want to include pyc or pyo files install_lib.compile = 0 install_lib.optimize = 0 diff --git a/src/distutils2/command/build_ext.py b/src/distutils2/command/build_ext.py index ae11330..7e160ab 100644 --- a/src/distutils2/command/build_ext.py +++ b/src/distutils2/command/build_ext.py @@ -188,7 +188,24 @@ class build_ext(Command): 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 DistutilsSetupError( + "'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 DistutilsSetupError( + "'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. @@ -396,86 +413,7 @@ class build_ext(Command): # Now actually compile and link everything. self.build_extensions() - def check_extensions_list(self, extensions): - """Ensure that the list of extensions (presumably provided as a - command option 'extensions') is valid, i.e. it is a list of - Extension objects. We also support the old-style list of 2-tuples, - where the tuples are (ext_name, build_info), which are converted to - Extension instances here. - - Raise DistutilsSetupError if the structure is invalid anywhere; - just returns otherwise. - """ - if not isinstance(extensions, list): - raise DistutilsSetupError, \ - "'ext_modules' option must be a list of Extension instances" - - for i, ext in enumerate(extensions): - if isinstance(ext, Extension): - continue # OK! (assume type-checking done - # by Extension constructor) - - if not isinstance(ext, tuple) or len(ext) != 2: - raise DistutilsSetupError, \ - ("each element of 'ext_modules' option must be an " - "Extension instance or 2-tuple") - - ext_name, build_info = ext - - log.warn(("old-style (ext_name, build_info) tuple found in " - "ext_modules for extension '%s'" - "-- please convert to Extension instance" % ext_name)) - - if not (isinstance(ext_name, str) and - extension_name_re.match(ext_name)): - raise DistutilsSetupError, \ - ("first element of each tuple in 'ext_modules' " - "must be the extension name (a string)") - - if not isinstance(build_info, dict): - raise DistutilsSetupError, \ - ("second element of each tuple in 'ext_modules' " - "must be a dictionary (build info)") - - # OK, the (ext_name, build_info) dict is type-safe: convert it - # to an Extension instance. - ext = Extension(ext_name, build_info['sources']) - - # Easy stuff: one-to-one mapping from dict elements to - # instance attributes. - for key in ('include_dirs', 'library_dirs', 'libraries', - 'extra_objects', 'extra_compile_args', - 'extra_link_args'): - val = build_info.get(key) - if val is not None: - setattr(ext, key, val) - - # Medium-easy stuff: same syntax/semantics, different names. - ext.runtime_library_dirs = build_info.get('rpath') - if 'def_file' in build_info: - log.warn("'def_file' element of build info dict " - "no longer supported") - - # Non-trivial stuff: 'macros' split into 'define_macros' - # and 'undef_macros'. - macros = build_info.get('macros') - if macros: - ext.define_macros = [] - ext.undef_macros = [] - for macro in macros: - if not (isinstance(macro, tuple) and len(macro) in (1, 2)): - raise DistutilsSetupError, \ - ("'macros' element of build info dict " - "must be 1- or 2-tuple") - if len(macro) == 1: - ext.undef_macros.append(macro[0]) - elif len(macro) == 2: - ext.define_macros.append(macro) - - extensions[i] = ext - def get_source_files(self): - self.check_extensions_list(self.extensions) filenames = [] # Wouldn't it be neat if we knew the names of header files too... @@ -485,11 +423,6 @@ class build_ext(Command): return filenames def get_outputs(self): - # Sanity check the 'extensions' list -- can't assume this is being - # done in the same run as a 'build_extensions()' call (in fact, we - # can probably assume that it *isn't*!). - self.check_extensions_list(self.extensions) - # And build the list of output (built) filenames. Note that this # ignores the 'inplace' flag, and assumes everything goes in the # "build" tree. @@ -499,9 +432,6 @@ class build_ext(Command): return outputs def build_extensions(self): - # First, sanity-check the 'extensions' list - self.check_extensions_list(self.extensions) - for ext in self.extensions: try: self.build_extension(ext) diff --git a/src/distutils2/command/build_py.py b/src/distutils2/command/build_py.py index 821128f..c4c6d68 100644 --- a/src/distutils2/command/build_py.py +++ b/src/distutils2/command/build_py.py @@ -6,14 +6,66 @@ __revision__ = "$Id: build_py.py 76956 2009-12-21 01:22:46Z tarek.ziade $" import os import sys +import logging from glob import glob +import distutils2 from distutils2.core import Command from distutils2.errors import DistutilsOptionError, DistutilsFileError from distutils2.util import convert_path -from distutils2 import log - -class build_py(Command): +from distutils2.converter.refactor import DistutilsRefactoringTool + +# marking public APIs +__all__ = ['Mixin2to3', 'build_py'] + +try: + from distutils2.util import Mixin2to3 as _Mixin2to3 + from lib2to3.refactor import get_fixers_from_package + _CONVERT = True + _KLASS = _Mixin2to3 +except ImportError: + _CONVERT = False + _KLASS = object + +class Mixin2to3(_KLASS): + """ The base class which can be used for refactoring. When run under + Python 3.0, the run_2to3 method provided by Mixin2to3 is overridden. + When run on Python 2.x, it merely creates a class which overrides run_2to3, + yet does nothing in particular with it. + """ + if _CONVERT: + def _run_2to3(self, files, doctests=[]): + """ Takes a list of files and doctests, and performs conversion + on those. + - First, the files which contain the code(`files`) are converted. + - Second, the doctests in `files` are converted. + - Thirdly, the doctests in `doctests` are converted. + """ + + # Convert the ".py" files. + logging.info("Converting Python code") + _KLASS.run_2to3(self, files) + + # Convert the doctests in the ".py" files. + logging.info("Converting doctests with '.py' files") + _KLASS.run_2to3(self, files, doctests_only=True) + + # If the following conditions are met, then convert:- + # 1. User has specified the 'convert_2to3_doctests' option. So, we + # can expect that the list 'doctests' is not empty. + # 2. The default is allow distutils2 to allow conversion of text files + # containing doctests. It is set as + # distutils2.run_2to3_on_doctests + + if doctests != [] and distutils2.run_2to3_on_doctests: + logging.info("Converting text files which contain doctests") + _KLASS.run_2to3(self, doctests, doctests_only=True) + else: + # If run on Python 2.x, there is nothing to do. + def _run_2to3(self, files, doctests=[]): + pass + +class build_py(Command, Mixin2to3): description = "\"build\" pure Python modules (copy to build directory)" @@ -39,6 +91,8 @@ class build_py(Command): self.compile = 0 self.optimize = 0 self.force = None + self._updated_files = [] + self._doctests_2to3 = [] def finalize_options(self): self.set_undefined_options('build', @@ -93,10 +147,18 @@ class build_py(Command): self.build_packages() self.build_package_data() + if self.distribution.use_2to3 and self_updated_files: + self.run_2to3(self._updated_files, self._doctests_2to3) + self.byte_compile(self.get_outputs(include_bytecode=0)) + # -- Top-level worker functions ------------------------------------ + def get_data_files(self): - """Generate list of '(package,src_dir,build_dir,filenames)' tuples""" + """Generate list of '(package,src_dir,build_dir,filenames)' tuples. + + Helper function for `finalize_options()`. + """ data = [] if not self.packages: return data @@ -120,7 +182,10 @@ class build_py(Command): return data def find_data_files(self, package, src_dir): - """Return filenames for package's data files in '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 = [] @@ -132,14 +197,21 @@ class build_py(Command): return files def build_package_data(self): - """Copy data files into build directory""" + """Copy data files into build directory. + + Helper function for `run()`. + """ for package, src_dir, build_dir, filenames in self.data_files: for filename in filenames: target = os.path.join(build_dir, filename) self.mkpath(os.path.dirname(target)) - self.copy_file(os.path.join(src_dir, filename), target, - preserve_mode=False) + outf, copied = self.copy_file(os.path.join(src_dir, filename), + 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 @@ -181,6 +253,8 @@ class build_py(Command): 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 @@ -200,8 +274,8 @@ class build_py(Command): if os.path.isfile(init_py): return init_py else: - log.warn(("package init file '%s' not found " + - "(or not a regular file)"), init_py) + logging.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. @@ -209,7 +283,8 @@ class build_py(Command): def check_module(self, module, module_file): if not os.path.isfile(module_file): - log.warn("file %s (for module %s) not found", module_file, module) + logging.warning("file %s (for module %s) not found", + module_file, module) return False else: return True diff --git a/src/distutils2/command/cmd.py b/src/distutils2/command/cmd.py index ba88b89..fa6f281 100644 --- a/src/distutils2/command/cmd.py +++ b/src/distutils2/command/cmd.py @@ -6,7 +6,7 @@ in the distutils.command package. __revision__ = "$Id: cmd.py 75192 2009-10-02 23:49:48Z tarek.ziade $" -import sys, os, re +import os, re from distutils2.errors import DistutilsOptionError from distutils2 import util from distutils2 import log @@ -19,7 +19,7 @@ try: except ImportError: from distutils2._backport.shutil import make_archive -class Command: +class Command(object): """Abstract base class for defining command classes, the "worker bees" of the Distutils. A useful analogy for command classes is to think of them as subroutines with local variables called "options". The options @@ -188,6 +188,31 @@ class Command: """ log.log(level, msg) + # -- 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) # @@ -307,10 +332,8 @@ class Command: cmd_obj.ensure_finalized() return cmd_obj - # XXX rename to 'get_reinitialized_command()'? (should do the - # same in dist.py, if so) - def reinitialize_command(self, command, reinit_subcommands=0): - return self.distribution.reinitialize_command( + def get_reinitialized_command(self, command, reinit_subcommands=0): + return self.distribution.get_reinitialized_command( command, reinit_subcommands) def run_command(self, command): @@ -350,8 +373,10 @@ class Command: if os.path.isdir(name) or name == '': return if dry_run: + head = '' for part in name.split(os.sep): - self.log(part) + log.info("created directory %s%s", head, part) + head += part + os.sep return os.makedirs(name, mode) @@ -386,7 +411,7 @@ class Command: def spawn(self, cmd, search_path=1, level=1): """Spawn an external command respecting dry-run flag.""" - from distutils2.spawn import spawn + from distutils2.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, @@ -422,7 +447,7 @@ class Command: # 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 dep_util.newer_group(infiles, outfile): + if self.force or util.newer_group(infiles, outfile): self.execute(func, args, exec_msg, level) # Otherwise, print the "skip" message diff --git a/src/distutils2/command/register.py b/src/distutils2/command/register.py index 8abb3c8..29b87e4 100644 --- a/src/distutils2/command/register.py +++ b/src/distutils2/command/register.py @@ -28,8 +28,6 @@ class register(PyPIRCCommand): boolean_options = PyPIRCCommand.boolean_options + [ 'verify', 'list-classifiers', 'strict'] - sub_commands = [('check', lambda self: True)] - def initialize_options(self): PyPIRCCommand.initialize_options(self) self.list_classifiers = 0 @@ -46,9 +44,8 @@ class register(PyPIRCCommand): self.finalize_options() self._set_config() - # Run sub commands - for cmd_name in self.get_sub_commands(): - self.run_command(cmd_name) + # Check the package metadata + self.run_command('check') if self.dry_run: self.verify_metadata() diff --git a/src/distutils2/command/sdist.py b/src/distutils2/command/sdist.py index bb4cd1f..37f74c5 100644 --- a/src/distutils2/command/sdist.py +++ b/src/distutils2/command/sdist.py @@ -18,12 +18,11 @@ except ImportError: from distutils2._backport.shutil import get_archive_formats from distutils2.core import Command -from distutils2 import util from distutils2.errors import (DistutilsPlatformError, DistutilsOptionError, - DistutilsTemplateError) + DistutilsTemplateError) from distutils2.manifest import Manifest from distutils2 import log -from distutils2.util import convert_path, newer +from distutils2.util import convert_path def show_formats(): """Print all possible values for the 'formats' option (used by @@ -45,12 +44,6 @@ class sdist(Command): description = "create a source distribution (tarball, zip file, etc.)" - def checking_metadata(self): - """Callable used for the check sub-command. - - Placed here so user_options can view it""" - return self.metadata_check - user_options = [ ('template=', 't', "name of manifest template file [default: MANIFEST.in]"), @@ -100,8 +93,6 @@ class sdist(Command): default_format = {'posix': 'gztar', 'nt': 'zip' } - sub_commands = [('check', checking_metadata)] - def initialize_options(self): # 'template' and 'manifest' are, respectively, the names of # the manifest template and manifest file. @@ -162,9 +153,9 @@ class sdist(Command): # manifest self.filelist.clear() - # Run sub commands - for cmd_name in self.get_sub_commands(): - self.run_command(cmd_name) + # 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, diff --git a/src/distutils2/command/upload.py b/src/distutils2/command/upload.py index 14fe119..796ca73 100644 --- a/src/distutils2/command/upload.py +++ b/src/distutils2/command/upload.py @@ -15,7 +15,7 @@ except ImportError: from distutils2.errors import DistutilsOptionError from distutils2.core import PyPIRCCommand -from distutils2.spawn import spawn +from distutils2.util import spawn from distutils2 import log class upload(PyPIRCCommand): diff --git a/src/distutils2/command/upload_docs.py b/src/distutils2/command/upload_docs.py index 860f07f..120837b 100644 --- a/src/distutils2/command/upload_docs.py +++ b/src/distutils2/command/upload_docs.py @@ -1,4 +1,4 @@ -import base64, httplib, os.path, socket, tempfile, urlparse, zipfile +import base64, httplib, os.path, socket, urlparse, zipfile from cStringIO import StringIO from distutils2 import log from distutils2.command.upload import upload @@ -81,7 +81,6 @@ class upload_docs(PyPIRCCommand): raise DistutilsFileError(mesg % upload_dir) def run(self): - tmp_dir = tempfile.mkdtemp() name = self.distribution.metadata['Name'] version = self.distribution.metadata['Version'] zip_file = zip_dir(self.upload_dir) @@ -125,7 +124,7 @@ class upload_docs(PyPIRCCommand): elif r.status == 301: location = r.getheader('Location') if location is None: - location = 'http://packages.python.org/%s/' % meta.get_name() + location = 'http://packages.python.org/%s/' % name self.announce('Upload successful. Visit %s' % location, log.INFO) else: diff --git a/src/distutils2/compiler/bcppcompiler.py b/src/distutils2/compiler/bcppcompiler.py index 71a116b..7587048 100644 --- a/src/distutils2/compiler/bcppcompiler.py +++ b/src/distutils2/compiler/bcppcompiler.py @@ -16,7 +16,7 @@ __revision__ = "$Id: bcppcompiler.py 76956 2009-12-21 01:22:46Z tarek.ziade $" import os from distutils2.errors import (DistutilsExecError, CompileError, LibError, - LinkError, UnknownFileError) + LinkError, UnknownFileError) from distutils2.compiler.ccompiler import CCompiler, gen_preprocess_options from distutils2.file_util import write_file from distutils2.dep_util import newer diff --git a/src/distutils2/compiler/ccompiler.py b/src/distutils2/compiler/ccompiler.py index 025febd..c9bb129 100644 --- a/src/distutils2/compiler/ccompiler.py +++ b/src/distutils2/compiler/ccompiler.py @@ -10,9 +10,8 @@ import os import re from distutils2.errors import (CompileError, LinkError, UnknownFileError, - DistutilsPlatformError, DistutilsModuleError) -from distutils2.spawn import spawn -from distutils2.util import split_quoted, execute, newer_group + DistutilsPlatformError, DistutilsModuleError) +from distutils2.util import split_quoted, execute, newer_group, spawn from distutils2 import log from shutil import move @@ -76,7 +75,7 @@ def customize_compiler(compiler): compiler.shared_lib_extension = so_ext -class CCompiler: +class CCompiler(object): """Abstract base class to define the interface that must be implemented by real compiler classes. Also has some utility methods used by several compiler classes. @@ -800,14 +799,16 @@ class CCompiler: library_dirs = [] fd, fname = tempfile.mkstemp(".c", funcname, text=True) f = os.fdopen(fd, "w") - for incl in includes: - f.write("""#include "%s"\n""" % incl) - f.write("""\ + try: + for incl in includes: + f.write("""#include "%s"\n""" % incl) + f.write("""\ main (int argc, char **argv) { %s(); } """ % funcname) - f.close() + finally: + f.close() try: objects = self.compile([fname], include_dirs=include_dirs) except CompileError: @@ -938,8 +939,10 @@ main (int argc, char **argv) { if os.path.isdir(name) or name == '': return if self.dry_run: + head = '' for part in name.split(os.sep): - self.log(part) + log.info("created directory %s%s", head, part) + head += part + os.sep return os.makedirs(name, mode) @@ -1048,7 +1051,7 @@ def new_compiler(plat=None, compiler=None, verbose=0, dry_run=0, force=0): module_name = "distutils2.compiler." + module_name __import__ (module_name) module = sys.modules[module_name] - klass = vars(module)[class_name] + cls = vars(module)[class_name] except ImportError: raise DistutilsModuleError, \ "can't compile C/C++ code: unable to load module '%s'" % \ @@ -1061,7 +1064,7 @@ def new_compiler(plat=None, compiler=None, verbose=0, dry_run=0, force=0): # XXX The None is necessary to preserve backwards compatibility # with classes that expect verbose to be the first positional # argument. - return klass(None, dry_run, force) + return cls(None, dry_run, force) def gen_preprocess_options(macros, include_dirs): diff --git a/src/distutils2/compiler/emxccompiler.py b/src/distutils2/compiler/emxccompiler.py index cb6fb16..6247c00 100644 --- a/src/distutils2/compiler/emxccompiler.py +++ b/src/distutils2/compiler/emxccompiler.py @@ -25,9 +25,8 @@ import os, sys, copy from warnings import warn from distutils2.compiler.unixccompiler import UnixCCompiler -from distutils2.util import write_file from distutils2.errors import DistutilsExecError, CompileError, UnknownFileError -from distutils2.util import get_compiler_versions +from distutils2.util import get_compiler_versions, write_file class EMXCCompiler (UnixCCompiler): @@ -273,8 +272,10 @@ def check_config_h(): # It would probably better to read single lines to search. # But we do this only once, and it is fast enough f = open(fn) - s = f.read() - f.close() + try: + s = f.read() + finally: + f.close() except IOError, exc: # if we can't read this file, we cannot say it is wrong diff --git a/src/distutils2/compiler/msvc9compiler.py b/src/distutils2/compiler/msvc9compiler.py index 9f98d60..46cffa5 100644 --- a/src/distutils2/compiler/msvc9compiler.py +++ b/src/distutils2/compiler/msvc9compiler.py @@ -20,7 +20,7 @@ import sys import re from distutils2.errors import (DistutilsExecError, DistutilsPlatformError, - CompileError, LibError, LinkError) + CompileError, LibError, LinkError) from distutils2.compiler.ccompiler import CCompiler, gen_lib_options from distutils2 import log from distutils2.util import get_platform @@ -50,7 +50,7 @@ PLAT_TO_VCVARS = { 'win-ia64' : 'ia64', } -class Reg: +class Reg(object): """Helper class to read values from the registry """ @@ -112,7 +112,7 @@ class Reg: return s convert_mbcs = staticmethod(convert_mbcs) -class MacroExpander: +class MacroExpander(object): def __init__(self, version): self.macros = {} diff --git a/src/distutils2/compiler/msvccompiler.py b/src/distutils2/compiler/msvccompiler.py index 46f8517..d2b049f 100644 --- a/src/distutils2/compiler/msvccompiler.py +++ b/src/distutils2/compiler/msvccompiler.py @@ -15,7 +15,7 @@ import os import string from distutils2.errors import (DistutilsExecError, DistutilsPlatformError, - CompileError, LibError, LinkError) + CompileError, LibError, LinkError) from distutils2.compiler.ccompiler import CCompiler, gen_lib_options from distutils2 import log @@ -104,7 +104,7 @@ def convert_mbcs(s): pass return s -class MacroExpander: +class MacroExpander(object): def __init__(self, version): self.macros = {} diff --git a/src/distutils2/compiler/unixccompiler.py b/src/distutils2/compiler/unixccompiler.py index 18276f7..7e52959 100644 --- a/src/distutils2/compiler/unixccompiler.py +++ b/src/distutils2/compiler/unixccompiler.py @@ -19,10 +19,10 @@ import os, sys from types import StringType, NoneType from distutils2.util import newer -from distutils2.compiler.ccompiler import \ - CCompiler, gen_preprocess_options, gen_lib_options -from distutils2.errors import \ - DistutilsExecError, CompileError, LibError, LinkError +from distutils2.compiler.ccompiler import (CCompiler, gen_preprocess_options, + gen_lib_options) +from distutils2.errors import (DistutilsExecError, CompileError, + LibError, LinkError) from distutils2 import log try: diff --git a/src/distutils2/config.py b/src/distutils2/config.py index a56ae02..30ad479 100644 --- a/src/distutils2/config.py +++ b/src/distutils2/config.py @@ -4,7 +4,7 @@ Provides the PyPIRCCommand class, the base class for the command classes that uses .pypirc in the distutils.command package. """ import os -from ConfigParser import ConfigParser +from ConfigParser import RawConfigParser from distutils2.command.cmd import Command @@ -59,7 +59,7 @@ class PyPIRCCommand(Command): if os.path.exists(rc): self.announce('Using PyPI login from %s' % rc) repository = self.repository or self.DEFAULT_REPOSITORY - config = ConfigParser() + config = RawConfigParser() config.read(rc) sections = config.sections() if 'distutils' in sections: diff --git a/src/distutils2/converter/fixers/fix_imports.py b/src/distutils2/converter/fixers/fix_imports.py index d765843..38d185e 100644 --- a/src/distutils2/converter/fixers/fix_imports.py +++ b/src/distutils2/converter/fixers/fix_imports.py @@ -36,11 +36,16 @@ class FixImports(BaseFix): pattern = [] next = imp.next_sibling while next is not None: + # Get the first child if we have a Node + if not hasattr(next, "value"): + next = next.children[0] pattern.append(next.value) if not hasattr(next, "next_sibling"): next.next_sibling = next.get_next_sibling() next = next.next_sibling - if pattern == ['import', 'setup']: + + if set(pattern).issubset(set( + ['import', ',', 'setup', 'find_packages'])): imp.value = 'distutils2.core' imp.changed() diff --git a/src/distutils2/core.py b/src/distutils2/core.py index d10a558..a81780b 100644 --- a/src/distutils2/core.py +++ b/src/distutils2/core.py @@ -2,8 +2,9 @@ The only module that needs to be imported to use the Distutils; provides the 'setup' function (which is to be called from the setup script). Also -indirectly provides the Distribution and Command classes, although they are -really defined in distutils2.dist and distutils2.cmd. +exports useful classes so that setup scripts can import them from here +although they are really defined in other modules: Distribution, Command, +PyPIRCommand, Extension, find_packages. """ __revision__ = "$Id: core.py 77704 2010-01-23 09:23:15Z tarek.ziade $" @@ -12,7 +13,7 @@ import sys import os from distutils2.errors import (DistutilsSetupError, DistutilsArgError, - DistutilsError, CCompilerError) + DistutilsError, CCompilerError) from distutils2.util import grok_environment_error # Mainly import these so setup scripts can "from distutils2.core import" them. @@ -20,6 +21,7 @@ from distutils2.dist import Distribution from distutils2.command.cmd import Command from distutils2.config import PyPIRCCommand from distutils2.extension import Extension +from distutils2.util import find_packages # This is a barebones help message generated displayed when the user # runs the setup script with no arguments at all. More useful help @@ -47,7 +49,8 @@ setup_keywords = ('distclass', 'script_name', 'script_args', 'options', 'maintainer', 'maintainer_email', 'url', 'license', 'description', 'long_description', 'keywords', 'platforms', 'classifiers', 'download_url', - 'requires', 'provides', 'obsoletes', + 'requires', 'provides', 'obsoletes', 'use_2to3', + 'convert_2to3_doctests', ) # Legal keyword arguments for the Extension constructor @@ -94,11 +97,7 @@ def setup(**attrs): # Determine the distribution class -- either caller-supplied or # our Distribution (see below). - klass = attrs.get('distclass') - if klass: - del attrs['distclass'] - else: - klass = Distribution + distclass = attrs.pop('distclass', Distribution) if 'script_name' not in attrs: attrs['script_name'] = os.path.basename(sys.argv[0]) @@ -108,7 +107,7 @@ def setup(**attrs): # Create the Distribution instance, using the remaining arguments # (ie. everything except distclass) to initialize it try: - _setup_distribution = dist = klass(attrs) + _setup_distribution = dist = distclass(attrs) except DistutilsSetupError, msg: if 'name' in attrs: raise SystemExit, "error in %s setup command: %s" % \ diff --git a/src/distutils2/depgraph.py b/src/distutils2/depgraph.py index 8ab4690..75bc4d3 100644 --- a/src/distutils2/depgraph.py +++ b/src/distutils2/depgraph.py @@ -1,47 +1,49 @@ -""" -A dependency graph generator. The graph is represented as an instance of -:class:`DependencyGraph`, and DOT output is possible as well. +"""Analyse the relationships between the distributions in the system and generate +a dependency graph. """ -from distutils2._backport import pkgutil from distutils2.errors import DistutilsError from distutils2.version import VersionPredicate -__all__ = ['DependencyGraph', 'generate_graph'] +__all__ = ['DependencyGraph', 'generate_graph', 'dependent_dists', + 'graph_to_dot'] class DependencyGraph(object): """ Represents a dependency graph between distributions. - The depedency relationships are stored in an *adjacency_list* that maps + 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 labelled with ``label`` (i.e. the version - specifier, if such was provided). If any missing depencies are found, - they are stored in ``missing``. It maps distributions to a list of - requirements that were not provided by any other distributions. + 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 + depencies 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 distribution *x* to the graph. + """Add the *distribution* to the graph. :type distribution: :class:`pkgutil.Distribution` or :class:`pkgutil.EggInfoDistribution` """ self.adjacency_list[distribution] = list() + self.reverse_list[distribution] = list() self.missing[distribution] = list() def add_edge(self, x, y, label=None): - """ - Add an edge from distribution *x* to distribution *y* with the given + """Add an edge from distribution *x* to distribution *y* with the given *label*. - :type x: :class:`pkgutil.Distribution` or :class:`pkgutil.EggInfoDistribution` :type y: :class:`pkgutil.Distribution` or @@ -49,6 +51,9 @@ class DependencyGraph(object): :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): """ @@ -60,45 +65,42 @@ class DependencyGraph(object): """ self.missing[distribution].append(requirement) - def to_dot(self, f, skip_disconnected=True): - """ - Writes a DOT output for the graph to the provided *file*. - If *skip_disconnected* is set to ``True``, then all distributions - that are not dependent on any other distributions are skipped. - :type f: ``file`` - ;type skip_disconnected: ``bool`` - """ - if not isinstance(f, file): - raise TypeError('the argument has to be of type file') - - disconnected = [] - - f.write("digraph dependencies {\n") - for dist, adjs in self.adjacency_list.iteritems(): - 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') +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.iteritems(): + 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. + """Generates a dependency graph from the given distributions. :parameter dists: a list of distributions :type dists: list of :class:`pkgutil.Distribution` and @@ -115,7 +117,7 @@ def generate_graph(dists): provides = dist.metadata['Provides-Dist'] + dist.metadata['Provides'] for p in provides: - comps = p.split(" ", 1) + comps = p.strip().rsplit(" ", 1) name = comps[0] version = None if len(comps) == 2: @@ -133,22 +135,26 @@ def generate_graph(dists): requires = dist.metadata['Requires-Dist'] + dist.metadata['Requires'] for req in requires: predicate = VersionPredicate(req) - comps = req.split(" ", 1) + comps = req.strip().rsplit(" ", 1) name = comps[0] if not name in provided: graph.add_missing(dist, req) else: + matched = False for (version, provider) in provided[name]: if predicate.match(version): 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 + """Recursively generate a list of distributions from *dists* that are dependent on *dist*. :param dists: a list of distributions @@ -158,24 +164,26 @@ def dependent_dists(dists, dist): raise ValueError('The given distribution is not a member of the list') graph = generate_graph(dists) - dep = [dist] - fringe = [dist] # list of nodes we should expand + dep = [dist] # dependent distributions + fringe = graph.reverse_list[dist] # list of nodes we should inspect + while not len(fringe) == 0: - next = graph.adjacency_list[fringe.pop()] - for (dist, label) in next: - if not dist in dep: # avoid infinite loops - dep.append(dist) - fringe.append(dist) + node = fringe.pop() + dep.append(node) + for prev in graph.reverse_list[node]: + if not prev in dep: + fringe.append(prev) - dep.pop() + dep.pop(0) # remove dist from dep, was there to prevent infinite loops return dep if __name__ == '__main__': - dists = list(pkgutil.get_distributions(use_egg_info=True)) + from distutils2._backport.pkgutil import get_distributions + dists = list(get_distributions(use_egg_info=True)) graph = generate_graph(dists) for dist, reqs in graph.missing.iteritems(): if len(reqs) > 0: print("Missing dependencies for %s: %s" % (dist.name, ", ".join(reqs))) f = open('output.dot', 'w') - graph.to_dot(f, True) + graph_to_dot(graph, f, True) diff --git a/src/distutils2/dist.py b/src/distutils2/dist.py index 98e8cf1..1ea8891 100644 --- a/src/distutils2/dist.py +++ b/src/distutils2/dist.py @@ -13,8 +13,10 @@ try: except ImportError: warnings = None +from ConfigParser import RawConfigParser + from distutils2.errors import (DistutilsOptionError, DistutilsArgError, - DistutilsModuleError, DistutilsClassError) + DistutilsModuleError, DistutilsClassError) from distutils2.fancy_getopt import FancyGetopt, translate_longopt from distutils2.util import check_environ, strtobool from distutils2 import log @@ -112,7 +114,11 @@ Common commands: (see '--help-commands' for more) ('tests-require', None, "print the list of packages/modules required to run the test suite"), ('obsoletes', None, - "print the list of packages/modules made obsolete") + "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 = map(lambda x: translate_longopt(x[0]), display_options) @@ -206,6 +212,8 @@ Common commands: (see '--help-commands' for more) self.scripts = None self.data_files = None self.password = '' + self.use_2to3 = False + self.convert_2to3_doctests = [] # And now initialize bookkeeping stuff that can't be supplied by # the caller at all. 'command_obj' maps command names to @@ -250,7 +258,7 @@ Common commands: (see '--help-commands' for more) elif hasattr(self, key): setattr(self, key, val) else: - msg = "Unknown distribution option: %s" % repr(key) + msg = "Unknown distribution option: %r" % key if warnings is not None: warnings.warn(msg) else: @@ -364,14 +372,12 @@ Common commands: (see '--help-commands' for more) return files def parse_config_files(self, filenames=None): - from ConfigParser import ConfigParser - if filenames is None: filenames = self.find_config_files() log.debug("Distribution.parse_config_files():") - parser = ConfigParser() + parser = RawConfigParser() for filename in filenames: log.debug(" reading %s" % filename) parser.read(filename) @@ -385,7 +391,7 @@ Common commands: (see '--help-commands' for more) opt = opt.replace('-', '_') opt_dict[opt] = (filename, val) - # Make the ConfigParser forget everything (so we retain + # Make the RawConfigParser forget everything (so we retain # the original filenames that options come from) parser.__init__() @@ -583,15 +589,11 @@ Common commands: (see '--help-commands' for more) instance, analogous to the .finalize_options() method of Command objects. """ - - # XXX conversion -- removed - #for attr in ('keywords', 'platforms'): - # value = self.metadata.get_field(attr) - # if value is None: - # continue - # if isinstance(value, str): - # value = [elm.strip() for elm in value.split(',')] - # setattr(self.metadata, attr, value) + 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=1, display_options=1, commands=[]): @@ -629,16 +631,16 @@ Common commands: (see '--help-commands' for more) for command in self.commands: if isinstance(command, type) and issubclass(command, Command): - klass = command + cls = command else: - klass = self.get_command_class(command) - if (hasattr(klass, 'help_options') and - isinstance(klass.help_options, list)): - parser.set_option_table(klass.user_options + - fix_help_options(klass.help_options)) + cls = self.get_command_class(command) + if (hasattr(cls, 'help_options') and + isinstance(cls.help_options, list)): + parser.set_option_table(cls.user_options + + fix_help_options(cls.help_options)) else: - parser.set_option_table(klass.user_options) - parser.print_help("Options for '%s' command:" % klass.__name__) + parser.set_option_table(cls.user_options) + parser.print_help("Options for '%s' command:" % cls.__name__) print('') print(gen_usage(self.script_name)) @@ -690,11 +692,11 @@ Common commands: (see '--help-commands' for more) print(header + ":") for cmd in commands: - klass = self.cmdclass.get(cmd) - if not klass: - klass = self.get_command_class(cmd) + cls = self.cmdclass.get(cmd) + if not cls: + cls = self.get_command_class(cmd) try: - description = klass.description + description = cls.description except AttributeError: description = "(no description available)" @@ -756,11 +758,11 @@ Common commands: (see '--help-commands' for more) rv = [] for cmd in (std_commands + extra_commands): - klass = self.cmdclass.get(cmd) - if not klass: - klass = self.get_command_class(cmd) + cls = self.cmdclass.get(cmd) + if not cls: + cls = self.get_command_class(cmd) try: - description = klass.description + description = cls.description except AttributeError: description = "(no description available)" rv.append((cmd, description)) @@ -792,13 +794,13 @@ Common commands: (see '--help-commands' for more) Raises DistutilsModuleError if the expected module could not be found, or if that module does not define the expected class. """ - klass = self.cmdclass.get(command) - if klass: - return klass + cls = self.cmdclass.get(command) + if cls: + return cls for pkgname in self.get_command_packages(): module_name = "%s.%s" % (pkgname, command) - klass_name = command + class_name = command try: __import__ (module_name) @@ -807,14 +809,14 @@ Common commands: (see '--help-commands' for more) continue try: - klass = getattr(module, klass_name) + cls = getattr(module, class_name) except AttributeError: raise DistutilsModuleError, \ "invalid command '%s' (no class '%s' in module '%s')" \ - % (command, klass_name, module_name) + % (command, class_name, module_name) - self.cmdclass[command] = klass - return klass + self.cmdclass[command] = cls + return cls raise DistutilsModuleError("invalid command '%s'" % command) @@ -830,8 +832,8 @@ Common commands: (see '--help-commands' for more) log.debug("Distribution.get_command_obj(): " \ "creating '%s' command object" % command) - klass = self.get_command_class(command) - cmd_obj = self.command_obj[command] = klass(self) + cls = self.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 @@ -887,7 +889,7 @@ Common commands: (see '--help-commands' for more) except ValueError, msg: raise DistutilsOptionError, msg - def reinitialize_command(self, command, reinit_subcommands=0): + def get_reinitialized_command(self, command, reinit_subcommands=0): """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 @@ -922,7 +924,7 @@ Common commands: (see '--help-commands' for more) if reinit_subcommands: for sub in command.get_sub_commands(): - self.reinitialize_command(sub, reinit_subcommands) + self.get_reinitialized_command(sub, reinit_subcommands) return command diff --git a/src/distutils2/extension.py b/src/distutils2/extension.py index 430b919..05ccd20 100644 --- a/src/distutils2/extension.py +++ b/src/distutils2/extension.py @@ -5,14 +5,8 @@ modules in setup scripts.""" __revision__ = "$Id: extension.py 77704 2010-01-23 09:23:15Z tarek.ziade $" -import os import warnings -try: - import sysconfig -except ImportError: - from distutils2._backport import sysconfig - # 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 @@ -23,7 +17,7 @@ except ImportError: # import that large-ish module (indirectly, through distutils.core) in # order to do anything. -class Extension: +class Extension(object): """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). diff --git a/src/distutils2/fancy_getopt.py b/src/distutils2/fancy_getopt.py index 1382b24..e0ac736 100644 --- a/src/distutils2/fancy_getopt.py +++ b/src/distutils2/fancy_getopt.py @@ -30,7 +30,7 @@ neg_alias_re = re.compile("^(%s)=!(%s)$" % (longopt_pat, longopt_pat)) # (for use as attributes of some object). longopt_xlate = string.maketrans('-', '_') -class FancyGetopt: +class FancyGetopt(object): """Wrapper around the standard 'getopt()' module that provides some handy extra functionality: * short and long options are tied together @@ -473,7 +473,7 @@ def translate_longopt(opt): return string.translate(opt, longopt_xlate) -class OptionDummy: +class OptionDummy(object): """Dummy class just used as a place to hold command-line option values as instance attributes.""" diff --git a/src/distutils2/log.py b/src/distutils2/log.py index 7588570..1293bf4 100644 --- a/src/distutils2/log.py +++ b/src/distutils2/log.py @@ -11,14 +11,14 @@ FATAL = 5 import sys -class Log: +class Log(object): def __init__(self, threshold=WARN): self.threshold = threshold def _log(self, level, msg, args): if level not in (DEBUG, INFO, WARN, ERROR, FATAL): - raise ValueError('%s wrong log level' % str(level)) + raise ValueError('%s wrong log level' % level) if level >= self.threshold: if args: diff --git a/src/distutils2/metadata.py b/src/distutils2/metadata.py index 3d04ac4..55f1020 100644 --- a/src/distutils2/metadata.py +++ b/src/distutils2/metadata.py @@ -105,7 +105,6 @@ def _best_version(fields): keys = fields.keys() 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: @@ -128,9 +127,9 @@ def _best_version(fields): 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 work with all tools + # - 1.0 has a broken Summary field but works with all tools # - 1.1 is to avoid - # - 1.2 fixes Summary but is not spreaded yet + # - 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: @@ -185,14 +184,16 @@ _UNICODEFIELDS = ('Author', 'Maintainer', 'Summary', 'Description') class DistributionMetadata(object): """Distribution meta-data class (1.0 or 1.2). """ - def __init__(self, path=None, platform_dependant=False, - execution_context=None): + def __init__(self, path=None, platform_dependent=False, + execution_context=None, fileobj=None): self._fields = {} self.version = None self.docutils_support = _HAS_DOCUTILS - self.platform_dependant = platform_dependant + self.platform_dependent = platform_dependent if path is not None: self.read(path) + elif fileobj is not None: + self.read_file(fileobj) self.execution_context = execution_context def _set_best_version(self): @@ -261,7 +262,7 @@ class DistributionMetadata(object): return reporter.messages def _platform(self, value): - if not self.platform_dependant or ';' not in value: + if not self.platform_dependent or ';' not in value: return True, value value, marker = value.split(';') return _interpret(marker, self.execution_context), value @@ -516,7 +517,7 @@ class _Operation(object): raise NameError(value) def _nonsense_op(self): - msg = 'This operation is not supported : "%s"' % str(self) + msg = 'This operation is not supported : "%s"' % self raise SyntaxError(msg) def __call__(self): @@ -633,4 +634,3 @@ def _interpret(marker, execution_context=None): operations = _CHAIN(execution_context) tokenize(StringIO(marker).readline, operations.eat) return operations.result() - diff --git a/src/distutils2/mkpkg.py b/src/distutils2/mkpkg.py index 42ea7fe..3fe82e3 100755 --- a/src/distutils2/mkpkg.py +++ b/src/distutils2/mkpkg.py @@ -675,7 +675,7 @@ def buildTroveDict(troveList): troveDict = buildTroveDict(troveList) -class SetupClass: +class SetupClass(object): def __init__(self): self.config = None self.classifierDict = {} @@ -717,14 +717,16 @@ class SetupClass: def inspectFile(self, path): fp = open(path, 'r') - for line in [ fp.readline() for x in range(10) ]: - m = re.match(r'^#!.*python((?P<major>\d)(\.\d+)?)?$', line) - if m: - if m.group('major') == '3': - self.classifierDict['Programming Language :: Python :: 3'] = 1 - else: - self.classifierDict['Programming Language :: Python :: 2'] = 1 - fp.close() + try: + for line in [ fp.readline() for x in range(10) ]: + m = re.match(r'^#!.*python((?P<major>\d)(\.\d+)?)?$', line) + if m: + if m.group('major') == '3': + self.classifierDict['Programming Language :: Python :: 3'] = 1 + else: + self.classifierDict['Programming Language :: Python :: 2'] = 1 + finally: + fp.close() def inspectDirectory(self): @@ -885,38 +887,33 @@ Status''', required = False) if os.path.exists('setup.py'): shutil.move('setup.py', 'setup.py.old') fp = open('setup.py', 'w') - fp.write('#!/usr/bin/env python\n\n') - fp.write('from distutils2.core import setup\n\n') - - fp.write('from sys import version\n') - fp.write('if version < \'2.2.3\':\n') - fp.write(' from distutils2.dist import DistributionMetadata\n') - fp.write(' DistributionMetadata.classifier = None\n') - fp.write(' DistributionMetadata.download_url = None\n') - - fp.write('setup(name = %s,\n' % repr(self.setupData['name'])) - fp.write(' version = %s,\n' % repr(self.setupData['version'])) - fp.write(' description = %s,\n' - % repr(self.setupData['description'])) - fp.write(' author = %s,\n' % repr(self.setupData['author'])) - fp.write(' author_email = %s,\n' - % repr(self.setupData['author_email'])) - if self.setupData['url']: - fp.write(' url = %s,\n' % repr(self.setupData['url'])) - if self.setupData['classifier']: - fp.write(' classifier = [\n') - for classifier in sorted(self.setupData['classifier'].keys()): - fp.write(' %s,\n' % repr(classifier)) - fp.write(' ],\n') - if self.setupData['packages']: - fp.write(' packages = %s,\n' - % repr(self._dotted_packages(self.setupData['packages']))) - fp.write(' package_dir = %s,\n' - % repr(self.setupData['packages'])) - fp.write(' #scripts = [\'path/to/script\']\n') - - fp.write(' )\n') - fp.close() + try: + fp.write('#!/usr/bin/env python\n\n') + fp.write('from distutils2.core import setup\n\n') + fp.write('setup(name=%s,\n' % repr(self.setupData['name'])) + fp.write(' version=%s,\n' % repr(self.setupData['version'])) + fp.write(' description=%s,\n' + % repr(self.setupData['description'])) + fp.write(' author=%s,\n' % repr(self.setupData['author'])) + fp.write(' author_email=%s,\n' + % repr(self.setupData['author_email'])) + if self.setupData['url']: + fp.write(' url=%s,\n' % repr(self.setupData['url'])) + if self.setupData['classifier']: + fp.write(' classifier=[\n') + for classifier in sorted(self.setupData['classifier'].keys()): + fp.write(' %s,\n' % repr(classifier)) + fp.write(' ],\n') + if self.setupData['packages']: + fp.write(' packages=%s,\n' + % repr(self._dotted_packages(self.setupData['packages']))) + fp.write(' package_dir=%s,\n' + % repr(self.setupData['packages'])) + fp.write(' #scripts=[\'path/to/script\']\n') + + fp.write(' )\n') + finally: + fp.close() os.chmod('setup.py', 0755) print 'Wrote "setup.py".' diff --git a/src/distutils2/pypi/__init__.py b/src/distutils2/pypi/__init__.py index 0872262..88efa14 100644 --- a/src/distutils2/pypi/__init__.py +++ b/src/distutils2/pypi/__init__.py @@ -3,5 +3,6 @@ Package containing ways to interact with the PyPI APIs. """ -__all__ = ['package_index', +__all__ = ['simple', + 'dist', ] diff --git a/src/distutils2/pypi/dist.py b/src/distutils2/pypi/dist.py new file mode 100644 index 0000000..418186e --- /dev/null +++ b/src/distutils2/pypi/dist.py @@ -0,0 +1,315 @@ +"""distutils2.pypi.dist + +Provides the PyPIDistribution class thats represents a distribution retrieved +on PyPI. +""" +import re +import urlparse +import urllib +import tempfile +from operator import attrgetter + +try: + import hashlib +except ImportError: + from distutils2._backport import hashlib + +from distutils2.version import suggest_normalized_version, NormalizedVersion +from distutils2.pypi.errors import HashDoesNotMatch, UnsupportedHashName + +EXTENSIONS = ".tar.gz .tar.bz2 .tar .zip .tgz .egg".split() +MD5_HASH = re.compile(r'^.*#md5=([a-f0-9]+)$') + + +class PyPIDistribution(object): + """Represents a distribution retrieved from PyPI. + + This is a simple container for various attributes as name, version, + downloaded_location, url etc. + + The PyPIDistribution class is used by the pypi.*Index class to return + information about distributions. + """ + + @classmethod + def from_url(cls, url, probable_dist_name=None, is_external=True): + """Build a Distribution from a url archive (egg or zip or tgz). + + :param url: complete url of the distribution + :param probable_dist_name: A probable name of the distribution. + :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 = urlparse.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 PyPIDistribution(name, version, url=url, url_hashname="md5", + url_hashval=md5_hash, + url_is_external=is_external) + + def __init__(self, name, version, type=None, url=None, url_hashname=None, + url_hashval=None, url_is_external=True): + """Create a new instance of PyPIDistribution. + + :param name: the name of the distribution + :param version: the version of the distribution + :param type: the type of the dist (eg. source, bin-*, etc.) + :param url: URL where we found this distribution + :param url_hashname: the name of the hash we want to use. Refer to the + hashlib.new documentation for more information. + :param url_hashval: the hash value. + :param url_is_external: we need to know if the provided url comes from an + index browsing, or from an external resource. + + """ + self.name = name + self.version = NormalizedVersion(version) + self.type = type + # 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 informations + # 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, url_hashname, url_hashval, url_is_external) + + def add_url(self, url, 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) + + 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.type == 'source' + + @property + def is_final(self): + """proxy to version.is_final""" + return self.version.is_final + + 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 + """ + 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 = urlparse.urlparse(url)[2].split('/')[-1] + filename, headers = urllib.urlretrieve(url, + path + "/" + archive_name) + self.downloaded_location = filename + self._check_md5(filename) + return self.downloaded_location + + 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): + f = open(filename) + 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): + return "%s %s %s %s" \ + % (self.__class__.__name__, self.name, self.version, + self.type or "") + + def _check_is_comparable(self, other): + if not isinstance(other, PyPIDistribution): + 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 __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 PyPIDistributions(list): + """A container of PyPIDistribution objects. + + Contains methods and facilities to sort and filter distributions. + """ + def __init__(self, list=[]): + # To disable the ability to pass lists on instanciation + super(PyPIDistributions, self).__init__() + for item in list: + self.append(item) + + def filter(self, predicate): + """Filter the distributions and return a subset of distributions that + match the given predicate + """ + return PyPIDistributions( + [dist for dist in self if dist.name == predicate.name and + predicate.match(dist.version)]) + + def get_last(self, predicate, prefer_source=None, prefer_final=None): + """Return the most up to date version, that satisfy the given + predicate + """ + distributions = self.filter(predicate) + distributions.sort_distributions(prefer_source, prefer_final, reverse=True) + return distributions[0] + + def get_same_name_and_version(self): + """Return lists of PyPIDistribution objects that refer to the same + name and version number. This do not consider the type (source, binary, + etc.)""" + processed = [] + duplicates = [] + for dist in self: + if (dist.name, dist.version) not in processed: + processed.append((dist.name, dist.version)) + found_duplicates = [d for d in self if d.name == dist.name and + d.version == dist.version] + if len(found_duplicates) > 1: + duplicates.append(found_duplicates) + return duplicates + + def append(self, o): + """Append a new distribution to the list. + + If a distribution with the same name and version exists, just grab the + URL informations and add a new new url for the existing one. + """ + similar_dists = [d for d in self if d.name == o.name and + d.version == o.version and d.type == o.type] + if len(similar_dists) > 0: + dist = similar_dists[0] + dist.add_url(**o.url) + else: + super(PyPIDistributions, self).append(o) + + def sort_distributions(self, prefer_source=True, prefer_final=False, + reverse=True, *args, **kwargs): + """order the results with the given properties""" + + sort_by = [] + if prefer_final: + sort_by.append("is_final") + sort_by.append("version") + + if prefer_source: + sort_by.append("is_source") + + super(PyPIDistributions, self).sort( + key=lambda i: [getattr(i, arg) for arg in sort_by], + reverse=reverse, *args, **kwargs) + + +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 != "" and name != "": + return (name.lower(), version) + else: + raise CantParseArchiveName(archive_name) diff --git a/src/distutils2/pypi/errors.py b/src/distutils2/pypi/errors.py new file mode 100644 index 0000000..c9b232d --- /dev/null +++ b/src/distutils2/pypi/errors.py @@ -0,0 +1,33 @@ +"""distutils2.pypi.errors + +All errors and exceptions raised by PyPiIndex classes. +""" +from distutils2.errors import DistutilsError + + +class PyPIError(DistutilsError): + """The base class for errors of the pypi python package.""" + + +class DistributionNotFound(PyPIError): + """No distribution match the given requirements.""" + + +class CantParseArchiveName(PyPIError): + """An archive name can't be parsed to find distribution name and version""" + + +class DownloadError(PyPIError): + """An error has occurs while downloading""" + + +class HashDoesNotMatch(DownloadError): + """Compared hashes does not match""" + + +class UnsupportedHashName(PyPIError): + """A unsupported hashname has been used""" + + +class UnableToDownload(PyPIError): + """All mirrors have been tried, without success""" diff --git a/src/distutils2/pypi/pkg_resources.py b/src/distutils2/pypi/pkg_resources.py deleted file mode 100644 index 30dbc18..0000000 --- a/src/distutils2/pypi/pkg_resources.py +++ /dev/null @@ -1,2693 +0,0 @@ -"""Package resource API --------------------- - -A resource is a logical file contained within a package, or a logical -subdirectory thereof. The package resource API expects resource names -to have their path parts separated with ``/``, *not* whatever the local -path separator is. Do not use os.path operations to manipulate resource -names being passed into the API. - -The package resource API is designed to work with normal filesystem packages, -.egg files, and unpacked .egg files. It can also work in a limited way with -.zip files and with custom PEP 302 loaders that support the ``get_data()`` -method. -""" - -import sys, os, zipimport, time, re, imp, types -from urlparse import urlparse, urlunparse - -try: - frozenset -except NameError: - from sets import ImmutableSet as frozenset - -# capture these to bypass sandboxing -from os import utime -try: - from os import mkdir, rename, unlink - WRITE_SUPPORT = True -except ImportError: - # no write support, probably under GAE - WRITE_SUPPORT = False - -from os import open as os_open -from os.path import isdir, split - -# This marker is used to simplify the process that checks is the -# setuptools package was installed by the Setuptools project -# or by the Distribute project, in case Setuptools creates -# a distribution with the same version. -# -# The bootstrapping script for instance, will check if this -# attribute is present to decide wether to reinstall the package -_distribute = True - -def _bypass_ensure_directory(name, mode=0777): - # Sandbox-bypassing version of ensure_directory() - if not WRITE_SUPPORT: - raise IOError('"os.mkdir" not supported on this platform.') - dirname, filename = split(name) - if dirname and filename and not isdir(dirname): - _bypass_ensure_directory(dirname) - mkdir(dirname, mode) - - - - - - - - -def get_supported_platform(): - """Return this platform's maximum compatible version. - - distutils.util.get_platform() normally reports the minimum version - of Mac OS X that would be required to *use* extensions produced by - distutils. But what we want when checking compatibility is to know the - version of Mac OS X that we are *running*. To allow usage of packages that - explicitly require a newer version of Mac OS X, we must also know the - current version of the OS. - - If this condition occurs for any other platform with a version in its - platform strings, this function should be extended accordingly. - """ - plat = get_build_platform(); m = macosVersionString.match(plat) - if m is not None and sys.platform == "darwin": - try: - plat = 'macosx-%s-%s' % ('.'.join(_macosx_vers()[:2]), m.group(3)) - except ValueError: - pass # not Mac OS X - return plat - - - - - - - - - - - - - - - - - - - - - -__all__ = [ - # Basic resource access and distribution/entry point discovery - 'require', 'run_script', 'get_provider', 'get_distribution', - 'load_entry_point', 'get_entry_map', 'get_entry_info', 'iter_entry_points', - 'resource_string', 'resource_stream', 'resource_filename', - 'resource_listdir', 'resource_exists', 'resource_isdir', - - # Environmental control - 'declare_namespace', 'working_set', 'add_activation_listener', - 'find_distributions', 'set_extraction_path', 'cleanup_resources', - 'get_default_cache', - - # Primary implementation classes - 'Environment', 'WorkingSet', 'ResourceManager', - 'Distribution', 'Requirement', 'EntryPoint', - - # Exceptions - 'ResolutionError','VersionConflict','DistributionNotFound','UnknownExtra', - 'ExtractionError', - - # Parsing functions and string utilities - 'parse_requirements', 'parse_version', 'safe_name', 'safe_version', - 'get_platform', 'compatible_platforms', 'yield_lines', 'split_sections', - 'safe_extra', 'to_filename', - - # filesystem utilities - 'ensure_directory', 'normalize_path', - - # Distribution "precedence" constants - 'EGG_DIST', 'BINARY_DIST', 'SOURCE_DIST', 'CHECKOUT_DIST', 'DEVELOP_DIST', - - # "Provider" interfaces, implementations, and registration/lookup APIs - 'IMetadataProvider', 'IResourceProvider', 'FileMetadata', - 'PathMetadata', 'EggMetadata', 'EmptyProvider', 'empty_provider', - 'NullProvider', 'EggProvider', 'DefaultProvider', 'ZipProvider', - 'register_finder', 'register_namespace_handler', 'register_loader_type', - 'fixup_namespace_packages', 'get_importer', - - # Deprecated/backward compatibility only - 'run_main', 'AvailableDistributions', -] -class ResolutionError(Exception): - """Abstract base for dependency resolution errors""" - def __repr__(self): - return self.__class__.__name__+repr(self.args) - -class VersionConflict(ResolutionError): - """An already-installed version conflicts with the requested version""" - -class DistributionNotFound(ResolutionError): - """A requested distribution was not found""" - -class UnknownExtra(ResolutionError): - """Distribution doesn't have an "extra feature" of the given name""" -_provider_factories = {} - -PY_MAJOR = sys.version[:3] -EGG_DIST = 3 -BINARY_DIST = 2 -SOURCE_DIST = 1 -CHECKOUT_DIST = 0 -DEVELOP_DIST = -1 - -def register_loader_type(loader_type, provider_factory): - """Register `provider_factory` to make providers for `loader_type` - - `loader_type` is the type or class of a PEP 302 ``module.__loader__``, - and `provider_factory` is a function that, passed a *module* object, - returns an ``IResourceProvider`` for that module. - """ - _provider_factories[loader_type] = provider_factory - -def get_provider(moduleOrReq): - """Return an IResourceProvider for the named module or requirement""" - if isinstance(moduleOrReq,Requirement): - return working_set.find(moduleOrReq) or require(str(moduleOrReq))[0] - try: - module = sys.modules[moduleOrReq] - except KeyError: - __import__(moduleOrReq) - module = sys.modules[moduleOrReq] - loader = getattr(module, '__loader__', None) - return _find_adapter(_provider_factories, loader)(module) - -def _macosx_vers(_cache=[]): - if not _cache: - import platform - version = platform.mac_ver()[0] - # fallback for MacPorts - if version == '': - import plistlib - plist = '/System/Library/CoreServices/SystemVersion.plist' - if os.path.exists(plist): - if hasattr(plistlib, 'readPlist'): - plist_content = plistlib.readPlist(plist) - if 'ProductVersion' in plist_content: - version = plist_content['ProductVersion'] - - _cache.append(version.split('.')) - return _cache[0] - -def _macosx_arch(machine): - return {'PowerPC':'ppc', 'Power_Macintosh':'ppc'}.get(machine,machine) - -def get_build_platform(): - """Return this platform's string for platform-specific distributions - - XXX Currently this is the same as ``distutils.util.get_platform()``, but it - needs some hacks for Linux and Mac OS X. - """ - try: - from distutils.util import get_platform - except ImportError: - from sysconfig import get_platform - - plat = get_platform() - if sys.platform == "darwin" and not plat.startswith('macosx-'): - try: - version = _macosx_vers() - machine = os.uname()[4].replace(" ", "_") - return "macosx-%d.%d-%s" % (int(version[0]), int(version[1]), - _macosx_arch(machine)) - except ValueError: - # if someone is running a non-Mac darwin system, this will fall - # through to the default implementation - pass - return plat - -macosVersionString = re.compile(r"macosx-(\d+)\.(\d+)-(.*)") -darwinVersionString = re.compile(r"darwin-(\d+)\.(\d+)\.(\d+)-(.*)") -get_platform = get_build_platform # XXX backward compat - -def compatible_platforms(provided,required): - """Can code for the `provided` platform run on the `required` platform? - - Returns true if either platform is ``None``, or the platforms are equal. - - XXX Needs compatibility checks for Linux and other unixy OSes. - """ - if provided is None or required is None or provided==required: - return True # easy case - - # Mac OS X special cases - reqMac = macosVersionString.match(required) - if reqMac: - provMac = macosVersionString.match(provided) - - # is this a Mac package? - if not provMac: - # this is backwards compatibility for packages built before - # setuptools 0.6. All packages built after this point will - # use the new macosx designation. - provDarwin = darwinVersionString.match(provided) - if provDarwin: - dversion = int(provDarwin.group(1)) - macosversion = "%s.%s" % (reqMac.group(1), reqMac.group(2)) - if dversion == 7 and macosversion >= "10.3" or \ - dversion == 8 and macosversion >= "10.4": - - #import warnings - #warnings.warn("Mac eggs should be rebuilt to " - # "use the macosx designation instead of darwin.", - # category=DeprecationWarning) - return True - return False # egg isn't macosx or legacy darwin - - # are they the same major version and machine type? - if provMac.group(1) != reqMac.group(1) or \ - provMac.group(3) != reqMac.group(3): - return False - - - - # is the required OS major update >= the provided one? - if int(provMac.group(2)) > int(reqMac.group(2)): - return False - - return True - - # XXX Linux and other platforms' special cases should go here - return False - - -def run_script(dist_spec, script_name): - """Locate distribution `dist_spec` and run its `script_name` script""" - ns = sys._getframe(1).f_globals - name = ns['__name__'] - ns.clear() - ns['__name__'] = name - require(dist_spec)[0].run_script(script_name, ns) - -run_main = run_script # backward compatibility - -def get_distribution(dist): - """Return a current distribution object for a Requirement or string""" - if isinstance(dist,basestring): dist = Requirement.parse(dist) - if isinstance(dist,Requirement): dist = get_provider(dist) - if not isinstance(dist,Distribution): - raise TypeError("Expected string, Requirement, or Distribution", dist) - return dist - -def load_entry_point(dist, group, name): - """Return `name` entry point of `group` for `dist` or raise ImportError""" - return get_distribution(dist).load_entry_point(group, name) - -def get_entry_map(dist, group=None): - """Return the entry point map for `group`, or the full entry map""" - return get_distribution(dist).get_entry_map(group) - -def get_entry_info(dist, group, name): - """Return the EntryPoint object for `group`+`name`, or ``None``""" - return get_distribution(dist).get_entry_info(group, name) - - -class IMetadataProvider: - - def has_metadata(name): - """Does the package's distribution contain the named metadata?""" - - def get_metadata(name): - """The named metadata resource as a string""" - - def get_metadata_lines(name): - """Yield named metadata resource as list of non-blank non-comment lines - - Leading and trailing whitespace is stripped from each line, and lines - with ``#`` as the first non-blank character are omitted.""" - - def metadata_isdir(name): - """Is the named metadata a directory? (like ``os.path.isdir()``)""" - - def metadata_listdir(name): - """List of metadata names in the directory (like ``os.listdir()``)""" - - def run_script(script_name, namespace): - """Execute the named script in the supplied namespace dictionary""" - - - - - - - - - - -class IResourceProvider(IMetadataProvider): - """An object that provides access to package resources""" - - def get_resource_filename(manager, resource_name): - """Return a true filesystem path for `resource_name` - - `manager` must be an ``IResourceManager``""" - - def get_resource_stream(manager, resource_name): - """Return a readable file-like object for `resource_name` - - `manager` must be an ``IResourceManager``""" - - def get_resource_string(manager, resource_name): - """Return a string containing the contents of `resource_name` - - `manager` must be an ``IResourceManager``""" - - def has_resource(resource_name): - """Does the package contain the named resource?""" - - def resource_isdir(resource_name): - """Is the named resource a directory? (like ``os.path.isdir()``)""" - - def resource_listdir(resource_name): - """List of resource names in the directory (like ``os.listdir()``)""" - - - - - - - - - - - - - - - -class WorkingSet(object): - """A collection of active distributions on sys.path (or a similar list)""" - - def __init__(self, entries=None): - """Create working set from list of path entries (default=sys.path)""" - self.entries = [] - self.entry_keys = {} - self.by_key = {} - self.callbacks = [] - - if entries is None: - entries = sys.path - - for entry in entries: - self.add_entry(entry) - - - def add_entry(self, entry): - """Add a path item to ``.entries``, finding any distributions on it - - ``find_distributions(entry,True)`` is used to find distributions - corresponding to the path entry, and they are added. `entry` is - always appended to ``.entries``, even if it is already present. - (This is because ``sys.path`` can contain the same value more than - once, and the ``.entries`` of the ``sys.path`` WorkingSet should always - equal ``sys.path``.) - """ - self.entry_keys.setdefault(entry, []) - self.entries.append(entry) - for dist in find_distributions(entry, True): - self.add(dist, entry, False) - - - def __contains__(self,dist): - """True if `dist` is the active distribution for its project""" - return self.by_key.get(dist.key) == dist - - - - - - def find(self, req): - """Find a distribution matching requirement `req` - - If there is an active distribution for the requested project, this - returns it as long as it meets the version requirement specified by - `req`. But, if there is an active distribution for the project and it - does *not* meet the `req` requirement, ``VersionConflict`` is raised. - If there is no active distribution for the requested project, ``None`` - is returned. - """ - dist = self.by_key.get(req.key) - if dist is not None and dist not in req: - raise VersionConflict(dist,req) # XXX add more info - else: - return dist - - def iter_entry_points(self, group, name=None): - """Yield entry point objects from `group` matching `name` - - If `name` is None, yields all entry points in `group` from all - distributions in the working set, otherwise only ones matching - both `group` and `name` are yielded (in distribution order). - """ - for dist in self: - entries = dist.get_entry_map(group) - if name is None: - for ep in entries.values(): - yield ep - elif name in entries: - yield entries[name] - - def run_script(self, requires, script_name): - """Locate distribution for `requires` and run `script_name` script""" - ns = sys._getframe(1).f_globals - name = ns['__name__'] - ns.clear() - ns['__name__'] = name - self.require(requires)[0].run_script(script_name, ns) - - - - def __iter__(self): - """Yield distributions for non-duplicate projects in the working set - - The yield order is the order in which the items' path entries were - added to the working set. - """ - seen = {} - for item in self.entries: - for key in self.entry_keys[item]: - if key not in seen: - seen[key]=1 - yield self.by_key[key] - - def add(self, dist, entry=None, insert=True): - """Add `dist` to working set, associated with `entry` - - If `entry` is unspecified, it defaults to the ``.location`` of `dist`. - On exit from this routine, `entry` is added to the end of the working - set's ``.entries`` (if it wasn't already present). - - `dist` is only added to the working set if it's for a project that - doesn't already have a distribution in the set. If it's added, any - callbacks registered with the ``subscribe()`` method will be called. - """ - if insert: - dist.insert_on(self.entries, entry) - - if entry is None: - entry = dist.location - keys = self.entry_keys.setdefault(entry,[]) - keys2 = self.entry_keys.setdefault(dist.location,[]) - if dist.key in self.by_key: - return # ignore hidden distros - - self.by_key[dist.key] = dist - if dist.key not in keys: - keys.append(dist.key) - if dist.key not in keys2: - keys2.append(dist.key) - self._added_new(dist) - - def resolve(self, requirements, env=None, installer=None, replacement=True): - """List all distributions needed to (recursively) meet `requirements` - - `requirements` must be a sequence of ``Requirement`` objects. `env`, - if supplied, should be an ``Environment`` instance. If - not supplied, it defaults to all distributions available within any - entry or distribution in the working set. `installer`, if supplied, - will be invoked with each requirement that cannot be met by an - already-installed distribution; it should return a ``Distribution`` or - ``None``. - """ - - requirements = list(requirements)[::-1] # set up the stack - processed = {} # set of processed requirements - best = {} # key -> dist - to_activate = [] - - while requirements: - req = requirements.pop(0) # process dependencies breadth-first - if _override_setuptools(req) and replacement: - req = Requirement.parse('distribute') - - if req in processed: - # Ignore cyclic or redundant dependencies - continue - dist = best.get(req.key) - if dist is None: - # Find the best distribution and add it to the map - dist = self.by_key.get(req.key) - if dist is None: - if env is None: - env = Environment(self.entries) - dist = best[req.key] = env.best_match(req, self, installer) - if dist is None: - #msg = ("The '%s' distribution was not found on this " - # "system, and is required by this application.") - #raise DistributionNotFound(msg % req) - - # unfortunately, zc.buildout uses a str(err) - # to get the name of the distribution here.. - raise DistributionNotFound(req) - to_activate.append(dist) - if dist not in req: - # Oops, the "best" so far conflicts with a dependency - raise VersionConflict(dist,req) # XXX put more info here - requirements.extend(dist.requires(req.extras)[::-1]) - processed[req] = True - - return to_activate # return list of distros to activate - - def find_plugins(self, - plugin_env, full_env=None, installer=None, fallback=True - ): - """Find all activatable distributions in `plugin_env` - - Example usage:: - - distributions, errors = working_set.find_plugins( - Environment(plugin_dirlist) - ) - map(working_set.add, distributions) # add plugins+libs to sys.path - print 'Could not load', errors # display errors - - The `plugin_env` should be an ``Environment`` instance that contains - only distributions that are in the project's "plugin directory" or - directories. The `full_env`, if supplied, should be an ``Environment`` - contains all currently-available distributions. If `full_env` is not - supplied, one is created automatically from the ``WorkingSet`` this - method is called on, which will typically mean that every directory on - ``sys.path`` will be scanned for distributions. - - `installer` is a standard installer callback as used by the - ``resolve()`` method. The `fallback` flag indicates whether we should - attempt to resolve older versions of a plugin if the newest version - cannot be resolved. - - This method returns a 2-tuple: (`distributions`, `error_info`), where - `distributions` is a list of the distributions found in `plugin_env` - that were loadable, along with any other distributions that are needed - to resolve their dependencies. `error_info` is a dictionary mapping - unloadable plugin distributions to an exception instance describing the - error that occurred. Usually this will be a ``DistributionNotFound`` or - ``VersionConflict`` instance. - """ - - plugin_projects = list(plugin_env) - plugin_projects.sort() # scan project names in alphabetic order - - error_info = {} - distributions = {} - - if full_env is None: - env = Environment(self.entries) - env += plugin_env - else: - env = full_env + plugin_env - - shadow_set = self.__class__([]) - map(shadow_set.add, self) # put all our entries in shadow_set - - for project_name in plugin_projects: - - for dist in plugin_env[project_name]: - - req = [dist.as_requirement()] - - try: - resolvees = shadow_set.resolve(req, env, installer) - - except ResolutionError,v: - error_info[dist] = v # save error info - if fallback: - continue # try the next older version of project - else: - break # give up on this project, keep going - - else: - map(shadow_set.add, resolvees) - distributions.update(dict.fromkeys(resolvees)) - - # success, no need to try any more versions of this project - break - - distributions = list(distributions) - distributions.sort() - - return distributions, error_info - - - - - - def require(self, *requirements): - """Ensure that distributions matching `requirements` are activated - - `requirements` must be a string or a (possibly-nested) sequence - thereof, specifying the distributions and versions required. The - return value is a sequence of the distributions that needed to be - activated to fulfill the requirements; all relevant distributions are - included, even if they were already activated in this working set. - """ - - needed = self.resolve(parse_requirements(requirements)) - - for dist in needed: - self.add(dist) - - return needed - - - def subscribe(self, callback): - """Invoke `callback` for all distributions (including existing ones)""" - if callback in self.callbacks: - return - self.callbacks.append(callback) - for dist in self: - callback(dist) - - - def _added_new(self, dist): - for callback in self.callbacks: - callback(dist) - - - - - - - - - - - -class Environment(object): - """Searchable snapshot of distributions on a search path""" - - def __init__(self, search_path=None, platform=get_supported_platform(), python=PY_MAJOR): - """Snapshot distributions available on a search path - - Any distributions found on `search_path` are added to the environment. - `search_path` should be a sequence of ``sys.path`` items. If not - supplied, ``sys.path`` is used. - - `platform` is an optional string specifying the name of the platform - that platform-specific distributions must be compatible with. If - unspecified, it defaults to the current platform. `python` is an - optional string naming the desired version of Python (e.g. ``'2.4'``); - it defaults to the current version. - - You may explicitly set `platform` (and/or `python`) to ``None`` if you - wish to map *all* distributions, not just those compatible with the - running platform or Python version. - """ - self._distmap = {} - self._cache = {} - self.platform = platform - self.python = python - self.scan(search_path) - - def can_add(self, dist): - """Is distribution `dist` acceptable for this environment? - - The distribution must match the platform and python version - requirements specified when this environment was created, or False - is returned. - """ - return (self.python is None or dist.py_version is None - or dist.py_version==self.python) \ - and compatible_platforms(dist.platform,self.platform) - - def remove(self, dist): - """Remove `dist` from the environment""" - self._distmap[dist.key].remove(dist) - - def scan(self, search_path=None): - """Scan `search_path` for distributions usable in this environment - - Any distributions found are added to the environment. - `search_path` should be a sequence of ``sys.path`` items. If not - supplied, ``sys.path`` is used. Only distributions conforming to - the platform/python version defined at initialization are added. - """ - if search_path is None: - search_path = sys.path - - for item in search_path: - for dist in find_distributions(item): - self.add(dist) - - def __getitem__(self,project_name): - """Return a newest-to-oldest list of distributions for `project_name` - """ - try: - return self._cache[project_name] - except KeyError: - project_name = project_name.lower() - if project_name not in self._distmap: - return [] - - if project_name not in self._cache: - dists = self._cache[project_name] = self._distmap[project_name] - _sort_dists(dists) - - return self._cache[project_name] - - def add(self,dist): - """Add `dist` if we ``can_add()`` it and it isn't already added""" - if self.can_add(dist) and dist.has_version(): - dists = self._distmap.setdefault(dist.key,[]) - if dist not in dists: - dists.append(dist) - if dist.key in self._cache: - _sort_dists(self._cache[dist.key]) - - - def best_match(self, req, working_set, installer=None): - """Find distribution best matching `req` and usable on `working_set` - - This calls the ``find(req)`` method of the `working_set` to see if a - suitable distribution is already active. (This may raise - ``VersionConflict`` if an unsuitable version of the project is already - active in the specified `working_set`.) If a suitable distribution - isn't active, this method returns the newest distribution in the - environment that meets the ``Requirement`` in `req`. If no suitable - distribution is found, and `installer` is supplied, then the result of - calling the environment's ``obtain(req, installer)`` method will be - returned. - """ - dist = working_set.find(req) - if dist is not None: - return dist - for dist in self[req.key]: - if dist in req: - return dist - return self.obtain(req, installer) # try and download/install - - def obtain(self, requirement, installer=None): - """Obtain a distribution matching `requirement` (e.g. via download) - - Obtain a distro that matches requirement (e.g. via download). In the - base ``Environment`` class, this routine just returns - ``installer(requirement)``, unless `installer` is None, in which case - None is returned instead. This method is a hook that allows subclasses - to attempt other ways of obtaining a distribution before falling back - to the `installer` argument.""" - if installer is not None: - return installer(requirement) - - def __iter__(self): - """Yield the unique project names of the available distributions""" - for key in self._distmap.keys(): - if self[key]: yield key - - - - - def __iadd__(self, other): - """In-place addition of a distribution or environment""" - if isinstance(other,Distribution): - self.add(other) - elif isinstance(other,Environment): - for project in other: - for dist in other[project]: - self.add(dist) - else: - raise TypeError("Can't add %r to environment" % (other,)) - return self - - def __add__(self, other): - """Add an environment or distribution to an environment""" - new = self.__class__([], platform=None, python=None) - for env in self, other: - new += env - return new - - -AvailableDistributions = Environment # XXX backward compatibility - - -class ExtractionError(RuntimeError): - """An error occurred extracting a resource - - The following attributes are available from instances of this exception: - - manager - The resource manager that raised this exception - - cache_path - The base directory for resource extraction - - original_error - The exception instance that caused extraction to fail - """ - - - - -class ResourceManager: - """Manage resource extraction and packages""" - extraction_path = None - - def __init__(self): - self.cached_files = {} - - def resource_exists(self, package_or_requirement, resource_name): - """Does the named resource exist?""" - return get_provider(package_or_requirement).has_resource(resource_name) - - def resource_isdir(self, package_or_requirement, resource_name): - """Is the named resource an existing directory?""" - return get_provider(package_or_requirement).resource_isdir( - resource_name - ) - - def resource_filename(self, package_or_requirement, resource_name): - """Return a true filesystem path for specified resource""" - return get_provider(package_or_requirement).get_resource_filename( - self, resource_name - ) - - def resource_stream(self, package_or_requirement, resource_name): - """Return a readable file-like object for specified resource""" - return get_provider(package_or_requirement).get_resource_stream( - self, resource_name - ) - - def resource_string(self, package_or_requirement, resource_name): - """Return specified resource as a string""" - return get_provider(package_or_requirement).get_resource_string( - self, resource_name - ) - - def resource_listdir(self, package_or_requirement, resource_name): - """List the contents of the named resource directory""" - return get_provider(package_or_requirement).resource_listdir( - resource_name - ) - - def extraction_error(self): - """Give an error message for problems extracting file(s)""" - - old_exc = sys.exc_info()[1] - cache_path = self.extraction_path or get_default_cache() - - err = ExtractionError("""Can't extract file(s) to egg cache - -The following error occurred while trying to extract file(s) to the Python egg -cache: - - %s - -The Python egg cache directory is currently set to: - - %s - -Perhaps your account does not have write access to this directory? You can -change the cache directory by setting the PYTHON_EGG_CACHE environment -variable to point to an accessible directory. -""" % (old_exc, cache_path) - ) - err.manager = self - err.cache_path = cache_path - err.original_error = old_exc - raise err - - - - - - - - - - - - - - - - def get_cache_path(self, archive_name, names=()): - """Return absolute location in cache for `archive_name` and `names` - - The parent directory of the resulting path will be created if it does - not already exist. `archive_name` should be the base filename of the - enclosing egg (which may not be the name of the enclosing zipfile!), - including its ".egg" extension. `names`, if provided, should be a - sequence of path name parts "under" the egg's extraction location. - - This method should only be called by resource providers that need to - obtain an extraction location, and only for names they intend to - extract, as it tracks the generated names for possible cleanup later. - """ - extract_path = self.extraction_path or get_default_cache() - target_path = os.path.join(extract_path, archive_name+'-tmp', *names) - try: - _bypass_ensure_directory(target_path) - except: - self.extraction_error() - - self.cached_files[target_path] = 1 - return target_path - - - - - - - - - - - - - - - - - - - - def postprocess(self, tempname, filename): - """Perform any platform-specific postprocessing of `tempname` - - This is where Mac header rewrites should be done; other platforms don't - have anything special they should do. - - Resource providers should call this method ONLY after successfully - extracting a compressed resource. They must NOT call it on resources - that are already in the filesystem. - - `tempname` is the current (temporary) name of the file, and `filename` - is the name it will be renamed to by the caller after this routine - returns. - """ - - if os.name == 'posix': - # Make the resource executable - mode = ((os.stat(tempname).st_mode) | 0555) & 07777 - os.chmod(tempname, mode) - - - - - - - - - - - - - - - - - - - - - - - def set_extraction_path(self, path): - """Set the base path where resources will be extracted to, if needed. - - If you do not call this routine before any extractions take place, the - path defaults to the return value of ``get_default_cache()``. (Which - is based on the ``PYTHON_EGG_CACHE`` environment variable, with various - platform-specific fallbacks. See that routine's documentation for more - details.) - - Resources are extracted to subdirectories of this path based upon - information given by the ``IResourceProvider``. You may set this to a - temporary directory, but then you must call ``cleanup_resources()`` to - delete the extracted files when done. There is no guarantee that - ``cleanup_resources()`` will be able to remove all extracted files. - - (Note: you may not change the extraction path for a given resource - manager once resources have been extracted, unless you first call - ``cleanup_resources()``.) - """ - if self.cached_files: - raise ValueError( - "Can't change extraction path, files already extracted" - ) - - self.extraction_path = path - - def cleanup_resources(self, force=False): - """ - Delete all extracted resource files and directories, returning a list - of the file and directory names that could not be successfully removed. - This function does not have any concurrency protection, so it should - generally only be called when the extraction path is a temporary - directory exclusive to a single process. This method is not - automatically called; you must call it explicitly or register it as an - ``atexit`` function if you wish to ensure cleanup of a temporary - directory used for extractions. - """ - # XXX - - - -def get_default_cache(): - """Determine the default cache location - - This returns the ``PYTHON_EGG_CACHE`` environment variable, if set. - Otherwise, on Windows, it returns a "Python-Eggs" subdirectory of the - "Application Data" directory. On all other systems, it's "~/.python-eggs". - """ - try: - return os.environ['PYTHON_EGG_CACHE'] - except KeyError: - pass - - if os.name!='nt': - return os.path.expanduser('~/.python-eggs') - - app_data = 'Application Data' # XXX this may be locale-specific! - app_homes = [ - (('APPDATA',), None), # best option, should be locale-safe - (('USERPROFILE',), app_data), - (('HOMEDRIVE','HOMEPATH'), app_data), - (('HOMEPATH',), app_data), - (('HOME',), None), - (('WINDIR',), app_data), # 95/98/ME - ] - - for keys, subdir in app_homes: - dirname = '' - for key in keys: - if key in os.environ: - dirname = os.path.join(dirname, os.environ[key]) - else: - break - else: - if subdir: - dirname = os.path.join(dirname,subdir) - return os.path.join(dirname, 'Python-Eggs') - else: - raise RuntimeError( - "Please set the PYTHON_EGG_CACHE enviroment variable" - ) - -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 safe_extra(extra): - """Convert an arbitrary string to a standard 'extra' name - - Any runs of non-alphanumeric characters are replaced with a single '_', - and the result is always lowercased. - """ - return re.sub('[^A-Za-z0-9.]+', '_', extra).lower() - - -def to_filename(name): - """Convert a project or version name to its filename-escaped form - - Any '-' characters are currently replaced with '_'. - """ - return name.replace('-','_') - - - - - - - - -class NullProvider: - """Try to implement resources and metadata for arbitrary PEP 302 loaders""" - - egg_name = None - egg_info = None - loader = None - - def __init__(self, module): - self.loader = getattr(module, '__loader__', None) - self.module_path = os.path.dirname(getattr(module, '__file__', '')) - - def get_resource_filename(self, manager, resource_name): - return self._fn(self.module_path, resource_name) - - def get_resource_stream(self, manager, resource_name): - return StringIO(self.get_resource_string(manager, resource_name)) - - def get_resource_string(self, manager, resource_name): - return self._get(self._fn(self.module_path, resource_name)) - - def has_resource(self, resource_name): - return self._has(self._fn(self.module_path, resource_name)) - - def has_metadata(self, name): - return self.egg_info and self._has(self._fn(self.egg_info,name)) - - if sys.version_info <= (3,): - def get_metadata(self, name): - if not self.egg_info: - return "" - return self._get(self._fn(self.egg_info,name)) - else: - def get_metadata(self, name): - if not self.egg_info: - return "" - return self._get(self._fn(self.egg_info,name)).decode("utf-8") - - def get_metadata_lines(self, name): - return yield_lines(self.get_metadata(name)) - - def resource_isdir(self,resource_name): - return self._isdir(self._fn(self.module_path, resource_name)) - - def metadata_isdir(self,name): - return self.egg_info and self._isdir(self._fn(self.egg_info,name)) - - - def resource_listdir(self,resource_name): - return self._listdir(self._fn(self.module_path,resource_name)) - - def metadata_listdir(self,name): - if self.egg_info: - return self._listdir(self._fn(self.egg_info,name)) - return [] - - def run_script(self,script_name,namespace): - script = 'scripts/'+script_name - if not self.has_metadata(script): - raise ResolutionError("No script named %r" % script_name) - script_text = self.get_metadata(script).replace('\r\n','\n') - script_text = script_text.replace('\r','\n') - script_filename = self._fn(self.egg_info,script) - namespace['__file__'] = script_filename - if os.path.exists(script_filename): - execfile(script_filename, namespace, namespace) - else: - from linecache import cache - cache[script_filename] = ( - len(script_text), 0, script_text.split('\n'), script_filename - ) - script_code = compile(script_text,script_filename,'exec') - exec script_code in namespace, namespace - - def _has(self, path): - raise NotImplementedError( - "Can't perform this operation for unregistered loader type" - ) - - def _isdir(self, path): - raise NotImplementedError( - "Can't perform this operation for unregistered loader type" - ) - - def _listdir(self, path): - raise NotImplementedError( - "Can't perform this operation for unregistered loader type" - ) - - def _fn(self, base, resource_name): - if resource_name: - return os.path.join(base, *resource_name.split('/')) - return base - - def _get(self, path): - if hasattr(self.loader, 'get_data'): - return self.loader.get_data(path) - raise NotImplementedError( - "Can't perform this operation for loaders without 'get_data()'" - ) - -register_loader_type(object, NullProvider) - - -class EggProvider(NullProvider): - """Provider based on a virtual filesystem""" - - def __init__(self,module): - NullProvider.__init__(self,module) - self._setup_prefix() - - def _setup_prefix(self): - # we assume here that our metadata may be nested inside a "basket" - # of multiple eggs; that's why we use module_path instead of .archive - path = self.module_path - old = None - while path!=old: - if path.lower().endswith('.egg'): - self.egg_name = os.path.basename(path) - self.egg_info = os.path.join(path, 'EGG-INFO') - self.egg_root = path - break - old = path - path, base = os.path.split(path) - - - - - - -class DefaultProvider(EggProvider): - """Provides access to package resources in the filesystem""" - - def _has(self, path): - return os.path.exists(path) - - def _isdir(self,path): - return os.path.isdir(path) - - def _listdir(self,path): - return os.listdir(path) - - def get_resource_stream(self, manager, resource_name): - return open(self._fn(self.module_path, resource_name), 'rb') - - def _get(self, path): - stream = open(path, 'rb') - try: - return stream.read() - finally: - stream.close() - -register_loader_type(type(None), DefaultProvider) - - -class EmptyProvider(NullProvider): - """Provider that returns nothing for all requests""" - - _isdir = _has = lambda self,path: False - _get = lambda self,path: '' - _listdir = lambda self,path: [] - module_path = None - - def __init__(self): - pass - -empty_provider = EmptyProvider() - - - - -class ZipProvider(EggProvider): - """Resource support for zips and eggs""" - - eagers = None - - def __init__(self, module): - EggProvider.__init__(self,module) - self.zipinfo = zipimport._zip_directory_cache[self.loader.archive] - self.zip_pre = self.loader.archive+os.sep - - def _zipinfo_name(self, fspath): - # Convert a virtual filename (full path to file) into a zipfile subpath - # usable with the zipimport directory cache for our target archive - if fspath.startswith(self.zip_pre): - return fspath[len(self.zip_pre):] - raise AssertionError( - "%s is not a subpath of %s" % (fspath,self.zip_pre) - ) - - def _parts(self,zip_path): - # Convert a zipfile subpath into an egg-relative path part list - fspath = self.zip_pre+zip_path # pseudo-fs path - if fspath.startswith(self.egg_root+os.sep): - return fspath[len(self.egg_root)+1:].split(os.sep) - raise AssertionError( - "%s is not a subpath of %s" % (fspath,self.egg_root) - ) - - def get_resource_filename(self, manager, resource_name): - if not self.egg_name: - raise NotImplementedError( - "resource_filename() only supported for .egg, not .zip" - ) - # no need to lock for extraction, since we use temp names - zip_path = self._resource_to_zip(resource_name) - eagers = self._get_eager_resources() - if '/'.join(self._parts(zip_path)) in eagers: - for name in eagers: - self._extract_resource(manager, self._eager_to_zip(name)) - return self._extract_resource(manager, zip_path) - - def _extract_resource(self, manager, zip_path): - - if zip_path in self._index(): - for name in self._index()[zip_path]: - last = self._extract_resource( - manager, os.path.join(zip_path, name) - ) - return os.path.dirname(last) # return the extracted directory name - - zip_stat = self.zipinfo[zip_path] - t,d,size = zip_stat[5], zip_stat[6], zip_stat[3] - date_time = ( - (d>>9)+1980, (d>>5)&0xF, d&0x1F, # ymd - (t&0xFFFF)>>11, (t>>5)&0x3F, (t&0x1F) * 2, 0, 0, -1 # hms, etc. - ) - timestamp = time.mktime(date_time) - - try: - if not WRITE_SUPPORT: - raise IOError('"os.rename" and "os.unlink" are not supported ' - 'on this platform') - - real_path = manager.get_cache_path( - self.egg_name, self._parts(zip_path) - ) - - if os.path.isfile(real_path): - stat = os.stat(real_path) - if stat.st_size==size and stat.st_mtime==timestamp: - # size and stamp match, don't bother extracting - return real_path - - outf, tmpnam = _mkstemp(".$extract", dir=os.path.dirname(real_path)) - os.write(outf, self.loader.get_data(zip_path)) - os.close(outf) - utime(tmpnam, (timestamp,timestamp)) - manager.postprocess(tmpnam, real_path) - - try: - rename(tmpnam, real_path) - - except os.error: - if os.path.isfile(real_path): - stat = os.stat(real_path) - - if stat.st_size==size and stat.st_mtime==timestamp: - # size and stamp match, somebody did it just ahead of - # us, so we're done - return real_path - elif os.name=='nt': # Windows, del old file and retry - unlink(real_path) - rename(tmpnam, real_path) - return real_path - raise - - except os.error: - manager.extraction_error() # report a user-friendly error - - return real_path - - def _get_eager_resources(self): - if self.eagers is None: - eagers = [] - for name in ('native_libs.txt', 'eager_resources.txt'): - if self.has_metadata(name): - eagers.extend(self.get_metadata_lines(name)) - self.eagers = eagers - return self.eagers - - def _index(self): - try: - return self._dirindex - except AttributeError: - ind = {} - for path in self.zipinfo: - parts = path.split(os.sep) - while parts: - parent = os.sep.join(parts[:-1]) - if parent in ind: - ind[parent].append(parts[-1]) - break - else: - ind[parent] = [parts.pop()] - self._dirindex = ind - return ind - - def _has(self, fspath): - zip_path = self._zipinfo_name(fspath) - return zip_path in self.zipinfo or zip_path in self._index() - - def _isdir(self,fspath): - return self._zipinfo_name(fspath) in self._index() - - def _listdir(self,fspath): - return list(self._index().get(self._zipinfo_name(fspath), ())) - - def _eager_to_zip(self,resource_name): - return self._zipinfo_name(self._fn(self.egg_root,resource_name)) - - def _resource_to_zip(self,resource_name): - return self._zipinfo_name(self._fn(self.module_path,resource_name)) - -register_loader_type(zipimport.zipimporter, ZipProvider) - - - - - - - - - - - - - - - - - - - - - - - - -class FileMetadata(EmptyProvider): - """Metadata handler for standalone PKG-INFO files - - Usage:: - - metadata = FileMetadata("/path/to/PKG-INFO") - - This provider rejects all data and metadata requests except for PKG-INFO, - which is treated as existing, and will be the contents of the file at - the provided location. - """ - - def __init__(self,path): - self.path = path - - def has_metadata(self,name): - return name=='PKG-INFO' - - def get_metadata(self,name): - if name=='PKG-INFO': - f = open(self.path,'rU') - metadata = f.read() - f.close() - return metadata - raise KeyError("No metadata except PKG-INFO is available") - - def get_metadata_lines(self,name): - return yield_lines(self.get_metadata(name)) - - - - - - - - - - - - - - - - -class PathMetadata(DefaultProvider): - """Metadata provider for egg directories - - Usage:: - - # Development eggs: - - egg_info = "/path/to/PackageName.egg-info" - base_dir = os.path.dirname(egg_info) - metadata = PathMetadata(base_dir, egg_info) - dist_name = os.path.splitext(os.path.basename(egg_info))[0] - dist = Distribution(basedir,project_name=dist_name,metadata=metadata) - - # Unpacked egg directories: - - egg_path = "/path/to/PackageName-ver-pyver-etc.egg" - metadata = PathMetadata(egg_path, os.path.join(egg_path,'EGG-INFO')) - dist = Distribution.from_filename(egg_path, metadata=metadata) - """ - - def __init__(self, path, egg_info): - self.module_path = path - self.egg_info = egg_info - - -class EggMetadata(ZipProvider): - """Metadata provider for .egg files""" - - def __init__(self, importer): - """Create a metadata provider from a zipimporter""" - - self.zipinfo = zipimport._zip_directory_cache[importer.archive] - self.zip_pre = importer.archive+os.sep - self.loader = importer - if importer.prefix: - self.module_path = os.path.join(importer.archive, importer.prefix) - else: - self.module_path = importer.archive - self._setup_prefix() - - -class ImpWrapper: - """PEP 302 Importer that wraps Python's "normal" import algorithm""" - - def __init__(self, path=None): - self.path = path - - def find_module(self, fullname, path=None): - subname = fullname.split(".")[-1] - if subname != fullname and self.path is None: - return None - if self.path is None: - path = None - else: - path = [self.path] - try: - file, filename, etc = imp.find_module(subname, path) - except ImportError: - return None - return ImpLoader(file, filename, etc) - - -class ImpLoader: - """PEP 302 Loader that wraps Python's "normal" import algorithm""" - - def __init__(self, file, filename, etc): - self.file = file - self.filename = filename - self.etc = etc - - def load_module(self, fullname): - try: - mod = imp.load_module(fullname, self.file, self.filename, self.etc) - finally: - if self.file: self.file.close() - # Note: we don't set __loader__ because we want the module to look - # normal; i.e. this is just a wrapper for standard import machinery - return mod - - - - -def get_importer(path_item): - """Retrieve a PEP 302 "importer" for the given path item - - If there is no importer, this returns a wrapper around the builtin import - machinery. The returned importer is only cached if it was created by a - path hook. - """ - try: - importer = sys.path_importer_cache[path_item] - except KeyError: - for hook in sys.path_hooks: - try: - importer = hook(path_item) - except ImportError: - pass - else: - break - else: - importer = None - - sys.path_importer_cache.setdefault(path_item,importer) - if importer is None: - try: - importer = ImpWrapper(path_item) - except ImportError: - pass - return importer - -try: - from pkgutil import get_importer, ImpImporter -except ImportError: - pass # Python 2.3 or 2.4, use our own implementation -else: - ImpWrapper = ImpImporter # Python 2.5, use pkgutil's implementation - del ImpLoader, ImpImporter - - - - - - -_distribution_finders = {} - -def register_finder(importer_type, distribution_finder): - """Register `distribution_finder` to find distributions in sys.path items - - `importer_type` is the type or class of a PEP 302 "Importer" (sys.path item - handler), and `distribution_finder` is a callable that, passed a path - item and the importer instance, yields ``Distribution`` instances found on - that path item. See ``pkg_resources.find_on_path`` for an example.""" - _distribution_finders[importer_type] = distribution_finder - - -def find_distributions(path_item, only=False): - """Yield distributions accessible via `path_item`""" - importer = get_importer(path_item) - finder = _find_adapter(_distribution_finders, importer) - return finder(importer, path_item, only) - -def find_in_zip(importer, path_item, only=False): - metadata = EggMetadata(importer) - if metadata.has_metadata('PKG-INFO'): - yield Distribution.from_filename(path_item, metadata=metadata) - if only: - return # don't yield nested distros - for subitem in metadata.resource_listdir('/'): - if subitem.endswith('.egg'): - subpath = os.path.join(path_item, subitem) - for dist in find_in_zip(zipimport.zipimporter(subpath), subpath): - yield dist - -register_finder(zipimport.zipimporter, find_in_zip) - -def StringIO(*args, **kw): - """Thunk to load the real StringIO on demand""" - global StringIO - try: - from cStringIO import StringIO - except ImportError: - from StringIO import StringIO - return StringIO(*args,**kw) - -def find_nothing(importer, path_item, only=False): - return () -register_finder(object,find_nothing) - -def find_on_path(importer, path_item, only=False): - """Yield distributions accessible on a sys.path directory""" - path_item = _normalize_cached(path_item) - - if os.path.isdir(path_item) and os.access(path_item, os.R_OK): - if path_item.lower().endswith('.egg'): - # unpacked egg - yield Distribution.from_filename( - path_item, metadata=PathMetadata( - path_item, os.path.join(path_item,'EGG-INFO') - ) - ) - else: - # scan for .egg and .egg-info in directory - for entry in os.listdir(path_item): - lower = entry.lower() - if lower.endswith('.egg-info'): - fullpath = os.path.join(path_item, entry) - if os.path.isdir(fullpath): - # egg-info directory, allow getting metadata - metadata = PathMetadata(path_item, fullpath) - else: - metadata = FileMetadata(fullpath) - yield Distribution.from_location( - path_item,entry,metadata,precedence=DEVELOP_DIST - ) - elif not only and lower.endswith('.egg'): - for dist in find_distributions(os.path.join(path_item, entry)): - yield dist - elif not only and lower.endswith('.egg-link'): - for line in open(os.path.join(path_item, entry)): - if not line.strip(): continue - for item in find_distributions(os.path.join(path_item,line.rstrip())): - yield item - break -register_finder(ImpWrapper,find_on_path) - -_namespace_handlers = {} -_namespace_packages = {} - -def register_namespace_handler(importer_type, namespace_handler): - """Register `namespace_handler` to declare namespace packages - - `importer_type` is the type or class of a PEP 302 "Importer" (sys.path item - handler), and `namespace_handler` is a callable like this:: - - def namespace_handler(importer,path_entry,moduleName,module): - # return a path_entry to use for child packages - - Namespace handlers are only called if the importer object has already - agreed that it can handle the relevant path item, and they should only - return a subpath if the module __path__ does not already contain an - equivalent subpath. For an example namespace handler, see - ``pkg_resources.file_ns_handler``. - """ - _namespace_handlers[importer_type] = namespace_handler - -def _handle_ns(packageName, path_item): - """Ensure that named package includes a subpath of path_item (if needed)""" - importer = get_importer(path_item) - if importer is None: - return None - loader = importer.find_module(packageName) - if loader is None: - return None - module = sys.modules.get(packageName) - if module is None: - module = sys.modules[packageName] = types.ModuleType(packageName) - module.__path__ = []; _set_parent_ns(packageName) - elif not hasattr(module,'__path__'): - raise TypeError("Not a package:", packageName) - handler = _find_adapter(_namespace_handlers, importer) - subpath = handler(importer,path_item,packageName,module) - if subpath is not None: - path = module.__path__; path.append(subpath) - loader.load_module(packageName); module.__path__ = path - return subpath - -def declare_namespace(packageName): - """Declare that package 'packageName' is a namespace package""" - - imp.acquire_lock() - try: - if packageName in _namespace_packages: - return - - path, parent = sys.path, None - if '.' in packageName: - parent = '.'.join(packageName.split('.')[:-1]) - declare_namespace(parent) - __import__(parent) - try: - path = sys.modules[parent].__path__ - except AttributeError: - raise TypeError("Not a package:", parent) - - # Track what packages are namespaces, so when new path items are added, - # they can be updated - _namespace_packages.setdefault(parent,[]).append(packageName) - _namespace_packages.setdefault(packageName,[]) - - for path_item in path: - # Ensure all the parent's path items are reflected in the child, - # if they apply - _handle_ns(packageName, path_item) - - finally: - imp.release_lock() - -def fixup_namespace_packages(path_item, parent=None): - """Ensure that previously-declared namespace packages include path_item""" - imp.acquire_lock() - try: - for package in _namespace_packages.get(parent,()): - subpath = _handle_ns(package, path_item) - if subpath: fixup_namespace_packages(subpath,package) - finally: - imp.release_lock() - -def file_ns_handler(importer, path_item, packageName, module): - """Compute an ns-package subpath for a filesystem or zipfile importer""" - - subpath = os.path.join(path_item, packageName.split('.')[-1]) - normalized = _normalize_cached(subpath) - for item in module.__path__: - if _normalize_cached(item)==normalized: - break - else: - # Only return the path if it's not already there - return subpath - -register_namespace_handler(ImpWrapper,file_ns_handler) -register_namespace_handler(zipimport.zipimporter,file_ns_handler) - - -def null_ns_handler(importer, path_item, packageName, module): - return None - -register_namespace_handler(object,null_ns_handler) - - -def normalize_path(filename): - """Normalize a file/dir name for comparison purposes""" - return os.path.normcase(os.path.realpath(filename)) - -def _normalize_cached(filename,_cache={}): - try: - return _cache[filename] - except KeyError: - _cache[filename] = result = normalize_path(filename) - return result - -def _set_parent_ns(packageName): - parts = packageName.split('.') - name = parts.pop() - if parts: - parent = '.'.join(parts) - setattr(sys.modules[parent], name, sys.modules[packageName]) - - -def yield_lines(strs): - """Yield non-empty/non-comment lines of a ``basestring`` or sequence""" - if isinstance(strs,basestring): - for s in strs.splitlines(): - s = s.strip() - if s and not s.startswith('#'): # skip blank lines/comments - yield s - else: - for ss in strs: - for s in yield_lines(ss): - yield s - -LINE_END = re.compile(r"\s*(#.*)?$").match # whitespace and comment -CONTINUE = re.compile(r"\s*\\\s*(#.*)?$").match # line continuation -DISTRO = re.compile(r"\s*((\w|[-.])+)").match # Distribution or extra -VERSION = re.compile(r"\s*(<=?|>=?|==|!=)\s*((\w|[-.])+)").match # ver. info -COMMA = re.compile(r"\s*,").match # comma between items -OBRACKET = re.compile(r"\s*\[").match -CBRACKET = re.compile(r"\s*\]").match -MODULE = re.compile(r"\w+(\.\w+)*$").match -EGG_NAME = re.compile( - r"(?P<name>[^-]+)" - r"( -(?P<ver>[^-]+) (-py(?P<pyver>[^-]+) (-(?P<plat>.+))? )? )?", - re.VERBOSE | re.IGNORECASE -).match - -component_re = re.compile(r'(\d+ | [a-z]+ | \.| -)', re.VERBOSE) -replace = {'pre':'c', 'preview':'c','-':'final-','rc':'c','dev':'@'}.get - -def _parse_version_parts(s): - for part in component_re.split(s): - part = replace(part,part) - if not part or part=='.': - continue - if part[:1] in '0123456789': - yield part.zfill(8) # pad for numeric comparison - else: - yield '*'+part - - yield '*final' # ensure that alpha/beta/candidate are before final - -def parse_version(s): - """Convert a version string to a chronologically-sortable key - - This is a rough cross between distutils' StrictVersion and LooseVersion; - if you give it versions that would work with StrictVersion, then it behaves - the same; otherwise it acts like a slightly-smarter LooseVersion. It is - *possible* to create pathological version coding schemes that will fool - this parser, but they should be very rare in practice. - - The returned value will be a tuple of strings. Numeric portions of the - version are padded to 8 digits so they will compare numerically, but - without relying on how numbers compare relative to strings. Dots are - dropped, but dashes are retained. Trailing zeros between alpha segments - or dashes are suppressed, so that e.g. "2.4.0" is considered the same as - "2.4". Alphanumeric parts are lower-cased. - - The algorithm assumes that strings like "-" and any alpha string that - alphabetically follows "final" represents a "patch level". So, "2.4-1" - is assumed to be a branch or patch of "2.4", and therefore "2.4.1" is - considered newer than "2.4-1", which in turn is newer than "2.4". - - Strings like "a", "b", "c", "alpha", "beta", "candidate" and so on (that - come before "final" alphabetically) are assumed to be pre-release versions, - so that the version "2.4" is considered newer than "2.4a1". - - Finally, to handle miscellaneous cases, the strings "pre", "preview", and - "rc" are treated as if they were "c", i.e. as though they were release - candidates, and therefore are not as new as a version string that does not - contain them, and "dev" is replaced with an '@' so that it sorts lower than - than any other pre-release tag. - """ - parts = [] - for part in _parse_version_parts(s.lower()): - if part.startswith('*'): - if part<'*final': # remove '-' before a prerelease tag - while parts and parts[-1]=='*final-': parts.pop() - # remove trailing zeros from each series of numeric parts - while parts and parts[-1]=='00000000': - parts.pop() - parts.append(part) - return tuple(parts) - -class EntryPoint(object): - """Object representing an advertised importable object""" - - def __init__(self, name, module_name, attrs=(), extras=(), dist=None): - if not MODULE(module_name): - raise ValueError("Invalid module name", module_name) - self.name = name - self.module_name = module_name - self.attrs = tuple(attrs) - self.extras = Requirement.parse(("x[%s]" % ','.join(extras))).extras - self.dist = dist - - def __str__(self): - s = "%s = %s" % (self.name, self.module_name) - if self.attrs: - s += ':' + '.'.join(self.attrs) - if self.extras: - s += ' [%s]' % ','.join(self.extras) - return s - - def __repr__(self): - return "EntryPoint.parse(%r)" % str(self) - - def load(self, require=True, env=None, installer=None): - if require: self.require(env, installer) - entry = __import__(self.module_name, globals(),globals(), ['__name__']) - for attr in self.attrs: - try: - entry = getattr(entry,attr) - except AttributeError: - raise ImportError("%r has no %r attribute" % (entry,attr)) - return entry - - def require(self, env=None, installer=None): - if self.extras and not self.dist: - raise UnknownExtra("Can't require() without a distribution", self) - map(working_set.add, - working_set.resolve(self.dist.requires(self.extras),env,installer)) - - - - #@classmethod - def parse(cls, src, dist=None): - """Parse a single entry point from string `src` - - Entry point syntax follows the form:: - - name = some.module:some.attr [extra1,extra2] - - The entry name and module name are required, but the ``:attrs`` and - ``[extras]`` parts are optional - """ - try: - attrs = extras = () - name,value = src.split('=',1) - if '[' in value: - value,extras = value.split('[',1) - req = Requirement.parse("x["+extras) - if req.specs: raise ValueError - extras = req.extras - if ':' in value: - value,attrs = value.split(':',1) - if not MODULE(attrs.rstrip()): - raise ValueError - attrs = attrs.rstrip().split('.') - except ValueError: - raise ValueError( - "EntryPoint must be in 'name=module:attrs [extras]' format", - src - ) - else: - return cls(name.strip(), value.strip(), attrs, extras, dist) - - parse = classmethod(parse) - - - - - - - - - #@classmethod - def parse_group(cls, group, lines, dist=None): - """Parse an entry point group""" - if not MODULE(group): - raise ValueError("Invalid group name", group) - this = {} - for line in yield_lines(lines): - ep = cls.parse(line, dist) - if ep.name in this: - raise ValueError("Duplicate entry point", group, ep.name) - this[ep.name]=ep - return this - - parse_group = classmethod(parse_group) - - #@classmethod - def parse_map(cls, data, dist=None): - """Parse a map of entry point groups""" - if isinstance(data,dict): - data = data.items() - else: - data = split_sections(data) - maps = {} - for group, lines in data: - if group is None: - if not lines: - continue - raise ValueError("Entry points must be listed in groups") - group = group.strip() - if group in maps: - raise ValueError("Duplicate group name", group) - maps[group] = cls.parse_group(group, lines, dist) - return maps - - parse_map = classmethod(parse_map) - - -def _remove_md5_fragment(location): - if not location: - return '' - parsed = urlparse(location) - if parsed[-1].startswith('md5='): - return urlunparse(parsed[:-1] + ('',)) - return location - - -class Distribution(object): - """Wrap an actual or potential sys.path entry w/metadata""" - def __init__(self, - location=None, metadata=None, project_name=None, version=None, - py_version=PY_MAJOR, platform=None, precedence = EGG_DIST - ): - self.project_name = safe_name(project_name or 'Unknown') - if version is not None: - self._version = safe_version(version) - self.py_version = py_version - self.platform = platform - self.location = location - self.precedence = precedence - self._provider = metadata or empty_provider - - #@classmethod - def from_location(cls,location,basename,metadata=None,**kw): - project_name, version, py_version, platform = [None]*4 - basename, ext = os.path.splitext(basename) - if ext.lower() in (".egg",".egg-info"): - match = EGG_NAME(basename) - if match: - project_name, version, py_version, platform = match.group( - 'name','ver','pyver','plat' - ) - return cls( - location, metadata, project_name=project_name, version=version, - py_version=py_version, platform=platform, **kw - ) - from_location = classmethod(from_location) - - - hashcmp = property( - lambda self: ( - getattr(self,'parsed_version',()), - self.precedence, - self.key, - _remove_md5_fragment(self.location), - self.py_version, - self.platform - ) - ) - def __hash__(self): return hash(self.hashcmp) - def __lt__(self, other): - return self.hashcmp < other.hashcmp - def __le__(self, other): - return self.hashcmp <= other.hashcmp - def __gt__(self, other): - return self.hashcmp > other.hashcmp - def __ge__(self, other): - return self.hashcmp >= other.hashcmp - def __eq__(self, other): - if not isinstance(other, self.__class__): - # It's not a Distribution, so they are not equal - return False - return self.hashcmp == other.hashcmp - def __ne__(self, other): - return not self == other - - # These properties have to be lazy so that we don't have to load any - # metadata until/unless it's actually needed. (i.e., some distributions - # may not know their name or version without loading PKG-INFO) - - #@property - def key(self): - try: - return self._key - except AttributeError: - self._key = key = self.project_name.lower() - return key - key = property(key) - - #@property - def parsed_version(self): - try: - return self._parsed_version - except AttributeError: - self._parsed_version = pv = parse_version(self.version) - return pv - - parsed_version = property(parsed_version) - - #@property - def version(self): - try: - return self._version - except AttributeError: - for line in self._get_metadata('PKG-INFO'): - if line.lower().startswith('version:'): - self._version = safe_version(line.split(':',1)[1].strip()) - return self._version - else: - raise ValueError( - "Missing 'Version:' header and/or PKG-INFO file", self - ) - version = property(version) - - - - - #@property - def _dep_map(self): - try: - return self.__dep_map - except AttributeError: - dm = self.__dep_map = {None: []} - for name in 'requires.txt', 'depends.txt': - for extra,reqs in split_sections(self._get_metadata(name)): - if extra: extra = safe_extra(extra) - dm.setdefault(extra,[]).extend(parse_requirements(reqs)) - return dm - _dep_map = property(_dep_map) - - def requires(self,extras=()): - """List of Requirements needed for this distro if `extras` are used""" - dm = self._dep_map - deps = [] - deps.extend(dm.get(None,())) - for ext in extras: - try: - deps.extend(dm[safe_extra(ext)]) - except KeyError: - raise UnknownExtra( - "%s has no such extra feature %r" % (self, ext) - ) - return deps - - def _get_metadata(self,name): - if self.has_metadata(name): - for line in self.get_metadata_lines(name): - yield line - - def activate(self,path=None): - """Ensure distribution is importable on `path` (default=sys.path)""" - if path is None: path = sys.path - self.insert_on(path) - if path is sys.path: - fixup_namespace_packages(self.location) - map(declare_namespace, self._get_metadata('namespace_packages.txt')) - - - def egg_name(self): - """Return what this distribution's standard .egg filename should be""" - filename = "%s-%s-py%s" % ( - to_filename(self.project_name), to_filename(self.version), - self.py_version or PY_MAJOR - ) - - if self.platform: - filename += '-'+self.platform - return filename - - def __repr__(self): - if self.location: - return "%s (%s)" % (self,self.location) - else: - return str(self) - - def __str__(self): - try: version = getattr(self,'version',None) - except ValueError: version = None - version = version or "[unknown version]" - return "%s %s" % (self.project_name,version) - - def __getattr__(self,attr): - """Delegate all unrecognized public attributes to .metadata provider""" - if attr.startswith('_'): - raise AttributeError,attr - return getattr(self._provider, attr) - - #@classmethod - def from_filename(cls,filename,metadata=None, **kw): - return cls.from_location( - _normalize_cached(filename), os.path.basename(filename), metadata, - **kw - ) - from_filename = classmethod(from_filename) - - def as_requirement(self): - """Return a ``Requirement`` that matches this distribution exactly""" - return Requirement.parse('%s==%s' % (self.project_name, self.version)) - - def load_entry_point(self, group, name): - """Return the `name` entry point of `group` or raise ImportError""" - ep = self.get_entry_info(group,name) - if ep is None: - raise ImportError("Entry point %r not found" % ((group,name),)) - return ep.load() - - def get_entry_map(self, group=None): - """Return the entry point map for `group`, or the full entry map""" - try: - ep_map = self._ep_map - except AttributeError: - ep_map = self._ep_map = EntryPoint.parse_map( - self._get_metadata('entry_points.txt'), self - ) - if group is not None: - return ep_map.get(group,{}) - return ep_map - - def get_entry_info(self, group, name): - """Return the EntryPoint object for `group`+`name`, or ``None``""" - return self.get_entry_map(group).get(name) - - - - - - - - - - - - - - - - - - - - def insert_on(self, path, loc = None): - """Insert self.location in path before its nearest parent directory""" - - loc = loc or self.location - - if self.project_name == 'setuptools': - try: - version = self.version - except ValueError: - version = '' - if '0.7' in version: - raise ValueError( - "A 0.7-series setuptools cannot be installed " - "with distribute. Found one at %s" % str(self.location)) - - if not loc: - return - - if path is sys.path: - self.check_version_conflict() - - nloc = _normalize_cached(loc) - bdir = os.path.dirname(nloc) - npath= map(_normalize_cached, path) - - bp = None - for p, item in enumerate(npath): - if item==nloc: - break - elif item==bdir and self.precedence==EGG_DIST: - # if it's an .egg, give it precedence over its directory - path.insert(p, loc) - npath.insert(p, nloc) - break - else: - path.append(loc) - return - - # p is the spot where we found or inserted loc; now remove duplicates - while 1: - try: - np = npath.index(nloc, p+1) - except ValueError: - break - else: - del npath[np], path[np] - p = np # ha! - - return - - - - def check_version_conflict(self): - if self.key=='distribute': - return # ignore the inevitable setuptools self-conflicts :( - - nsp = dict.fromkeys(self._get_metadata('namespace_packages.txt')) - loc = normalize_path(self.location) - for modname in self._get_metadata('top_level.txt'): - if (modname not in sys.modules or modname in nsp - or modname in _namespace_packages - ): - continue - if modname in ('pkg_resources', 'setuptools', 'site'): - continue - fn = getattr(sys.modules[modname], '__file__', None) - if fn and (normalize_path(fn).startswith(loc) or - fn.startswith(self.location)): - continue - issue_warning( - "Module %s was already imported from %s, but %s is being added" - " to sys.path" % (modname, fn, self.location), - ) - - def has_version(self): - try: - self.version - except ValueError: - issue_warning("Unbuilt egg for "+repr(self)) - return False - return True - - def clone(self,**kw): - """Copy this distribution, substituting in any changed keyword args""" - for attr in ( - 'project_name', 'version', 'py_version', 'platform', 'location', - 'precedence' - ): - kw.setdefault(attr, getattr(self,attr,None)) - kw.setdefault('metadata', self._provider) - return self.__class__(**kw) - - - - - #@property - def extras(self): - return [dep for dep in self._dep_map if dep] - extras = property(extras) - - -def issue_warning(*args,**kw): - level = 1 - g = globals() - try: - # find the first stack frame that is *not* code in - # the pkg_resources module, to use for the warning - while sys._getframe(level).f_globals is g: - level += 1 - except ValueError: - pass - from warnings import warn - warn(stacklevel = level+1, *args, **kw) - - - - - - - - - - - - - - - - - - - - - - - -def parse_requirements(strs): - """Yield ``Requirement`` objects for each specification in `strs` - - `strs` must be an instance of ``basestring``, or a (possibly-nested) - iterable thereof. - """ - # create a steppable iterator, so we can handle \-continuations - lines = iter(yield_lines(strs)) - - def scan_list(ITEM,TERMINATOR,line,p,groups,item_name): - - items = [] - - while not TERMINATOR(line,p): - if CONTINUE(line,p): - try: - line = lines.next(); p = 0 - except StopIteration: - raise ValueError( - "\\ must not appear on the last nonblank line" - ) - - match = ITEM(line,p) - if not match: - raise ValueError("Expected "+item_name+" in",line,"at",line[p:]) - - items.append(match.group(*groups)) - p = match.end() - - match = COMMA(line,p) - if match: - p = match.end() # skip the comma - elif not TERMINATOR(line,p): - raise ValueError( - "Expected ',' or end-of-list in",line,"at",line[p:] - ) - - match = TERMINATOR(line,p) - if match: p = match.end() # skip the terminator, if any - return line, p, items - - for line in lines: - match = DISTRO(line) - if not match: - raise ValueError("Missing distribution spec", line) - project_name = match.group(1) - p = match.end() - extras = [] - - match = OBRACKET(line,p) - if match: - p = match.end() - line, p, extras = scan_list( - DISTRO, CBRACKET, line, p, (1,), "'extra' name" - ) - - line, p, specs = scan_list(VERSION,LINE_END,line,p,(1,2),"version spec") - specs = [(op,safe_version(val)) for op,val in specs] - yield Requirement(project_name, specs, extras) - - -def _sort_dists(dists): - tmp = [(dist.hashcmp,dist) for dist in dists] - tmp.sort() - dists[::-1] = [d for hc,d in tmp] - - - - - - - - - - - - - - - - - -class Requirement: - def __init__(self, project_name, specs, extras): - """DO NOT CALL THIS UNDOCUMENTED METHOD; use Requirement.parse()!""" - self.unsafe_name, project_name = project_name, safe_name(project_name) - self.project_name, self.key = project_name, project_name.lower() - index = [(parse_version(v),state_machine[op],op,v) for op,v in specs] - index.sort() - self.specs = [(op,ver) for parsed,trans,op,ver in index] - self.index, self.extras = index, tuple(map(safe_extra,extras)) - self.hashCmp = ( - self.key, tuple([(op,parsed) for parsed,trans,op,ver in index]), - frozenset(self.extras) - ) - self.__hash = hash(self.hashCmp) - - def __str__(self): - specs = ','.join([''.join(s) for s in self.specs]) - extras = ','.join(self.extras) - if extras: extras = '[%s]' % extras - return '%s%s%s' % (self.project_name, extras, specs) - - def __eq__(self,other): - return isinstance(other,Requirement) and self.hashCmp==other.hashCmp - - def __contains__(self,item): - if isinstance(item,Distribution): - if item.key <> self.key: return False - if self.index: item = item.parsed_version # only get if we need it - elif isinstance(item,basestring): - item = parse_version(item) - last = None - compare = lambda a, b: (a > b) - (a < b) # -1, 0, 1 - for parsed,trans,op,ver in self.index: - action = trans[compare(item,parsed)] # Indexing: 0, 1, -1 - if action=='F': return False - elif action=='T': return True - elif action=='+': last = True - elif action=='-' or last is None: last = False - if last is None: last = True # no rules encountered - return last - - - def __hash__(self): - return self.__hash - - def __repr__(self): return "Requirement.parse(%r)" % str(self) - - #@staticmethod - def parse(s, replacement=True): - reqs = list(parse_requirements(s)) - if reqs: - if len(reqs) == 1: - founded_req = reqs[0] - # if asked for setuptools distribution - # and if distribute is installed, we want to give - # distribute instead - if _override_setuptools(founded_req) and replacement: - distribute = list(parse_requirements('distribute')) - if len(distribute) == 1: - return distribute[0] - return founded_req - else: - return founded_req - - raise ValueError("Expected only one requirement", s) - raise ValueError("No requirements found", s) - - parse = staticmethod(parse) - -state_machine = { - # =>< - '<' : '--T', - '<=': 'T-T', - '>' : 'F+F', - '>=': 'T+F', - '==': 'T..', - '!=': 'F++', -} - - -def _override_setuptools(req): - """Return True when distribute wants to override a setuptools dependency. - - We want to override when the requirement is setuptools and the version is - a variant of 0.6. - - """ - if req.project_name == 'setuptools': - if not len(req.specs): - # Just setuptools: ok - return True - for comparator, version in req.specs: - if comparator in ['==', '>=', '>']: - if '0.7' in version: - # We want some setuptools not from the 0.6 series. - return False - return True - return False - - -def _get_mro(cls): - """Get an mro for a type or classic class""" - if not isinstance(cls,type): - class cls(cls,object): pass - return cls.__mro__[1:] - return cls.__mro__ - -def _find_adapter(registry, ob): - """Return an adapter factory for `ob` from `registry`""" - for t in _get_mro(getattr(ob, '__class__', type(ob))): - if t in registry: - return registry[t] - - -def ensure_directory(path): - """Ensure that the parent directory of `path` exists""" - dirname = os.path.dirname(path) - if not os.path.isdir(dirname): - os.makedirs(dirname) - -def split_sections(s): - """Split a string or iterable thereof into (section,content) pairs - - Each ``section`` is a stripped version of the section header ("[section]") - and each ``content`` is a list of stripped lines excluding blank lines and - comment-only lines. If there are any such lines before the first section - header, they're returned in a first ``section`` of ``None``. - """ - section = None - content = [] - for line in yield_lines(s): - if line.startswith("["): - if line.endswith("]"): - if section or content: - yield section, content - section = line[1:-1].strip() - content = [] - else: - raise ValueError("Invalid section heading", line) - else: - content.append(line) - - # wrap up last segment - yield section, content - -def _mkstemp(*args,**kw): - from tempfile import mkstemp - old_open = os.open - try: - os.open = os_open # temporarily bypass sandboxing - return mkstemp(*args,**kw) - finally: - os.open = old_open # and then put it back - - -# Set up global resource manager -_manager = ResourceManager() -def _initialize(g): - for name in dir(_manager): - if not name.startswith('_'): - g[name] = getattr(_manager, name) -_initialize(globals()) - -# Prepare the master working set and make the ``require()`` API available -working_set = WorkingSet() -try: - # Does the main program list any requirements? - from __main__ import __requires__ -except ImportError: - pass # No: just use the default working set based on sys.path -else: - # Yes: ensure the requirements are met, by prefixing sys.path if necessary - try: - working_set.require(__requires__) - except VersionConflict: # try it without defaults already on sys.path - working_set = WorkingSet([]) # by starting with an empty path - for dist in working_set.resolve( - parse_requirements(__requires__), Environment() - ): - working_set.add(dist) - for entry in sys.path: # add any missing entries from sys.path - if entry not in working_set.entries: - working_set.add_entry(entry) - sys.path[:] = working_set.entries # then copy back to sys.path - -require = working_set.require -iter_entry_points = working_set.iter_entry_points -add_activation_listener = working_set.subscribe -run_script = working_set.run_script -run_main = run_script # backward compatibility -# Activate all distributions already on sys.path, and ensure that -# all distributions added to the working set in the future (e.g. by -# calling ``require()``) will get activated as well. -add_activation_listener(lambda dist: dist.activate()) -working_set.entries=[]; map(working_set.add_entry,sys.path) # match order - diff --git a/src/distutils2/pypi/simple.py b/src/distutils2/pypi/simple.py index 3079d4f..1a6a35d 100644 --- a/src/distutils2/pypi/simple.py +++ b/src/distutils2/pypi/simple.py @@ -1,797 +1,393 @@ -"""distutils2.pypi.package_index - -Implement a simple way to use the PyPI API's, by using known URIs from -PyPI. Works with the API defined on -http://peak.telecommunity.com/DevCenter/EasyInstall#package-index-api , -and available at http://pypi.python.org/simple/ . +"""pypi.simple +Contains the class "SimpleIndex", a simple spider to find and retrieve +distributions on the Python Package Index, using it's "simple" API, +avalaible at http://pypi.python.org/simple/ """ -# XXX Make a explaination document on how this API is made. - -import sys -import os.path +from fnmatch import translate +import httplib import re -import urlparse -import urllib2 -import shutil import socket -import cStringIO -import httplib -import logging +import sys +import urllib2 +import urlparse -from distutils2.errors import DistutilsError +from distutils2.version import VersionPredicate +from distutils2.pypi.dist import (PyPIDistribution, PyPIDistributions, + EXTENSIONS) +from distutils2.pypi.errors import (PyPIError, DistributionNotFound, + DownloadError, UnableToDownload) from distutils2 import __version__ as __distutils2_version__ -try: - from hashlib import md5 -except ImportError: - from md5 import md5 -from fnmatch import translate - -# -- Constants ---------------------------------------------------------------- - -__all__ = ['PackageIndex', - 'distros_for_url', - 'parse_bdist_wininst', - 'interpret_distro_name', -] +# -- Constants ----------------------------------------------- +PYPI_DEFAULT_INDEX_URL = "http://pypi.python.org/simple/" +PYPI_DEFAULT_MIRROR_URL = "mirrors.pypi.python.org" +DEFAULT_HOSTS = ("*",) +SOCKET_TIMEOUT = 15 +USER_AGENT = "Python-urllib/%s distutils2/%s" % ( + sys.version[:3], __distutils2_version__) -_SOCKET_TIMEOUT = 15 -SOURCE_DIST = 1 +# -- Regexps ------------------------------------------------- EGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.]+)$') HREF = re.compile("""href\\s*=\\s*['"]?([^'"> ]+)""", re.I) -# this is here to fix emacs' cruddy broken syntax highlighting PYPI_MD5 = re.compile( '<a href="([^"#]+)">([^<]+)</a>\n\s+\\(<a (?:title="MD5 hash"\n\s+)' - 'href="[^?]+\?:action=show_md5&digest=([0-9a-f]{32})">md5</a>\\)' -) -URL_SCHEME = re.compile('([-+.a-z0-9]{2,}):',re.I).match -EXTENSIONS = ".tar.gz .tar.bz2 .tar .zip .tgz".split() + 'href="[^?]+\?:action=show_md5&digest=([0-9a-f]{32})">md5</a>\\)') +URL_SCHEME = re.compile('([-+.a-z0-9]{2,}):', re.I).match -# -- Dependencies from distribute --------------------------------------------- +# 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) -# XXX Remove these dependencies, refactor the package_index codebase to not -# depends anymore on distribute classes and internal workings. -from pkg_resources import * -# -- Base functions ----------------------------------------------------------- +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 parse_bdist_wininst(name): - """Parse a bdist_win package name, and return it's package base name - and version number if prossible. - Return a tuple of (base,pyversion) for possible .exe names""" +class SimpleIndex(object): + """Provides useful tools to request the Python Package Index simple API + + :param index_url: the url of the simple index to search on. + :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 prefer_source: if there is binary and source distributions, the + source prevails. + :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 mirrors_url: the url to look on for DNS records giving mirror + adresses. + :param mirrors: a list of mirrors to check out if problems + occurs while working with the one given in "url" + :param timeout: time in seconds to consider a url has timeouted. + """ - lower = name.lower() - base, py_ver = None, None + def __init__(self, index_url=PYPI_DEFAULT_INDEX_URL, hosts=DEFAULT_HOSTS, + follow_externals=False, prefer_source=True, + prefer_final=False, mirrors_url=PYPI_DEFAULT_MIRROR_URL, + mirrors=None, timeout=SOCKET_TIMEOUT): + self.follow_externals = follow_externals + + if not index_url.endswith("/"): + index_url += "/" + self._index_urls = [index_url] + # if no mirrors are defined, use the method described in PEP 381. + if mirrors is None: + try: + mirrors = socket.gethostbyname_ex(mirrors_url)[-1] + except socket.gaierror: + mirrors = [] + self._index_urls.extend(mirrors) + self._current_index_url = 0 + self._timeout = timeout + self._prefer_source = prefer_source + self._prefer_final = prefer_final + + # 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._distributions = {} + + def find(self, requirements, prefer_source=None, prefer_final=None): + """Browse the PyPI to find distributions that fullfil the given + requirements. + + :param requirements: A project name and it's distribution, using + version specifiers, as described in PEP345. + :type requirements: You can pass either a version.VersionPredicate + or a string. + :param prefer_source: if there is binary and source distributions, the + source prevails. + :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. + """ + requirements = self._get_version_predicate(requirements) + if prefer_source is None: + prefer_source = self._prefer_source + if prefer_final is None: + prefer_final = self._prefer_final + + # process the index for this project + self._process_pypi_page(requirements.name) + + # filter with requirements and return the results + if requirements.name in self._distributions: + dists = self._distributions[requirements.name].filter(requirements) + dists.sort_distributions(prefer_source=prefer_source, + prefer_final=prefer_final) + else: + dists = [] - if lower.endswith('.exe'): - if lower.endswith('.win32.exe'): - base = name[:-10] - elif lower.startswith('.win32-py',-16): - py_ver = name[-7:-4] - base = name[:-16] + return dists - return base,py_ver + def get(self, requirements, *args, **kwargs): + """Browse the PyPI index to find distributions that fullfil the + given requirements, and return the most recent one. -def egg_info_for_url(url): - """ - """ - scheme, server, path, parameters, query, fragment = urlparse.urlparse(url) - base = urllib2.unquote(path.split('/')[-1]) - if '#' in base: - base, fragment = base.split('#',1) - return base,fragment - -def distros_for_url(url, metadata=None): - """Yield egg or source distribution objects that might be found at a URL""" - base, fragment = egg_info_for_url(url) - for dist in distros_for_location(url, base, metadata): - yield dist - if fragment: - match = EGG_FRAGMENT.match(fragment) - if match: - for dist in interpret_distro_name(url, match.group(1), metadata, - precedence = CHECKOUT_DIST): - yield dist - -def distros_for_location(location, basename, metadata=None): - """Yield egg or source distribution objects based on basename""" - if basename.endswith('.egg.zip'): - basename = basename[:-4] # strip the .zip - if basename.endswith('.egg') and '-' in basename: - # only one, unambiguous interpretation - return [Distribution.from_location(location, basename, metadata)] - - if basename.endswith('.exe'): - win_base, py_ver = parse_bdist_wininst(basename) - if win_base is not None: - return interpret_distro_name(location, win_base, metadata, py_ver, - BINARY_DIST, "win32") - - # Try source distro extensions (.zip, .tgz, etc.) - for ext in EXTENSIONS: - if basename.endswith(ext): - basename = basename[:-len(ext)] - return interpret_distro_name(location, basename, metadata) - return [] # no extension matched - -def distros_for_filename(filename, metadata=None): - """Yield possible egg or source distribution objects based on a - filename. - """ - return distros_for_location(normalize_path(filename), - os.path.basename(filename), metadata) + You can specify prefer_final and prefer_source arguments here. + If not, the default one will be used. + """ + predicate = self._get_version_predicate(requirements) + dists = self.find(predicate, *args, **kwargs) -def interpret_distro_name(location, basename, metadata, - py_version=None, precedence=SOURCE_DIST, platform=None): - """Generate alternative interpretations of a source distro name + if len(dists) == 0: + raise DistributionNotFound(requirements) - Note: if `location` is a filesystem filename, you should call - ``pkg_resources.normalize_path()`` on it before passing it to this - routine! - """ - # Generate alternative interpretations of a source distro name - # Because some packages are ambiguous as to name/versions split - # e.g. "adns-python-1.1.0", "egenix-mx-commercial", etc. - # So, we generate each possible interepretation (e.g. "adns, python-1.1.0" - # "adns-python, 1.1.0", and "adns-python-1.1.0, no version"). In practice, - # the spurious interpretations should be ignored, because in the event - # there's also an "adns" package, the spurious "python-1.1.0" version will - # compare lower than any numeric version number, and is therefore unlikely - # to match a request for it. It's still a potential problem, though, and - # in the long run PyPI and the distutils should go for "safe" names and - # versions in distribution archive names (sdist and bdist). - - parts = basename.split('-') - if not py_version: - for i,p in enumerate(parts[2:]): - if len(p)==5 and p.startswith('py2.'): - return # It's a bdist_dumb, not an sdist -- bail out - - for p in range(1,len(parts)+1): - yield Distribution( - location, metadata, '-'.join(parts[:p]), '-'.join(parts[p:]), - py_version=py_version, precedence = precedence, - platform = platform) + return dists.get_last(predicate) -REL = re.compile("""<([^>]*\srel\s*=\s*['"]?([^'">]+)[^>]*)>""", re.I) + def download(self, requirements, temp_path=None, *args, **kwargs): + """Download the distribution, using the requirements. -def find_external_links(url, page): - """Find rel="homepage" and rel="download" links in `page`, yielding URLs""" - - for match in REL.finditer(page): - tag, rel = match.groups() - rels = map(str.strip, rel.lower().split(',')) - if 'homepage' in rels or 'download' in rels: - for match in HREF.finditer(tag): - yield urlparse.urljoin(url, htmldecode(match.group(1))) - - for tag in ("<th>Home Page", "<th>Download URL"): - pos = page.find(tag) - if pos!=-1: - match = HREF.search(page,pos) - if match: - yield urlparse.urljoin(url, htmldecode(match.group(1))) - -user_agent = "Python-urllib/%s distutils2/%s" % ( - sys.version[:3], __distutils2_version__ -) - -class PackageIndex(Environment): - """A distribution index that scans web pages for download URLs""" - - def __init__(self, index_url="http://pypi.python.org/simple", hosts=('*',), - *args, **kw): - self._init_logging() - Environment.__init__(self,*args,**kw) - self.index_url = index_url + "/"[:not index_url.endswith('/')] - self.scanned_urls = {} - self.fetched_urls = {} - self.package_pages = {} - self.allows = re.compile('|'.join(map(translate,hosts))).match - self.to_scan = [] - - def _init_logging(self): - import sys -# logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) - - def process_url(self, url, retrieve=False): - """Evaluate a URL as a possible download, and maybe retrieve it""" - if url in self.scanned_urls and not retrieve: - return - self.scanned_urls[url] = True - if not URL_SCHEME(url): - self.process_filename(url) - return + 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. + + :param requirements: The same as the find attribute of `find`. + + You can specify prefer_final and prefer_source arguments here. + If not, the default one will be used. + """ + return self.get(requirements, *args, **kwargs)\ + .download(path=temp_path) + + def _get_version_predicate(self, requirements): + """Return a VersionPredicate object, from a string or an already + existing object. + """ + if isinstance(requirements, str): + requirements = VersionPredicate(requirements) + return requirements + + @property + def index_url(self): + return self._index_urls[self._current_index_url] + + def _switch_to_next_mirror(self): + """Switch to the next mirror (eg. point self.index_url to the next + url. + """ + # Internally, iter over the _index_url iterable, if we have read all + # of the available indexes, raise an exception. + if self._current_index_url < len(self._index_urls): + self._current_index_url = self._current_index_url + 1 else: - dists = list(distros_for_url(url)) - if dists: - if not self.url_ok(url): - return - self.debug("Found link: %s", url) - - if dists or not retrieve or url in self.fetched_urls: - map(self.add, dists) - return # don't need the actual page - - if not self.url_ok(url): - self.fetched_urls[url] = True - return - - self.info("Reading %s", url) - f = self.open_url(url, "Download error: %s -- Some packages may not be found!") - if f is None: return - self.fetched_urls[url] = self.fetched_urls[f.url] = True - - if 'html' not in f.headers.get('content-type', '').lower(): - f.close() # not html, we can't process it - return - - base = f.url # handle redirects - page = f.read() - if sys.version_info >= (3,): - charset = f.headers.get_param('charset') or 'latin-1' - page = page.decode(charset, "ignore") - f.close() - for match in HREF.finditer(page): - link = urlparse.urljoin(base, htmldecode(match.group(1))) - self.process_url(link) - if url.startswith(self.index_url) and getattr(f,'code',None)!=404: - page = self.process_index(url, page) - - def process_filename(self, fn, nested=False): - # process filenames or directories - if not os.path.exists(fn): - self.warn("Not found: %s", fn) - return - - if os.path.isdir(fn) and not nested: - path = os.path.realpath(fn) - for item in os.listdir(path): - self.process_filename(os.path.join(path,item), True) - - dists = distros_for_filename(fn) - if dists: - self.debug("Found: %s", fn) - map(self.add, dists) - - def url_ok(self, url, fatal=False): - s = URL_SCHEME(url) - if (s and s.group(1).lower()=='file') or self.allows(urlparse.urlparse(url)[1]): + raise UnableToDownload("All mirrors fails") + + 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 urlparse.urlparse(url)[0] == "file": return True - msg = "\nLink to % s ***BLOCKED*** by --allow-hosts\n" - if fatal: - raise DistutilsError(msg % url) - else: - self.warn(msg, url) + elif self.follow_externals: + if self._allowed_hosts(urlparse.urlparse(url)[1]): # 1 is netloc + return True + else: + return False return False - def scan_egg_links(self, search_path): - for item in search_path: - if os.path.isdir(item): - for entry in os.listdir(item): - if entry.endswith('.egg-link'): - self.scan_egg_link(item, entry) - - def scan_egg_link(self, path, entry): - lines = filter(None, map(str.strip, open(os.path.join(path, entry)))) - if len(lines)==2: - for dist in find_distributions(os.path.join(path, lines[0])): - dist.location = os.path.join(path, *lines) - dist.precedence = SOURCE_DIST - self.add(dist) - - def process_index(self,url,page): - """Process the contents of a PyPI page""" - def scan(link): - # Process a URL to see if it's for a package page - if link.startswith(self.index_url): - parts = map( - urllib2.unquote, link[len(self.index_url):].split('/') - ) - if len(parts)==2 and '#' not in parts[1]: - # it's a package page, sanitize and index it - pkg = safe_name(parts[0]) - ver = safe_version(parts[1]) - self.package_pages.setdefault(pkg.lower(),{})[link] = True - return to_filename(pkg), to_filename(ver) - return None, None - - # process an index page into the package-page index - for match in HREF.finditer(page): - try: - scan( urlparse.urljoin(url, htmldecode(match.group(1))) ) - except ValueError: - pass - - pkg, ver = scan(url) # ensure this page is in the page index - if pkg: - # process individual package page - for new_url in find_external_links(url, page): - # Process the found URL - base, frag = egg_info_for_url(new_url) - if base.endswith('.py') and not frag: - if ver: - new_url+='#egg=%s-%s' % (pkg,ver) + 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_dist(self, dist): + """Register a distribution as a part of fetched distributions for + SimpleIndex. + + Return the PyPIDistributions object for the specified project name + """ + # Internally, check if a entry exists with the project name, if not, + # create a new one, and if exists, add the dist to the pool. + if not dist.name in self._distributions: + self._distributions[dist.name] = PyPIDistributions() + self._distributions[dist.name].append(dist) + return self._distributions[dist.name] + + 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) + """ + f = self._open_url(url) + 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(), 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 + dist = PyPIDistribution.from_url(link, project_name, + is_external=not self.index_url in url) + self._register_dist(dist) else: - self.need_version_info(url) - self.scan_url(new_url) + if self._is_browsable(link) and follow_links: + self._process_url(link, project_name, + follow_links=False) - return PYPI_MD5.sub( - lambda m: '<a href="%s#md5=%s">%s</a>' % m.group(1,3,2), page - ) + 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 "" # no sense double-scanning non-package pages - - def need_version_info(self, url): - self.scan_all( - "Page at %s links to .py file(s) without version info; an index " - "scan is required.", url - ) - - def scan_all(self, msg=None, *args): - if self.index_url not in self.fetched_urls: - if msg: self.warn(msg,*args) - self.info( - "Scanning index of all packages (this may take a while)" - ) - self.scan_url(self.index_url) - - def find_packages(self, requirement): - self.scan_url(self.index_url + requirement.unsafe_name+'/') - - if not self.package_pages.get(requirement.key): - # Fall back to safe version of the name - self.scan_url(self.index_url + requirement.project_name+'/') - - if not self.package_pages.get(requirement.key): - # We couldn't find the target package, so search the index page too - self.not_found_in_index(requirement) - - for url in list(self.package_pages.get(requirement.key,())): - # scan each page that might be related to the desired package - self.scan_url(url) - - def obtain(self, requirement, installer=None): - self.prescan(); self.find_packages(requirement) - for dist in self[requirement.key]: - if dist in requirement: - return dist - self.debug("%s does not match %s", requirement, dist) - return super(PackageIndex, self).obtain(requirement,installer) - - def check_md5(self, cs, info, filename, tfp): - if re.match('md5=[0-9a-f]{32}$', info): - self.debug("Validating md5 checksum for %s", filename) - if cs.hexdigest()<>info[4:]: - tfp.close() - os.unlink(filename) - raise DistutilsError( - "MD5 validation failed for "+os.path.basename(filename)+ - "; possible download problem?") - - def add_find_links(self, urls): - """Add `urls` to the list that will be prescanned for searches""" - for url in urls: - if ( - self.to_scan is None # if we have already "gone online" - or not URL_SCHEME(url) # or it's a local file/directory - or url.startswith('file:') - or list(distros_for_url(url)) # or a direct package link - ): - # then go ahead and process it now - self.scan_url(url) - else: - # otherwise, defer retrieval till later - self.to_scan.append(url) - - def prescan(self): - """Scan urls scheduled for prescanning (e.g. --find-links)""" - if self.to_scan: - map(self.scan_url, self.to_scan) - self.to_scan = None # from now on, go ahead and process immediately - - def not_found_in_index(self, requirement): - if self[requirement.key]: # we've seen at least one distro - meth, msg = self.info, "Couldn't retrieve index page for %r" - else: # no distros seen for this name, might be misspelled - meth, msg = (self.warn, - "Couldn't find index page for %r (maybe misspelled?)") - meth(msg, requirement.unsafe_name) - self.scan_all() - - def download(self, spec, tmpdir): - """Locate and/or download `spec` to `tmpdir`, returning a local path - - `spec` may be a ``Requirement`` object, or a string containing a URL, - an existing local filename, or a project/version requirement spec - (i.e. the string form of a ``Requirement`` object). If it is the URL - of a .py file with an unambiguous ``#egg=name-version`` tag (i.e., one - that escapes ``-`` as ``_`` throughout), a trivial ``setup.py`` is - automatically created alongside the downloaded file. - - If `spec` is a ``Requirement`` object or a string containing a - project/version requirement spec, this method returns the location of - a matching distribution (possibly after downloading it to `tmpdir`). - If `spec` is a locally existing file or directory name, it is simply - returned unchanged. If `spec` is a URL, it is downloaded to a subpath - of `tmpdir`, and the local filename is returned. Various errors may be - raised if a problem occurs during downloading. + return self._default_link_matcher + + 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. """ - if not isinstance(spec,Requirement): - scheme = URL_SCHEME(spec) - if scheme: - # It's a url, download it to tmpdir - found = self._download_url(scheme.group(1), spec, tmpdir) - base, fragment = egg_info_for_url(spec) - if base.endswith('.py'): - found = self.gen_setup(found,fragment,tmpdir) - return found - elif os.path.exists(spec): - # Existing file or directory, just return it - return spec - else: - try: - spec = Requirement.parse(spec) - except ValueError: - raise DistutilsError( - "Not a URL, existing file, or requirement spec: %r" % - (spec,) - ) - return getattr(self.fetch_distribution(spec, tmpdir),'location',None) - - def fetch_distribution(self, - requirement, tmpdir, force_scan=False, source=False, develop_ok=False - ): - """Obtain a distribution suitable for fulfilling `requirement` - - `requirement` must be a ``pkg_resources.Requirement`` instance. - If necessary, or if the `force_scan` flag is set, the requirement is - searched for in the (online) package index as well as the locally - installed packages. If a distribution matching `requirement` is found, - the returned distribution's ``location`` is the value you would have - gotten from calling the ``download()`` method with the matching - distribution's URL or filename. If no matching distribution is found, - ``None`` is returned. - - If the `source` flag is set, only source distributions and source - checkout links will be considered. Unless the `develop_ok` flag is - set, development and system eggs (i.e., those using the ``.egg-info`` - format) will be ignored. + for match in REL.finditer(content): + tag, rel = match.groups() + rels = map(str.strip, rel.lower().split(',')) + if 'homepage' in rels or 'download' in rels: + for match in HREF.finditer(tag): + url = urlparse.urljoin(base_url, + self._htmldecode(match.group(1))) + if 'download' in rels or self._is_browsable(url): + # yield a list of (url, is_download) + yield (urlparse.urljoin(base_url, url), + 'download' in rels) + + def _default_link_matcher(self, content, base_url): + """Yield all links found on the page. """ - - # process a Requirement - self.info("Searching for %s", requirement) - skipped = {} - - def find(req): - # Find a matching distribution; may be called more than once - - for dist in self[req.key]: - - if dist.precedence==DEVELOP_DIST and not develop_ok: - if dist not in skipped: - self.warn("Skipping development or system egg: %s",dist) - skipped[dist] = 1 - continue - - if dist in req and (dist.precedence<=SOURCE_DIST or not source): - self.info("Best match: %s", dist) - return dist.clone( - location=self.download(dist.location, tmpdir) - ) - - if force_scan: - self.prescan() - self.find_packages(requirement) - - dist = find(requirement) - if dist is None and self.to_scan is not None: - self.prescan() - dist = find(requirement) - - if dist is None and not force_scan: - self.find_packages(requirement) - dist = find(requirement) - - if dist is None: - self.warn( - "No local packages or download links found for %s%s", - (source and "a source distribution of " or ""), - requirement, - ) - return dist - - def fetch(self, requirement, tmpdir, force_scan=False, source=False): - """Obtain a file suitable for fulfilling `requirement` - - DEPRECATED; use the ``fetch_distribution()`` method now instead. For - backward compatibility, this routine is identical but returns the - ``location`` of the downloaded distribution instead of a distribution - object. + for match in HREF.finditer(content): + url = urlparse.urljoin(base_url, self._htmldecode(match.group(1))) + if self._is_browsable(url): + yield (url, False) + + def _process_pypi_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 """ - dist = self.fetch_distribution(requirement,tmpdir,force_scan,source) - if dist is not None: - return dist.location - return None - - def gen_setup(self, filename, fragment, tmpdir): - match = EGG_FRAGMENT.match(fragment) - dists = match and [d for d in - interpret_distro_name(filename, match.group(1), None) if d.version - ] or [] - - if len(dists)==1: # unambiguous ``#egg`` fragment - basename = os.path.basename(filename) - - # Make sure the file has been downloaded to the temp dir. - if os.path.dirname(filename) != tmpdir: - dst = os.path.join(tmpdir, basename) - from setuptools.command.easy_install import samefile - if not samefile(filename, dst): - shutil.copy2(filename, dst) - filename=dst - - file = open(os.path.join(tmpdir, 'setup.py'), 'w') - file.write( - "from setuptools import setup\n" - "setup(name=%r, version=%r, py_modules=[%r])\n" - % ( - dists[0].project_name, dists[0].version, - os.path.splitext(basename)[0] - ) - ) - file.close() - return filename - - elif match: - raise DistutilsError( - "Can't unambiguously interpret project/version identifier %r; " - "any dashes in the name or version should be escaped using " - "underscores. %r" % (fragment,dists) - ) - else: - raise DistutilsError( - "Can't process plain .py without an '#egg=name-version'" - " suffix to enable automatic setup script generation." - ) - - dl_blocksize = 8192 - def _download_to(self, url, filename): - self.info("Downloading %s", url) - # Download the file - fp, tfp, info = None, None, None try: - if '#' in url: - url, info = url.split('#', 1) - fp = self.open_url(url) - if isinstance(fp, urllib2.HTTPError): - raise DistutilsError( - "Can't download %s: %s %s" % (url, fp.code,fp.msg) - ) - cs = md5() - headers = fp.info() - blocknum = 0 - bs = self.dl_blocksize - size = -1 - if "content-length" in headers: - size = int(headers["Content-Length"]) - self.reporthook(url, filename, blocknum, bs, size) - tfp = open(filename,'wb') - while True: - block = fp.read(bs) - if block: - cs.update(block) - tfp.write(block) - blocknum += 1 - self.reporthook(url, filename, blocknum, bs, size) - else: - break - if info: self.check_md5(cs, info, filename, tfp) - return headers - finally: - if fp: fp.close() - if tfp: tfp.close() - - def reporthook(self, url, filename, blocknum, blksize, size): - pass # no-op - - def open_url(self, url, warning=None): - if url.startswith('file:'): - return local_open(url) + # Browse and index the content of the given PyPI page. + url = self.index_url + name + "/" + self._process_url(url, name) + except DownloadError: + # if an error occurs, try with the next index_url + # (provided by the mirrors) + self._switch_to_next_mirror() + self._distributions.clear() + self._process_pypi_page(name) + + @socket_timeout() + def _open_url(self, url): + """Open a urllib2 request, handling HTTP authentication, and local + files support. + + """ try: - return open_with_auth(url) + scheme, netloc, path, params, query, frag = urlparse.urlparse(url) + + if scheme in ('http', 'https'): + auth, host = urllib2.splituser(netloc) + else: + auth = None + + # add index.html automatically for filesystem paths + if scheme == 'file': + if url.endswith('/'): + url += "index.html" + + if auth: + auth = "Basic " + \ + urllib2.unquote(auth).encode('base64').strip() + new_url = urlparse.urlunparse(( + scheme, host, path, params, query, frag)) + request = urllib2.Request(new_url) + request.add_header("Authorization", auth) + else: + request = urllib2.Request(url) + request.add_header('User-Agent', USER_AGENT) + fp = urllib2.urlopen(request) + + 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 = \ + urlparse.urlparse(fp.url) + if s2 == scheme and h2 == host: + fp.url = urlparse.urlunparse( + (s2, netloc, path2, param2, query2, frag2)) + + return fp except (ValueError, httplib.InvalidURL), v: msg = ' '.join([str(arg) for arg in v.args]) - if warning: - self.warn(warning, msg) - else: - raise DistutilsError('%s %s' % (url, msg)) + raise PyPIError('%s %s' % (url, msg)) except urllib2.HTTPError, v: return v except urllib2.URLError, v: - if warning: - self.warn(warning, v.reason) - else: - raise DistutilsError("Download error for %s: %s" - % (url, v.reason)) + raise DownloadError("Download error for %s: %s" % (url, v.reason)) except httplib.BadStatusLine, v: - if warning: - self.warn(warning, v.line) - else: - raise DistutilsError('%s returned a bad status line. ' - 'The server might be down, %s' % \ - (url, v.line)) + raise DownloadError('%s returned a bad status line. ' + 'The server might be down, %s' % (url, v.line)) except httplib.HTTPException, v: - if warning: - self.warn(warning, v) - else: - raise DistutilsError("Download error for %s: %s" - % (url, v)) - - def _download_url(self, scheme, url, tmpdir): - # Determine download filename - name = filter(None,urlparse.urlparse(url)[2].split('/')) - if name: - name = name[-1] - while '..' in name: - name = name.replace('..','.').replace('\\','_') - else: - name = "__downloaded__" # default if URL has no path contents - - if name.endswith('.egg.zip'): - name = name[:-4] # strip the extra .zip before download - - filename = os.path.join(tmpdir,name) - - # Download the file - if scheme=='svn' or scheme.startswith('svn+'): - return self._download_svn(url, filename) - elif scheme=='file': - return urllib2.url2pathname(urlparse.urlparse(url)[2]) - else: - self.url_ok(url, True) # raises error if not allowed - return self._attempt_download(url, filename) - - def scan_url(self, url): - self.process_url(url, True) - - def _attempt_download(self, url, filename): - headers = self._download_to(url, filename) - if 'html' in headers.get('content-type','').lower(): - return self._download_html(url, headers, filename) + raise DownloadError("Download error for %s: %s" % (url, v)) + + 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: - return filename - - def _download_html(self, url, headers, filename): - file = open(filename) - for line in file: - if line.strip(): - # Check for a subversion index page - if re.search(r'<title>([^- ]+ - )?Revision \d+:', line): - # it's a subversion index page: - file.close() - os.unlink(filename) - return self._download_svn(url, filename) - break # not an index page - file.close() - os.unlink(filename) - raise DistutilsError("Unexpected HTML page found at "+url) - - def _download_svn(self, url, filename): - url = url.split('#',1)[0] # remove any fragment for svn's sake - self.info("Doing subversion checkout from %s to %s", url, filename) - os.system("svn checkout -q %s %s" % (url, filename)) - return filename - - def debug(self, msg, *args): - logging.debug(msg, *args) - - def info(self, msg, *args): - logging.info(msg, *args) - - def warn(self, msg, *args): - logging.warn(msg, *args) + from htmlentitydefs import name2codepoint + what = name2codepoint.get(what, match.group(0)) + return unichr(what) -# 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 - -def uchr(c): - if not isinstance(c, int): - return c - if c>255: - return unichr(c) - return chr(c) - -def decode_entity(match): - what = match.group(1) - if what.startswith('#x'): - what = int(what[2:], 16) - elif what.startswith('#'): - what = int(what[1:]) - else: - from htmlentitydefs import name2codepoint - what = name2codepoint.get(what, match.group(0)) - return uchr(what) - -def htmldecode(text): - """Decode HTML entities in the given text.""" - return entity_sub(decode_entity, text) - -def socket_timeout(timeout=15): - def _socket_timeout(func): - def _socket_timeout(*args, **kwargs): - old_timeout = socket.getdefaulttimeout() - socket.setdefaulttimeout(timeout) - try: - return func(*args, **kwargs) - finally: - socket.setdefaulttimeout(old_timeout) - return _socket_timeout - return _socket_timeout - -def open_with_auth(url): - """Open a urllib2 request, handling HTTP authentication""" - - scheme, netloc, path, params, query, frag = urlparse.urlparse(url) - - if scheme in ('http', 'https'): - auth, host = urllib2.splituser(netloc) - else: - auth = None - - if auth: - auth = "Basic " + urllib2.unquote(auth).encode('base64').strip() - new_url = urlparse.urlunparse((scheme,host,path,params,query,frag)) - request = urllib2.Request(new_url) - request.add_header("Authorization", auth) - else: - request = urllib2.Request(url) - - request.add_header('User-Agent', user_agent) - fp = urllib2.urlopen(request) - - 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 = urlparse.urlparse(fp.url) - if s2==scheme and h2==host: - fp.url = urlparse.urlunparse((s2,netloc,path2,param2,query2,frag2)) - - return fp - - -# adding a timeout to avoid freezing package_index -open_with_auth = socket_timeout(_SOCKET_TIMEOUT)(open_with_auth) - -def fix_sf_url(url): - return url # backward compatibility - -def local_open(url): - """Read a local path, with special support for directories""" - scheme, server, path, param, query, frag = urlparse.urlparse(url) - filename = urllib2.url2pathname(path) - if os.path.isfile(filename): - return urllib2.urlopen(url) - elif path.endswith('/') and os.path.isdir(filename): - files = [] - for f in os.listdir(filename): - if f=='index.html': - fp = open(os.path.join(filename,f),'rb') - body = fp.read() - fp.close() - break - elif os.path.isdir(os.path.join(filename,f)): - f+='/' - files.append("<a href=%r>%s</a>" % (f,f)) - else: - body = ("<html><head><title>%s</title>" % url) + \ - "</head><body>%s</body></html>" % '\n'.join(files) - status, message = 200, "OK" - else: - status, message, body = 404, "Path not found", "Not found" - - return urllib2.HTTPError(url, status, message, - {'content-type':'text/html'}, cStringIO.StringIO(body)) + def _htmldecode(self, text): + """Decode HTML entities in the given text.""" + return ENTITY_SUB(self._decode_entity, text) diff --git a/src/distutils2/spawn.py b/src/distutils2/spawn.py deleted file mode 100644 index a875459..0000000 --- a/src/distutils2/spawn.py +++ /dev/null @@ -1,173 +0,0 @@ -"""distutils.spawn - -Provides the 'spawn()' function, a front-end to various platform- -specific functions for launching another program in a sub-process. -Also provides the 'find_executable()' to search the path for a given -executable name. -""" - -__revision__ = "$Id: spawn.py 73147 2009-06-02 15:58:43Z tarek.ziade $" - -import sys -import os - -from distutils2.errors import DistutilsPlatformError, DistutilsExecError -from distutils2 import log - -def spawn(cmd, search_path=1, verbose=0, dry_run=0): - """Run another program, specified as a command list 'cmd', in a new process. - - 'cmd' is just the argument list for the new process, ie. - cmd[0] is the program to run and cmd[1:] are the rest of its arguments. - There is no way to run a program with a name different from that of its - executable. - - If 'search_path' is true (the default), the system's executable - search path will be used to find the program; otherwise, cmd[0] - must be the exact path to the executable. If 'dry_run' is true, - the command will not actually be run. - - Raise DistutilsExecError if running the program fails in any way; just - return on success. - """ - if os.name == 'posix': - _spawn_posix(cmd, search_path, dry_run=dry_run) - elif os.name == 'nt': - _spawn_nt(cmd, search_path, dry_run=dry_run) - elif os.name == 'os2': - _spawn_os2(cmd, search_path, dry_run=dry_run) - else: - raise DistutilsPlatformError, \ - "don't know how to spawn programs on platform '%s'" % os.name - -def _nt_quote_args(args): - """Quote command-line arguments for DOS/Windows conventions. - - Just wraps every argument which contains blanks in double quotes, and - returns a new argument list. - """ - # XXX this doesn't seem very robust to me -- but if the Windows guys - # say it'll work, I guess I'll have to accept it. (What if an arg - # contains quotes? What other magic characters, other than spaces, - # have to be escaped? Is there an escaping mechanism other than - # quoting?) - for i, arg in enumerate(args): - if ' ' in arg: - args[i] = '"%s"' % arg - return args - -def _spawn_nt(cmd, search_path=1, verbose=0, dry_run=0): - executable = cmd[0] - cmd = _nt_quote_args(cmd) - if search_path: - # either we find one or it stays the same - executable = find_executable(executable) or executable - log.info(' '.join([executable] + cmd[1:])) - if not dry_run: - # spawn for NT requires a full path to the .exe - try: - rc = os.spawnv(os.P_WAIT, executable, cmd) - except OSError, exc: - # this seems to happen when the command isn't found - raise DistutilsExecError, \ - "command '%s' failed: %s" % (cmd[0], exc[-1]) - if rc != 0: - # and this reflects the command running but failing - raise DistutilsExecError, \ - "command '%s' failed with exit status %d" % (cmd[0], rc) - -def _spawn_os2(cmd, search_path=1, verbose=0, dry_run=0): - executable = cmd[0] - if search_path: - # either we find one or it stays the same - executable = find_executable(executable) or executable - log.info(' '.join([executable] + cmd[1:])) - if not dry_run: - # spawnv for OS/2 EMX requires a full path to the .exe - try: - rc = os.spawnv(os.P_WAIT, executable, cmd) - except OSError, exc: - # this seems to happen when the command isn't found - raise DistutilsExecError, \ - "command '%s' failed: %s" % (cmd[0], exc[-1]) - if rc != 0: - # and this reflects the command running but failing - log.debug("command '%s' failed with exit status %d" % (cmd[0], rc)) - raise DistutilsExecError, \ - "command '%s' failed with exit status %d" % (cmd[0], rc) - - -def _spawn_posix(cmd, search_path=1, verbose=0, dry_run=0): - log.info(' '.join(cmd)) - if dry_run: - return - exec_fn = search_path and os.execvp or os.execv - pid = os.fork() - - if pid == 0: # in the child - try: - exec_fn(cmd[0], cmd) - except OSError, e: - sys.stderr.write("unable to execute %s: %s\n" % - (cmd[0], e.strerror)) - os._exit(1) - - sys.stderr.write("unable to execute %s for unknown reasons" % cmd[0]) - os._exit(1) - else: # in the parent - # Loop until the child either exits or is terminated by a signal - # (ie. keep waiting if it's merely stopped) - while 1: - try: - pid, status = os.waitpid(pid, 0) - except OSError, exc: - import errno - if exc.errno == errno.EINTR: - continue - raise DistutilsExecError, \ - "command '%s' failed: %s" % (cmd[0], exc[-1]) - if os.WIFSIGNALED(status): - raise DistutilsExecError, \ - "command '%s' terminated by signal %d" % \ - (cmd[0], os.WTERMSIG(status)) - - elif os.WIFEXITED(status): - exit_status = os.WEXITSTATUS(status) - if exit_status == 0: - return # hey, it succeeded! - else: - raise DistutilsExecError, \ - "command '%s' failed with exit status %d" % \ - (cmd[0], exit_status) - - elif os.WIFSTOPPED(status): - continue - - else: - raise DistutilsExecError, \ - "unknown error executing '%s': termination status %d" % \ - (cmd[0], status) - -def find_executable(executable, path=None): - """Tries to find 'executable' in the directories listed in 'path'. - - A string listing directories separated by 'os.pathsep'; defaults to - os.environ['PATH']. Returns the complete filename or None if not found. - """ - if path is None: - path = os.environ['PATH'] - paths = path.split(os.pathsep) - base, ext = os.path.splitext(executable) - - if (sys.platform == 'win32' or os.name == 'os2') and (ext != '.exe'): - executable = executable + '.exe' - - if not os.path.isfile(executable): - for p in paths: - f = os.path.join(p, executable) - if os.path.isfile(f): - # the file exists, we have a shot at spawn working - return f - return None - else: - return executable diff --git a/src/distutils2/tests/__init__.py b/src/distutils2/tests/__init__.py index f5b4b1e..8a8d187 100644 --- a/src/distutils2/tests/__init__.py +++ b/src/distutils2/tests/__init__.py @@ -45,7 +45,7 @@ class TestFailed(Error): """Test failed.""" -class BasicTestRunner: +class BasicTestRunner(object): def run(self, test): result = unittest.TestResult() test(result) diff --git a/src/distutils2/tests/conversions/05_after.py b/src/distutils2/tests/conversions/05_after.py new file mode 100644 index 0000000..2f6a7b3 --- /dev/null +++ b/src/distutils2/tests/conversions/05_after.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2003-2009 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. + +from distutils2.core import setup, find_packages + +extra = {} + +try: + import babel + + extractors = [ + ('**.py', 'python', None), + ('**/templates/**.html', 'genshi', None), + ('**/templates/**.txt', 'genshi', + {'template_class': 'genshi.template:NewTextTemplate'}), + ] + extra['message_extractors'] = { + 'trac': extractors, + 'tracopt': extractors, + } + + from trac.util.dist import get_l10n_js_cmdclass + extra['cmdclass'] = get_l10n_js_cmdclass() + +except ImportError, e: + pass + +setup( + name = 'Trac', + version = '0.12.1', + summary = 'Integrated SCM, wiki, issue tracker and project environment', + description = """ +Trac is a minimalistic web-based software project management and bug/issue +tracking system. It provides an interface to the Subversion revision control +systems, an integrated wiki, flexible issue tracking and convenient report +facilities. +""", + author = 'Edgewall Software', + author_email = 'info@edgewall.com', + license = 'BSD', + home_page = 'http://trac.edgewall.org/', + download_url = 'http://trac.edgewall.org/wiki/TracDownload', + classifiers = [ + 'Environment :: Web Environment', + 'Framework :: Trac', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Software Development :: Bug Tracking', + 'Topic :: Software Development :: Version Control', + ], + + packages = find_packages(exclude=['*.tests']), + package_data = { + '': ['templates/*'], + 'trac': ['htdocs/*.*', 'htdocs/README', 'htdocs/js/*.*', + 'htdocs/js/messages/*.*', 'htdocs/css/*.*', + 'htdocs/guide/*', 'locale/*/LC_MESSAGES/messages.mo'], + 'trac.wiki': ['default-pages/*'], + 'trac.ticket': ['workflows/*.ini'], + }, + + test_suite = 'trac.test.suite', + zip_safe = True, + + requires_dist = [ + 'setuptools>=0.6b1', + 'Genshi>=0.6', + ], + extras_require = { + 'Babel': ['Babel>=0.9.5'], + 'Pygments': ['Pygments>=0.6'], + 'reST': ['docutils>=0.3'], + 'SilverCity': ['SilverCity>=0.9.4'], + 'Textile': ['textile>=2.0'], + }, + + entry_points = """ + [console_scripts] + trac-admin = trac.admin.console:run + tracd = trac.web.standalone:main + + [trac.plugins] + trac.about = trac.about + trac.admin.console = trac.admin.console + trac.admin.web_ui = trac.admin.web_ui + trac.attachment = trac.attachment + trac.db.mysql = trac.db.mysql_backend + trac.db.postgres = trac.db.postgres_backend + trac.db.sqlite = trac.db.sqlite_backend + trac.mimeview.patch = trac.mimeview.patch + trac.mimeview.pygments = trac.mimeview.pygments[Pygments] + trac.mimeview.rst = trac.mimeview.rst[reST] + trac.mimeview.silvercity = trac.mimeview.silvercity[SilverCity] + trac.mimeview.txtl = trac.mimeview.txtl[Textile] + trac.prefs = trac.prefs.web_ui + trac.search = trac.search.web_ui + trac.ticket.admin = trac.ticket.admin + trac.ticket.query = trac.ticket.query + trac.ticket.report = trac.ticket.report + trac.ticket.roadmap = trac.ticket.roadmap + trac.ticket.web_ui = trac.ticket.web_ui + trac.timeline = trac.timeline.web_ui + trac.versioncontrol.admin = trac.versioncontrol.admin + trac.versioncontrol.svn_authz = trac.versioncontrol.svn_authz + trac.versioncontrol.svn_fs = trac.versioncontrol.svn_fs + trac.versioncontrol.svn_prop = trac.versioncontrol.svn_prop + trac.versioncontrol.web_ui = trac.versioncontrol.web_ui + trac.web.auth = trac.web.auth + trac.web.session = trac.web.session + trac.wiki.admin = trac.wiki.admin + trac.wiki.interwiki = trac.wiki.interwiki + trac.wiki.macros = trac.wiki.macros + trac.wiki.web_ui = trac.wiki.web_ui + trac.wiki.web_api = trac.wiki.web_api + tracopt.mimeview.enscript = tracopt.mimeview.enscript + tracopt.mimeview.php = tracopt.mimeview.php + tracopt.perm.authz_policy = tracopt.perm.authz_policy + tracopt.perm.config_perm_provider = tracopt.perm.config_perm_provider + tracopt.ticket.commit_updater = tracopt.ticket.commit_updater + tracopt.ticket.deleter = tracopt.ticket.deleter + """, + + **extra +) diff --git a/src/distutils2/tests/conversions/05_before.py b/src/distutils2/tests/conversions/05_before.py new file mode 100755 index 0000000..ccce17b --- /dev/null +++ b/src/distutils2/tests/conversions/05_before.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2003-2009 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.org/wiki/TracLicense. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. + +from setuptools import setup, find_packages + +extra = {} + +try: + import babel + + extractors = [ + ('**.py', 'python', None), + ('**/templates/**.html', 'genshi', None), + ('**/templates/**.txt', 'genshi', + {'template_class': 'genshi.template:NewTextTemplate'}), + ] + extra['message_extractors'] = { + 'trac': extractors, + 'tracopt': extractors, + } + + from trac.util.dist import get_l10n_js_cmdclass + extra['cmdclass'] = get_l10n_js_cmdclass() + +except ImportError, e: + pass + +setup( + name = 'Trac', + version = '0.12.1', + description = 'Integrated SCM, wiki, issue tracker and project environment', + long_description = """ +Trac is a minimalistic web-based software project management and bug/issue +tracking system. It provides an interface to the Subversion revision control +systems, an integrated wiki, flexible issue tracking and convenient report +facilities. +""", + author = 'Edgewall Software', + author_email = 'info@edgewall.com', + license = 'BSD', + url = 'http://trac.edgewall.org/', + download_url = 'http://trac.edgewall.org/wiki/TracDownload', + classifiers = [ + 'Environment :: Web Environment', + 'Framework :: Trac', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Software Development :: Bug Tracking', + 'Topic :: Software Development :: Version Control', + ], + + packages = find_packages(exclude=['*.tests']), + package_data = { + '': ['templates/*'], + 'trac': ['htdocs/*.*', 'htdocs/README', 'htdocs/js/*.*', + 'htdocs/js/messages/*.*', 'htdocs/css/*.*', + 'htdocs/guide/*', 'locale/*/LC_MESSAGES/messages.mo'], + 'trac.wiki': ['default-pages/*'], + 'trac.ticket': ['workflows/*.ini'], + }, + + test_suite = 'trac.test.suite', + zip_safe = True, + + install_requires = [ + 'setuptools>=0.6b1', + 'Genshi>=0.6', + ], + extras_require = { + 'Babel': ['Babel>=0.9.5'], + 'Pygments': ['Pygments>=0.6'], + 'reST': ['docutils>=0.3'], + 'SilverCity': ['SilverCity>=0.9.4'], + 'Textile': ['textile>=2.0'], + }, + + entry_points = """ + [console_scripts] + trac-admin = trac.admin.console:run + tracd = trac.web.standalone:main + + [trac.plugins] + trac.about = trac.about + trac.admin.console = trac.admin.console + trac.admin.web_ui = trac.admin.web_ui + trac.attachment = trac.attachment + trac.db.mysql = trac.db.mysql_backend + trac.db.postgres = trac.db.postgres_backend + trac.db.sqlite = trac.db.sqlite_backend + trac.mimeview.patch = trac.mimeview.patch + trac.mimeview.pygments = trac.mimeview.pygments[Pygments] + trac.mimeview.rst = trac.mimeview.rst[reST] + trac.mimeview.silvercity = trac.mimeview.silvercity[SilverCity] + trac.mimeview.txtl = trac.mimeview.txtl[Textile] + trac.prefs = trac.prefs.web_ui + trac.search = trac.search.web_ui + trac.ticket.admin = trac.ticket.admin + trac.ticket.query = trac.ticket.query + trac.ticket.report = trac.ticket.report + trac.ticket.roadmap = trac.ticket.roadmap + trac.ticket.web_ui = trac.ticket.web_ui + trac.timeline = trac.timeline.web_ui + trac.versioncontrol.admin = trac.versioncontrol.admin + trac.versioncontrol.svn_authz = trac.versioncontrol.svn_authz + trac.versioncontrol.svn_fs = trac.versioncontrol.svn_fs + trac.versioncontrol.svn_prop = trac.versioncontrol.svn_prop + trac.versioncontrol.web_ui = trac.versioncontrol.web_ui + trac.web.auth = trac.web.auth + trac.web.session = trac.web.session + trac.wiki.admin = trac.wiki.admin + trac.wiki.interwiki = trac.wiki.interwiki + trac.wiki.macros = trac.wiki.macros + trac.wiki.web_ui = trac.wiki.web_ui + trac.wiki.web_api = trac.wiki.web_api + tracopt.mimeview.enscript = tracopt.mimeview.enscript + tracopt.mimeview.php = tracopt.mimeview.php + tracopt.perm.authz_policy = tracopt.perm.authz_policy + tracopt.perm.config_perm_provider = tracopt.perm.config_perm_provider + tracopt.ticket.commit_updater = tracopt.ticket.commit_updater + tracopt.ticket.deleter = tracopt.ticket.deleter + """, + + **extra +) diff --git a/src/distutils2/tests/pypi_server.py b/src/distutils2/tests/pypi_server.py index cd57ce5..3927262 100644 --- a/src/distutils2/tests/pypi_server.py +++ b/src/distutils2/tests/pypi_server.py @@ -26,10 +26,12 @@ def use_pypi_server(*server_args, **server_kwargs): def wrapped(*args, **kwargs): server = PyPIServer(*server_args, **server_kwargs) server.start() - func(server=server, *args, **kwargs) - server.stop() + try: + func(server=server, *args, **kwargs) + finally: + server.stop() return wrapped - return wrapper + return wrapper class PyPIServerTestCase(unittest.TestCase): @@ -50,7 +52,7 @@ class PyPIServer(threading.Thread): """ def __init__(self, test_static_path=None, - static_filesystem_paths=["default"], static_uri_paths=["simple"]): + static_filesystem_paths=["default"], static_uri_paths=["simple"]): """Initialize the server. static_uri_paths and static_base_path are parameters used to provides @@ -59,7 +61,7 @@ class PyPIServer(threading.Thread): """ threading.Thread.__init__(self) self._run = True - self.httpd = HTTPServer(('', 0), PyPIRequestHandler) + self.httpd = HTTPServer(('', 0), PyPIRequestHandler) self.httpd.RequestHandlerClass.log_request = lambda *_: None self.httpd.RequestHandlerClass.pypi_server = self self.address = (self.httpd.server_name, self.httpd.server_port) @@ -131,7 +133,7 @@ class PyPIRequestHandler(SimpleHTTPRequestHandler): """ # record the request. Read the input only on PUT or POST requests if self.command in ("PUT", "POST"): - if self.headers.dict.has_key("content-length"): + if 'content-length' in self.headers.dict: request_data = self.rfile.read( int(self.headers['content-length'])) else: @@ -158,7 +160,11 @@ class PyPIRequestHandler(SimpleHTTPRequestHandler): relative_path += "index.html" file = open(fs_path + relative_path) data = file.read() - self.make_response(data) + if relative_path.endswith('.tar.gz'): + headers=[('Content-type', 'application/x-gtar')] + else: + headers=[('Content-type', 'text/html')] + self.make_response(data, headers=headers) except IOError: pass @@ -171,8 +177,8 @@ class PyPIRequestHandler(SimpleHTTPRequestHandler): status, headers, data = self.pypi_server.get_next_response() self.make_response(data, status, headers) - def make_response(self, data, status=200, - headers=[('Content-type', 'text/html')]): + 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: @@ -180,7 +186,7 @@ class PyPIRequestHandler(SimpleHTTPRequestHandler): except ValueError: # we probably got something like YYY Codename. # Just get the first 3 digits - status = int(status[:3]) + status = int(status[:3]) self.send_response(status) for header, value in headers: diff --git a/src/distutils2/tests/pypiserver/downloads_with_md5/simple/badmd5/badmd5-0.1.tar.gz b/src/distutils2/tests/pypiserver/downloads_with_md5/simple/badmd5/badmd5-0.1.tar.gz new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/distutils2/tests/pypiserver/downloads_with_md5/simple/badmd5/badmd5-0.1.tar.gz diff --git a/src/distutils2/tests/pypiserver/downloads_with_md5/simple/badmd5/index.html b/src/distutils2/tests/pypiserver/downloads_with_md5/simple/badmd5/index.html new file mode 100644 index 0000000..b89f1bd --- /dev/null +++ b/src/distutils2/tests/pypiserver/downloads_with_md5/simple/badmd5/index.html @@ -0,0 +1,3 @@ +<html><body> +<a href="badmd5-0.1.tar.gz#md5=3e3d86693d6564c807272b11b3069dfe" rel="download">badmd5-0.1.tar.gz</a><br/> +</body></html> diff --git a/src/distutils2/tests/pypiserver/downloads_with_md5/simple/foobar/foobar-0.1.tar.gz b/src/distutils2/tests/pypiserver/downloads_with_md5/simple/foobar/foobar-0.1.tar.gz new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/distutils2/tests/pypiserver/downloads_with_md5/simple/foobar/foobar-0.1.tar.gz diff --git a/src/distutils2/tests/pypiserver/downloads_with_md5/simple/foobar/index.html b/src/distutils2/tests/pypiserver/downloads_with_md5/simple/foobar/index.html new file mode 100644 index 0000000..add0b85 --- /dev/null +++ b/src/distutils2/tests/pypiserver/downloads_with_md5/simple/foobar/index.html @@ -0,0 +1,3 @@ +<html><body> +<a href="foobar-0.1.tar.gz#md5=d41d8cd98f00b204e9800998ecf8427e" rel="download">foobar-0.1.tar.gz</a><br/> +</body></html> diff --git a/src/distutils2/tests/pypiserver/downloads_with_md5/simple/index.html b/src/distutils2/tests/pypiserver/downloads_with_md5/simple/index.html new file mode 100644 index 0000000..9baee04 --- /dev/null +++ b/src/distutils2/tests/pypiserver/downloads_with_md5/simple/index.html @@ -0,0 +1,2 @@ +<a href="foobar/">foobar/</a> +<a href="badmd5/">badmd5/</a> diff --git a/src/distutils2/tests/pypiserver/foo_bar_baz/simple/bar/index.html b/src/distutils2/tests/pypiserver/foo_bar_baz/simple/bar/index.html new file mode 100644 index 0000000..c3d42c5 --- /dev/null +++ b/src/distutils2/tests/pypiserver/foo_bar_baz/simple/bar/index.html @@ -0,0 +1,6 @@ +<html><head><title>Links for bar</title></head><body><h1>Links for bar</h1> +<a rel="download" href="../../packages/source/F/bar/bar-1.0.tar.gz">bar-1.0.tar.gz</a><br/> +<a rel="download" href="../../packages/source/F/bar/bar-1.0.1.tar.gz">bar-1.0.1.tar.gz</a><br/> +<a rel="download" href="../../packages/source/F/bar/bar-2.0.tar.gz">bar-2.0.tar.gz</a><br/> +<a rel="download" href="../../packages/source/F/bar/bar-2.0.1.tar.gz">bar-2.0.1.tar.gz</a><br/> +</body></html> diff --git a/src/distutils2/tests/pypiserver/foo_bar_baz/simple/baz/index.html b/src/distutils2/tests/pypiserver/foo_bar_baz/simple/baz/index.html new file mode 100644 index 0000000..4f34312 --- /dev/null +++ b/src/distutils2/tests/pypiserver/foo_bar_baz/simple/baz/index.html @@ -0,0 +1,6 @@ +<html><head><title>Links for baz</title></head><body><h1>Links for baz</h1> +<a rel="download" href="../../packages/source/F/baz/baz-1.0.tar.gz">baz-1.0.tar.gz</a><br/> +<a rel="download" href="../../packages/source/F/baz/baz-1.0.1.tar.gz">baz-1.0.1.tar.gz</a><br/> +<a rel="download" href="../../packages/source/F/baz/baz-2.0.tar.gz">baz-2.0.tar.gz</a><br/> +<a rel="download" href="../../packages/source/F/baz/baz-2.0.1.tar.gz">baz-2.0.1.tar.gz</a><br/> +</body></html> diff --git a/src/distutils2/tests/pypiserver/foo_bar_baz/simple/foo/index.html b/src/distutils2/tests/pypiserver/foo_bar_baz/simple/foo/index.html new file mode 100644 index 0000000..0565e11 --- /dev/null +++ b/src/distutils2/tests/pypiserver/foo_bar_baz/simple/foo/index.html @@ -0,0 +1,6 @@ +<html><head><title>Links for foo</title></head><body><h1>Links for foo</h1> +<a rel="download" href="../../packages/source/F/foo/foo-1.0.tar.gz">foo-1.0.tar.gz</a><br/> +<a rel="download" href="../../packages/source/F/foo/foo-1.0.1.tar.gz">foo-1.0.1.tar.gz</a><br/> +<a rel="download" href="../../packages/source/F/foo/foo-2.0.tar.gz">foo-2.0.tar.gz</a><br/> +<a rel="download" href="../../packages/source/F/foo/foo-2.0.1.tar.gz">foo-2.0.1.tar.gz</a><br/> +</body></html> diff --git a/src/distutils2/tests/pypiserver/foo_bar_baz/simple/index.html b/src/distutils2/tests/pypiserver/foo_bar_baz/simple/index.html new file mode 100644 index 0000000..a70cfd3 --- /dev/null +++ b/src/distutils2/tests/pypiserver/foo_bar_baz/simple/index.html @@ -0,0 +1,3 @@ +<a href="foo/">foo/</a> +<a href="bar/">bar/</a> +<a href="baz/">baz/</a> diff --git a/src/distutils2/tests/pypiserver/test_found_links/simple/foobar/index.html b/src/distutils2/tests/pypiserver/test_found_links/simple/foobar/index.html index 2da4bba..a282a4e 100644 --- a/src/distutils2/tests/pypiserver/test_found_links/simple/foobar/index.html +++ b/src/distutils2/tests/pypiserver/test_found_links/simple/foobar/index.html @@ -1,6 +1,6 @@ <html><head><title>Links for Foobar</title></head><body><h1>Links for Foobar</h1> -<a href="../../packages/source/F/Foobar/Foobar-1.0.tar.gz#md5=98fa833fdabcdd78d00245aead66c174">Foobar-1.0.tar.gz</a><br/> -<a href="../../packages/source/F/Foobar/Foobar-1.0.1.tar.gz#md5=2351efb20f6b7b5d9ce80fa4cb1bd9ca">Foobar-1.0.1.tar.gz</a><br/> -<a href="../../packages/source/F/Foobar/Foobar-2.0.tar.gz#md5=98fa833fdabcdd78d00245aead66c274">Foobar-2.0.tar.gz</a><br/> -<a href="../../packages/source/F/Foobar/Foobar-2.0.1.tar.gz#md5=2352efb20f6b7b5d9ce80fa4cb2bd9ca">Foobar-2.0.1.tar.gz</a><br/> +<a rel="download" href="../../packages/source/F/Foobar/Foobar-1.0.tar.gz#md5=98fa833fdabcdd78d00245aead66c174">Foobar-1.0.tar.gz</a><br/> +<a rel="download" href="../../packages/source/F/Foobar/Foobar-1.0.1.tar.gz#md5=2351efb20f6b7b5d9ce80fa4cb1bd9ca">Foobar-1.0.1.tar.gz</a><br/> +<a rel="download" href="../../packages/source/F/Foobar/Foobar-2.0.tar.gz#md5=98fa833fdabcdd78d00245aead66c274">Foobar-2.0.tar.gz</a><br/> +<a rel="download" href="../../packages/source/F/Foobar/Foobar-2.0.1.tar.gz#md5=2352efb20f6b7b5d9ce80fa4cb2bd9ca">Foobar-2.0.1.tar.gz</a><br/> </body></html> diff --git a/src/distutils2/tests/pypiserver/test_link_priority/external/external.html b/src/distutils2/tests/pypiserver/with_externals/external/external.html index 92e4702..92e4702 100644 --- a/src/distutils2/tests/pypiserver/test_link_priority/external/external.html +++ b/src/distutils2/tests/pypiserver/with_externals/external/external.html diff --git a/src/distutils2/tests/pypiserver/test_link_priority/simple/foobar/index.html b/src/distutils2/tests/pypiserver/with_externals/simple/foobar/index.html index 2af83d8..b100a26 100644 --- a/src/distutils2/tests/pypiserver/test_link_priority/simple/foobar/index.html +++ b/src/distutils2/tests/pypiserver/with_externals/simple/foobar/index.html @@ -1,4 +1,4 @@ <html><body> -<a href="/foobar-0.1.tar.gz#md5=0_correct_md5">foobar-0.1.tar.gz</a><br/> +<a rel ="download" href="/foobar-0.1.tar.gz#md5=12345678901234567">foobar-0.1.tar.gz</a><br/> <a href="../../external/external.html" rel="homepage">external homepage</a><br/> </body></html> diff --git a/src/distutils2/tests/pypiserver/test_link_priority/simple/index.html b/src/distutils2/tests/pypiserver/with_externals/simple/index.html index a1a7bb7..a1a7bb7 100644 --- a/src/distutils2/tests/pypiserver/test_link_priority/simple/index.html +++ b/src/distutils2/tests/pypiserver/with_externals/simple/index.html diff --git a/src/distutils2/tests/pypiserver/with_norel_links/external/homepage.html b/src/distutils2/tests/pypiserver/with_norel_links/external/homepage.html new file mode 100644 index 0000000..1cc0c32 --- /dev/null +++ b/src/distutils2/tests/pypiserver/with_norel_links/external/homepage.html @@ -0,0 +1,7 @@ +<html> +<body> +<p>a rel=homepage HTML page</p> +<a href="/foobar-2.0.tar.gz">foobar 2.0</a> +</body> +</html> + diff --git a/src/distutils2/tests/pypiserver/with_norel_links/external/nonrel.html b/src/distutils2/tests/pypiserver/with_norel_links/external/nonrel.html new file mode 100644 index 0000000..f6ace22 --- /dev/null +++ b/src/distutils2/tests/pypiserver/with_norel_links/external/nonrel.html @@ -0,0 +1 @@ +A page linked without rel="download" or rel="homepage" link. diff --git a/src/distutils2/tests/pypiserver/with_norel_links/simple/foobar/index.html b/src/distutils2/tests/pypiserver/with_norel_links/simple/foobar/index.html new file mode 100644 index 0000000..171df93 --- /dev/null +++ b/src/distutils2/tests/pypiserver/with_norel_links/simple/foobar/index.html @@ -0,0 +1,6 @@ +<html><body> +<a rel="download" href="/foobar-0.1.tar.gz" rel="download">foobar-0.1.tar.gz</a><br/> +<a href="../../external/homepage.html" rel="homepage">external homepage</a><br/> +<a href="../../external/nonrel.html">unrelated link</a><br/> +<a href="/unrelated-0.2.tar.gz">unrelated download</a></br/> +</body></html> diff --git a/src/distutils2/tests/pypiserver/with_norel_links/simple/index.html b/src/distutils2/tests/pypiserver/with_norel_links/simple/index.html new file mode 100644 index 0000000..a1a7bb7 --- /dev/null +++ b/src/distutils2/tests/pypiserver/with_norel_links/simple/index.html @@ -0,0 +1 @@ +<a href="foobar/">foobar/</a> diff --git a/src/distutils2/tests/pypiserver/with_real_externals/simple/foobar/index.html b/src/distutils2/tests/pypiserver/with_real_externals/simple/foobar/index.html new file mode 100644 index 0000000..b2885ae --- /dev/null +++ b/src/distutils2/tests/pypiserver/with_real_externals/simple/foobar/index.html @@ -0,0 +1,4 @@ +<html><body> +<a rel="download" href="/foobar-0.1.tar.gz#md5=0_correct_md5">foobar-0.1.tar.gz</a><br/> +<a href="http://a-really-external-website/external/external.html" rel="homepage">external homepage</a><br/> +</body></html> diff --git a/src/distutils2/tests/pypiserver/with_real_externals/simple/index.html b/src/distutils2/tests/pypiserver/with_real_externals/simple/index.html new file mode 100644 index 0000000..a1a7bb7 --- /dev/null +++ b/src/distutils2/tests/pypiserver/with_real_externals/simple/index.html @@ -0,0 +1 @@ +<a href="foobar/">foobar/</a> diff --git a/src/distutils2/tests/support.py b/src/distutils2/tests/support.py index fd0dc2d..518cedf 100644 --- a/src/distutils2/tests/support.py +++ b/src/distutils2/tests/support.py @@ -14,7 +14,6 @@ import warnings from distutils2 import log from distutils2.log import DEBUG, INFO, WARN, ERROR, FATAL -from distutils2.core import Distribution if sys.version_info >= (2, 7): # improved unittest package from 2.7's standard library @@ -42,7 +41,7 @@ class LoggingSilencer(object): def _log(self, level, msg, args): if level not in (DEBUG, INFO, WARN, ERROR, FATAL): - raise ValueError('%s wrong log level' % str(level)) + raise ValueError('%s wrong log level' % level) self.logs.append((level, msg, args)) def get_logs(self, *levels): @@ -65,12 +64,22 @@ class TempdirManager(object): def setUp(self): super(TempdirManager, self).setUp() self.tempdirs = [] + self.tempfiles = [] def tearDown(self): super(TempdirManager, self).tearDown() while self.tempdirs: d = self.tempdirs.pop() shutil.rmtree(d, os.name in ('nt', 'cygwin')) + for file_ in self.tempfiles: + if os.path.exists(file_): + os.remove(file_) + + def mktempfile(self): + """Create a temporary file that will be cleaned up.""" + tempfile_ = tempfile.NamedTemporaryFile() + self.tempfiles.append(tempfile_.name) + return tempfile_ def mkdtemp(self): """Create a temporary directory that will be cleaned up. @@ -105,6 +114,7 @@ class TempdirManager(object): It returns the package directory and the distribution instance. """ + from distutils2.dist import Distribution tmp_dir = self.mkdtemp() pkg_dir = os.path.join(tmp_dir, pkg_name) os.mkdir(pkg_dir) diff --git a/src/distutils2/tests/test_Mixin2to3.py b/src/distutils2/tests/test_Mixin2to3.py new file mode 100644 index 0000000..a455fc2 --- /dev/null +++ b/src/distutils2/tests/test_Mixin2to3.py @@ -0,0 +1,54 @@ +"""Tests for distutils.command.build_py.""" +import sys +import tempfile + +import distutils2 +from distutils2.tests import support +from distutils2.tests.support import unittest +from distutils2.command.build_py import Mixin2to3 + + +class Mixin2to3TestCase(support.TempdirManager, unittest.TestCase): + + @unittest.skipUnless(sys.version > '2.6', 'Need >= 2.6') + def test_convert_code_only(self): + # used to check if code gets converted properly. + code_content = "print 'test'\n" + code_handle = self.mktempfile() + code_name = code_handle.name + + code_handle.write(code_content) + code_handle.flush() + + mixin2to3 = Mixin2to3() + mixin2to3._run_2to3([code_name]) + converted_code_content = "print('test')\n" + new_code_content = "".join(open(code_name).readlines()) + + self.assertEquals(new_code_content, converted_code_content) + + @unittest.skipUnless(sys.version > '2.6', 'Need >= 2.6') + def test_doctests_only(self): + # used to check if doctests gets converted properly. + doctest_content = '"""\n>>> print test\ntest\n"""\nprint test\n\n' + doctest_handle = self.mktempfile() + doctest_name = doctest_handle.name + + doctest_handle.write(doctest_content) + doctest_handle.flush() + + mixin2to3 = Mixin2to3() + mixin2to3._run_2to3([doctest_name]) + + converted_doctest_content = ['"""', '>>> print(test)', 'test', '"""', + 'print(test)', '', '', ''] + converted_doctest_content = '\n'.join(converted_doctest_content) + new_doctest_content = "".join(open(doctest_name).readlines()) + + self.assertEquals(new_doctest_content, converted_doctest_content) + +def test_suite(): + return unittest.makeSuite(Mixin2to3TestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/src/distutils2/tests/test_bdist.py b/src/distutils2/tests/test_bdist.py index ba2280b..2be01e1 100644 --- a/src/distutils2/tests/test_bdist.py +++ b/src/distutils2/tests/test_bdist.py @@ -1,8 +1,6 @@ """Tests for distutils.command.bdist.""" import sys import os -import tempfile -import shutil from distutils2.tests import run_unittest @@ -10,8 +8,7 @@ from distutils2.core import Distribution from distutils2.command.bdist import bdist from distutils2.tests import support from distutils2.tests.support import unittest -from distutils2.spawn import find_executable -from distutils2 import spawn +from distutils2.util import find_executable from distutils2.errors import DistutilsExecError class BuildTestCase(support.TempdirManager, @@ -25,7 +22,7 @@ class BuildTestCase(support.TempdirManager, cmd = bdist(dist) cmd.formats = ['msi'] cmd.ensure_finalized() - self.assertEquals(cmd.formats, ['msi']) + self.assertEqual(cmd.formats, ['msi']) # what format bdist offers ? # XXX an explicit list in bdist is @@ -33,9 +30,9 @@ class BuildTestCase(support.TempdirManager, # we should add a registry formats = ['zip', 'gztar', 'bztar', 'ztar', 'tar', 'wininst', 'msi'] formats.sort() - founded = cmd.format_command.keys() - founded.sort() - self.assertEquals(founded, formats) + found = cmd.format_command.keys() + found.sort() + self.assertEqual(found, formats) def test_suite(): return unittest.makeSuite(BuildTestCase) diff --git a/src/distutils2/tests/test_bdist_dumb.py b/src/distutils2/tests/test_bdist_dumb.py index 2cdfe67..770827c 100644 --- a/src/distutils2/tests/test_bdist_dumb.py +++ b/src/distutils2/tests/test_bdist_dumb.py @@ -78,7 +78,7 @@ class BuildDumbTestCase(support.TempdirManager, base = base.replace(':', '-') wanted = ['%s.zip' % base] - self.assertEquals(dist_created, wanted) + self.assertEqual(dist_created, wanted) # now let's check what we have in the zip file # XXX to be done @@ -87,16 +87,16 @@ class BuildDumbTestCase(support.TempdirManager, pkg_dir, dist = self.create_dist() os.chdir(pkg_dir) cmd = bdist_dumb(dist) - self.assertEquals(cmd.bdist_dir, None) + 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.assertEquals(cmd.bdist_dir, os.path.join(base, 'dumb')) + 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.assertEquals(cmd.format, default) + self.assertEqual(cmd.format, default) def test_suite(): return unittest.makeSuite(BuildDumbTestCase) diff --git a/src/distutils2/tests/test_build.py b/src/distutils2/tests/test_build.py index 32be44a..abf9ef8 100644 --- a/src/distutils2/tests/test_build.py +++ b/src/distutils2/tests/test_build.py @@ -20,11 +20,11 @@ class BuildTestCase(support.TempdirManager, cmd.finalize_options() # if not specified, plat_name gets the current platform - self.assertEquals(cmd.plat_name, get_platform()) + self.assertEqual(cmd.plat_name, get_platform()) # build_purelib is build + lib wanted = os.path.join(cmd.build_base, 'lib') - self.assertEquals(cmd.build_purelib, wanted) + self.assertEqual(cmd.build_purelib, wanted) # build_platlib is 'build/lib.platform-x.x[-pydebug]' # examples: @@ -34,21 +34,21 @@ class BuildTestCase(support.TempdirManager, self.assertTrue(cmd.build_platlib.endswith('-pydebug')) plat_spec += '-pydebug' wanted = os.path.join(cmd.build_base, 'lib' + plat_spec) - self.assertEquals(cmd.build_platlib, wanted) + self.assertEqual(cmd.build_platlib, wanted) # by default, build_lib = build_purelib - self.assertEquals(cmd.build_lib, cmd.build_purelib) + self.assertEqual(cmd.build_lib, cmd.build_purelib) # build_temp is build/temp.<plat> wanted = os.path.join(cmd.build_base, 'temp' + plat_spec) - self.assertEquals(cmd.build_temp, wanted) + 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.assertEquals(cmd.build_scripts, wanted) + self.assertEqual(cmd.build_scripts, wanted) # executable is os.path.normpath(sys.executable) - self.assertEquals(cmd.executable, os.path.normpath(sys.executable)) + self.assertEqual(cmd.executable, os.path.normpath(sys.executable)) def test_suite(): return unittest.makeSuite(BuildTestCase) diff --git a/src/distutils2/tests/test_build_clib.py b/src/distutils2/tests/test_build_clib.py index 452b65a..348f700 100644 --- a/src/distutils2/tests/test_build_clib.py +++ b/src/distutils2/tests/test_build_clib.py @@ -5,7 +5,7 @@ import sys from distutils2.command.build_clib import build_clib from distutils2.errors import DistutilsSetupError from distutils2.tests import support -from distutils2.spawn import find_executable +from distutils2.util import find_executable from distutils2.tests.support import unittest class BuildCLibTestCase(support.TempdirManager, @@ -55,14 +55,14 @@ class BuildCLibTestCase(support.TempdirManager, self.assertRaises(DistutilsSetupError, cmd.get_source_files) cmd.libraries = [('name', {'sources': ['a', 'b']})] - self.assertEquals(cmd.get_source_files(), ['a', 'b']) + self.assertEqual(cmd.get_source_files(), ['a', 'b']) cmd.libraries = [('name', {'sources': ('a', 'b')})] - self.assertEquals(cmd.get_source_files(), ['a', 'b']) + self.assertEqual(cmd.get_source_files(), ['a', 'b']) cmd.libraries = [('name', {'sources': ('a', 'b')}), ('name2', {'sources': ['c', 'd']})] - self.assertEquals(cmd.get_source_files(), ['a', 'b', 'c', 'd']) + self.assertEqual(cmd.get_source_files(), ['a', 'b', 'c', 'd']) def test_build_libraries(self): @@ -91,11 +91,11 @@ class BuildCLibTestCase(support.TempdirManager, cmd.include_dirs = 'one-dir' cmd.finalize_options() - self.assertEquals(cmd.include_dirs, ['one-dir']) + self.assertEqual(cmd.include_dirs, ['one-dir']) cmd.include_dirs = None cmd.finalize_options() - self.assertEquals(cmd.include_dirs, []) + self.assertEqual(cmd.include_dirs, []) cmd.distribution.libraries = 'WONTWORK' self.assertRaises(DistutilsSetupError, cmd.finalize_options) diff --git a/src/distutils2/tests/test_build_ext.py b/src/distutils2/tests/test_build_ext.py index 8b5998d..f561923 100644 --- a/src/distutils2/tests/test_build_ext.py +++ b/src/distutils2/tests/test_build_ext.py @@ -1,6 +1,5 @@ import sys import os -import tempfile import shutil from StringIO import StringIO import warnings @@ -81,11 +80,11 @@ class BuildExtTestCase(support.TempdirManager, for attr in ('error', 'foo', 'new', 'roj'): self.assertTrue(hasattr(xx, attr)) - self.assertEquals(xx.foo(2, 5), 7) - self.assertEquals(xx.foo(13,15), 28) - self.assertEquals(xx.new().demo(), None) + 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.assertEquals(xx.__doc__, doc) + self.assertEqual(xx.__doc__, doc) self.assertTrue(isinstance(xx.Null(), xx.Null)) self.assertTrue(isinstance(xx.Str(), xx.Str)) @@ -195,7 +194,7 @@ class BuildExtTestCase(support.TempdirManager, cmd = build_ext(dist) cmd.libraries = 'my_lib' cmd.finalize_options() - self.assertEquals(cmd.libraries, ['my_lib']) + self.assertEqual(cmd.libraries, ['my_lib']) # make sure cmd.library_dirs is turned into a list # if it's a string @@ -209,7 +208,7 @@ class BuildExtTestCase(support.TempdirManager, cmd = build_ext(dist) cmd.rpath = os.pathsep.join(['one', 'two']) cmd.finalize_options() - self.assertEquals(cmd.rpath, ['one', 'two']) + self.assertEqual(cmd.rpath, ['one', 'two']) # XXX more tests to perform for win32 @@ -218,79 +217,32 @@ class BuildExtTestCase(support.TempdirManager, cmd = build_ext(dist) cmd.define = 'one,two' cmd.finalize_options() - self.assertEquals(cmd.define, [('one', '1'), ('two', '1')]) + 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.assertEquals(cmd.undef, ['one', 'two']) + 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.assertEquals(cmd.swig_opts, []) + self.assertEqual(cmd.swig_opts, []) cmd = build_ext(dist) cmd.swig_opts = '1 2' cmd.finalize_options() - self.assertEquals(cmd.swig_opts, ['1', '2']) - - def test_check_extensions_list(self): - dist = Distribution() - cmd = build_ext(dist) - cmd.finalize_options() - - #'extensions' option must be a list of Extension instances - self.assertRaises(DistutilsSetupError, cmd.check_extensions_list, 'foo') - - # each element of 'ext_modules' option must be an - # Extension instance or 2-tuple - exts = [('bar', 'foo', 'bar'), 'foo'] - self.assertRaises(DistutilsSetupError, cmd.check_extensions_list, exts) - - # first element of each tuple in 'ext_modules' - # must be the extension name (a string) and match - # a python dotted-separated name - exts = [('foo-bar', '')] - self.assertRaises(DistutilsSetupError, cmd.check_extensions_list, exts) - - # second element of each tuple in 'ext_modules' - # must be a ary (build info) - exts = [('foo.bar', '')] - self.assertRaises(DistutilsSetupError, cmd.check_extensions_list, exts) - - # ok this one should pass - exts = [('foo.bar', {'sources': [''], 'libraries': 'foo', - 'some': 'bar'})] - cmd.check_extensions_list(exts) - ext = exts[0] - self.assertTrue(isinstance(ext, Extension)) - - # check_extensions_list adds in ext the values passed - # when they are in ('include_dirs', 'library_dirs', 'libraries' - # 'extra_objects', 'extra_compile_args', 'extra_link_args') - self.assertEquals(ext.libraries, 'foo') - self.assertTrue(not hasattr(ext, 'some')) - - # 'macros' element of build info dict must be 1- or 2-tuple - exts = [('foo.bar', {'sources': [''], 'libraries': 'foo', - 'some': 'bar', 'macros': [('1', '2', '3'), 'foo']})] - self.assertRaises(DistutilsSetupError, cmd.check_extensions_list, exts) - - exts[0][1]['macros'] = [('1', '2'), ('3',)] - cmd.check_extensions_list(exts) - self.assertEquals(exts[0].undef_macros, ['3']) - self.assertEquals(exts[0].define_macros, [('1', '2')]) + 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.assertEquals(cmd.get_source_files(), ['xxx']) + self.assertEqual(cmd.get_source_files(), ['xxx']) def test_compiler_option(self): # cmd.compiler is an option and @@ -301,7 +253,7 @@ class BuildExtTestCase(support.TempdirManager, cmd.compiler = 'unix' cmd.ensure_finalized() cmd.run() - self.assertEquals(cmd.compiler, 'unix') + self.assertEqual(cmd.compiler, 'unix') def test_get_outputs(self): tmp_dir = self.mkdtemp() @@ -312,7 +264,7 @@ class BuildExtTestCase(support.TempdirManager, 'ext_modules': [ext]}) cmd = build_ext(dist) cmd.ensure_finalized() - self.assertEquals(len(cmd.get_outputs()), 1) + self.assertEqual(len(cmd.get_outputs()), 1) if os.name == "nt": cmd.debug = sys.executable.endswith("_d.exe") @@ -332,19 +284,19 @@ class BuildExtTestCase(support.TempdirManager, finally: os.chdir(old_wd) self.assertTrue(os.path.exists(so_file)) - self.assertEquals(os.path.splitext(so_file)[-1], + self.assertEqual(os.path.splitext(so_file)[-1], sysconfig.get_config_var('SO')) so_dir = os.path.dirname(so_file) - self.assertEquals(so_dir, other_tmp_dir) + self.assertEqual(so_dir, other_tmp_dir) cmd.inplace = 0 cmd.run() so_file = cmd.get_outputs()[0] self.assertTrue(os.path.exists(so_file)) - self.assertEquals(os.path.splitext(so_file)[-1], + self.assertEqual(os.path.splitext(so_file)[-1], sysconfig.get_config_var('SO')) so_dir = os.path.dirname(so_file) - self.assertEquals(so_dir, cmd.build_lib) + self.assertEqual(so_dir, cmd.build_lib) # inplace = 0, cmd.package = 'bar' build_py = cmd.get_finalized_command('build_py') @@ -352,7 +304,7 @@ class BuildExtTestCase(support.TempdirManager, path = cmd.get_ext_fullpath('foo') # checking that the last directory is the build_dir path = os.path.split(path)[0] - self.assertEquals(path, cmd.build_lib) + self.assertEqual(path, cmd.build_lib) # inplace = 1, cmd.package = 'bar' cmd.inplace = 1 @@ -366,7 +318,7 @@ class BuildExtTestCase(support.TempdirManager, # checking that the last directory is bar path = os.path.split(path)[0] lastdir = os.path.split(path)[-1] - self.assertEquals(lastdir, 'bar') + self.assertEqual(lastdir, 'bar') def test_ext_fullpath(self): ext = sysconfig.get_config_vars()['SO'] @@ -382,14 +334,14 @@ class BuildExtTestCase(support.TempdirManager, curdir = os.getcwd() wanted = os.path.join(curdir, 'src', 'lxml', 'etree' + ext) path = cmd.get_ext_fullpath('lxml.etree') - self.assertEquals(wanted, path) + self.assertEqual(wanted, path) # building lxml.etree not inplace cmd.inplace = 0 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.assertEquals(wanted, path) + self.assertEqual(wanted, path) # building twisted.runner.portmap not inplace build_py = cmd.get_finalized_command('build_py') @@ -398,13 +350,13 @@ class BuildExtTestCase(support.TempdirManager, path = cmd.get_ext_fullpath('twisted.runner.portmap') wanted = os.path.join(curdir, 'tmpdir', 'twisted', 'runner', 'portmap' + ext) - self.assertEquals(wanted, path) + self.assertEqual(wanted, path) # building twisted.runner.portmap inplace cmd.inplace = 1 path = cmd.get_ext_fullpath('twisted.runner.portmap') wanted = os.path.join(curdir, 'twisted', 'runner', 'portmap' + ext) - self.assertEquals(wanted, path) + self.assertEqual(wanted, path) def test_suite(): src = _get_source_filename() @@ -416,4 +368,4 @@ def test_suite(): else: return unittest.makeSuite(BuildExtTestCase) if __name__ == '__main__': - distsutils2.tests.run_unittest(test_suite()) + distutils2.tests.run_unittest(test_suite()) diff --git a/src/distutils2/tests/test_build_py.py b/src/distutils2/tests/test_build_py.py index 7f7e1ec..4b46da9 100644 --- a/src/distutils2/tests/test_build_py.py +++ b/src/distutils2/tests/test_build_py.py @@ -19,11 +19,15 @@ class BuildPyTestCase(support.TempdirManager, def test_package_data(self): sources = self.mkdtemp() f = open(os.path.join(sources, "__init__.py"), "w") - f.write("# Pretend this is a package.") - f.close() + try: + f.write("# Pretend this is a package.") + finally: + f.close() f = open(os.path.join(sources, "README.txt"), "w") - f.write("Info about this package") - f.close() + try: + f.write("Info about this package") + finally: + f.close() destination = self.mkdtemp() diff --git a/src/distutils2/tests/test_build_scripts.py b/src/distutils2/tests/test_build_scripts.py index 2167ac1..6a724bd 100644 --- a/src/distutils2/tests/test_build_scripts.py +++ b/src/distutils2/tests/test_build_scripts.py @@ -74,8 +74,10 @@ class BuildScriptsTestCase(support.TempdirManager, def write_script(self, dir, name, text): f = open(os.path.join(dir, name), "w") - f.write(text) - f.close() + try: + f.write(text) + finally: + f.close() def test_version_int(self): source = self.mkdtemp() diff --git a/src/distutils2/tests/test_ccompiler.py b/src/distutils2/tests/test_ccompiler.py index 85f46bf..df25253 100644 --- a/src/distutils2/tests/test_ccompiler.py +++ b/src/distutils2/tests/test_ccompiler.py @@ -31,7 +31,7 @@ class CCompilerTestCase(support.EnvironGuard, unittest.TestCase): opts = gen_lib_options(compiler, libdirs, runlibdirs, libs) wanted = ['-Llib1', '-Llib2', '-cool', '-Rrunlib1', 'found', '-lname2'] - self.assertEquals(opts, wanted) + self.assertEqual(opts, wanted) def test_customize_compiler(self): @@ -51,7 +51,7 @@ class CCompilerTestCase(support.EnvironGuard, unittest.TestCase): comp = compiler() customize_compiler(comp) - self.assertEquals(comp.exes['archiver'], 'my_ar -arflags') + self.assertEqual(comp.exes['archiver'], 'my_ar -arflags') def test_suite(): return unittest.makeSuite(CCompilerTestCase) diff --git a/src/distutils2/tests/test_check.py b/src/distutils2/tests/test_check.py index 757e8c5..7a672e1 100644 --- a/src/distutils2/tests/test_check.py +++ b/src/distutils2/tests/test_check.py @@ -37,7 +37,7 @@ class CheckTestCase(support.LoggingSilencer, 'name': 'xxx', 'version': 'xxx' } cmd = self._run(metadata) - self.assertEquals(len(cmd._warnings), 0) + self.assertEqual(len(cmd._warnings), 0) # now with the strict mode, we should # get an error if there are missing metadata @@ -45,7 +45,7 @@ class CheckTestCase(support.LoggingSilencer, # and of course, no error when all metadata are present cmd = self._run(metadata, strict=1) - self.assertEquals(len(cmd._warnings), 0) + self.assertEqual(len(cmd._warnings), 0) def test_check_restructuredtext(self): if not _HAS_DOCUTILS: # won't test without docutils @@ -55,7 +55,7 @@ class CheckTestCase(support.LoggingSilencer, pkg_info, dist = self.create_dist(description=broken_rest) cmd = check(dist) cmd.check_restructuredtext() - self.assertEquals(len(cmd._warnings), 1) + self.assertEqual(len(cmd._warnings), 1) # let's see if we have an error with strict=1 metadata = {'home_page': 'xxx', 'author': 'xxx', @@ -69,7 +69,7 @@ class CheckTestCase(support.LoggingSilencer, # and non-broken rest metadata['description'] = 'title\n=====\n\ntest' cmd = self._run(metadata, strict=1, restructuredtext=1) - self.assertEquals(len(cmd._warnings), 0) + self.assertEqual(len(cmd._warnings), 0) def test_check_all(self): diff --git a/src/distutils2/tests/test_cmd.py b/src/distutils2/tests/test_cmd.py index 09095e8..aa4a709 100644 --- a/src/distutils2/tests/test_cmd.py +++ b/src/distutils2/tests/test_cmd.py @@ -43,7 +43,7 @@ class CommandTestCase(unittest.TestCase): # making sure execute gets called properly def _execute(func, args, exec_msg, level): - self.assertEquals(exec_msg, 'generating out from in') + self.assertEqual(exec_msg, 'generating out from in') cmd.force = True cmd.execute = _execute cmd.make_file(infiles='in', outfile='out', func='func', args=()) @@ -62,7 +62,7 @@ class CommandTestCase(unittest.TestCase): wanted = ["command options for 'MyCmd':", ' option1 = 1', ' option2 = 1'] - self.assertEquals(msgs, wanted) + self.assertEqual(msgs, wanted) def test_ensure_string(self): cmd = self.cmd @@ -80,7 +80,7 @@ class CommandTestCase(unittest.TestCase): cmd = self.cmd cmd.option1 = 'ok,dok' cmd.ensure_string_list('option1') - self.assertEquals(cmd.option1, ['ok', 'dok']) + self.assertEqual(cmd.option1, ['ok', 'dok']) cmd.option2 = ['xxx', 'www'] cmd.ensure_string_list('option2') diff --git a/src/distutils2/tests/test_config.py b/src/distutils2/tests/test_config.py index 95bdc3f..09b6d02 100644 --- a/src/distutils2/tests/test_config.py +++ b/src/distutils2/tests/test_config.py @@ -1,7 +1,6 @@ """Tests for distutils.pypirc.pypirc.""" import sys import os -import tempfile import shutil from distutils2.core import PyPIRCCommand @@ -87,20 +86,20 @@ class PyPIRCCommandTestCase(support.TempdirManager, config = config.items() config.sort() - waited = [('password', 'secret'), ('realm', 'pypi'), - ('repository', 'http://pypi.python.org/pypi'), - ('server', 'server1'), ('username', 'me')] - self.assertEquals(config, waited) + expected = [('password', 'secret'), ('realm', 'pypi'), + ('repository', 'http://pypi.python.org/pypi'), + ('server', 'server1'), ('username', 'me')] + self.assertEqual(config, expected) # old format self.write_file(self.rc, PYPIRC_OLD) config = cmd._read_pypirc() config = config.items() config.sort() - waited = [('password', 'secret'), ('realm', 'pypi'), - ('repository', 'http://pypi.python.org/pypi'), - ('server', 'server-login'), ('username', 'tarek')] - self.assertEquals(config, waited) + expected = [('password', 'secret'), ('realm', 'pypi'), + ('repository', 'http://pypi.python.org/pypi'), + ('server', 'server-login'), ('username', 'tarek')] + self.assertEqual(config, expected) def test_server_empty_registration(self): cmd = self._cmd(self.dist) @@ -109,7 +108,7 @@ class PyPIRCCommandTestCase(support.TempdirManager, cmd._store_pypirc('tarek', 'xxx') self.assertTrue(os.path.exists(rc)) content = open(rc).read() - self.assertEquals(content, WANTED) + self.assertEqual(content, WANTED) def test_suite(): return unittest.makeSuite(PyPIRCCommandTestCase) diff --git a/src/distutils2/tests/test_config_cmd.py b/src/distutils2/tests/test_config_cmd.py index 9e81079..1995b0c 100644 --- a/src/distutils2/tests/test_config_cmd.py +++ b/src/distutils2/tests/test_config_cmd.py @@ -34,7 +34,7 @@ class ConfigTestCase(support.LoggingSilencer, f.close() dump_file(this_file, 'I am the header') - self.assertEquals(len(self._logs), numlines+1) + self.assertEqual(len(self._logs), numlines+1) def test_search_cpp(self): if sys.platform == 'win32': @@ -44,10 +44,10 @@ class ConfigTestCase(support.LoggingSilencer, # simple pattern searches match = cmd.search_cpp(pattern='xxx', body='// xxx') - self.assertEquals(match, 0) + self.assertEqual(match, 0) match = cmd.search_cpp(pattern='_configtest', body='// xxx') - self.assertEquals(match, 1) + self.assertEqual(match, 1) def test_finalize_options(self): # finalize_options does a bit of transformation @@ -59,9 +59,9 @@ class ConfigTestCase(support.LoggingSilencer, cmd.library_dirs = 'three%sfour' % os.pathsep cmd.ensure_finalized() - self.assertEquals(cmd.include_dirs, ['one', 'two']) - self.assertEquals(cmd.libraries, ['one']) - self.assertEquals(cmd.library_dirs, ['three', 'four']) + 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 diff --git a/src/distutils2/tests/test_converter.py b/src/distutils2/tests/test_converter.py index 86b844a..f0717cc 100644 --- a/src/distutils2/tests/test_converter.py +++ b/src/distutils2/tests/test_converter.py @@ -30,7 +30,7 @@ class ConverterTestCase(unittest.TestCase): wanted = file_.replace('before', 'after') wanted = _read_file(os.path.join(convdir, wanted)) res = ref.refactor_string(original, 'setup.py') - self.assertEquals(str(res), wanted) + self.assertEqual(str(res), wanted) def test_suite(): return unittest.makeSuite(ConverterTestCase) diff --git a/src/distutils2/tests/test_cygwinccompiler.py b/src/distutils2/tests/test_cygwinccompiler.py index c94ef22..e16a309 100644 --- a/src/distutils2/tests/test_cygwinccompiler.py +++ b/src/distutils2/tests/test_cygwinccompiler.py @@ -44,48 +44,48 @@ class CygwinCCompilerTestCase(support.TempdirManager, sys.version = ('2.6.1 (r261:67515, Dec 6 2008, 16:42:21) \n[GCC ' '4.0.1 (Apple Computer, Inc. build 5370)]') - self.assertEquals(check_config_h()[0], CONFIG_H_OK) + 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.assertEquals(check_config_h()[0], 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.assertEquals(check_config_h()[0], CONFIG_H_NOTOK) + 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.assertEquals(check_config_h()[0], CONFIG_H_OK) + 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.assertEquals(get_msvcr(), None) + 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.assertEquals(get_msvcr(), ['msvcr70']) + 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.assertEquals(get_msvcr(), ['msvcr71']) + 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.assertEquals(get_msvcr(), ['msvcr80']) + 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.assertEquals(get_msvcr(), ['msvcr90']) + self.assertEqual(get_msvcr(), ['msvcr90']) # unknown sys.version = ('2.5.1 (r251:54863, Apr 18 2007, 08:51:08) ' diff --git a/src/distutils2/tests/test_depgraph.py b/src/distutils2/tests/test_depgraph.py new file mode 100644 index 0000000..6ab265a --- /dev/null +++ b/src/distutils2/tests/test_depgraph.py @@ -0,0 +1,187 @@ +"""Tests for distutils.depgraph """ + +from distutils2.tests import support +from distutils2.tests.support import unittest +from distutils2 import depgraph +from distutils2._backport import pkgutil + +import os +import sys +import re +try: + import cStringIO as StringIO +except ImportError: + import StringIO + +class DepGraphTestCase(support.LoggingSilencer, + unittest.TestCase): + + DISTROS_DIST = ('choxie', 'grammar', 'towel-stuff') + DISTROS_EGG = ('bacon', 'banana', 'strawberry', 'cheese') + EDGE = re.compile( + r'"(?P<from>.*)" -> "(?P<to>.*)" \[label="(?P<label>.*)"\]' + ) + + def checkLists(self, l1, l2): + """ Compare two lists without taking the order into consideration """ + self.assertListEqual(sorted(l1), sorted(l2)) + + def setUp(self): + super(DepGraphTestCase, self).setUp() + path = os.path.join(os.path.dirname(__file__), '..', '_backport', + 'tests', 'fake_dists') + path = os.path.abspath(path) + self.sys_path = sys.path[:] + sys.path[0:0] = [path] + + def test_generate_graph(self): + dists = [] + for name in self.DISTROS_DIST: + dist = pkgutil.get_distribution(name) + self.assertNotEqual(dist, None) + dists.append(dist) + + choxie, grammar, towel = dists + + graph = depgraph.generate_graph(dists) + + deps = [(x.name, y) for (x,y) in graph.adjacency_list[choxie]] + self.checkLists([('towel-stuff', 'towel-stuff (0.1)')], deps) + self.assertTrue(choxie in graph.reverse_list[towel]) + self.checkLists(graph.missing[choxie], []) + + deps = [(x.name, y) for (x,y) in graph.adjacency_list[grammar]] + self.checkLists([], deps) + self.checkLists(graph.missing[grammar], ['truffles (>=1.2)']) + + deps = [(x.name, y) for (x,y) in graph.adjacency_list[towel]] + self.checkLists([], deps) + self.checkLists(graph.missing[towel], ['bacon (<=0.2)']) + + def test_generate_graph_egg(self): + dists = [] + for name in self.DISTROS_DIST + self.DISTROS_EGG: + dist = pkgutil.get_distribution(name, use_egg_info=True) + self.assertNotEqual(dist, None) + dists.append(dist) + + choxie, grammar, towel, bacon, banana, strawberry, cheese = dists + + graph = depgraph.generate_graph(dists) + + deps = [(x.name, y) for (x,y) in graph.adjacency_list[choxie]] + self.checkLists([('towel-stuff', 'towel-stuff (0.1)')], deps) + self.assertTrue(choxie in graph.reverse_list[towel]) + self.checkLists(graph.missing[choxie], []) + + deps = [(x.name, y) for (x,y) in graph.adjacency_list[grammar]] + self.checkLists([('bacon', 'truffles (>=1.2)')], deps) + self.checkLists(graph.missing[grammar], []) + self.assertTrue(grammar in graph.reverse_list[bacon]) + + deps = [(x.name, y) for (x,y) in graph.adjacency_list[towel]] + self.checkLists([('bacon', 'bacon (<=0.2)')], deps) + self.checkLists(graph.missing[towel], []) + self.assertTrue(towel in graph.reverse_list[bacon]) + + deps = [(x.name, y) for (x,y) in graph.adjacency_list[bacon]] + self.checkLists([], deps) + self.checkLists(graph.missing[bacon], []) + + deps = [(x.name, y) for (x,y) in graph.adjacency_list[banana]] + self.checkLists([('strawberry', 'strawberry (>=0.5)')], deps) + self.checkLists(graph.missing[banana], []) + self.assertTrue(banana in graph.reverse_list[strawberry]) + + deps = [(x.name, y) for (x,y) in graph.adjacency_list[strawberry]] + self.checkLists([], deps) + self.checkLists(graph.missing[strawberry], []) + + deps = [(x.name, y) for (x,y) in graph.adjacency_list[cheese]] + self.checkLists([], deps) + self.checkLists(graph.missing[cheese], []) + + def test_dependent_dists(self): + dists = [] + for name in self.DISTROS_DIST: + dist = pkgutil.get_distribution(name) + self.assertNotEqual(dist, None) + dists.append(dist) + + choxie, grammar, towel = dists + + deps = [d.name for d in depgraph.dependent_dists(dists, choxie)] + self.checkLists([], deps) + + deps = [d.name for d in depgraph.dependent_dists(dists, grammar)] + self.checkLists([], deps) + + deps = [d.name for d in depgraph.dependent_dists(dists, towel)] + self.checkLists(['choxie'], deps) + + + def test_dependent_dists_egg(self): + dists = [] + for name in self.DISTROS_DIST + self.DISTROS_EGG: + dist = pkgutil.get_distribution(name, use_egg_info=True) + self.assertNotEqual(dist, None) + dists.append(dist) + + choxie, grammar, towel, bacon, banana, strawberry, cheese = dists + + deps = [d.name for d in depgraph.dependent_dists(dists, choxie)] + self.checkLists([], deps) + + deps = [d.name for d in depgraph.dependent_dists(dists, grammar)] + self.checkLists([], deps) + + deps = [d.name for d in depgraph.dependent_dists(dists, towel)] + self.checkLists(['choxie'], deps) + + deps = [d.name for d in depgraph.dependent_dists(dists, bacon)] + self.checkLists(['choxie', 'towel-stuff', 'grammar'], deps) + + deps = [d.name for d in depgraph.dependent_dists(dists, strawberry)] + self.checkLists(['banana'], deps) + + deps = [d.name for d in depgraph.dependent_dists(dists, cheese)] + self.checkLists([], deps) + + def test_graph_to_dot(self): + expected = ( + ('towel-stuff', 'bacon', 'bacon (<=0.2)'), + ('grammar', 'bacon', 'truffles (>=1.2)'), + ('choxie', 'towel-stuff', 'towel-stuff (0.1)'), + ('banana', 'strawberry', 'strawberry (>=0.5)') + ) + + dists = [] + for name in self.DISTROS_DIST + self.DISTROS_EGG: + dist = pkgutil.get_distribution(name, use_egg_info=True) + self.assertNotEqual(dist, None) + dists.append(dist) + + graph = depgraph.generate_graph(dists) + buf = StringIO.StringIO() + depgraph.graph_to_dot(graph, buf) + buf.seek(0) + matches = [] + lines = buf.readlines() + for line in lines[1:-1]: # skip the first and the last lines + if line[-1] == '\n': + line = line[:-1] + match = self.EDGE.match(line.strip()) + self.assertTrue(match is not None) + matches.append(match.groups()) + + self.checkLists(matches, expected) + + def tearDown(self): + super(DepGraphTestCase, self).tearDown() + sys.path = self.sys_path + +def test_suite(): + return unittest.makeSuite(DepGraphTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/src/distutils2/tests/test_dist.py b/src/distutils2/tests/test_dist.py index 825ea3a..f528684 100644 --- a/src/distutils2/tests/test_dist.py +++ b/src/distutils2/tests/test_dist.py @@ -71,11 +71,11 @@ class DistributionTestCase(support.TempdirManager, sys.argv.append("build") __, stdout = captured_stdout(self.create_distribution, files) - self.assertEquals(stdout, '') + self.assertEqual(stdout, '') distutils2.dist.DEBUG = True try: __, stdout = captured_stdout(self.create_distribution, files) - self.assertEquals(stdout, '') + self.assertEqual(stdout, '') finally: distutils2.dist.DEBUG = False @@ -129,13 +129,13 @@ class DistributionTestCase(support.TempdirManager, # Check DistributionMetadata handling of Unicode fields tmp_dir = self.mkdtemp() my_file = os.path.join(tmp_dir, 'f') - klass = Distribution + cls = Distribution - dist = klass(attrs={'author': u'Mister Café', - 'name': 'my.package', - 'maintainer': u'Café Junior', - 'summary': u'Café torréfié', - 'description': u'Héhéhé'}) + dist = cls(attrs={'author': u'Mister Café', + 'name': 'my.package', + 'maintainer': u'Café Junior', + 'summary': u'Café torréfié', + 'description': u'Héhéhé'}) # let's make sure the file can be written @@ -144,11 +144,11 @@ class DistributionTestCase(support.TempdirManager, dist.metadata.write_file(open(my_file, 'w')) # regular ascii is of course always usable - dist = klass(attrs={'author': 'Mister Cafe', - 'name': 'my.package', - 'maintainer': 'Cafe Junior', - 'summary': 'Cafe torrefie', - 'description': 'Hehehe'}) + dist = cls(attrs={'author': 'Mister Cafe', + 'name': 'my.package', + 'maintainer': 'Cafe Junior', + 'summary': 'Cafe torrefie', + 'description': 'Hehehe'}) my_file2 = os.path.join(tmp_dir, 'f2') dist.metadata.write_file(open(my_file, 'w')) @@ -156,7 +156,7 @@ class DistributionTestCase(support.TempdirManager, def test_empty_options(self): # an empty options dictionary should not stay in the # list of attributes - klass = Distribution + cls = Distribution # catching warnings warns = [] @@ -166,15 +166,15 @@ class DistributionTestCase(support.TempdirManager, old_warn = warnings.warn warnings.warn = _warn try: - dist = klass(attrs={'author': 'xxx', - 'name': 'xxx', - 'version': 'xxx', - 'url': 'xxxx', - 'options': {}}) + dist = cls(attrs={'author': 'xxx', + 'name': 'xxx', + 'version': 'xxx', + 'url': 'xxxx', + 'options': {}}) finally: warnings.warn = old_warn - self.assertEquals(len(warns), 0) + self.assertEqual(len(warns), 0) def test_finalize_options(self): @@ -185,20 +185,20 @@ class DistributionTestCase(support.TempdirManager, dist.finalize_options() # finalize_option splits platforms and keywords - self.assertEquals(dist.metadata['platform'], ['one', 'two']) - self.assertEquals(dist.metadata['keywords'], ['one', 'two']) + self.assertEqual(dist.metadata['platform'], ['one', 'two']) + self.assertEqual(dist.metadata['keywords'], ['one', 'two']) def test_get_command_packages(self): dist = Distribution() - self.assertEquals(dist.command_packages, None) + self.assertEqual(dist.command_packages, None) cmds = dist.get_command_packages() - self.assertEquals(cmds, ['distutils2.command']) - self.assertEquals(dist.command_packages, + self.assertEqual(cmds, ['distutils2.command']) + self.assertEqual(dist.command_packages, ['distutils2.command']) dist.command_packages = 'one,two' cmds = dist.get_command_packages() - self.assertEquals(cmds, ['distutils2.command', 'one', 'two']) + self.assertEqual(cmds, ['distutils2.command', 'one', 'two']) def test_announce(self): @@ -238,7 +238,7 @@ class DistributionTestCase(support.TempdirManager, os.path.expanduser = old_expander # make sure --no-user-cfg disables the user cfg file - self.assertEquals(len(all_files)-1, len(files)) + self.assertEqual(len(all_files)-1, len(files)) class MetadataTestCase(support.TempdirManager, support.EnvironGuard, @@ -340,8 +340,10 @@ class MetadataTestCase(support.TempdirManager, support.EnvironGuard, temp_dir = self.mkdtemp() user_filename = os.path.join(temp_dir, user_filename) f = open(user_filename, 'w') - f.write('.') - f.close() + try: + f.write('.') + finally: + f.close() try: dist = Distribution() @@ -365,8 +367,8 @@ class MetadataTestCase(support.TempdirManager, support.EnvironGuard, def test_fix_help_options(self): help_tuples = [('a', 'b', 'c', 'd'), (1, 2, 3, 4)] fancy_options = fix_help_options(help_tuples) - self.assertEquals(fancy_options[0], ('a', 'b', 'c')) - self.assertEquals(fancy_options[1], (1, 2, 3)) + self.assertEqual(fancy_options[0], ('a', 'b', 'c')) + self.assertEqual(fancy_options[1], (1, 2, 3)) def test_show_help(self): # smoke test, just makes sure some help is displayed @@ -412,14 +414,14 @@ class MetadataTestCase(support.TempdirManager, support.EnvironGuard, PKG_INFO.seek(0) metadata.read_file(PKG_INFO) - self.assertEquals(metadata['name'], "package") - self.assertEquals(metadata['version'], "1.0") - self.assertEquals(metadata['summary'], "xxx") - self.assertEquals(metadata['download_url'], 'http://example.com') - self.assertEquals(metadata['keywords'], ['one', 'two']) - self.assertEquals(metadata['platform'], []) - self.assertEquals(metadata['obsoletes'], []) - self.assertEquals(metadata['requires-dist'], ['foo']) + self.assertEqual(metadata['name'], "package") + self.assertEqual(metadata['version'], "1.0") + self.assertEqual(metadata['summary'], "xxx") + self.assertEqual(metadata['download_url'], 'http://example.com') + self.assertEqual(metadata['keywords'], ['one', 'two']) + self.assertEqual(metadata['platform'], []) + self.assertEqual(metadata['obsoletes'], []) + self.assertEqual(metadata['requires-dist'], ['foo']) def test_suite(): suite = unittest.TestSuite() diff --git a/src/distutils2/tests/test_install.py b/src/distutils2/tests/test_install.py index b37efae..5cf6cd9 100644 --- a/src/distutils2/tests/test_install.py +++ b/src/distutils2/tests/test_install.py @@ -139,23 +139,23 @@ class InstallTestCase(support.TempdirManager, # two elements cmd.handle_extra_path() - self.assertEquals(cmd.extra_path, ['path', 'dirs']) - self.assertEquals(cmd.extra_dirs, 'dirs') - self.assertEquals(cmd.path_file, '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.assertEquals(cmd.extra_path, ['path']) - self.assertEquals(cmd.extra_dirs, 'path') - self.assertEquals(cmd.path_file, '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.assertEquals(cmd.extra_path, None) - self.assertEquals(cmd.extra_dirs, '') - self.assertEquals(cmd.path_file, None) + 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' @@ -199,7 +199,7 @@ class InstallTestCase(support.TempdirManager, # line (the egg info file) f = open(cmd.record) try: - self.assertEquals(len(f.readlines()), 1) + self.assertEqual(len(f.readlines()), 1) finally: f.close() diff --git a/src/distutils2/tests/test_install_data.py b/src/distutils2/tests/test_install_data.py index 4d7c3cb..48cea63 100644 --- a/src/distutils2/tests/test_install_data.py +++ b/src/distutils2/tests/test_install_data.py @@ -27,14 +27,14 @@ class InstallDataTestCase(support.TempdirManager, self.write_file(two, 'xxx') cmd.data_files = [one, (inst2, [two])] - self.assertEquals(cmd.get_inputs(), [one, (inst2, [two])]) + self.assertEqual(cmd.get_inputs(), [one, (inst2, [two])]) # let's run the command cmd.ensure_finalized() cmd.run() # let's check the result - self.assertEquals(len(cmd.get_outputs()), 2) + 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] @@ -47,7 +47,7 @@ class InstallDataTestCase(support.TempdirManager, cmd.run() # let's check the result - self.assertEquals(len(cmd.get_outputs()), 2) + 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 = [] @@ -65,7 +65,7 @@ class InstallDataTestCase(support.TempdirManager, cmd.run() # let's check the result - self.assertEquals(len(cmd.get_outputs()), 4) + self.assertEqual(len(cmd.get_outputs()), 4) self.assertTrue(os.path.exists(os.path.join(inst2, rtwo))) self.assertTrue(os.path.exists(os.path.join(inst, rone))) diff --git a/src/distutils2/tests/test_install_headers.py b/src/distutils2/tests/test_install_headers.py index 371b3d0..4e0dce7 100644 --- a/src/distutils2/tests/test_install_headers.py +++ b/src/distutils2/tests/test_install_headers.py @@ -23,7 +23,7 @@ class InstallHeadersTestCase(support.TempdirManager, pkg_dir, dist = self.create_dist(headers=headers) cmd = install_headers(dist) - self.assertEquals(cmd.get_inputs(), headers) + self.assertEqual(cmd.get_inputs(), headers) # let's run the command cmd.install_dir = os.path.join(pkg_dir, 'inst') @@ -31,7 +31,7 @@ class InstallHeadersTestCase(support.TempdirManager, cmd.run() # let's check the results - self.assertEquals(len(cmd.get_outputs()), 2) + self.assertEqual(len(cmd.get_outputs()), 2) def test_suite(): return unittest.makeSuite(InstallHeadersTestCase) diff --git a/src/distutils2/tests/test_install_lib.py b/src/distutils2/tests/test_install_lib.py index 71f90dd..680eaf1 100644 --- a/src/distutils2/tests/test_install_lib.py +++ b/src/distutils2/tests/test_install_lib.py @@ -25,8 +25,8 @@ class InstallLibTestCase(support.TempdirManager, cmd = install_lib(dist) cmd.finalize_options() - self.assertEquals(cmd.compile, 1) - self.assertEquals(cmd.optimize, 0) + self.assertEqual(cmd.compile, 1) + self.assertEqual(cmd.optimize, 0) # optimize must be 0, 1, or 2 cmd.optimize = 'foo' @@ -36,7 +36,7 @@ class InstallLibTestCase(support.TempdirManager, cmd.optimize = '2' cmd.finalize_options() - self.assertEquals(cmd.optimize, 2) + self.assertEqual(cmd.optimize, 2) @unittest.skipIf(no_bytecode, 'byte-compile not supported') def test_byte_compile(self): @@ -82,7 +82,7 @@ class InstallLibTestCase(support.TempdirManager, cmd.distribution.script_name = 'setup.py' # get_input should return 2 elements - self.assertEquals(len(cmd.get_inputs()), 2) + self.assertEqual(len(cmd.get_inputs()), 2) @unittest.skipUnless(bytecode_support, 'sys.dont_write_bytecode not supported') def test_dont_write_bytecode(self): diff --git a/src/distutils2/tests/test_install_scripts.py b/src/distutils2/tests/test_install_scripts.py index 32c800d..be65d65 100644 --- a/src/distutils2/tests/test_install_scripts.py +++ b/src/distutils2/tests/test_install_scripts.py @@ -42,8 +42,10 @@ class InstallScriptsTestCase(support.TempdirManager, def write_script(name, text): expected.append(name) f = open(os.path.join(source, name), "w") - f.write(text) - f.close() + try: + f.write(text) + finally: + f.close() write_script("script1.py", ("#! /usr/bin/env python2.3\n" "# bogus script w/ Python sh-bang\n" diff --git a/src/distutils2/tests/test_manifest.py b/src/distutils2/tests/test_manifest.py index f9eb9b3..13893eb 100644 --- a/src/distutils2/tests/test_manifest.py +++ b/src/distutils2/tests/test_manifest.py @@ -3,6 +3,7 @@ import os import sys import logging +from distutils2.tests import run_unittest from distutils2.tests import support from distutils2.tests.support import unittest from distutils2.manifest import Manifest @@ -44,7 +45,7 @@ class ManifestTestCase(support.TempdirManager, # the manifest should have been read # and 3 warnings issued (we ddidn't provided the files) - self.assertEquals(len(warns), 3) + self.assertEqual(len(warns), 3) for warn in warns: self.assertIn('warning: no files found matching', warn) diff --git a/src/distutils2/tests/test_metadata.py b/src/distutils2/tests/test_metadata.py index 16e19bb..ee763c0 100644 --- a/src/distutils2/tests/test_metadata.py +++ b/src/distutils2/tests/test_metadata.py @@ -5,9 +5,12 @@ from StringIO import StringIO from distutils2.metadata import (DistributionMetadata, _interpret, PKG_INFO_PREFERRED_VERSION) -from distutils2.tests.support import unittest +from distutils2.tests import run_unittest +from distutils2.tests.support import unittest, LoggingSilencer +from distutils2.errors import (MetadataConflictError, + MetadataUnrecognizedVersionError) -class DistributionMetadataTestCase(unittest.TestCase): +class DistributionMetadataTestCase(LoggingSilencer, unittest.TestCase): def test_interpret(self): @@ -15,15 +18,15 @@ class DistributionMetadataTestCase(unittest.TestCase): version = sys.version.split()[0] os_name = os.name - assert _interpret("sys.platform == '%s'" % platform) - assert _interpret("sys.platform == '%s' or python_version == '2.4'" \ - % platform) - assert _interpret("sys.platform == '%s' and " - "python_full_version == '%s'"\ - % (platform, version)) - assert _interpret("'%s' == sys.platform" % platform) + self.assertTrue(_interpret("sys.platform == '%s'" % platform)) + self.assertTrue(_interpret( + "sys.platform == '%s' or python_version == '2.4'" % platform)) + self.assertTrue(_interpret( + "sys.platform == '%s' and python_full_version == '%s'" % + (platform, version))) + self.assertTrue(_interpret("'%s' == sys.platform" % platform)) - assert _interpret('os.name == "%s"' % os_name) + self.assertTrue(_interpret('os.name == "%s"' % os_name)) # stuff that need to raise a syntax error ops = ('os.name == os.name', 'os.name == 2', "'2' == '2'", @@ -35,24 +38,25 @@ class DistributionMetadataTestCase(unittest.TestCase): OP = 'os.name == "%s"' % os_name AND = ' and ' OR = ' or ' - assert _interpret(OP+AND+OP) - assert _interpret(OP+AND+OP+AND+OP) - assert _interpret(OP+OR+OP) - assert _interpret(OP+OR+OP+OR+OP) + self.assertTrue(_interpret(OP + AND + OP)) + self.assertTrue(_interpret(OP + AND + OP + AND + OP)) + self.assertTrue(_interpret(OP + OR + OP)) + self.assertTrue(_interpret(OP + OR + OP + OR + OP)) # other operators - assert _interpret("os.name != 'buuuu'") - assert _interpret("python_version > '1.0'") - assert _interpret("python_version < '5.0'") - assert _interpret("python_version <= '5.0'") - assert _interpret("python_version >= '1.0'") - assert _interpret("'%s' in os.name" % os_name) - assert _interpret("'buuuu' not in os.name") - assert _interpret("'buuuu' not in os.name and '%s' in os.name" \ - % os_name) + self.assertTrue(_interpret("os.name != 'buuuu'")) + self.assertTrue(_interpret("python_version > '1.0'")) + self.assertTrue(_interpret("python_version < '5.0'")) + self.assertTrue(_interpret("python_version <= '5.0'")) + self.assertTrue(_interpret("python_version >= '1.0'")) + self.assertTrue(_interpret("'%s' in os.name" % os_name)) + self.assertTrue(_interpret("'buuuu' not in os.name")) + self.assertTrue(_interpret( + "'buuuu' not in os.name and '%s' in os.name" % os_name)) # execution context - assert _interpret('python_version == "0.1"', {'python_version': '0.1'}) + self.assertTrue(_interpret('python_version == "0.1"', + {'python_version': '0.1'})) def test_metadata_read_write(self): @@ -63,25 +67,28 @@ class DistributionMetadataTestCase(unittest.TestCase): res.seek(0) res = res.read() f = open(PKG_INFO) - wanted = f.read() + try: + # XXX this is not used + wanted = f.read() + finally: + f.close() self.assertTrue('Keywords: keyring,password,crypt' in res) - f.close() def test_metadata_markers(self): # see if we can be platform-aware PKG_INFO = os.path.join(os.path.dirname(__file__), 'PKG-INFO') content = open(PKG_INFO).read() content = content % sys.platform - metadata = DistributionMetadata(platform_dependant=True) + metadata = DistributionMetadata(platform_dependent=True) metadata.read_file(StringIO(content)) - self.assertEquals(metadata['Requires-Dist'], ['bar']) + self.assertEqual(metadata['Requires-Dist'], ['bar']) # test with context context = {'sys.platform': 'okook'} - metadata = DistributionMetadata(platform_dependant=True, + metadata = DistributionMetadata(platform_dependent=True, execution_context=context) metadata.read_file(StringIO(content)) - self.assertEquals(metadata['Requires-Dist'], ['foo']) + self.assertEqual(metadata['Requires-Dist'], ['foo']) def test_description(self): PKG_INFO = os.path.join(os.path.dirname(__file__), 'PKG-INFO') @@ -93,14 +100,14 @@ class DistributionMetadataTestCase(unittest.TestCase): # see if we can read the description now DESC = os.path.join(os.path.dirname(__file__), 'LONG_DESC.txt') wanted = open(DESC).read() - self.assertEquals(wanted, metadata['Description']) + self.assertEqual(wanted, metadata['Description']) # save the file somewhere and make sure we can read it back out = StringIO() metadata.write_file(out) out.seek(0) metadata.read_file(out) - self.assertEquals(wanted, metadata['Description']) + self.assertEqual(wanted, metadata['Description']) def test_mapper_apis(self): PKG_INFO = os.path.join(os.path.dirname(__file__), 'PKG-INFO') @@ -115,25 +122,32 @@ class DistributionMetadataTestCase(unittest.TestCase): def test_versions(self): metadata = DistributionMetadata() metadata['Obsoletes'] = 'ok' - self.assertEquals(metadata['Metadata-Version'], '1.1') + self.assertEqual(metadata['Metadata-Version'], '1.1') del metadata['Obsoletes'] metadata['Obsoletes-Dist'] = 'ok' - self.assertEquals(metadata['Metadata-Version'], '1.2') + self.assertEqual(metadata['Metadata-Version'], '1.2') + + self.assertRaises(MetadataConflictError, metadata.set, + 'Obsoletes', 'ok') + del metadata['Obsoletes'] del metadata['Obsoletes-Dist'] metadata['Version'] = '1' - self.assertEquals(metadata['Metadata-Version'], '1.0') + self.assertEqual(metadata['Metadata-Version'], '1.0') PKG_INFO = os.path.join(os.path.dirname(__file__), 'SETUPTOOLS-PKG-INFO') metadata.read_file(StringIO(open(PKG_INFO).read())) - self.assertEquals(metadata['Metadata-Version'], '1.0') + self.assertEqual(metadata['Metadata-Version'], '1.0') PKG_INFO = os.path.join(os.path.dirname(__file__), 'SETUPTOOLS-PKG-INFO2') metadata.read_file(StringIO(open(PKG_INFO).read())) - self.assertEquals(metadata['Metadata-Version'], '1.1') + self.assertEqual(metadata['Metadata-Version'], '1.1') + + metadata.version = '1.618' + self.assertRaises(MetadataUnrecognizedVersionError, metadata.keys) def test_warnings(self): metadata = DistributionMetadata() @@ -161,7 +175,7 @@ class DistributionMetadataTestCase(unittest.TestCase): # we should have a certain amount of warnings num_wanted = len(values) - self.assertEquals(num_wanted, res) + self.assertEqual(num_wanted, res) def test_multiple_predicates(self): metadata = DistributionMetadata() @@ -184,29 +198,29 @@ class DistributionMetadataTestCase(unittest.TestCase): res = m.warns del m.warns - self.assertEquals(res, 0) + self.assertEqual(res, 0) def test_project_url(self): metadata = DistributionMetadata() metadata['Project-URL'] = [('one', 'http://ok')] - self.assertEquals(metadata['Project-URL'], + self.assertEqual(metadata['Project-URL'], [('one', 'http://ok')]) - self.assertEquals(metadata.version, '1.2') + self.assertEqual(metadata.version, '1.2') def test_check(self): metadata = DistributionMetadata() metadata['Version'] = 'rr' metadata['Requires-dist'] = ['Foo (a)'] missing, warnings = metadata.check() - self.assertEquals(missing, ['Name', 'Home-page']) - self.assertEquals(len(warnings), 2) + self.assertEqual(missing, ['Name', 'Home-page']) + self.assertEqual(len(warnings), 2) def test_best_choice(self): metadata = DistributionMetadata() metadata['Version'] = '1.0' - self.assertEquals(metadata.version, PKG_INFO_PREFERRED_VERSION) + self.assertEqual(metadata.version, PKG_INFO_PREFERRED_VERSION) metadata['Classifier'] = ['ok'] - self.assertEquals(metadata.version, '1.2') + self.assertEqual(metadata.version, '1.2') def test_project_urls(self): # project-url is a bit specific, make sure we write it @@ -214,7 +228,7 @@ class DistributionMetadataTestCase(unittest.TestCase): metadata = DistributionMetadata() metadata['Version'] = '1.0' metadata['Project-Url'] = [('one', 'http://ok')] - self.assertEquals(metadata['Project-Url'], [('one', 'http://ok')]) + self.assertEqual(metadata['Project-Url'], [('one', 'http://ok')]) file_ = StringIO() metadata.write_file(file_) file_.seek(0) @@ -224,7 +238,7 @@ class DistributionMetadataTestCase(unittest.TestCase): file_.seek(0) metadata = DistributionMetadata() metadata.read_file(file_) - self.assertEquals(metadata['Project-Url'], [('one', 'http://ok')]) + self.assertEqual(metadata['Project-Url'], [('one', 'http://ok')]) def test_suite(): diff --git a/src/distutils2/tests/test_msvc9compiler.py b/src/distutils2/tests/test_msvc9compiler.py index 346b20c..452c3f5 100644 --- a/src/distutils2/tests/test_msvc9compiler.py +++ b/src/distutils2/tests/test_msvc9compiler.py @@ -105,7 +105,7 @@ class msvc9compilerTestCase(support.TempdirManager, import _winreg HKCU = _winreg.HKEY_CURRENT_USER keys = Reg.read_keys(HKCU, 'xxxx') - self.assertEquals(keys, None) + self.assertEqual(keys, None) keys = Reg.read_keys(HKCU, r'Control Panel') self.assertTrue('Desktop' in keys) @@ -116,20 +116,24 @@ class msvc9compilerTestCase(support.TempdirManager, tempdir = self.mkdtemp() manifest = os.path.join(tempdir, 'manifest') f = open(manifest, 'w') - f.write(_MANIFEST) - f.close() + try: + f.write(_MANIFEST) + finally: + f.close() compiler = MSVCCompiler() compiler._remove_visual_c_ref(manifest) # see what we got f = open(manifest) - # removing trailing spaces - content = '\n'.join([line.rstrip() for line in f.readlines()]) - f.close() + try: + # removing trailing spaces + content = '\n'.join([line.rstrip() for line in f.readlines()]) + finally: + f.close() # makes sure the manifest was properly cleaned - self.assertEquals(content, _CLEANED_MANIFEST) + self.assertEqual(content, _CLEANED_MANIFEST) def test_suite(): diff --git a/src/distutils2/tests/test_pypi_dist.py b/src/distutils2/tests/test_pypi_dist.py new file mode 100644 index 0000000..0267760 --- /dev/null +++ b/src/distutils2/tests/test_pypi_dist.py @@ -0,0 +1,247 @@ +"""Tests for the distutils2.pypi.dist module.""" + +import os +import shutil +import tempfile + +from distutils2.tests.pypi_server import use_pypi_server +from distutils2.tests import run_unittest +from distutils2.tests.support import unittest, TempdirManager +from distutils2.version import VersionPredicate +from distutils2.pypi.errors import HashDoesNotMatch, UnsupportedHashName +from distutils2.pypi.dist import (PyPIDistribution as Dist, + PyPIDistributions as Dists, + split_archive_name) + + +class TestPyPIDistribution(TempdirManager, + unittest.TestCase): + """Tests the pypi.dist.PyPIDistribution class""" + + def test_instanciation(self): + # Test the Distribution class provides us the good attributes when + # given on construction + dist = Dist("FooBar", "1.1") + self.assertEqual("FooBar", dist.name) + self.assertEqual("1.1", "%s" % dist.version) + + def test_create_from_url(self): + # Test that the Distribution object can be built from a single URL + url_list = { + 'FooBar-1.1.0.tar.gz': { + 'name': 'foobar', # lowercase the name + 'version': '1.1', + }, + 'Foo-Bar-1.1.0.zip': { + 'name': 'foo-bar', # keep the dash + 'version': '1.1', + }, + 'foobar-1.1b2.tar.gz#md5=123123123123123': { + 'name': 'foobar', + 'version': '1.1b2', + 'url': { + 'url': 'http://test.tld/foobar-1.1b2.tar.gz', # no hash + 'hashval': '123123123123123', + 'hashname': 'md5', + } + }, + 'foobar-1.1-rc2.tar.gz': { # use suggested name + 'name': 'foobar', + 'version': '1.1c2', + 'url': { + 'url': 'http://test.tld/foobar-1.1-rc2.tar.gz', + } + } + } + + for url, attributes in url_list.items(): + dist = Dist.from_url("http://test.tld/" + url) + for attribute, value in attributes.items(): + if isinstance(value, dict): + mylist = getattr(dist, attribute) + for val in value.keys(): + self.assertEqual(value[val], mylist[val]) + else: + if attribute == "version": + self.assertEqual("%s" % getattr(dist, "version"), value) + else: + self.assertEqual(getattr(dist, attribute), value) + + def test_get_url(self): + # Test that the url property works well + + d = Dist("foobar", "1.1", url="test_url") + self.assertDictEqual(d.url, { + "url": "test_url", + "is_external": True, + "hashname": None, + "hashval": None, + }) + + # add a new url + d.add_url(url="internal_url", is_external=False) + self.assertEqual(d._url, None) + self.assertDictEqual(d.url, { + "url": "internal_url", + "is_external": False, + "hashname": None, + "hashval": None, + }) + self.assertEqual(2, len(d._urls)) + + def test_comparaison(self): + # Test that we can compare PyPIDistributions + foo1 = Dist("foo", "1.0") + foo2 = Dist("foo", "2.0") + bar = Dist("bar", "2.0") + # assert we use the version to compare + self.assertTrue(foo1 < foo2) + self.assertFalse(foo1 > foo2) + self.assertFalse(foo1 == foo2) + + # assert we can't compare dists with different names + self.assertRaises(TypeError, foo1.__eq__, bar) + + def test_split_archive_name(self): + # Test we can split the archive names + names = { + 'foo-bar-baz-1.0-rc2': ('foo-bar-baz', '1.0c2'), + 'foo-bar-baz-1.0': ('foo-bar-baz', '1.0'), + 'foobarbaz-1.0': ('foobarbaz', '1.0'), + } + for name, results in names.items(): + self.assertEqual(results, split_archive_name(name)) + + @use_pypi_server("downloads_with_md5") + def test_download(self, server): + # Download is possible, and the md5 is checked if given + + add_to_tmpdirs = lambda x: self.tempdirs.append(os.path.dirname(x)) + + url = "%s/simple/foobar/foobar-0.1.tar.gz" % server.full_address + # check md5 if given + dist = Dist("FooBar", "0.1", url=url, + url_hashname="md5", url_hashval="d41d8cd98f00b204e9800998ecf8427e") + add_to_tmpdirs(dist.download()) + + # a wrong md5 fails + dist2 = Dist("FooBar", "0.1", url=url, + url_hashname="md5", url_hashval="wrongmd5") + + self.assertRaises(HashDoesNotMatch, dist2.download) + add_to_tmpdirs(dist2.downloaded_location) + + # we can omit the md5 hash + dist3 = Dist("FooBar", "0.1", url=url) + add_to_tmpdirs(dist3.download()) + + # and specify a temporary location + # for an already downloaded dist + path1 = self.mkdtemp() + dist3.download(path=path1) + # and for a new one + path2_base = self.mkdtemp() + dist4 = Dist("FooBar", "0.1", url=url) + path2 = dist4.download(path=path2_base) + self.assertTrue(path2_base in path2) + + def test_hashname(self): + # Invalid hashnames raises an exception on assignation + Dist("FooBar", "0.1", url_hashname="md5", url_hashval="value") + + self.assertRaises(UnsupportedHashName, Dist, "FooBar", "0.1", + url_hashname="invalid_hashname", url_hashval="value") + + +class TestPyPIDistributions(unittest.TestCase): + + def test_filter(self): + # Test we filter the distributions the right way, using version + # predicate match method + dists = Dists(( + Dist("FooBar", "1.1"), + Dist("FooBar", "1.1.1"), + Dist("FooBar", "1.2"), + Dist("FooBar", "1.2.1"), + )) + filtered = dists.filter(VersionPredicate("FooBar (<1.2)")) + self.assertNotIn(dists[2], filtered) + self.assertNotIn(dists[3], filtered) + self.assertIn(dists[0], filtered) + self.assertIn(dists[1], filtered) + + def test_append(self): + # When adding a new item to the list, the behavior is to test if + # a distribution with the same name and version number already exists, + # and if so, to add url informations to the existing PyPIDistribution + # object. + # If no object matches, just add "normally" the object to the list. + + dists = Dists([ + Dist("FooBar", "1.1", url="external_url", type="source"), + ]) + self.assertEqual(1, len(dists)) + dists.append(Dist("FooBar", "1.1", url="internal_url", + url_is_external=False, type="source")) + self.assertEqual(1, len(dists)) + self.assertEqual(2, len(dists[0]._urls)) + + dists.append(Dist("Foobar", "1.1.1", type="source")) + self.assertEqual(2, len(dists)) + + # when adding a distribution whith a different type, a new distribution + # has to be added. + dists.append(Dist("Foobar", "1.1.1", type="binary")) + self.assertEqual(3, len(dists)) + + def test_prefer_final(self): + # Can order the distributions using prefer_final + + fb10 = Dist("FooBar", "1.0") # final distribution + fb11a = Dist("FooBar", "1.1a1") # alpha + fb12a = Dist("FooBar", "1.2a1") # alpha + fb12b = Dist("FooBar", "1.2b1") # beta + dists = Dists([fb10, fb11a, fb12a, fb12b]) + + dists.sort_distributions(prefer_final=True) + self.assertEqual(fb10, dists[0]) + + dists.sort_distributions(prefer_final=False) + self.assertEqual(fb12b, dists[0]) + + def test_prefer_source(self): + # Ordering support prefer_source + fb_source = Dist("FooBar", "1.0", type="source") + fb_binary = Dist("FooBar", "1.0", type="binary") + fb2_binary = Dist("FooBar", "2.0", type="binary") + dists = Dists([fb_binary, fb_source]) + + dists.sort_distributions(prefer_source=True) + self.assertEqual(fb_source, dists[0]) + + dists.sort_distributions(prefer_source=False) + self.assertEqual(fb_binary, dists[0]) + + dists.append(fb2_binary) + dists.sort_distributions(prefer_source=True) + self.assertEqual(fb2_binary, dists[0]) + + def test_get_same_name_and_version(self): + # PyPIDistributions can return a list of "duplicates" + fb_source = Dist("FooBar", "1.0", type="source") + fb_binary = Dist("FooBar", "1.0", type="binary") + fb2_binary = Dist("FooBar", "2.0", type="binary") + dists = Dists([fb_binary, fb_source, fb2_binary]) + duplicates = dists.get_same_name_and_version() + self.assertTrue(1, len(duplicates)) + self.assertIn(fb_source, duplicates[0]) + + +def test_suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestPyPIDistribution)) + suite.addTest(unittest.makeSuite(TestPyPIDistributions)) + return suite + +if __name__ == '__main__': + run_unittest(test_suite()) diff --git a/src/distutils2/tests/test_pypi_server.py b/src/distutils2/tests/test_pypi_server.py index 78174e7..65061ec 100644 --- a/src/distutils2/tests/test_pypi_server.py +++ b/src/distutils2/tests/test_pypi_server.py @@ -10,7 +10,7 @@ from distutils2.tests.support import unittest class PyPIServerTest(unittest.TestCase): def test_records_requests(self): - """We expect that PyPIServer can log our requests""" + # We expect that PyPIServer can log our requests server = PyPIServer() server.start() self.assertEqual(len(server.requests), 0) @@ -23,14 +23,13 @@ class PyPIServerTest(unittest.TestCase): self.assertEqual(len(server.requests), 1) handler, request_data = server.requests[-1] self.assertIn("Rock Around The Bunker", request_data) - self.assertTrue(handler.headers.dict.has_key("x-test-header")) - self.assertEqual(handler.headers.dict["x-test-header"], - "Mister Iceberg") + self.assertIn("x-test-header", handler.headers.dict) + self.assertEqual(handler.headers.dict["x-test-header"], + "Mister Iceberg") server.stop() def test_serve_static_content(self): - """PYPI Mocked server can serve static content from disk. - """ + # PYPI Mocked server can serve static content from disk. def uses_local_files_for(server, url_path): """Test that files are served statically (eg. the output from the diff --git a/src/distutils2/tests/test_pypi_simple.py b/src/distutils2/tests/test_pypi_simple.py index 6429ab8..85d8b77 100644 --- a/src/distutils2/tests/test_pypi_simple.py +++ b/src/distutils2/tests/test_pypi_simple.py @@ -5,20 +5,34 @@ import sys import os import shutil import tempfile -import unittest2 import urllib2 -from distutils2.tests.pypi_server import use_pypi_server from distutils2.pypi import simple +from distutils2.tests import support, run_unittest +from distutils2.tests.support import unittest +from distutils2.tests.pypi_server import (use_pypi_server, PyPIServer, + PYPI_DEFAULT_STATIC_PATH) -class PyPISimpleTestCase(unittest2.TestCase): +class PyPISimpleTestCase(support.TempdirManager, + unittest.TestCase): + + def _get_simple_index(self, server, base_url="/simple/", hosts=None, + *args, **kwargs): + """Build and return a SimpleSimpleIndex instance, with the test server + urls + """ + if hosts is None: + hosts = (server.full_address.strip("http://"),) + kwargs['hosts'] = hosts + return simple.SimpleIndex(server.full_address + base_url, *args, + **kwargs) def test_bad_urls(self): - index = simple.PackageIndex() + index = simple.SimpleIndex() url = 'http://127.0.0.1:0/nonesuch/test_simple' try: - v = index.open_url(url) + v = index._open_url(url) except Exception, v: self.assertTrue(url in str(v)) else: @@ -27,12 +41,10 @@ class PyPISimpleTestCase(unittest2.TestCase): # issue 16 # easy_install inquant.contentmirror.plone breaks because of a typo # in its home URL - index = simple.PackageIndex( - hosts=('www.example.com',)) - + index = simple.SimpleIndex(hosts=('www.example.com',)) url = 'url:%20https://svn.plone.org/svn/collective/inquant.contentmirror.plone/trunk' try: - v = index.open_url(url) + v = index._open_url(url) except Exception, v: self.assertTrue(url in str(v)) else: @@ -47,7 +59,7 @@ class PyPISimpleTestCase(unittest2.TestCase): url = 'http://example.com' try: try: - v = index.open_url(url) + v = index._open_url(url) except Exception, v: self.assertTrue('line' in str(v)) else: @@ -58,7 +70,7 @@ class PyPISimpleTestCase(unittest2.TestCase): # issue 20 url = 'http://http://svn.pythonpaste.org/Paste/wphp/trunk' try: - index.open_url(url) + index._open_url(url) except Exception, v: self.assertTrue('nonnumeric port' in str(v)) @@ -68,103 +80,198 @@ class PyPISimpleTestCase(unittest2.TestCase): url = 'http://example.com' page = ('<a href="http://www.famfamfam.com](' 'http://www.famfamfam.com/">') - index.process_index(url, page) - - def test_url_ok(self): - index = simple.PackageIndex( - hosts=('www.example.com',)) - url = 'file:///tmp/test_simple' - self.assertTrue(index.url_ok(url, True)) - + index._process_url(url, page) + @use_pypi_server("test_found_links") def test_found_links(self, server): - """Browse the index, asking for a specified distribution version - """ + # Browse the index, asking for a specified distribution version # The PyPI index contains links for version 1.0, 1.1, 2.0 and 2.0.1 - # We query only for the version 1.1, so all distributions must me - # filled in the package_index (as the url has been scanned), but - # obtain must only return the one we want. - pi = simple.PackageIndex(server.full_address + "/simple/") - last_distribution = pi.obtain(simple.Requirement.parse("foobar")) + index = self._get_simple_index(server) + last_distribution = index.get("foobar") # we have scanned the index page - self.assertTrue(pi.package_pages.has_key('foobar')) self.assertIn(server.full_address + "/simple/foobar/", - pi.package_pages['foobar']) - + index._processed_urls) + # we have found 4 distributions in this page - self.assertEqual(len(pi["foobar"]), 4) + self.assertEqual(len(index._distributions["foobar"]), 4) # and returned the most recent one - self.assertEqual(last_distribution.version, '2.0.1') + self.assertEqual("%s" % last_distribution.version, '2.0.1') - @use_pypi_server("with_external_pages") - def test_process_external_pages(self, server): - """If the index provides links external to the pypi index, - they need to be processed, in order to discover new distributions. - """ + def test_is_browsable(self): + index = simple.SimpleIndex(follow_externals=False) + self.assertTrue(index._is_browsable(index.index_url + "test")) - @use_pypi_server() - def test_scan_all(self, server): - """Test that we can process the whole index and discover all related - links. + # Now, when following externals, we can have a list of hosts to trust. + # and don't follow other external links than the one described here. + index = simple.SimpleIndex(hosts=["pypi.python.org", "test.org"], + follow_externals=True) + good_urls = ( + "http://pypi.python.org/foo/bar", + "http://pypi.python.org/simple/foobar", + "http://test.org", + "http://test.org/", + "http://test.org/simple/", + ) + bad_urls = ( + "http://python.org", + "http://test.tld", + ) - """ - # FIXME remove? - - @use_pypi_server("with_external_pages") - def test_disable_external_pages(self, server): - """Test that when we tell the simple client to not retreive external - urls, it does well.""" - - @use_pypi_server() - def test_process_index(self, server): - """Test that we can process a simple given string and find related links - to distributions. - """ - - @use_pypi_server() - def test_process_url(self, server): - """Test that we can process an alternative url (not pypi related) to - find links in it. - """ - - @use_pypi_server(static_filesystem_paths=["test_link_priority"], + for url in good_urls: + self.assertTrue(index._is_browsable(url)) + + for url in bad_urls: + self.assertFalse(index._is_browsable(url)) + + # allow all hosts + index = simple.SimpleIndex(follow_externals=True, hosts=("*",)) + self.assertTrue(index._is_browsable("http://an-external.link/path")) + self.assertTrue(index._is_browsable("pypi.test.tld/a/path")) + + # specify a list of hosts we want to allow + index = simple.SimpleIndex(follow_externals=True, + hosts=("*.test.tld",)) + self.assertFalse(index._is_browsable("http://an-external.link/path")) + self.assertTrue(index._is_browsable("http://pypi.test.tld/a/path")) + + @use_pypi_server("with_externals") + def test_restrict_hosts(self, server): + # Include external pages + # Try to request the package index, wich contains links to "externals" + # resources. They have to be scanned too. + index = self._get_simple_index(server, follow_externals=True) + index.get("foobar") + self.assertIn(server.full_address + "/external/external.html", + index._processed_urls) + + @use_pypi_server("with_real_externals") + def test_restrict_hosts(self, server): + # Only use a list of allowed hosts is possible + # Test that telling the simple pyPI client to not retrieve external + # works + index = self._get_simple_index(server, follow_externals=False) + index.get("foobar") + self.assertNotIn(server.full_address + "/external/external.html", + index._processed_urls) + + @use_pypi_server(static_filesystem_paths=["with_externals"], static_uri_paths=["simple", "external"]) def test_links_priority(self, server): - """ - Download links from the pypi simple index should be used before - external download links. - http://bitbucket.org/tarek/distribute/issue/163/md5-validation-error - - Usecase : - - someone uploads a package on pypi, a md5 is generated - - someone manually copies this link (with the md5 in the url) onto an - external page accessible from the package page. - - someone reuploads the package (with a different md5) - - while easy_installing, an MD5 error occurs because the external link - is used - -> Distribute should use the link from pypi, not the external one. - """ + # Download links from the pypi simple index should be used before + # external download links. + # http://bitbucket.org/tarek/distribute/issue/163/md5-validation-error + # + # Usecase : + # - someone uploads a package on pypi, a md5 is generated + # - someone manually coindexes this link (with the md5 in the url) onto + # an external page accessible from the package page. + # - someone reuploads the package (with a different md5) + # - while easy_installing, an MD5 error occurs because the external link + # is used + # -> The index should use the link from pypi, not the external one. + # start an index server index_url = server.full_address + '/simple/' # scan a test index - pi = simple.PackageIndex(index_url) - requirement = simple.Requirement.parse('foobar') - pi.find_packages(requirement) + index = simple.SimpleIndex(index_url, follow_externals=True) + dists = index.find("foobar") server.stop() - # the distribution has been found - self.assertTrue('foobar' in pi) # we have only one link, because links are compared without md5 - self.assertEqual(len(pi['foobar']), 1) + self.assertEqual(len(dists), 1) # the link should be from the index - self.assertTrue('correct_md5' in pi['foobar'][0].location) + self.assertEqual('12345678901234567', dists[0].url['hashval']) + self.assertEqual('md5', dists[0].url['hashname']) + + @use_pypi_server(static_filesystem_paths=["with_norel_links"], + static_uri_paths=["simple", "external"]) + def test_not_scan_all_links(self, server): + # Do not follow all index page links. + # The links not tagged with rel="download" and rel="homepage" have + # to not be processed by the package index, while processing "pages". + + # process the pages + index = self._get_simple_index(server, follow_externals=True) + index.find("foobar") + # now it should have processed only pages with links rel="download" + # and rel="homepage" + self.assertIn("%s/simple/foobar/" % server.full_address, + index._processed_urls) # it's the simple index page + self.assertIn("%s/external/homepage.html" % server.full_address, + index._processed_urls) # the external homepage is rel="homepage" + self.assertNotIn("%s/external/nonrel.html" % server.full_address, + index._processed_urls) # this link contains no rel=* + self.assertNotIn("%s/unrelated-0.2.tar.gz" % server.full_address, + index._processed_urls) # linked from simple index (no rel) + self.assertIn("%s/foobar-0.1.tar.gz" % server.full_address, + index._processed_urls) # linked from simple index (rel) + self.assertIn("%s/foobar-2.0.tar.gz" % server.full_address, + index._processed_urls) # linked from external homepage (rel) + + def test_uses_mirrors(self): + # When the main repository seems down, try using the given mirrors""" + server = PyPIServer("foo_bar_baz") + mirror = PyPIServer("foo_bar_baz") + mirror.start() # we dont start the server here + + try: + # create the index using both servers + index = simple.SimpleIndex(server.full_address + "/simple/", + hosts=('*',), timeout=1, # set the timeout to 1s for the tests + mirrors=[mirror.full_address + "/simple/",]) + + # this should not raise a timeout + self.assertEqual(4, len(index.find("foo"))) + finally: + mirror.stop() + + def test_simple_link_matcher(self): + # Test that the simple link matcher yields the right links""" + index = simple.SimpleIndex(follow_externals=False) + + # Here, we define: + # 1. one link that must be followed, cause it's a download one + # 2. one link that must *not* be followed, cause the is_browsable + # returns false for it. + # 3. one link that must be followed cause it's a homepage that is + # browsable + self.assertTrue(index._is_browsable("%stest" % index.index_url)) + self.assertFalse(index._is_browsable("http://dl-link2")) + content = """ + <a href="http://dl-link1" rel="download">download_link1</a> + <a href="http://dl-link2" rel="homepage">homepage_link1</a> + <a href="%stest" rel="homepage">homepage_link2</a> + """ % index.index_url + + # Test that the simple link matcher yield the good links. + generator = index._simple_link_matcher(content, index.index_url) + self.assertEqual(('http://dl-link1', True), generator.next()) + self.assertEqual(('%stest' % index.index_url, False), + generator.next()) + self.assertRaises(StopIteration, generator.next) + + # Follow the external links is possible + index.follow_externals = True + generator = index._simple_link_matcher(content, index.index_url) + self.assertEqual(('http://dl-link1', True), generator.next()) + self.assertEqual(('http://dl-link2', False), generator.next()) + self.assertEqual(('%stest' % index.index_url, False), + generator.next()) + self.assertRaises(StopIteration, generator.next) + def test_browse_local_files(self): + # Test that we can browse local files""" + index_path = os.sep.join(["file://" + PYPI_DEFAULT_STATIC_PATH, + "test_found_links", "simple"]) + index = simple.SimpleIndex(index_path) + dists = index.find("foobar") + self.assertEqual(4, len(dists)) def test_suite(): - return unittest2.makeSuite(PyPISimpleTestCase) + return unittest.makeSuite(PyPISimpleTestCase) if __name__ == '__main__': - unittest2.main(defaultTest="test_suite") + unittest.main(defaultTest="test_suite") diff --git a/src/distutils2/tests/test_pypi_versions.py b/src/distutils2/tests/test_pypi_versions.py index 4e97147..7781982 100644 --- a/src/distutils2/tests/test_pypi_versions.py +++ b/src/distutils2/tests/test_pypi_versions.py @@ -19,6 +19,7 @@ import xmlrpclib import os.path from distutils2.version import suggest_normalized_version +from distutils2.tests import run_unittest from distutils2.tests.support import unittest def test_pypi(): @@ -52,7 +53,7 @@ def test_pypi(): print "Saving package info..." f = open(INDEX_PICKLE_FILE, 'wb') try: - pickle.dump(package_info, o) + pickle.dump(package_info, f) finally: f.close() diff --git a/src/distutils2/tests/test_register.py b/src/distutils2/tests/test_register.py index 3261610..be782d1 100644 --- a/src/distutils2/tests/test_register.py +++ b/src/distutils2/tests/test_register.py @@ -124,7 +124,7 @@ class RegisterTestCase(PyPIRCCommandTestCase): # with the content similar to WANTED_PYPIRC content = open(self.rc).read() - self.assertEquals(content, WANTED_PYPIRC) + self.assertEqual(content, WANTED_PYPIRC) # now let's make sure the .pypirc file generated # really works : we shouldn't be asked anything @@ -141,7 +141,7 @@ class RegisterTestCase(PyPIRCCommandTestCase): self.assertTrue(self.conn.reqs, 2) req1 = dict(self.conn.reqs[0].headers) req2 = dict(self.conn.reqs[1].headers) - self.assertEquals(req2['Content-length'], req1['Content-length']) + self.assertEqual(req2['Content-length'], req1['Content-length']) self.assertTrue('xxx' in self.conn.reqs[1].data) def test_password_not_in_file(self): @@ -154,7 +154,7 @@ class RegisterTestCase(PyPIRCCommandTestCase): # dist.password should be set # therefore used afterwards by other commands - self.assertEquals(cmd.distribution.password, 'password') + self.assertEqual(cmd.distribution.password, 'password') def test_registering(self): # this test runs choice 2 @@ -171,7 +171,7 @@ class RegisterTestCase(PyPIRCCommandTestCase): self.assertTrue(self.conn.reqs, 1) req = self.conn.reqs[0] headers = dict(req.headers) - self.assertEquals(headers['Content-length'], '608') + self.assertEqual(headers['Content-length'], '608') self.assertTrue('tarek' in req.data) def test_password_reset(self): @@ -189,7 +189,7 @@ class RegisterTestCase(PyPIRCCommandTestCase): self.assertTrue(self.conn.reqs, 1) req = self.conn.reqs[0] headers = dict(req.headers) - self.assertEquals(headers['Content-length'], '290') + self.assertEqual(headers['Content-length'], '290') self.assertTrue('tarek' in req.data) @unittest.skipUnless(DOCUTILS_SUPPORT, 'needs docutils') @@ -246,8 +246,8 @@ class RegisterTestCase(PyPIRCCommandTestCase): cmd.ensure_finalized() cmd.distribution.metadata['Requires-Dist'] = ['lxml'] data = cmd.build_post_data('submit') - self.assertEquals(data['metadata_version'], '1.2') - self.assertEquals(data['requires_dist'], ['lxml']) + self.assertEqual(data['metadata_version'], '1.2') + self.assertEqual(data['requires_dist'], ['lxml']) def test_suite(): return unittest.makeSuite(RegisterTestCase) diff --git a/src/distutils2/tests/test_sdist.py b/src/distutils2/tests/test_sdist.py index 964857b..8859a3d 100644 --- a/src/distutils2/tests/test_sdist.py +++ b/src/distutils2/tests/test_sdist.py @@ -20,7 +20,6 @@ except ImportError: from os.path import join import sys -import tempfile import warnings from distutils2.tests import captured_stdout @@ -31,7 +30,7 @@ from distutils2.core import Distribution from distutils2.tests.support import unittest from distutils2.tests.test_config import PyPIRCCommandTestCase from distutils2.errors import DistutilsExecError, DistutilsOptionError -from distutils2.spawn import find_executable +from distutils2.util import find_executable from distutils2.tests import support from distutils2.log import WARN try: @@ -128,7 +127,7 @@ class SDistTestCase(PyPIRCCommandTestCase): # now let's check what we have dist_folder = join(self.tmp_dir, 'dist') files = os.listdir(dist_folder) - self.assertEquals(files, ['fake-1.0.zip']) + self.assertEqual(files, ['fake-1.0.zip']) zip_file = zipfile.ZipFile(join(dist_folder, 'fake-1.0.zip')) try: @@ -137,7 +136,7 @@ class SDistTestCase(PyPIRCCommandTestCase): zip_file.close() # making sure everything has been pruned correctly - self.assertEquals(len(content), 4) + self.assertEqual(len(content), 4) @unittest.skipUnless(zlib, "requires zlib") def test_make_distribution(self): @@ -159,7 +158,7 @@ class SDistTestCase(PyPIRCCommandTestCase): dist_folder = join(self.tmp_dir, 'dist') result = os.listdir(dist_folder) result.sort() - self.assertEquals(result, + self.assertEqual(result, ['fake-1.0.tar', 'fake-1.0.tar.gz'] ) os.remove(join(dist_folder, 'fake-1.0.tar')) @@ -173,7 +172,7 @@ class SDistTestCase(PyPIRCCommandTestCase): result = os.listdir(dist_folder) result.sort() - self.assertEquals(result, + self.assertEqual(result, ['fake-1.0.tar', 'fake-1.0.tar.gz']) @unittest.skipUnless(zlib, "requires zlib") @@ -223,7 +222,7 @@ class SDistTestCase(PyPIRCCommandTestCase): # now let's check what we have dist_folder = join(self.tmp_dir, 'dist') files = os.listdir(dist_folder) - self.assertEquals(files, ['fake-1.0.zip']) + self.assertEqual(files, ['fake-1.0.zip']) zip_file = zipfile.ZipFile(join(dist_folder, 'fake-1.0.zip')) try: @@ -232,11 +231,11 @@ class SDistTestCase(PyPIRCCommandTestCase): zip_file.close() # making sure everything was added - self.assertEquals(len(content), 11) + self.assertEqual(len(content), 11) # checking the MANIFEST manifest = open(join(self.tmp_dir, 'MANIFEST')).read() - self.assertEquals(manifest, MANIFEST % {'sep': os.sep}) + self.assertEqual(manifest, MANIFEST % {'sep': os.sep}) @unittest.skipUnless(zlib, "requires zlib") def test_metadata_check_option(self): @@ -248,7 +247,7 @@ class SDistTestCase(PyPIRCCommandTestCase): cmd.ensure_finalized() cmd.run() warnings = self.get_logs(WARN) - self.assertEquals(len(warnings), 1) + self.assertEqual(len(warnings), 1) # trying with a complete set of metadata self.clear_logs() @@ -260,7 +259,7 @@ class SDistTestCase(PyPIRCCommandTestCase): # removing manifest generated warnings warnings = [warn for warn in warnings if not warn.endswith('-- skipping')] - self.assertEquals(len(warnings), 0) + self.assertEqual(len(warnings), 0) def test_show_formats(self): @@ -270,7 +269,7 @@ class SDistTestCase(PyPIRCCommandTestCase): num_formats = len(get_archive_formats()) output = [line for line in stdout.split('\n') if line.strip().startswith('--formats=')] - self.assertEquals(len(output), num_formats) + self.assertEqual(len(output), num_formats) def test_finalize_options(self): @@ -278,9 +277,9 @@ class SDistTestCase(PyPIRCCommandTestCase): cmd.finalize_options() # default options set by finalize - self.assertEquals(cmd.manifest, 'MANIFEST') - self.assertEquals(cmd.template, 'MANIFEST.in') - self.assertEquals(cmd.dist_dir, 'dist') + self.assertEqual(cmd.manifest, 'MANIFEST') + self.assertEqual(cmd.template, 'MANIFEST.in') + self.assertEqual(cmd.dist_dir, 'dist') # formats has to be a string splitable on (' ', ',') or # a stringlist @@ -317,8 +316,8 @@ class SDistTestCase(PyPIRCCommandTestCase): archive = tarfile.open(archive_name) try: for member in archive.getmembers(): - self.assertEquals(member.uid, 0) - self.assertEquals(member.gid, 0) + self.assertEqual(member.uid, 0) + self.assertEqual(member.gid, 0) finally: archive.close() @@ -339,7 +338,7 @@ class SDistTestCase(PyPIRCCommandTestCase): # rights (see #7408) try: for member in archive.getmembers(): - self.assertEquals(member.uid, os.getuid()) + self.assertEqual(member.uid, os.getuid()) finally: archive.close() diff --git a/src/distutils2/tests/test_spawn.py b/src/distutils2/tests/test_spawn.py deleted file mode 100644 index 17c7430..0000000 --- a/src/distutils2/tests/test_spawn.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Tests for distutils.spawn.""" -import os -import time -from distutils2.tests import captured_stdout - -from distutils2.spawn import _nt_quote_args -from distutils2.spawn import spawn, find_executable -from distutils2.errors import DistutilsExecError -from distutils2.tests import support -from distutils2.tests.support import unittest - -class SpawnTestCase(support.TempdirManager, - support.LoggingSilencer, - unittest.TestCase): - - def test_nt_quote_args(self): - - for (args, wanted) in ((['with space', 'nospace'], - ['"with space"', 'nospace']), - (['nochange', 'nospace'], - ['nochange', 'nospace'])): - res = _nt_quote_args(args) - self.assertEquals(res, wanted) - - - @unittest.skipUnless(os.name in ('nt', 'posix'), - 'Runs only under posix or nt') - def test_spawn(self): - tmpdir = self.mkdtemp() - - # creating something executable - # through the shell that returns 1 - if os.name == 'posix': - exe = os.path.join(tmpdir, 'foo.sh') - self.write_file(exe, '#!/bin/sh\nexit 1') - os.chmod(exe, 0777) - else: - exe = os.path.join(tmpdir, 'foo.bat') - self.write_file(exe, 'exit 1') - - os.chmod(exe, 0777) - self.assertRaises(DistutilsExecError, spawn, [exe]) - - # now something that works - if os.name == 'posix': - exe = os.path.join(tmpdir, 'foo.sh') - self.write_file(exe, '#!/bin/sh\nexit 0') - os.chmod(exe, 0777) - else: - exe = os.path.join(tmpdir, 'foo.bat') - self.write_file(exe, 'exit 0') - - os.chmod(exe, 0777) - spawn([exe]) # should work without any error - -def test_suite(): - return unittest.makeSuite(SpawnTestCase) - -if __name__ == "__main__": - unittest.main(defaultTest="test_suite") diff --git a/src/distutils2/tests/test_upload.py b/src/distutils2/tests/test_upload.py index a0a7330..5ca88a1 100644 --- a/src/distutils2/tests/test_upload.py +++ b/src/distutils2/tests/test_upload.py @@ -1,12 +1,13 @@ """Tests for distutils.command.upload.""" # -*- encoding: utf8 -*- -import os, sys +import os +import sys from distutils2.command.upload import upload from distutils2.core import Distribution -from distutils2.tests.pypi_server import PyPIServer, PyPIServerTestCase from distutils2.tests import support +from distutils2.tests.pypi_server import PyPIServer, PyPIServerTestCase from distutils2.tests.support import unittest from distutils2.tests.test_config import PYPIRC, PyPIRCCommandTestCase @@ -29,10 +30,10 @@ class UploadTestCase(PyPIServerTestCase, PyPIRCCommandTestCase): dist = Distribution() cmd = upload(dist) cmd.finalize_options() - for attr, waited in (('username', 'me'), ('password', 'secret'), - ('realm', 'pypi'), - ('repository', 'http://pypi.python.org/pypi')): - self.assertEqual(getattr(cmd, attr), waited) + for attr, expected in (('username', 'me'), ('password', 'secret'), + ('realm', 'pypi'), + ('repository', 'http://pypi.python.org/pypi')): + self.assertEqual(getattr(cmd, attr), expected) def test_saved_password(self): # file with no password diff --git a/src/distutils2/tests/test_util.py b/src/distutils2/tests/test_util.py index b9b8f4a..51097a7 100644 --- a/src/distutils2/tests/test_util.py +++ b/src/distutils2/tests/test_util.py @@ -4,16 +4,21 @@ import sys from copy import copy from StringIO import StringIO import subprocess +import tempfile +import time +from distutils2.tests import captured_stdout +from distutils2.tests.support import unittest from distutils2.errors import (DistutilsPlatformError, DistutilsByteCompileError, - DistutilsFileError) - + DistutilsFileError, + DistutilsExecError) from distutils2.util import (convert_path, change_root, - check_environ, split_quoted, strtobool, - rfc822_escape, get_compiler_versions, - _find_exe_version, _MAC_OS_X_LD_VERSION, - byte_compile, find_packages) + check_environ, split_quoted, strtobool, + rfc822_escape, get_compiler_versions, + _find_exe_version, _MAC_OS_X_LD_VERSION, + byte_compile, find_packages, spawn, find_executable, + _nt_quote_args) from distutils2 import util from distutils2.tests import support from distutils2.tests.support import unittest @@ -100,7 +105,7 @@ class UtilTestCase(support.EnvironGuard, return '/'.join(path) os.path.join = _join - self.assertEquals(convert_path('/home/to/my/stuff'), + self.assertEqual(convert_path('/home/to/my/stuff'), '/home/to/my/stuff') # win @@ -112,9 +117,9 @@ class UtilTestCase(support.EnvironGuard, self.assertRaises(ValueError, convert_path, '/home/to/my/stuff') self.assertRaises(ValueError, convert_path, 'home/to/my/stuff/') - self.assertEquals(convert_path('home/to/my/stuff'), + self.assertEqual(convert_path('home/to/my/stuff'), 'home\\to\\my\\stuff') - self.assertEquals(convert_path('.'), + self.assertEqual(convert_path('.'), os.curdir) def test_change_root(self): @@ -127,9 +132,9 @@ class UtilTestCase(support.EnvironGuard, return '/'.join(path) os.path.join = _join - self.assertEquals(change_root('/root', '/old/its/here'), + self.assertEqual(change_root('/root', '/old/its/here'), '/root/old/its/here') - self.assertEquals(change_root('/root', 'its/here'), + self.assertEqual(change_root('/root', 'its/here'), '/root/its/here') # windows @@ -146,9 +151,9 @@ class UtilTestCase(support.EnvironGuard, return '\\'.join(path) os.path.join = _join - self.assertEquals(change_root('c:\\root', 'c:\\old\\its\\here'), + self.assertEqual(change_root('c:\\root', 'c:\\old\\its\\here'), 'c:\\root\\old\\its\\here') - self.assertEquals(change_root('c:\\root', 'its\\here'), + self.assertEqual(change_root('c:\\root', 'its\\here'), 'c:\\root\\its\\here') # BugsBunny os (it's a great os) @@ -159,7 +164,7 @@ class UtilTestCase(support.EnvironGuard, # XXX platforms to be covered: os2, mac def test_split_quoted(self): - self.assertEquals(split_quoted('""one"" "two" \'three\' \\four'), + self.assertEqual(split_quoted('""one"" "two" \'three\' \\four'), ['one', 'two', 'three', 'four']) def test_strtobool(self): @@ -177,7 +182,7 @@ class UtilTestCase(support.EnvironGuard, res = rfc822_escape(header) wanted = ('I am a%(8s)spoor%(8s)slonesome%(8s)s' 'header%(8s)s') % {'8s': '\n'+8*' '} - self.assertEquals(res, wanted) + self.assertEqual(res, wanted) def test_find_exe_version(self): # the ld version scheme under MAC OS is: @@ -195,7 +200,7 @@ class UtilTestCase(support.EnvironGuard, ('@(#)PROGRAM:ld PROJECT:ld64-95.2.12', '95.2.12')): result = _MAC_OS_X_LD_VERSION.search(output) - self.assertEquals(result.group(1), version) + self.assertEqual(result.group(1), version) def _find_executable(self, name): if name in self._exes: @@ -205,43 +210,43 @@ class UtilTestCase(support.EnvironGuard, def test_get_compiler_versions(self): # get_versions calls distutils.spawn.find_executable on # 'gcc', 'ld' and 'dllwrap' - self.assertEquals(get_compiler_versions(), (None, None, None)) + self.assertEqual(get_compiler_versions(), (None, None, None)) # Let's fake we have 'gcc' and it returns '3.4.5' self._exes['gcc'] = 'gcc (GCC) 3.4.5 (mingw special)\nFSF' res = get_compiler_versions() - self.assertEquals(str(res[0]), '3.4.5') + self.assertEqual(str(res[0]), '3.4.5') # and let's see what happens when the version # doesn't match the regular expression # (\d+\.\d+(\.\d+)*) self._exes['gcc'] = 'very strange output' res = get_compiler_versions() - self.assertEquals(res[0], None) + self.assertEqual(res[0], None) # same thing for ld if sys.platform != 'darwin': self._exes['ld'] = 'GNU ld version 2.17.50 20060824' res = get_compiler_versions() - self.assertEquals(str(res[1]), '2.17.50') + self.assertEqual(str(res[1]), '2.17.50') self._exes['ld'] = '@(#)PROGRAM:ld PROJECT:ld64-77' res = get_compiler_versions() - self.assertEquals(res[1], None) + self.assertEqual(res[1], None) else: self._exes['ld'] = 'GNU ld version 2.17.50 20060824' res = get_compiler_versions() - self.assertEquals(res[1], None) + self.assertEqual(res[1], None) self._exes['ld'] = '@(#)PROGRAM:ld PROJECT:ld64-77' res = get_compiler_versions() - self.assertEquals(str(res[1]), '77') + self.assertEqual(str(res[1]), '77') # and dllwrap self._exes['dllwrap'] = 'GNU dllwrap 2.17.50 20060824\nFSF' res = get_compiler_versions() - self.assertEquals(str(res[2]), '2.17.50') + self.assertEqual(str(res[2]), '2.17.50') self._exes['dllwrap'] = 'Cheese Wrap' res = get_compiler_versions() - self.assertEquals(res[2], None) + self.assertEqual(res[2], None) @unittest.skipUnless(hasattr(sys, 'dont_write_bytecode'), 'no dont_write_bytecode support') @@ -257,7 +262,10 @@ class UtilTestCase(support.EnvironGuard, def test_newer(self): self.assertRaises(DistutilsFileError, util.newer, 'xxx', 'xxx') - + self.newer_f1 = tempfile.NamedTemporaryFile() + time.sleep(1) + self.newer_f2 = tempfile.NamedTemporaryFile() + self.assertTrue(util.newer(self.newer_f2.name, self.newer_f1.name)) def test_find_packages(self): # let's create a structure we want to scan: @@ -294,7 +302,78 @@ class UtilTestCase(support.EnvironGuard, self.write_file(os.path.join(pkg5, '__init__.py')) res = find_packages([root], ['pkg1.pkg2']) - self.assertEquals(set(res), set(['pkg1', 'pkg5', 'pkg1.pkg3', 'pkg1.pkg3.pkg6'])) + self.assertEqual(set(res), set(['pkg1', 'pkg5', 'pkg1.pkg3', 'pkg1.pkg3.pkg6'])) + + @unittest.skipUnless(sys.version > '2.6', 'Need Python 2.6 or more') + def test_run_2to3_on_code(self): + content = "print 'test'" + converted_content = "print('test')" + file_handle = self.mktempfile() + file_name = file_handle.name + file_handle.write(content) + file_handle.flush() + file_handle.seek(0) + from distutils2.util import run_2to3 + run_2to3([file_name]) + new_content = "".join(file_handle.read()) + file_handle.close() + self.assertEquals(new_content, converted_content) + + @unittest.skipUnless(sys.version > '2.6', 'Need Python 2.6 or more') + def test_run_2to3_on_doctests(self): + # to check if text files containing doctests only get converted. + content = ">>> print 'test'\ntest\n" + converted_content = ">>> print('test')\ntest\n\n" + file_handle = self.mktempfile() + file_name = file_handle.name + file_handle.write(content) + file_handle.flush() + file_handle.seek(0) + from distutils2.util import run_2to3 + run_2to3([file_name], doctests_only=True) + new_content = "".join(file_handle.readlines()) + file_handle.close() + self.assertEquals(new_content, converted_content) + + def test_nt_quote_args(self): + + for (args, wanted) in ((['with space', 'nospace'], + ['"with space"', 'nospace']), + (['nochange', 'nospace'], + ['nochange', 'nospace'])): + res = _nt_quote_args(args) + self.assertEqual(res, wanted) + + + @unittest.skipUnless(os.name in ('nt', 'posix'), + 'Runs only under posix or nt') + def test_spawn(self): + tmpdir = self.mkdtemp() + + # creating something executable + # through the shell that returns 1 + if os.name == 'posix': + exe = os.path.join(tmpdir, 'foo.sh') + self.write_file(exe, '#!/bin/sh\nexit 1') + os.chmod(exe, 0777) + else: + exe = os.path.join(tmpdir, 'foo.bat') + self.write_file(exe, 'exit 1') + + os.chmod(exe, 0777) + self.assertRaises(DistutilsExecError, spawn, [exe]) + + # now something that works + if os.name == 'posix': + exe = os.path.join(tmpdir, 'foo.sh') + self.write_file(exe, '#!/bin/sh\nexit 0') + os.chmod(exe, 0777) + else: + exe = os.path.join(tmpdir, 'foo.bat') + self.write_file(exe, 'exit 0') + + os.chmod(exe, 0777) + spawn([exe]) # should work without any error def test_suite(): diff --git a/src/distutils2/tests/test_version.py b/src/distutils2/tests/test_version.py index cc725cf..343b397 100644 --- a/src/distutils2/tests/test_version.py +++ b/src/distutils2/tests/test_version.py @@ -3,7 +3,7 @@ import doctest import os from distutils2.version import NormalizedVersion as V -from distutils2.version import IrrationalVersionError +from distutils2.version import HugeMajorVersionNumError, IrrationalVersionError from distutils2.version import suggest_normalized_version as suggest from distutils2.version import VersionPredicate from distutils2.tests.support import unittest @@ -22,18 +22,22 @@ class VersionTestCase(unittest.TestCase): (V('1.0.dev345'), '1.0.dev345'), (V('1.0.post456.dev623'), '1.0.post456.dev623')) + def test_repr(self): + + self.assertEqual(repr(V('1.0')), "NormalizedVersion('1.0')") + def test_basic_versions(self): for v, s in self.versions: - self.assertEquals(str(v), s) + self.assertEqual(str(v), s) def test_from_parts(self): for v, s in self.versions: parts = v.parts v2 = V.from_parts(*v.parts) - self.assertEquals(v, v2) - self.assertEquals(str(v), str(v2)) + self.assertEqual(v, v2) + self.assertEqual(str(v), str(v2)) def test_irrational_versions(self): @@ -44,6 +48,12 @@ class VersionTestCase(unittest.TestCase): for s in irrational: self.assertRaises(IrrationalVersionError, V, s) + def test_huge_version(self): + + self.assertEquals(str(V('1980.0')), '1980.0') + self.assertRaises(HugeMajorVersionNumError, V, '1981.0') + self.assertEquals(str(V('1981.0', error_on_huge_major_num=False)), '1981.0') + def test_comparison(self): r""" >>> V('1.2.0') == '1.2' @@ -51,12 +61,33 @@ class VersionTestCase(unittest.TestCase): ... TypeError: cannot compare NormalizedVersion and str + >>> V('1.2') < '1.3' + Traceback (most recent call last): + ... + TypeError: cannot compare NormalizedVersion and str + >>> V('1.2.0') == V('1.2') True >>> V('1.2.0') == V('1.2.3') False + >>> V('1.2.0') != V('1.2.3') + True >>> V('1.2.0') < V('1.2.3') True + >>> V('1.2.0') < V('1.2.0') + False + >>> V('1.2.0') <= V('1.2.0') + True + >>> V('1.2.0') <= V('1.2.3') + True + >>> V('1.2.3') <= V('1.2.0') + False + >>> V('1.2.0') >= V('1.2.0') + True + >>> V('1.2.3') >= V('1.2.0') + True + >>> V('1.2.0') >= V('1.2.3') + False >>> (V('1.0') > V('1.0b2')) True >>> (V('1.0') > V('1.0c2') > V('1.0c1') > V('1.0b2') > V('1.0b1') @@ -96,34 +127,35 @@ class VersionTestCase(unittest.TestCase): def test_suggest_normalized_version(self): - self.assertEquals(suggest('1.0'), '1.0') - self.assertEquals(suggest('1.0-alpha1'), '1.0a1') - self.assertEquals(suggest('1.0c2'), '1.0c2') - self.assertEquals(suggest('walla walla washington'), None) - self.assertEquals(suggest('2.4c1'), '2.4c1') + self.assertEqual(suggest('1.0'), '1.0') + self.assertEqual(suggest('1.0-alpha1'), '1.0a1') + self.assertEqual(suggest('1.0c2'), '1.0c2') + self.assertEqual(suggest('walla walla washington'), None) + self.assertEqual(suggest('2.4c1'), '2.4c1') + self.assertEqual(suggest('v1.0'), '1.0') # from setuptools - self.assertEquals(suggest('0.4a1.r10'), '0.4a1.post10') - self.assertEquals(suggest('0.7a1dev-r66608'), '0.7a1.dev66608') - self.assertEquals(suggest('0.6a9.dev-r41475'), '0.6a9.dev41475') - self.assertEquals(suggest('2.4preview1'), '2.4c1') - self.assertEquals(suggest('2.4pre1') , '2.4c1') - self.assertEquals(suggest('2.1-rc2'), '2.1c2') + self.assertEqual(suggest('0.4a1.r10'), '0.4a1.post10') + self.assertEqual(suggest('0.7a1dev-r66608'), '0.7a1.dev66608') + self.assertEqual(suggest('0.6a9.dev-r41475'), '0.6a9.dev41475') + self.assertEqual(suggest('2.4preview1'), '2.4c1') + self.assertEqual(suggest('2.4pre1') , '2.4c1') + self.assertEqual(suggest('2.1-rc2'), '2.1c2') # from pypi - self.assertEquals(suggest('0.1dev'), '0.1.dev0') - self.assertEquals(suggest('0.1.dev'), '0.1.dev0') + self.assertEqual(suggest('0.1dev'), '0.1.dev0') + self.assertEqual(suggest('0.1.dev'), '0.1.dev0') # we want to be able to parse Twisted # development versions are like post releases in Twisted - self.assertEquals(suggest('9.0.0+r2363'), '9.0.0.post2363') + self.assertEqual(suggest('9.0.0+r2363'), '9.0.0.post2363') # pre-releases are using markers like "pre1" - self.assertEquals(suggest('9.0.0pre1'), '9.0.0c1') + self.assertEqual(suggest('9.0.0pre1'), '9.0.0c1') # we want to be able to parse Tcl-TK # they us "p1" "p2" for post releases - self.assertEquals(suggest('1.4p1'), '1.4.post1') + self.assertEqual(suggest('1.4p1'), '1.4.post1') def test_predicate(self): # VersionPredicate knows how to parse stuff like: @@ -151,15 +183,37 @@ class VersionTestCase(unittest.TestCase): self.assertFalse(VersionPredicate('Hey (<=2.5)').match('2.6.0')) self.assertTrue(VersionPredicate('Hey (>=2.5)').match('2.5.1')) + self.assertRaises(ValueError, VersionPredicate, '') + # XXX need to silent the micro version in this case #assert not VersionPredicate('Ho (<3.0,!=2.6)').match('2.6.3') + def test_is_final(self): + # VersionPredicate knows is a distribution is a final one or not. + final_versions = ('1.0', '1.0.post456') + other_versions = ('1.0.dev1', '1.0a2', '1.0c3') + + for version in final_versions: + self.assertTrue(V(version).is_final) + for version in other_versions: + self.assertFalse(V(version).is_final) + +class VersionWhiteBoxTestCase(unittest.TestCase): + + def test_parse_numdots(self): + # For code coverage completeness, as pad_zeros_length can't be set or + # influenced from the public interface + self.assertEquals(V('1.0')._parse_numdots('1.0', '1.0', + pad_zeros_length=3), + [1, 0, 0]) + + def test_suite(): #README = os.path.join(os.path.dirname(__file__), 'README.txt') #suite = [doctest.DocFileSuite(README), unittest.makeSuite(VersionTestCase)] - suite = [unittest.makeSuite(VersionTestCase)] + suite = [unittest.makeSuite(VersionTestCase), + unittest.makeSuite(VersionWhiteBoxTestCase)] return unittest.TestSuite(suite) if __name__ == "__main__": unittest.main(defaultTest="test_suite") - diff --git a/src/distutils2/util.py b/src/distutils2/util.py index 0ab2e3e..8f9f81d 100644 --- a/src/distutils2/util.py +++ b/src/distutils2/util.py @@ -1,22 +1,25 @@ """distutils.util -Miscellaneous utility functions -- anything that doesn't fit into -one of the other *util.py modules. +Miscellaneous utility functions. """ __revision__ = "$Id: util.py 77761 2010-01-26 22:46:15Z tarek.ziade $" -import sys, os, string, re +import sys +import os +import string +import re +from copy import copy from fnmatch import fnmatchcase from distutils2.errors import (DistutilsPlatformError, DistutilsFileError, - DistutilsByteCompileError) -from distutils2.spawn import spawn, find_executable + DistutilsByteCompileError, DistutilsExecError) from distutils2 import log from distutils2._backport import sysconfig as _sysconfig _PLATFORM = None + def newer(source, target): """Tells if the target is newer than the source. @@ -37,6 +40,7 @@ def newer(source, target): return os.stat(source).st_mtime > os.stat(target).st_mtime + def get_platform(): """Return a string that identifies the current platform. @@ -48,6 +52,7 @@ def get_platform(): _PLATFORM = _sysconfig.get_platform() return _PLATFORM + def set_platform(identifier): """Sets the platform string identifier returned by get_platform(). @@ -57,6 +62,7 @@ def set_platform(identifier): global _PLATFORM _PLATFORM = identifier + def convert_path(pathname): """Return 'pathname' as a name that will work on the native filesystem. @@ -125,6 +131,7 @@ def change_root(new_root, pathname): _environ_checked = 0 + def check_environ(): """Ensure that 'os.environ' has all the environment variables needed. @@ -147,6 +154,7 @@ def check_environ(): _environ_checked = 1 + def subst_vars(s, local_vars): """Perform shell/Perl-style variable substitution on 'string'. @@ -158,7 +166,8 @@ def subst_vars(s, local_vars): variables not found in either 'local_vars' or 'os.environ'. """ check_environ() - def _subst (match, local_vars=local_vars): + + def _subst(match, local_vars=local_vars): var_name = match.group(1) if var_name in local_vars: return str(local_vars[var_name]) @@ -170,6 +179,7 @@ def subst_vars(s, local_vars): except KeyError, var: raise ValueError("invalid variable '$%s'" % var) + def grok_environment_error(exc, prefix="error: "): """Generate a useful error message from an EnvironmentError. @@ -196,12 +206,14 @@ def grok_environment_error(exc, prefix="error: "): # Needed by 'split_quoted()' _wordchars_re = _squote_re = _dquote_re = None + def _init_regex(): global _wordchars_re, _squote_re, _dquote_re _wordchars_re = re.compile(r'[^\\\'\"%s ]*' % string.whitespace) _squote_re = re.compile(r"'(?:[^'\\]|\\.)*'") _dquote_re = re.compile(r'"(?:[^"\\]|\\.)*"') + def split_quoted(s): """Split a string up according to Unix shell-like rules for quotes and backslashes. @@ -217,7 +229,8 @@ def split_quoted(s): # This is a nice algorithm for splitting up a single string, since it # doesn't require character-by-character examination. It was a little # bit of a brain-bender to get it working right, though... - if _wordchars_re is None: _init_regex() + if _wordchars_re is None: + _init_regex() s = s.strip() words = [] @@ -237,8 +250,8 @@ def split_quoted(s): elif s[end] == '\\': # preserve whatever is being escaped; # will become part of the current word - s = s[:end] + s[end+1:] - pos = end+1 + s = s[:end] + s[end + 1:] + pos = end + 1 else: if s[end] == "'": # slurp singly-quoted string @@ -253,7 +266,7 @@ def split_quoted(s): raise ValueError("bad string (mismatched %s quotes?)" % s[end]) (beg, end) = m.span() - s = s[:beg] + s[beg+1:end-1] + s[end:] + s = s[:beg] + s[beg + 1:end - 1] + s[end:] pos = m.end() - 2 if pos >= len(s): @@ -296,7 +309,7 @@ def strtobool(val): elif val in ('n', 'no', 'f', 'false', 'off', '0'): return 0 else: - raise ValueError, "invalid truth value %r" % (val,) + raise ValueError("invalid truth value %r" % (val,)) def byte_compile(py_files, optimize=0, force=0, prefix=None, base_dir=None, @@ -350,12 +363,8 @@ def byte_compile(py_files, optimize=0, force=0, prefix=None, base_dir=None, # "Indirect" byte-compilation: write a temporary script and then # run it with the appropriate flags. if not direct: - try: - from tempfile import mkstemp - (script_fd, script_name) = mkstemp(".py") - except ImportError: - from tempfile import mktemp - (script_fd, script_name) = None, mktemp(".py") + from tempfile import mkstemp + script_fd, script_name = mkstemp(".py") log.info("writing byte-compilation script '%s'", script_name) if not dry_run: if script_fd is not None: @@ -363,43 +372,50 @@ def byte_compile(py_files, optimize=0, force=0, prefix=None, base_dir=None, else: script = open(script_name, "w") - script.write("""\ -from distutils.util import byte_compile + try: + script.write("""\ +from distutils2.util import byte_compile files = [ """) - # XXX would be nice to write absolute filenames, just for - # safety's sake (script should be more robust in the face of - # chdir'ing before running it). But this requires abspath'ing - # 'prefix' as well, and that breaks the hack in build_lib's - # 'byte_compile()' method that carefully tacks on a trailing - # slash (os.sep really) to make sure the prefix here is "just - # right". This whole prefix business is rather delicate -- the - # problem is that it's really a directory, but I'm treating it - # as a dumb string, so trailing slashes and so forth matter. - - #py_files = map(os.path.abspath, py_files) - #if prefix: - # prefix = os.path.abspath(prefix) - - script.write(",\n".join(map(repr, py_files)) + "]\n") - script.write(""" + # XXX would be nice to write absolute filenames, just for + # safety's sake (script should be more robust in the face of + # chdir'ing before running it). But this requires abspath'ing + # 'prefix' as well, and that breaks the hack in build_lib's + # 'byte_compile()' method that carefully tacks on a trailing + # slash (os.sep really) to make sure the prefix here is "just + # right". This whole prefix business is rather delicate -- the + # problem is that it's really a directory, but I'm treating it + # as a dumb string, so trailing slashes and so forth matter. + + #py_files = map(os.path.abspath, py_files) + #if prefix: + # prefix = os.path.abspath(prefix) + + script.write(",\n".join(map(repr, py_files)) + "]\n") + script.write(""" byte_compile(files, optimize=%r, force=%r, prefix=%r, base_dir=%r, verbose=%r, dry_run=0, direct=1) """ % (optimize, force, prefix, base_dir, verbose)) - script.close() + finally: + script.close() cmd = [sys.executable, script_name] if optimize == 1: cmd.insert(1, "-O") elif optimize == 2: cmd.insert(1, "-OO") - spawn(cmd, dry_run=dry_run) - execute(os.remove, (script_name,), "removing %s" % script_name, - dry_run=dry_run) + + env = copy(os.environ) + env['PYTHONPATH'] = ':'.join(sys.path) + try: + spawn(cmd, dry_run=dry_run, env=env) + finally: + execute(os.remove, (script_name,), "removing %s" % script_name, + dry_run=dry_run) # "Direct" byte-compilation: use the py_compile module to compile # right here, right now. Note that the script generated in indirect @@ -447,7 +463,9 @@ def rfc822_escape(header): return sep.join(lines) _RE_VERSION = re.compile('(\d+\.\d+(\.\d+)*)') -_MAC_OS_X_LD_VERSION = re.compile('^@\(#\)PROGRAM:ld PROJECT:ld64-((\d+)(\.\d+)*)') +_MAC_OS_X_LD_VERSION = re.compile('^@\(#\)PROGRAM:ld ' + 'PROJECT:ld64-((\d+)(\.\d+)*)') + def _find_ld_version(): """Finds the ld version. The version scheme differs under Mac OSX.""" @@ -456,6 +474,7 @@ def _find_ld_version(): else: return _find_exe_version('ld -v') + def _find_exe_version(cmd, pattern=_RE_VERSION): """Find the version of an executable by running `cmd` in the shell. @@ -485,6 +504,7 @@ def _find_exe_version(cmd, pattern=_RE_VERSION): return None return result.group(1) + def get_compiler_versions(): """Returns a tuple providing the versions of gcc, ld and dllwrap @@ -496,6 +516,7 @@ def get_compiler_versions(): dllwrap = _find_exe_version('dllwrap --version') return gcc, ld, dllwrap + def newer_group(sources, target, missing='error'): """Return true if 'target' is out-of-date with respect to any file listed in 'sources'. @@ -535,14 +556,18 @@ def newer_group(sources, target, missing='error'): return False + def write_file(filename, contents): """Create a file with the specified name and write 'contents' (a sequence of strings without line terminators) to it. """ - f = open(filename, "w") - for line in contents: - f.write(line + "\n") - f.close() + try: + f = open(filename, "w") + for line in contents: + f.write(line + "\n") + finally: + f.close() + def _is_package(path): """Returns True if path is a package (a dir with an __init__ file.""" @@ -550,6 +575,7 @@ def _is_package(path): return False return os.path.isfile(os.path.join(path, '__init__.py')) + def _under(path, root): path = path.split(os.sep) root = root.split(os.sep) @@ -560,12 +586,14 @@ def _under(path, root): return False return True + def _package_name(root_path, path): """Returns a dotted package name, given a subpath.""" if not _under(path, root_path): raise ValueError('"%s" is not a subpath of "%s"' % (path, root_path)) return path[len(root_path) + 1:].replace(os.sep, '.') + def find_packages(paths=('.',), exclude=()): """Return a list all Python packages found recursively within directories 'paths' @@ -580,6 +608,7 @@ def find_packages(paths=('.',), exclude=()): """ packages = [] discarded = [] + def _discarded(path): for discard in discarded: if _under(path, discard): @@ -611,3 +640,232 @@ def find_packages(paths=('.',), exclude=()): packages.append(package_name) return packages + +# utility functions for 2to3 support + +def run_2to3(files, doctests_only=False, fixer_names=None, options=None, + explicit=None): + """ Wrapper function around the refactor() class which + performs the conversions on a list of python files. + Invoke 2to3 on a list of Python files. The files should all come + from the build area, as the modification is done in-place.""" + + if not files: + return + + # Make this class local, to delay import of 2to3 + from lib2to3.refactor import get_fixers_from_package + from distutils2.converter.refactor import DistutilsRefactoringTool + + if fixer_names is None: + fixer_names = get_fixers_from_package('lib2to3.fixes') + + r = DistutilsRefactoringTool(fixer_names, options=options) + if doctests_only: + r.refactor(files, doctests_only=True, write=True) + else: + r.refactor(files, write=True) + + +class Mixin2to3: + """ Wrapper class for commands that run 2to3. + To configure 2to3, setup scripts may either change + the class variables, or inherit from this class + to override how 2to3 is invoked. + """ + # provide list of fixers to run. + # defaults to all from lib2to3.fixers + fixer_names = None + + # options dictionary + options = None + + # list of fixers to invoke even though they are marked as explicit + explicit = None + + def run_2to3(self, files, doctests_only=False): + """ Issues a call to util.run_2to3. """ + return run_2to3(files, doctests_only, self.fixer_names, + self.options, self.explicit) + + +def spawn(cmd, search_path=1, verbose=0, dry_run=0, env=None): + """Run another program specified as a command list 'cmd' in a new process. + + 'cmd' is just the argument list for the new process, ie. + cmd[0] is the program to run and cmd[1:] are the rest of its arguments. + There is no way to run a program with a name different from that of its + executable. + + If 'search_path' is true (the default), the system's executable + search path will be used to find the program; otherwise, cmd[0] + must be the exact path to the executable. If 'dry_run' is true, + the command will not actually be run. + + If 'env' is given, it's a environment dictionary used for the execution + environment. + + Raise DistutilsExecError if running the program fails in any way; just + return on success. + """ + if os.name == 'posix': + _spawn_posix(cmd, search_path, dry_run=dry_run, env=env) + elif os.name == 'nt': + _spawn_nt(cmd, search_path, dry_run=dry_run, env=env) + elif os.name == 'os2': + _spawn_os2(cmd, search_path, dry_run=dry_run, env=env) + else: + raise DistutilsPlatformError( + "don't know how to spawn programs on platform '%s'" % os.name) + + +def _nt_quote_args(args): + """Quote command-line arguments for DOS/Windows conventions. + + Just wraps every argument which contains blanks in double quotes, and + returns a new argument list. + """ + # XXX this doesn't seem very robust to me -- but if the Windows guys + # say it'll work, I guess I'll have to accept it. (What if an arg + # contains quotes? What other magic characters, other than spaces, + # have to be escaped? Is there an escaping mechanism other than + # quoting?) + for i, arg in enumerate(args): + if ' ' in arg: + args[i] = '"%s"' % arg + return args + + +def _spawn_nt(cmd, search_path=1, verbose=0, dry_run=0, env=None): + executable = cmd[0] + cmd = _nt_quote_args(cmd) + if search_path: + # either we find one or it stays the same + executable = find_executable(executable) or executable + log.info(' '.join([executable] + cmd[1:])) + if not dry_run: + # spawn for NT requires a full path to the .exe + try: + if env is None: + rc = os.spawnv(os.P_WAIT, executable, cmd) + else: + rc = os.spawnve(os.P_WAIT, executable, cmd, env) + + except OSError, exc: + # this seems to happen when the command isn't found + raise DistutilsExecError( + "command '%s' failed: %s" % (cmd[0], exc[-1])) + if rc != 0: + # and this reflects the command running but failing + raise DistutilsExecError( + "command '%s' failed with exit status %d" % (cmd[0], rc)) + + +def _spawn_os2(cmd, search_path=1, verbose=0, dry_run=0, env=None): + executable = cmd[0] + if search_path: + # either we find one or it stays the same + executable = find_executable(executable) or executable + log.info(' '.join([executable] + cmd[1:])) + if not dry_run: + # spawnv for OS/2 EMX requires a full path to the .exe + try: + if env is None: + rc = os.spawnv(os.P_WAIT, executable, cmd) + else: + rc = os.spawnve(os.P_WAIT, executable, cmd, env) + + except OSError, exc: + # this seems to happen when the command isn't found + raise DistutilsExecError( + "command '%s' failed: %s" % (cmd[0], exc[-1])) + if rc != 0: + # and this reflects the command running but failing + log.debug("command '%s' failed with exit status %d" % (cmd[0], rc)) + raise DistutilsExecError( + "command '%s' failed with exit status %d" % (cmd[0], rc)) + + +def _spawn_posix(cmd, search_path=1, verbose=0, dry_run=0, env=None): + log.info(' '.join(cmd)) + if dry_run: + return + + if env is None: + exec_fn = search_path and os.execvp or os.execv + else: + exec_fn = search_path and os.execvpe or os.execve + + pid = os.fork() + + if pid == 0: # in the child + try: + if env is None: + exec_fn(cmd[0], cmd) + else: + exec_fn(cmd[0], cmd, env) + except OSError, e: + sys.stderr.write("unable to execute %s: %s\n" % + (cmd[0], e.strerror)) + os._exit(1) + + sys.stderr.write("unable to execute %s for unknown reasons" % cmd[0]) + os._exit(1) + else: # in the parent + # Loop until the child either exits or is terminated by a signal + # (ie. keep waiting if it's merely stopped) + while 1: + try: + pid, status = os.waitpid(pid, 0) + except OSError, exc: + import errno + if exc.errno == errno.EINTR: + continue + raise DistutilsExecError( + "command '%s' failed: %s" % (cmd[0], exc[-1])) + if os.WIFSIGNALED(status): + raise DistutilsExecError( + "command '%s' terminated by signal %d" % \ + (cmd[0], os.WTERMSIG(status))) + + elif os.WIFEXITED(status): + exit_status = os.WEXITSTATUS(status) + if exit_status == 0: + return # hey, it succeeded! + else: + raise DistutilsExecError( + "command '%s' failed with exit status %d" % \ + (cmd[0], exit_status)) + + elif os.WIFSTOPPED(status): + continue + + else: + raise DistutilsExecError( + "unknown error executing '%s': termination status %d" % \ + (cmd[0], status)) + + +def find_executable(executable, path=None): + """Tries to find 'executable' in the directories listed in 'path'. + + A string listing directories separated by 'os.pathsep'; defaults to + os.environ['PATH']. Returns the complete filename or None if not found. + """ + if path is None: + path = os.environ['PATH'] + paths = path.split(os.pathsep) + base, ext = os.path.splitext(executable) + + if (sys.platform == 'win32' or os.name == 'os2') and (ext != '.exe'): + executable = executable + '.exe' + + if not os.path.isfile(executable): + for p in paths: + f = os.path.join(p, executable) + if os.path.isfile(f): + # the file exists, we have a shot at spawn working + return f + return None + else: + return executable diff --git a/src/distutils2/version.py b/src/distutils2/version.py index dcf568b..e61c60e 100644 --- a/src/distutils2/version.py +++ b/src/distutils2/version.py @@ -1,8 +1,11 @@ -import sys import re from distutils2.errors import IrrationalVersionError, HugeMajorVersionNumError +__all__ = ['NormalizedVersion', 'suggest_normalized_version', + 'VersionPredicate', 'is_valid_version', 'is_valid_versions', + 'is_valid_predicate'] + # A marker used in the second and third parts of the `parts` tuple, for # versions that don't have those segments, to sort properly. An example # of versions in sort order ('highest' last): @@ -18,9 +21,9 @@ from distutils2.errors import IrrationalVersionError, HugeMajorVersionNumError # | # 'dev' < 'f' ----------------------------------------------/ # Other letters would do, but 'f' for 'final' is kind of nice. -FINAL_MARKER = ('f',) +_FINAL_MARKER = ('f',) -VERSION_RE = re.compile(r''' +_VERSION_RE = re.compile(r''' ^ (?P<version>\d+\.\d+) # minimum 'N.N' (?P<extraversion>(?:\.\d+)*) # any number of extra '.N' segments @@ -32,6 +35,7 @@ VERSION_RE = re.compile(r''' (?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)? $''', re.VERBOSE) + class NormalizedVersion(object): """A rational version. @@ -57,7 +61,8 @@ class NormalizedVersion(object): @param error_on_huge_major_num {bool} Whether to consider an apparent use of a year or full date as the major version number an error. Default True. One of the observed patterns on PyPI before - the introduction of `NormalizedVersion` was version numbers like this: + the introduction of `NormalizedVersion` was version numbers like + this: 2009.01.03 20040603 2005.01 @@ -67,16 +72,17 @@ class NormalizedVersion(object): the possibility of using a version number like "1.0" (i.e. where the major number is less than that huge major number). """ + self.is_final = True # by default, consider a version as final. self._parse(s, error_on_huge_major_num) @classmethod - def from_parts(cls, version, prerelease=FINAL_MARKER, - devpost=FINAL_MARKER): + def from_parts(cls, version, prerelease=_FINAL_MARKER, + devpost=_FINAL_MARKER): return cls(cls.parts_to_str((version, prerelease, devpost))) def _parse(self, s, error_on_huge_major_num=True): """Parses a string version into parts.""" - match = VERSION_RE.search(s) + match = _VERSION_RE.search(s) if not match: raise IrrationalVersionError(s) @@ -97,8 +103,9 @@ class NormalizedVersion(object): block += self._parse_numdots(groups.get('prerelversion'), s, pad_zeros_length=1) parts.append(tuple(block)) + self.is_final = False else: - parts.append(FINAL_MARKER) + parts.append(_FINAL_MARKER) # postdev if groups.get('postdev'): @@ -106,14 +113,15 @@ class NormalizedVersion(object): dev = groups.get('dev') postdev = [] if post is not None: - postdev.extend([FINAL_MARKER[0], 'post', int(post)]) + postdev.extend([_FINAL_MARKER[0], 'post', int(post)]) if dev is None: - postdev.append(FINAL_MARKER[0]) + postdev.append(_FINAL_MARKER[0]) if dev is not None: postdev.extend(['dev', int(dev)]) + self.is_final = False parts.append(tuple(postdev)) else: - parts.append(FINAL_MARKER) + parts.append(_FINAL_MARKER) self.parts = tuple(parts) if error_on_huge_major_num and self.parts[0][0] > 1980: raise HugeMajorVersionNumError("huge major version number, %r, " @@ -154,10 +162,10 @@ class NormalizedVersion(object): # XXX This doesn't check for invalid tuples main, prerel, postdev = parts s = '.'.join(str(v) for v in main) - if prerel is not FINAL_MARKER: + if prerel is not _FINAL_MARKER: s += prerel[0] s += '.'.join(str(v) for v in prerel[1:]) - if postdev and postdev is not FINAL_MARKER: + if postdev and postdev is not _FINAL_MARKER: if postdev[0] == 'f': postdev = postdev[1:] i = 0 @@ -200,6 +208,7 @@ class NormalizedVersion(object): # See http://docs.python.org/reference/datamodel#object.__hash__ __hash__ = object.__hash__ + def suggest_normalized_version(s): """Suggest a normalized version close to the given version string. @@ -211,7 +220,7 @@ def suggest_normalized_version(s): on observation of versions currently in use on PyPI. Given a dump of those version during PyCon 2009, 4287 of them: - 2312 (53.93%) match NormalizedVersion without change - - with the automatic suggestion + with the automatic suggestion - 3474 (81.04%) match when using this suggestion method @param s {str} An irrational version string. @@ -301,7 +310,6 @@ def suggest_normalized_version(s): # PyPI stats: ~21 (0.62%) better rs = re.sub(r"\.?(pre|preview|-c)(\d+)$", r"c\g<2>", rs) - # Tcl/Tk uses "px" for their post release markers rs = re.sub(r"p(\d+)$", r".post\1", rs) @@ -318,6 +326,7 @@ _VERSIONS = re.compile(r"^\s*\((.*)\)\s*$") _PLAIN_VERSIONS = re.compile(r"^\s*(.*)\s*$") _SPLIT_CMP = re.compile(r"^\s*(<=|>=|<|>|!=|==)\s*([^\s,]+)\s*$") + def _split_predicate(predicate): match = _SPLIT_CMP.match(predicate) if match is None: @@ -364,26 +373,25 @@ class VersionPredicate(object): return False return True -class Versions(VersionPredicate): + +class _Versions(VersionPredicate): def __init__(self, predicate): predicate = predicate.strip() match = _PLAIN_VERSIONS.match(predicate) - if match is None: - raise ValueError('Bad predicate "%s"' % predicate) self.name = None predicates = match.groups()[0] self.predicates = [_split_predicate(pred.strip()) for pred in predicates.split(',')] -class Version(VersionPredicate): + +class _Version(VersionPredicate): def __init__(self, predicate): predicate = predicate.strip() match = _PLAIN_VERSIONS.match(predicate) - if match is None: - raise ValueError('Bad predicate "%s"' % predicate) self.name = None self.predicates = _split_predicate(match.groups()[0]) + def is_valid_predicate(predicate): try: VersionPredicate(predicate) @@ -392,19 +400,20 @@ def is_valid_predicate(predicate): else: return True + def is_valid_versions(predicate): try: - Versions(predicate) + _Versions(predicate) except (ValueError, IrrationalVersionError): return False else: return True + def is_valid_version(predicate): try: - Version(predicate) + _Version(predicate) except (ValueError, IrrationalVersionError): return False else: return True - diff --git a/src/runtests-cov.py b/src/runtests-cov.py new file mode 100755 index 0000000..6c2ab41 --- /dev/null +++ b/src/runtests-cov.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python +"""Tests for distutils2. + +The tests for distutils2 are defined in the distutils2.tests package. +""" + +import sys +from os.path import dirname, islink, realpath +from optparse import OptionParser + +def ignore_prefixes(module): + """ Return a list of prefixes to ignore in the coverage report if + we want to completely skip `module`. + """ + # A function like that is needed because some GNU/Linux + # distributions, such a Ubuntu, really like to build link farm in + # /usr/lib in order to save a few bytes on the disk. + dirnames = [dirname(module.__file__)] + + pymod = module.__file__.rstrip("c") + if islink(pymod): + dirnames.append(dirname(realpath(pymod))) + return dirnames + +def parse_opts(): + parser = OptionParser(usage="%prog [OPTIONS]", + description="run the distutils2 unittests") + + parser.add_option("-q", "--quiet", help="do not print verbose messages", + action="store_true", default=False) + parser.add_option("-c", "--coverage", action="store_true", default=False, + help="produce a coverage report at the end of the run") + parser.add_option("-r", "--report", action="store_true", default=False, + help="produce a coverage report from the last test run") + parser.add_option("-m", "--show-missing", action="store_true", + default=False, + help=("Show line numbers of statements in each module " + "that weren't executed.")) + + opts, args = parser.parse_args() + return opts, args + +def coverage_report(opts): + import coverage + from distutils2.tests.support import unittest + cov = coverage.coverage() + cov.load() + + prefixes = ["runtests", "distutils2/tests", "distutils2/_backport"] + prefixes += ignore_prefixes(unittest) + + try: + import docutils + prefixes += ignore_prefixes(docutils) + except ImportError: + # that module is completely optional + pass + + try: + import roman + prefixes += ignore_prefixes(roman) + except ImportError: + # that module is also completely optional + pass + + cov.report(omit_prefixes=prefixes, show_missing=opts.show_missing) + + +def test_main(): + opts, args = parse_opts() + verbose = not opts.quiet + ret = 0 + + if opts.coverage or opts.report: + import coverage + + if opts.coverage: + cov = coverage.coverage() + cov.erase() + cov.start() + if not opts.report: + ret = run_tests(verbose) + if opts.coverage: + cov.stop() + cov.save() + + if opts.report or opts.coverage: + coverage_report(opts) + + return ret + +def run_tests(verbose): + import distutils2.tests + from distutils2.tests import run_unittest, reap_children, TestFailed + from distutils2._backport.tests import test_suite as btest_suite + # XXX just supporting -q right now to enable detailed/quiet output + if len(sys.argv) > 1: + verbose = sys.argv[-1] != '-q' + else: + verbose = 1 + try: + try: + run_unittest([distutils2.tests.test_suite(), btest_suite()], + verbose_=verbose) + return 0 + except TestFailed: + return 1 + finally: + reap_children() + +if __name__ == "__main__": + try: + from distutils2.tests.support import unittest + except ImportError: + sys.stderr.write('Error: You have to install unittest2') + sys.exit(1) + + sys.exit(test_main()) + diff --git a/src/runtests.py b/src/runtests.py index 99e3559..b9e2477 100644 --- a/src/runtests.py +++ b/src/runtests.py @@ -1,6 +1,6 @@ """Tests for distutils2. -The tests for distutils2 are defined in the distutils2.tests package; +The tests for distutils2 are defined in the distutils2.tests package. """ import sys @@ -8,8 +8,7 @@ def test_main(): import distutils2.tests from distutils2.tests import run_unittest, reap_children, TestFailed from distutils2._backport.tests import test_suite as btest_suite - # just supporting -q right now - # to enable detailed/quiet output + # XXX just supporting -q right now to enable detailed/quiet output if len(sys.argv) > 1: verbose = sys.argv[-1] != '-q' else: @@ -17,7 +16,7 @@ def test_main(): try: try: run_unittest([distutils2.tests.test_suite(), btest_suite()], - verbose_=verbose) + verbose_=verbose) return 0 except TestFailed: return 1 @@ -28,7 +27,7 @@ if __name__ == "__main__": try: from distutils2.tests.support import unittest except ImportError: - print('Error: You have to install unittest2') + sys.stderr.write('Error: You have to install unittest2') sys.exit(1) sys.exit(test_main()) diff --git a/src/setup.py b/src/setup.py index fa723fd..5345b37 100644 --- a/src/setup.py +++ b/src/setup.py @@ -3,11 +3,14 @@ __revision__ = "$Id$" import sys import os +import re -from distutils2.core import setup +from distutils2 import __version__ as VERSION +from distutils2 import log +from distutils2.core import setup, Extension +from distutils2.compiler.ccompiler import new_compiler from distutils2.command.sdist import sdist from distutils2.command.install import install -from distutils2 import __version__ as VERSION from distutils2.util import find_packages f = open('README.txt') @@ -31,6 +34,7 @@ def get_tip_revision(path=os.getcwd()): DEV_SUFFIX = '.dev%d' % get_tip_revision('..') + class install_hg(install): user_options = install.user_options + [ @@ -62,10 +66,141 @@ class sdist_hg(sdist): self.distribution.metadata.version += DEV_SUFFIX sdist.run(self) + +# additional paths to check, set from the command line +SSL_INCDIR = '' # --openssl-incdir= +SSL_LIBDIR = '' # --openssl-libdir= +SSL_DIR = '' # --openssl-prefix= + +def add_dir_to_list(dirlist, dir): + """Add the directory 'dir' to the list 'dirlist' (at the front) if + 'dir' actually exists and is a directory. If 'dir' is already in + 'dirlist' it is moved to the front. + """ + if dir is not None and os.path.isdir(dir) and dir not in dirlist: + if dir in dirlist: + dirlist.remove(dir) + dirlist.insert(0, dir) + + +def prepare_hashlib_extensions(): + """Decide which C extensions to build and create the appropriate + Extension objects to build them. Return a list of Extensions. + """ + # this CCompiler object is only used to locate include files + compiler = new_compiler() + + # Ensure that these paths are always checked + if os.name == 'posix': + add_dir_to_list(compiler.library_dirs, '/usr/local/lib') + add_dir_to_list(compiler.include_dirs, '/usr/local/include') + + add_dir_to_list(compiler.library_dirs, '/usr/local/ssl/lib') + add_dir_to_list(compiler.include_dirs, '/usr/local/ssl/include') + + add_dir_to_list(compiler.library_dirs, '/usr/contrib/ssl/lib') + add_dir_to_list(compiler.include_dirs, '/usr/contrib/ssl/include') + + add_dir_to_list(compiler.library_dirs, '/usr/lib') + add_dir_to_list(compiler.include_dirs, '/usr/include') + + # look in command line supplied paths + if SSL_LIBDIR: + add_dir_to_list(compiler.library_dirs, SSL_LIBDIR) + if SSL_INCDIR: + add_dir_to_list(compiler.include_dirs, SSL_INCDIR) + if SSL_DIR: + if os.name == 'nt': + add_dir_to_list(compiler.library_dirs, os.path.join(SSL_DIR, 'out32dll')) + # prefer the static library + add_dir_to_list(compiler.library_dirs, os.path.join(SSL_DIR, 'out32')) + else: + add_dir_to_list(compiler.library_dirs, os.path.join(SSL_DIR, 'lib')) + add_dir_to_list(compiler.include_dirs, os.path.join(SSL_DIR, 'include')) + + oslibs = {'posix': ['ssl', 'crypto'], + 'nt': ['libeay32', 'gdi32', 'advapi32', 'user32']} + + if os.name not in oslibs: + sys.stderr.write( + 'unknown operating system, impossible to compile _hashlib') + sys.exit(1) + + exts = [] + + ssl_inc_dirs = [] + ssl_incs = [] + for inc_dir in compiler.include_dirs: + f = os.path.join(inc_dir, 'openssl', 'ssl.h') + if os.path.exists(f): + ssl_incs.append(f) + ssl_inc_dirs.append(inc_dir) + + ssl_lib = compiler.find_library_file(compiler.library_dirs, oslibs[os.name][0]) + + # find out which version of OpenSSL we have + openssl_ver = 0 + openssl_ver_re = re.compile( + '^\s*#\s*define\s+OPENSSL_VERSION_NUMBER\s+(0x[0-9a-fA-F]+)' ) + ssl_inc_dir = '' + for ssl_inc_dir in ssl_inc_dirs: + name = os.path.join(ssl_inc_dir, 'openssl', 'opensslv.h') + if os.path.isfile(name): + try: + incfile = open(name, 'r') + for line in incfile: + m = openssl_ver_re.match(line) + if m: + openssl_ver = int(m.group(1), 16) + break + except IOError: + pass + + # first version found is what we'll use + if openssl_ver: + break + + if (ssl_inc_dir and ssl_lib is not None and openssl_ver >= 0x00907000): + + log.info('Using OpenSSL version 0x%08x from', openssl_ver) + log.info(' Headers:\t%s', ssl_inc_dir) + log.info(' Library:\t%s', ssl_lib) + + # The _hashlib module wraps optimized implementations + # of hash functions from the OpenSSL library. + exts.append(Extension('_hashlib', ['_hashopenssl.c'], + include_dirs = [ssl_inc_dir], + library_dirs = [os.path.dirname(ssl_lib)], + libraries = oslibs[os.name])) + else: + exts.append(Extension('_sha', ['shamodule.c']) ) + exts.append(Extension('_md5', + sources=['md5module.c', 'md5.c'], + depends=['md5.h']) ) + + if (not ssl_lib or openssl_ver < 0x00908000): + # OpenSSL doesn't do these until 0.9.8 so we'll bring our own + exts.append(Extension('_sha256', ['sha256module.c'])) + exts.append(Extension('_sha512', ['sha512module.c'])) + + def prepend_modules(filename): + return os.path.join('Modules', filename) + + # all the C code is in the Modules subdirectory, prepend the path + for ext in exts: + ext.sources = [prepend_modules(fn) for fn in ext.sources] + if hasattr(ext, 'depends') and ext.depends is not None: + ext.depends = [prepend_modules(fn) for fn in ext.depends] + + return exts + setup_kwargs = {} if sys.version < '2.6': setup_kwargs['scripts'] = ['distutils2/mkpkg.py'] +if sys.version < '2.5': + setup_kwargs['ext_modules'] = prepare_hashlib_extensions() + _CLASSIFIERS = """\ Development Status :: 3 - Alpha Intended Audience :: Developers @@ -77,26 +212,23 @@ Topic :: System :: Archiving :: Packaging Topic :: System :: Systems Administration Topic :: Utilities""" -setup (name="Distutils2", - version=VERSION, - summary="Python Distribution Utilities", - keywords=['packaging', 'distutils'], - author="Tarek Ziade", - author_email="tarek@ziade.org", - home_page="http://bitbucket.org/tarek/distutils2/wiki/Home", - license="PSF", - description=README, - classifier=_CLASSIFIERS.split('\n'), - packages=find_packages(), - cmdclass={'sdist': sdist_hg, 'install': install_hg}, - package_data={'distutils2._backport': ['sysconfig.cfg']}, - project_url=[('Mailing-list', +setup(name="Distutils2", + version=VERSION, + summary="Python Distribution Utilities", + keywords=['packaging', 'distutils'], + author="Tarek Ziade", + author_email="tarek@ziade.org", + home_page="http://bitbucket.org/tarek/distutils2/wiki/Home", + license="PSF", + description=README, + classifier=_CLASSIFIERS.split('\n'), + packages=find_packages(), + cmdclass={'sdist': sdist_hg, 'install': install_hg}, + package_data={'distutils2._backport': ['sysconfig.cfg']}, + project_url=[('Mailing list', 'http://mail.python.org/mailman/listinfo/distutils-sig/'), - ('Documentation', - 'http://packages.python.org/Distutils2'), - ('Repository', 'http://hg.python.org/distutils2'), - ('Bug tracker', 'http://bugs.python.org')], - **setup_kwargs - ) - - + ('Documentation', + 'http://packages.python.org/Distutils2'), + ('Repository', 'http://hg.python.org/distutils2'), + ('Bug tracker', 'http://bugs.python.org')], + **setup_kwargs) diff --git a/src/tests.sh b/src/tests.sh index 9c2627a..fae8fb0 100755 --- a/src/tests.sh +++ b/src/tests.sh @@ -1,28 +1,41 @@ #!/bin/sh -echo -n "Running tests for Python 2.4..." -python2.4 runtests.py -q > /dev/null 2> /dev/null +echo -n "Running tests for Python 2.4... " +rm -rf *.so +python2.4 setup.py build_ext -i -q 2> /dev/null > /dev/null +python2.4 -Wd runtests.py -q 2> /dev/null +rm -rf *.so if [ $? -ne 0 ];then echo "Failed" - exit $1 + exit 1 else echo "Success" fi -echo -n "Running tests for Python 2.5..." -python2.5 runtests.py -q > /dev/null 2> /dev/null +echo -n "Running tests for Python 2.5... " +python2.5 setup.py build_ext -i -q 2> /dev/null > /dev/null +python2.5 -Wd runtests.py -q 2> /dev/null +rm -rf *.so if [ $? -ne 0 ];then echo "Failed" - exit $1 + exit 1 else echo "Success" fi -echo -n "Running tests for Python 2.6..." -python2.6 runtests.py -q > /dev/null 2> /dev/null +echo -n "Running tests for Python 2.6... " +python2.6 -Wd -bb -3 runtests.py -q 2> /dev/null if [ $? -ne 0 ];then echo "Failed" - exit $1 + exit 1 else echo "Success" fi +echo -n "Running tests for Python 2.7... " +python2.7 -Wd -bb -3 runtests.py -q 2> /dev/null +if [ $? -ne 0 ];then + echo "Failed" + exit 1 +else + echo "Success" +fi |
