From fce67fcd84f7ab66dde11646eead6378698c04b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Araujo?= Date: Sat, 10 Sep 2011 01:34:44 +0200 Subject: [PATCH 01/10] Slight cleanup in distutils test_dist. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I have tests to add in this file and it’s always nice to start from a clean base. --- Lib/distutils/tests/test_dist.py | 98 +++++++++++++++----------------- 1 file changed, 47 insertions(+), 51 deletions(-) diff --git a/Lib/distutils/tests/test_dist.py b/Lib/distutils/tests/test_dist.py index a20d6c8ffcc..7050cbed260 100644 --- a/Lib/distutils/tests/test_dist.py +++ b/Lib/distutils/tests/test_dist.py @@ -74,7 +74,7 @@ def test_command_packages_cmdline(self): self.assertEqual(d.get_command_packages(), ["distutils.command", "foo.bar", "distutils.tests"]) cmd = d.get_command_obj("test_dist") - self.assertTrue(isinstance(cmd, test_dist)) + self.assertIsInstance(cmd, test_dist) self.assertEqual(cmd.sample_option, "sometext") def test_command_packages_configfile(self): @@ -106,28 +106,23 @@ def test_command_packages_configfile(self): def test_empty_options(self): # an empty options dictionary should not stay in the # list of attributes - klass = Distribution # catching warnings warns = [] + def _warn(msg): warns.append(msg) - old_warn = warnings.warn + self.addCleanup(setattr, warnings, 'warn', warnings.warn) warnings.warn = _warn - try: - dist = klass(attrs={'author': 'xxx', - 'name': 'xxx', - 'version': 'xxx', - 'url': 'xxxx', - 'options': {}}) - finally: - warnings.warn = old_warn + dist = Distribution(attrs={'author': 'xxx', 'name': 'xxx', + 'version': 'xxx', 'url': 'xxxx', + 'options': {}}) self.assertEqual(len(warns), 0) + self.assertNotIn('options', dir(dist)) def test_finalize_options(self): - attrs = {'keywords': 'one,two', 'platforms': 'one,two'} @@ -150,7 +145,6 @@ def test_get_command_packages(self): cmds = dist.get_command_packages() self.assertEqual(cmds, ['distutils.command', 'one', 'two']) - def test_announce(self): # make sure the level is known dist = Distribution() @@ -158,6 +152,7 @@ def test_announce(self): kwargs = {'level': 'ok2'} self.assertRaises(ValueError, dist.announce, args, kwargs) + class MetadataTestCase(support.TempdirManager, support.EnvironGuard, unittest.TestCase): @@ -170,15 +165,20 @@ def tearDown(self): sys.argv[:] = self.argv[1] super(MetadataTestCase, self).tearDown() + def format_metadata(self, dist): + sio = io.StringIO() + dist.metadata.write_pkg_file(sio) + return sio.getvalue() + def test_simple_metadata(self): attrs = {"name": "package", "version": "1.0"} dist = Distribution(attrs) meta = self.format_metadata(dist) - self.assertTrue("Metadata-Version: 1.0" in meta) - self.assertTrue("provides:" not in meta.lower()) - self.assertTrue("requires:" not in meta.lower()) - self.assertTrue("obsoletes:" not in meta.lower()) + self.assertIn("Metadata-Version: 1.0", meta) + self.assertNotIn("provides:", meta.lower()) + self.assertNotIn("requires:", meta.lower()) + self.assertNotIn("obsoletes:", meta.lower()) def test_provides(self): attrs = {"name": "package", @@ -190,9 +190,9 @@ def test_provides(self): self.assertEqual(dist.get_provides(), ["package", "package.sub"]) meta = self.format_metadata(dist) - self.assertTrue("Metadata-Version: 1.1" in meta) - self.assertTrue("requires:" not in meta.lower()) - self.assertTrue("obsoletes:" not in meta.lower()) + self.assertIn("Metadata-Version: 1.1", meta) + self.assertNotIn("requires:", meta.lower()) + self.assertNotIn("obsoletes:", meta.lower()) def test_provides_illegal(self): self.assertRaises(ValueError, Distribution, @@ -210,11 +210,11 @@ def test_requires(self): self.assertEqual(dist.get_requires(), ["other", "another (==1.0)"]) meta = self.format_metadata(dist) - self.assertTrue("Metadata-Version: 1.1" in meta) - self.assertTrue("provides:" not in meta.lower()) - self.assertTrue("Requires: other" in meta) - self.assertTrue("Requires: another (==1.0)" in meta) - self.assertTrue("obsoletes:" not in meta.lower()) + self.assertIn("Metadata-Version: 1.1", meta) + self.assertNotIn("provides:", meta.lower()) + self.assertIn("Requires: other", meta) + self.assertIn("Requires: another (==1.0)", meta) + self.assertNotIn("obsoletes:", meta.lower()) def test_requires_illegal(self): self.assertRaises(ValueError, Distribution, @@ -232,11 +232,11 @@ def test_obsoletes(self): self.assertEqual(dist.get_obsoletes(), ["other", "another (<1.0)"]) meta = self.format_metadata(dist) - self.assertTrue("Metadata-Version: 1.1" in meta) - self.assertTrue("provides:" not in meta.lower()) - self.assertTrue("requires:" not in meta.lower()) - self.assertTrue("Obsoletes: other" in meta) - self.assertTrue("Obsoletes: another (<1.0)" in meta) + self.assertIn("Metadata-Version: 1.1", meta) + self.assertNotIn("provides:", meta.lower()) + self.assertNotIn("requires:", meta.lower()) + self.assertIn("Obsoletes: other", meta) + self.assertIn("Obsoletes: another (<1.0)", meta) def test_obsoletes_illegal(self): self.assertRaises(ValueError, Distribution, @@ -244,10 +244,20 @@ def test_obsoletes_illegal(self): "version": "1.0", "obsoletes": ["my.pkg (splat)"]}) - def format_metadata(self, dist): - sio = io.StringIO() - dist.metadata.write_pkg_file(sio) - return sio.getvalue() + def test_long_description(self): + long_desc = textwrap.dedent("""\ + example:: + We start here + and continue here + and end here.""") + attrs = {"name": "package", + "version": "1.0", + "long_description": long_desc} + + dist = Distribution(attrs) + meta = self.format_metadata(dist) + meta = meta.replace('\n' + 8 * ' ', '\n') + self.assertIn(long_desc, meta) def test_custom_pydistutils(self): # fixes #2166 @@ -272,14 +282,14 @@ def test_custom_pydistutils(self): if sys.platform in ('linux', 'darwin'): os.environ['HOME'] = temp_dir files = dist.find_config_files() - self.assertTrue(user_filename in files) + self.assertIn(user_filename, files) # win32-style if sys.platform == 'win32': # home drive should be found os.environ['HOME'] = temp_dir files = dist.find_config_files() - self.assertTrue(user_filename in files, + self.assertIn(user_filename, files, '%r not found in %r' % (user_filename, files)) finally: os.remove(user_filename) @@ -301,22 +311,8 @@ def test_show_help(self): output = [line for line in s.getvalue().split('\n') if line.strip() != ''] - self.assertTrue(len(output) > 0) + self.assertTrue(output) - def test_long_description(self): - long_desc = textwrap.dedent("""\ - example:: - We start here - and continue here - and end here.""") - attrs = {"name": "package", - "version": "1.0", - "long_description": long_desc} - - dist = Distribution(attrs) - meta = self.format_metadata(dist) - meta = meta.replace('\n' + 8 * ' ', '\n') - self.assertTrue(long_desc in meta) def test_suite(): suite = unittest.TestSuite() From 13e8c8e7216ff52b92d9ba51125b939c021e26d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Araujo?= Date: Sat, 10 Sep 2011 01:51:40 +0200 Subject: [PATCH 02/10] =?UTF-8?q?Fix=20determination=20of=20Metadata=20ver?= =?UTF-8?q?sion=20(#8933).=20=20Patch=20by=20Filip=20Gruszczy=C5=84ski.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Lib/distutils/dist.py | 3 ++- Lib/distutils/tests/test_dist.py | 14 ++++++++++++++ Misc/NEWS | 4 ++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Lib/distutils/dist.py b/Lib/distutils/dist.py index 8ca5b6f4f12..69825f206f2 100644 --- a/Lib/distutils/dist.py +++ b/Lib/distutils/dist.py @@ -1018,7 +1018,8 @@ def write_pkg_file(self, file): """Write the PKG-INFO format data to a file object. """ version = '1.0' - if self.provides or self.requires or self.obsoletes: + if (self.provides or self.requires or self.obsoletes or + self.classifiers or self.download_url): version = '1.1' file.write('Metadata-Version: %s\n' % version) diff --git a/Lib/distutils/tests/test_dist.py b/Lib/distutils/tests/test_dist.py index 7050cbed260..8aaae88cae5 100644 --- a/Lib/distutils/tests/test_dist.py +++ b/Lib/distutils/tests/test_dist.py @@ -244,6 +244,20 @@ def test_obsoletes_illegal(self): "version": "1.0", "obsoletes": ["my.pkg (splat)"]}) + def test_classifier(self): + attrs = {'name': 'Boa', 'version': '3.0', + 'classifiers': ['Programming Language :: Python :: 3']} + dist = Distribution(attrs) + meta = self.format_metadata(dist) + self.assertIn('Metadata-Version: 1.1', meta) + + def test_download_url(self): + attrs = {'name': 'Boa', 'version': '3.0', + 'download_url': 'http://example.org/boa'} + dist = Distribution(attrs) + meta = self.format_metadata(dist) + self.assertIn('Metadata-Version: 1.1', meta) + def test_long_description(self): long_desc = textwrap.dedent("""\ example:: diff --git a/Misc/NEWS b/Misc/NEWS index 401e0225d5f..12ac26eb245 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -25,6 +25,10 @@ Core and Builtins Library ------- +- Issue #8933: distutils' PKG-INFO files will now correctly report + Metadata-Version: 1.1 instead of 1.0 if a Classifier or Download-URL field is + present. + - Issue #9561: distutils now reads and writes egg-info files using UTF-8, instead of the locale encoding. From 6bbd775377ae4a6e87ccce990750ac02afe83573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Araujo?= Date: Sat, 10 Sep 2011 05:18:20 +0200 Subject: [PATCH 03/10] Consolidate tests for packaging.metadata. New tests were added in test_metadata and old tests inherited from distutils were still in test_dist, so I moved them into test_metadata (except for one which was more at home in test_run) and merged duplicates. I also added some skips to lure contributors , optimized the Metadata.update method a trifle, and added notes about a number of issues. A note: The tests in test_dist used to dump the Metadata objects to a file in the METADATA format and look for strings in its contents; I updated them to use the mapping API of Metadata instead. For some fields with special writing rules, I have added tests to ensure my conversion did not lose anything. --- Lib/packaging/metadata.py | 14 +- Lib/packaging/tests/test_dist.py | 252 ++---------------- Lib/packaging/tests/test_metadata.py | 377 +++++++++++++++++++-------- Lib/packaging/tests/test_run.py | 23 +- 4 files changed, 324 insertions(+), 342 deletions(-) diff --git a/Lib/packaging/metadata.py b/Lib/packaging/metadata.py index 104600b54e9..7d7fc6b7754 100644 --- a/Lib/packaging/metadata.py +++ b/Lib/packaging/metadata.py @@ -354,11 +354,20 @@ def update(self, other=None, **kwargs): Keys that don't match a metadata field or that have an empty value are dropped. """ + # XXX the code should just use self.set, which does tbe same checks and + # conversions already, but that would break packaging.pypi: it uses the + # update method, which does not call _set_best_version (which set + # does), and thus allows having a Metadata object (as long as you don't + # modify or write it) with extra fields from PyPI that are not fields + # defined in Metadata PEPs. to solve it, the best_version system + # should be reworked so that it's called only for writing, or in a new + # strict mode, or with a new, more lax Metadata subclass in p7g.pypi def _set(key, value): if key in _ATTR2FIELD and value: self.set(self._convert_name(key), value) - if other is None: + if not other: + # other is None or empty container pass elif hasattr(other, 'keys'): for k in other.keys(): @@ -368,7 +377,8 @@ def _set(key, value): _set(k, v) if kwargs: - self.update(kwargs) + for k, v in kwargs.items(): + _set(k, v) def set(self, name, value): """Control then set a metadata field.""" diff --git a/Lib/packaging/tests/test_dist.py b/Lib/packaging/tests/test_dist.py index 1d78fa9b986..bd862450b2f 100644 --- a/Lib/packaging/tests/test_dist.py +++ b/Lib/packaging/tests/test_dist.py @@ -1,16 +1,13 @@ """Tests for packaging.dist.""" import os -import io import sys import logging import textwrap -import sysconfig import packaging.dist from packaging.dist import Distribution from packaging.command import set_command from packaging.command.cmd import Command -from packaging.metadata import Metadata from packaging.errors import PackagingModuleError, PackagingOptionError from packaging.tests import TESTFN, captured_stdout from packaging.tests import support, unittest @@ -49,6 +46,7 @@ def tearDown(self): sys.argv[:] = self.argv[1] super(DistributionTestCase, self).tearDown() + @unittest.skip('needs to be updated') def test_debug_mode(self): self.addCleanup(os.unlink, TESTFN) with open(TESTFN, "w") as f: @@ -59,6 +57,8 @@ def test_debug_mode(self): sys.argv.append("build") __, stdout = captured_stdout(create_distribution, files) self.assertEqual(stdout, '') + # XXX debug mode does not exist anymore, test logging levels in this + # test instead packaging.dist.DEBUG = True try: __, stdout = captured_stdout(create_distribution, files) @@ -66,34 +66,6 @@ def test_debug_mode(self): finally: packaging.dist.DEBUG = False - def test_write_pkg_file(self): - # Check Metadata handling of Unicode fields - tmp_dir = self.mkdtemp() - my_file = os.path.join(tmp_dir, 'f') - cls = Distribution - - dist = cls(attrs={'author': 'Mister Café', - 'name': 'my.package', - 'maintainer': 'Café Junior', - 'summary': 'Café torréfié', - 'description': 'Héhéhé'}) - - # let's make sure the file can be written - # with Unicode fields. they are encoded with - # PKG_INFO_ENCODING - with open(my_file, 'w', encoding='utf-8') as fp: - dist.metadata.write_file(fp) - - # regular ascii is of course always usable - dist = cls(attrs={'author': 'Mister Cafe', - 'name': 'my.package', - 'maintainer': 'Cafe Junior', - 'summary': 'Cafe torrefie', - 'description': 'Hehehe'}) - - with open(my_file, 'w') as fp: - dist.metadata.write_file(fp) - def test_bad_attr(self): Distribution(attrs={'author': 'xxx', 'name': 'xxx', @@ -101,28 +73,18 @@ def test_bad_attr(self): 'home-page': 'xxxx', 'badoptname': 'xxx'}) logs = self.get_logs(logging.WARNING) - self.assertEqual(1, len(logs)) + self.assertEqual(len(logs), 1) self.assertIn('unknown argument', logs[0]) - def test_bad_version(self): - Distribution(attrs={'author': 'xxx', - 'name': 'xxx', - 'version': 'xxx', - 'home-page': 'xxxx'}) - logs = self.get_logs(logging.WARNING) - self.assertEqual(1, len(logs)) - self.assertIn('not a valid version', logs[0]) - def test_empty_options(self): # an empty options dictionary should not stay in the # list of attributes - Distribution(attrs={'author': 'xxx', - 'name': 'xxx', - 'version': '1.2', - 'home-page': 'xxxx', - 'options': {}}) + dist = Distribution(attrs={'author': 'xxx', 'name': 'xxx', + 'version': '1.2', 'home-page': 'xxxx', + 'options': {}}) self.assertEqual([], self.get_logs(logging.WARNING)) + self.assertNotIn('options', dir(dist)) def test_non_empty_options(self): # TODO: how to actually use options is not documented except @@ -141,7 +103,6 @@ def test_non_empty_options(self): self.assertIn('owner', dist.get_option_dict('sdist')) def test_finalize_options(self): - attrs = {'keywords': 'one,two', 'platform': 'one,two'} @@ -152,6 +113,24 @@ def test_finalize_options(self): self.assertEqual(dist.metadata['platform'], ['one', 'two']) self.assertEqual(dist.metadata['keywords'], ['one', 'two']) + def test_custom_pydistutils(self): + # Bug #2166: make sure pydistutils.cfg is found + if os.name == 'posix': + user_filename = ".pydistutils.cfg" + else: + user_filename = "pydistutils.cfg" + + temp_dir = self.mkdtemp() + user_filename = os.path.join(temp_dir, user_filename) + with open(user_filename, 'w') as f: + f.write('.') + + dist = Distribution() + + os.environ['HOME'] = temp_dir + files = dist.find_config_files() + self.assertIn(user_filename, files) + def test_find_config_files_disable(self): # Bug #1180: Allow users to disable their own config file. temp_home = self.mkdtemp() @@ -270,185 +249,8 @@ def test_hooks_callable(self): self.assertRaises(PackagingOptionError, d.run_command, 'test_dist') -class MetadataTestCase(support.TempdirManager, - support.LoggingCatcher, - support.EnvironRestorer, - unittest.TestCase): - - restore_environ = ['HOME'] - - def setUp(self): - super(MetadataTestCase, self).setUp() - self.argv = sys.argv, sys.argv[:] - - def tearDown(self): - sys.argv = self.argv[0] - sys.argv[:] = self.argv[1] - super(MetadataTestCase, self).tearDown() - - def test_simple_metadata(self): - attrs = {"name": "package", - "version": "1.0"} - dist = Distribution(attrs) - meta = self.format_metadata(dist) - self.assertIn("Metadata-Version: 1.0", meta) - self.assertNotIn("provides:", meta.lower()) - self.assertNotIn("requires:", meta.lower()) - self.assertNotIn("obsoletes:", meta.lower()) - - def test_provides_dist(self): - attrs = {"name": "package", - "version": "1.0", - "provides_dist": ["package", "package.sub"]} - dist = Distribution(attrs) - self.assertEqual(dist.metadata['Provides-Dist'], - ["package", "package.sub"]) - meta = self.format_metadata(dist) - self.assertIn("Metadata-Version: 1.2", meta) - self.assertNotIn("requires:", meta.lower()) - self.assertNotIn("obsoletes:", meta.lower()) - - def _test_provides_illegal(self): - # XXX to do: check the versions - self.assertRaises(ValueError, Distribution, - {"name": "package", - "version": "1.0", - "provides_dist": ["my.pkg (splat)"]}) - - def test_requires_dist(self): - attrs = {"name": "package", - "version": "1.0", - "requires_dist": ["other", "another (==1.0)"]} - dist = Distribution(attrs) - self.assertEqual(dist.metadata['Requires-Dist'], - ["other", "another (==1.0)"]) - meta = self.format_metadata(dist) - self.assertIn("Metadata-Version: 1.2", meta) - self.assertNotIn("provides:", meta.lower()) - self.assertIn("Requires-Dist: other", meta) - self.assertIn("Requires-Dist: another (==1.0)", meta) - self.assertNotIn("obsoletes:", meta.lower()) - - def _test_requires_illegal(self): - # XXX - self.assertRaises(ValueError, Distribution, - {"name": "package", - "version": "1.0", - "requires": ["my.pkg (splat)"]}) - - def test_obsoletes_dist(self): - attrs = {"name": "package", - "version": "1.0", - "obsoletes_dist": ["other", "another (<1.0)"]} - dist = Distribution(attrs) - self.assertEqual(dist.metadata['Obsoletes-Dist'], - ["other", "another (<1.0)"]) - meta = self.format_metadata(dist) - self.assertIn("Metadata-Version: 1.2", meta) - self.assertNotIn("provides:", meta.lower()) - self.assertNotIn("requires:", meta.lower()) - self.assertIn("Obsoletes-Dist: other", meta) - self.assertIn("Obsoletes-Dist: another (<1.0)", meta) - - def _test_obsoletes_illegal(self): - # XXX - self.assertRaises(ValueError, Distribution, - {"name": "package", - "version": "1.0", - "obsoletes": ["my.pkg (splat)"]}) - - def format_metadata(self, dist): - sio = io.StringIO() - dist.metadata.write_file(sio) - return sio.getvalue() - - def test_custom_pydistutils(self): - # fixes #2166 - # make sure pydistutils.cfg is found - if os.name == 'posix': - user_filename = ".pydistutils.cfg" - else: - user_filename = "pydistutils.cfg" - - temp_dir = self.mkdtemp() - user_filename = os.path.join(temp_dir, user_filename) - with open(user_filename, 'w') as f: - f.write('.') - - dist = Distribution() - - # linux-style - if sys.platform in ('linux', 'darwin'): - os.environ['HOME'] = temp_dir - files = dist.find_config_files() - self.assertIn(user_filename, files) - - # win32-style - if sys.platform == 'win32': - # home drive should be found - os.environ['HOME'] = temp_dir - files = dist.find_config_files() - self.assertIn(user_filename, files) - - def test_show_help(self): - # smoke test, just makes sure some help is displayed - dist = Distribution() - sys.argv = [] - dist.help = True - dist.script_name = os.path.join(sysconfig.get_path('scripts'), - 'pysetup') - __, stdout = captured_stdout(dist.parse_command_line) - output = [line for line in stdout.split('\n') - if line.strip() != ''] - self.assertGreater(len(output), 0) - - def test_description(self): - desc = textwrap.dedent("""\ - example:: - We start here - and continue here - and end here.""") - attrs = {"name": "package", - "version": "1.0", - "description": desc} - - dist = packaging.dist.Distribution(attrs) - meta = self.format_metadata(dist) - meta = meta.replace('\n' + 7 * ' ' + '|', '\n') - self.assertIn(desc, meta) - - def test_read_metadata(self): - attrs = {"name": "package", - "version": "1.0", - "description": "desc", - "summary": "xxx", - "download_url": "http://example.com", - "keywords": ['one', 'two'], - "requires_dist": ['foo']} - - dist = Distribution(attrs) - PKG_INFO = io.StringIO() - dist.metadata.write_file(PKG_INFO) - PKG_INFO.seek(0) - - metadata = Metadata() - metadata.read_file(PKG_INFO) - - 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() - suite.addTest(unittest.makeSuite(DistributionTestCase)) - suite.addTest(unittest.makeSuite(MetadataTestCase)) - return suite + return unittest.makeSuite(DistributionTestCase) if __name__ == "__main__": unittest.main(defaultTest="test_suite") diff --git a/Lib/packaging/tests/test_metadata.py b/Lib/packaging/tests/test_metadata.py index 904019c34e2..b79f5662549 100644 --- a/Lib/packaging/tests/test_metadata.py +++ b/Lib/packaging/tests/test_metadata.py @@ -2,6 +2,7 @@ import os import sys import logging +from textwrap import dedent from io import StringIO from packaging.errors import (MetadataConflictError, MetadataMissingError, @@ -9,12 +10,29 @@ from packaging.metadata import Metadata, PKG_INFO_PREFERRED_VERSION from packaging.tests import unittest -from packaging.tests.support import LoggingCatcher +from packaging.tests.support import (LoggingCatcher, TempdirManager, + EnvironRestorer) class MetadataTestCase(LoggingCatcher, + TempdirManager, + EnvironRestorer, unittest.TestCase): + maxDiff = None + restore_environ = ['HOME'] + + def setUp(self): + super(MetadataTestCase, self).setUp() + self.argv = sys.argv, sys.argv[:] + + def tearDown(self): + sys.argv = self.argv[0] + sys.argv[:] = self.argv[1] + super(MetadataTestCase, self).tearDown() + + #### Test various methods of the Metadata class + def test_instantiation(self): PKG_INFO = os.path.join(os.path.dirname(__file__), 'PKG-INFO') with open(PKG_INFO, 'r', encoding='utf-8') as f: @@ -43,17 +61,6 @@ def test_instantiation(self): self.assertRaises(TypeError, Metadata, PKG_INFO, mapping=m, fileobj=fp) - def test_metadata_read_write(self): - PKG_INFO = os.path.join(os.path.dirname(__file__), 'PKG-INFO') - metadata = Metadata(PKG_INFO) - out = StringIO() - metadata.write_file(out) - out.seek(0) - res = Metadata() - res.read_file(out) - for k in metadata: - self.assertEqual(metadata[k], res[k]) - def test_metadata_markers(self): # see if we can be platform-aware PKG_INFO = os.path.join(os.path.dirname(__file__), 'PKG-INFO') @@ -70,31 +77,10 @@ def test_metadata_markers(self): # test with context context = {'sys.platform': 'okook'} - metadata = Metadata(platform_dependent=True, - execution_context=context) + metadata = Metadata(platform_dependent=True, execution_context=context) metadata.read_file(StringIO(content)) self.assertEqual(metadata['Requires-Dist'], ['foo']) - def test_description(self): - PKG_INFO = os.path.join(os.path.dirname(__file__), 'PKG-INFO') - with open(PKG_INFO, 'r', encoding='utf-8') as f: - content = f.read() % sys.platform - metadata = Metadata() - metadata.read_file(StringIO(content)) - - # see if we can read the description now - DESC = os.path.join(os.path.dirname(__file__), 'LONG_DESC.txt') - with open(DESC) as f: - wanted = f.read() - 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.assertEqual(wanted, metadata['Description']) - def test_mapping_api(self): PKG_INFO = os.path.join(os.path.dirname(__file__), 'PKG-INFO') with open(PKG_INFO, 'r', encoding='utf-8') as f: @@ -109,73 +95,86 @@ def test_mapping_api(self): metadata.update([('version', '0.7')]) self.assertEqual(metadata['Version'], '0.7') + # make sure update method checks values like the set method does + metadata.update({'version': '1--2'}) + self.assertEqual(len(self.get_logs()), 1) + + # XXX caveat: the keys method and friends are not 3.x-style views + # should be changed or documented self.assertEqual(list(metadata), list(metadata.keys())) - def test_versions(self): - metadata = Metadata() - metadata['Obsoletes'] = 'ok' - self.assertEqual(metadata['Metadata-Version'], '1.1') + def test_read_metadata(self): + fields = {'name': 'project', + 'version': '1.0', + 'description': 'desc', + 'summary': 'xxx', + 'download_url': 'http://example.com', + 'keywords': ['one', 'two'], + 'requires_dist': ['foo']} - del metadata['Obsoletes'] - metadata['Obsoletes-Dist'] = 'ok' - self.assertEqual(metadata['Metadata-Version'], '1.2') + metadata = Metadata(mapping=fields) + PKG_INFO = StringIO() + metadata.write_file(PKG_INFO) + PKG_INFO.seek(0) - self.assertRaises(MetadataConflictError, metadata.set, - 'Obsoletes', 'ok') + metadata = Metadata(fileobj=PKG_INFO) - del metadata['Obsoletes'] - del metadata['Obsoletes-Dist'] - metadata['Version'] = '1' - self.assertEqual(metadata['Metadata-Version'], '1.0') + self.assertEqual(metadata['name'], 'project') + 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']) - PKG_INFO = os.path.join(os.path.dirname(__file__), - 'SETUPTOOLS-PKG-INFO') - with open(PKG_INFO, 'r', encoding='utf-8') as f: - content = f.read() - metadata.read_file(StringIO(content)) - self.assertEqual(metadata['Metadata-Version'], '1.0') + def test_write_metadata(self): + # check support of non-ASCII values + tmp_dir = self.mkdtemp() + my_file = os.path.join(tmp_dir, 'f') - PKG_INFO = os.path.join(os.path.dirname(__file__), - 'SETUPTOOLS-PKG-INFO2') - with open(PKG_INFO, 'r', encoding='utf-8') as f: - content = f.read() - metadata.read_file(StringIO(content)) - self.assertEqual(metadata['Metadata-Version'], '1.1') + metadata = Metadata(mapping={'author': 'Mister Café', + 'name': 'my.project', + 'author': 'Café Junior', + 'summary': 'Café torréfié', + 'description': 'Héhéhé', + 'keywords': ['café', 'coffee']}) + metadata.write(my_file) - # Update the _fields dict directly to prevent 'Metadata-Version' - # from being updated by the _set_best_version() method. - metadata._fields['Metadata-Version'] = '1.618' - self.assertRaises(MetadataUnrecognizedVersionError, metadata.keys) + # the file should use UTF-8 + metadata2 = Metadata() + with open(my_file, encoding='utf-8') as fp: + metadata2.read_file(fp) - def test_warnings(self): - metadata = Metadata() + # XXX when keywords are not defined, metadata will have + # 'Keywords': [] but metadata2 will have 'Keywords': [''] + # because of a value.split(',') in Metadata.get + self.assertEqual(metadata.items(), metadata2.items()) - # these should raise a warning - values = (('Requires-Dist', 'Funky (Groovie)'), - ('Requires-Python', '1-4')) + # ASCII also works, it's a subset of UTF-8 + metadata = Metadata(mapping={'author': 'Mister Cafe', + 'name': 'my.project', + 'author': 'Cafe Junior', + 'summary': 'Cafe torrefie', + 'description': 'Hehehe'}) + metadata.write(my_file) - for name, value in values: - metadata.set(name, value) + metadata2 = Metadata() + with open(my_file, encoding='utf-8') as fp: + metadata2.read_file(fp) - # we should have a certain amount of warnings - self.assertEqual(len(self.get_logs()), 2) + def test_metadata_read_write(self): + PKG_INFO = os.path.join(os.path.dirname(__file__), 'PKG-INFO') + metadata = Metadata(PKG_INFO) + out = StringIO() + metadata.write_file(out) - def test_multiple_predicates(self): - metadata = Metadata() + out.seek(0) + res = Metadata() + res.read_file(out) + self.assertEqual(metadata.values(), res.values()) - # see for "3" instead of "3.0" ??? - # its seems like the MINOR VERSION can be omitted - metadata['Requires-Python'] = '>=2.6, <3.0' - metadata['Requires-Dist'] = ['Foo (>=2.6, <3.0)'] - - self.assertEqual([], self.get_logs(logging.WARNING)) - - def test_project_url(self): - metadata = Metadata() - metadata['Project-URL'] = [('one', 'http://ok')] - self.assertEqual(metadata['Project-URL'], - [('one', 'http://ok')]) - self.assertEqual(metadata['Metadata-Version'], '1.2') + #### Test checks def test_check_version(self): metadata = Metadata() @@ -238,39 +237,203 @@ def test_check_predicates(self): metadata['Requires-dist'] = ['Foo (a)'] metadata['Obsoletes-dist'] = ['Foo (a)'] metadata['Provides-dist'] = ['Foo (a)'] - if metadata.docutils_support: - missing, warnings = metadata.check() - self.assertEqual(len(warnings), 4) - metadata.docutils_support = False missing, warnings = metadata.check() self.assertEqual(len(warnings), 4) - def test_best_choice(self): - metadata = Metadata() - metadata['Version'] = '1.0' + #### Test fields and metadata versions + + def test_metadata_versions(self): + metadata = Metadata(mapping={'name': 'project', 'version': '1.0'}) self.assertEqual(metadata['Metadata-Version'], PKG_INFO_PREFERRED_VERSION) + self.assertNotIn('Provides', metadata) + self.assertNotIn('Requires', metadata) + self.assertNotIn('Obsoletes', metadata) + metadata['Classifier'] = ['ok'] self.assertEqual(metadata['Metadata-Version'], '1.2') - def test_project_urls(self): - # project-url is a bit specific, make sure we write it - # properly in PKG-INFO metadata = Metadata() - metadata['Version'] = '1.0' - metadata['Project-Url'] = [('one', 'http://ok')] - self.assertEqual(metadata['Project-Url'], [('one', 'http://ok')]) - file_ = StringIO() - metadata.write_file(file_) - file_.seek(0) - res = file_.read().split('\n') - self.assertIn('Project-URL: one,http://ok', res) + metadata['Obsoletes'] = 'ok' + self.assertEqual(metadata['Metadata-Version'], '1.1') - file_.seek(0) + del metadata['Obsoletes'] + metadata['Obsoletes-Dist'] = 'ok' + 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.assertEqual(metadata['Metadata-Version'], '1.0') + + PKG_INFO = os.path.join(os.path.dirname(__file__), + 'SETUPTOOLS-PKG-INFO') + metadata = Metadata(PKG_INFO) + self.assertEqual(metadata['Metadata-Version'], '1.0') + + PKG_INFO = os.path.join(os.path.dirname(__file__), + 'SETUPTOOLS-PKG-INFO2') + metadata = Metadata(PKG_INFO) + self.assertEqual(metadata['Metadata-Version'], '1.1') + + # Update the _fields dict directly to prevent 'Metadata-Version' + # from being updated by the _set_best_version() method. + metadata._fields['Metadata-Version'] = '1.618' + self.assertRaises(MetadataUnrecognizedVersionError, metadata.keys) + + def test_version(self): + Metadata(mapping={'author': 'xxx', + 'name': 'xxx', + 'version': 'xxx', + 'home-page': 'xxxx'}) + logs = self.get_logs(logging.WARNING) + self.assertEqual(1, len(logs)) + self.assertIn('not a valid version', logs[0]) + + def test_description(self): + PKG_INFO = os.path.join(os.path.dirname(__file__), 'PKG-INFO') + with open(PKG_INFO, 'r', encoding='utf-8') as f: + content = f.read() % sys.platform metadata = Metadata() - metadata.read_file(file_) + metadata.read_file(StringIO(content)) + + # see if we can read the description now + DESC = os.path.join(os.path.dirname(__file__), 'LONG_DESC.txt') + with open(DESC) as f: + wanted = f.read() + 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) + + out.seek(0) + metadata = Metadata() + metadata.read_file(out) + self.assertEqual(wanted, metadata['Description']) + + def test_description_folding(self): + # make sure the indentation is preserved + out = StringIO() + desc = dedent("""\ + example:: + We start here + and continue here + and end here. + """) + + metadata = Metadata() + metadata['description'] = desc + metadata.write_file(out) + + folded_desc = desc.replace('\n', '\n' + (7 * ' ') + '|') + self.assertIn(folded_desc, out.getvalue()) + + def test_project_url(self): + metadata = Metadata() + metadata['Project-URL'] = [('one', 'http://ok')] + self.assertEqual(metadata['Project-URL'], [('one', 'http://ok')]) + self.assertEqual(metadata['Metadata-Version'], '1.2') + + # make sure this particular field is handled properly when written + fp = StringIO() + metadata.write_file(fp) + self.assertIn('Project-URL: one,http://ok', fp.getvalue().split('\n')) + + fp.seek(0) + metadata = Metadata() + metadata.read_file(fp) self.assertEqual(metadata['Project-Url'], [('one', 'http://ok')]) + # TODO copy tests for v1.1 requires, obsoletes and provides from distutils + # (they're useless but we support them so we should test them anyway) + + def test_provides_dist(self): + fields = {'name': 'project', + 'version': '1.0', + 'provides_dist': ['project', 'my.project']} + metadata = Metadata(mapping=fields) + self.assertEqual(metadata['Provides-Dist'], + ['project', 'my.project']) + self.assertEqual(metadata['Metadata-Version'], '1.2', metadata) + self.assertNotIn('Requires', metadata) + self.assertNotIn('Obsoletes', metadata) + + @unittest.skip('needs to be implemented') + def test_provides_illegal(self): + # TODO check the versions (like distutils does for old provides field) + self.assertRaises(ValueError, Metadata, + mapping={'name': 'project', + 'version': '1.0', + 'provides_dist': ['my.pkg (splat)']}) + + def test_requires_dist(self): + fields = {'name': 'project', + 'version': '1.0', + 'requires_dist': ['other', 'another (==1.0)']} + metadata = Metadata(mapping=fields) + self.assertEqual(metadata['Requires-Dist'], + ['other', 'another (==1.0)']) + self.assertEqual(metadata['Metadata-Version'], '1.2') + self.assertNotIn('Provides', metadata) + self.assertEqual(metadata['Requires-Dist'], + ['other', 'another (==1.0)']) + self.assertNotIn('Obsoletes', metadata) + + # make sure write_file uses one RFC 822 header per item + fp = StringIO() + metadata.write_file(fp) + lines = fp.getvalue().split('\n') + self.assertIn('Requires-Dist: other', lines) + self.assertIn('Requires-Dist: another (==1.0)', lines) + + # test warnings for invalid version predicates + # XXX this would cause no warnings if we used update (or the mapping + # argument of the constructor), see comment in Metadata.update + metadata = Metadata() + metadata['Requires-Dist'] = 'Funky (Groovie)' + metadata['Requires-Python'] = '1-4' + self.assertEqual(len(self.get_logs()), 2) + + # test multiple version predicates + metadata = Metadata() + + # XXX check PEP and see if 3 == 3.0 + metadata['Requires-Python'] = '>=2.6, <3.0' + metadata['Requires-Dist'] = ['Foo (>=2.6, <3.0)'] + self.assertEqual([], self.get_logs(logging.WARNING)) + + @unittest.skip('needs to be implemented') + def test_requires_illegal(self): + self.assertRaises(ValueError, Metadata, + mapping={'name': 'project', + 'version': '1.0', + 'requires': ['my.pkg (splat)']}) + + def test_obsoletes_dist(self): + fields = {'name': 'project', + 'version': '1.0', + 'obsoletes_dist': ['other', 'another (<1.0)']} + metadata = Metadata(mapping=fields) + self.assertEqual(metadata['Obsoletes-Dist'], + ['other', 'another (<1.0)']) + self.assertEqual(metadata['Metadata-Version'], '1.2') + self.assertNotIn('Provides', metadata) + self.assertNotIn('Requires', metadata) + self.assertEqual(metadata['Obsoletes-Dist'], + ['other', 'another (<1.0)']) + + @unittest.skip('needs to be implemented') + def test_obsoletes_illegal(self): + self.assertRaises(ValueError, Metadata, + mapping={'name': 'project', + 'version': '1.0', + 'obsoletes': ['my.pkg (splat)']}) + def test_suite(): return unittest.makeSuite(MetadataTestCase) diff --git a/Lib/packaging/tests/test_run.py b/Lib/packaging/tests/test_run.py index cb576b7c1db..6a3b8fb1309 100644 --- a/Lib/packaging/tests/test_run.py +++ b/Lib/packaging/tests/test_run.py @@ -3,16 +3,16 @@ import os import sys import shutil -from tempfile import mkstemp from io import StringIO from packaging import install from packaging.tests import unittest, support, TESTFN from packaging.run import main +from test.script_helper import assert_python_ok + # setup script that uses __file__ setup_using___file__ = """\ - __file__ from packaging.run import setup @@ -20,7 +20,6 @@ """ setup_prints_cwd = """\ - import os print os.getcwd() @@ -29,11 +28,12 @@ """ -class CoreTestCase(support.TempdirManager, support.LoggingCatcher, - unittest.TestCase): +class RunTestCase(support.TempdirManager, + support.LoggingCatcher, + unittest.TestCase): def setUp(self): - super(CoreTestCase, self).setUp() + super(RunTestCase, self).setUp() self.old_stdout = sys.stdout self.cleanup_testfn() self.old_argv = sys.argv, sys.argv[:] @@ -43,7 +43,7 @@ def tearDown(self): self.cleanup_testfn() sys.argv = self.old_argv[0] sys.argv[:] = self.old_argv[1] - super(CoreTestCase, self).tearDown() + super(RunTestCase, self).tearDown() def cleanup_testfn(self): path = TESTFN @@ -77,9 +77,16 @@ def test_install(self): os.chmod(install_path, old_mod) install.get_path = old_get_path + def test_show_help(self): + # smoke test, just makes sure some help is displayed + status, out, err = assert_python_ok('-m', 'packaging.run', '--help') + self.assertEqual(status, 0) + self.assertGreater(out, b'') + self.assertEqual(err, b'') + def test_suite(): - return unittest.makeSuite(CoreTestCase) + return unittest.makeSuite(RunTestCase) if __name__ == "__main__": unittest.main(defaultTest="test_suite") From e6db7a3a298e3bfe5064de2298ba6cdcb470793e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Araujo?= Date: Sat, 10 Sep 2011 05:22:48 +0200 Subject: [PATCH 04/10] Fix determination of Metadata version in packaging (#8933). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Original patch by Filip Gruszczyński. --- Lib/packaging/metadata.py | 3 ++- Lib/packaging/tests/test_metadata.py | 15 ++++++++++++++- Misc/NEWS | 6 +++--- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Lib/packaging/metadata.py b/Lib/packaging/metadata.py index 7d7fc6b7754..dbb53b265c3 100644 --- a/Lib/packaging/metadata.py +++ b/Lib/packaging/metadata.py @@ -61,7 +61,8 @@ def system_message(self, level, message, *children, **kwargs): 'License', 'Classifier', 'Download-URL', 'Obsoletes', 'Provides', 'Requires') -_314_MARKERS = ('Obsoletes', 'Provides', 'Requires') +_314_MARKERS = ('Obsoletes', 'Provides', 'Requires', 'Classifier', + 'Download-URL') _345_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform', 'Supported-Platform', 'Summary', 'Description', diff --git a/Lib/packaging/tests/test_metadata.py b/Lib/packaging/tests/test_metadata.py index b79f5662549..68b3d9722ad 100644 --- a/Lib/packaging/tests/test_metadata.py +++ b/Lib/packaging/tests/test_metadata.py @@ -251,7 +251,11 @@ def test_metadata_versions(self): self.assertNotIn('Obsoletes', metadata) metadata['Classifier'] = ['ok'] - self.assertEqual(metadata['Metadata-Version'], '1.2') + self.assertEqual(metadata['Metadata-Version'], '1.1') + + metadata = Metadata() + metadata['Download-URL'] = 'ok' + self.assertEqual(metadata['Metadata-Version'], '1.1') metadata = Metadata() metadata['Obsoletes'] = 'ok' @@ -269,6 +273,15 @@ def test_metadata_versions(self): metadata['Version'] = '1' self.assertEqual(metadata['Metadata-Version'], '1.0') + # make sure the _best_version function works okay with + # non-conflicting fields from 1.1 and 1.2 (i.e. we want only the + # requires/requires-dist and co. pairs to cause a conflict, not all + # fields in _314_MARKERS) + metadata = Metadata() + metadata['Requires-Python'] = '3' + metadata['Classifier'] = ['Programming language :: Python :: 3'] + self.assertEqual(metadata['Metadata-Version'], '1.2') + PKG_INFO = os.path.join(os.path.dirname(__file__), 'SETUPTOOLS-PKG-INFO') metadata = Metadata(PKG_INFO) diff --git a/Misc/NEWS b/Misc/NEWS index 67b8cf85a19..3d358239e8a 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -274,9 +274,9 @@ Core and Builtins Library ------- -- Issue #8933: distutils' PKG-INFO files will now correctly report - Metadata-Version: 1.1 instead of 1.0 if a Classifier or Download-URL field is - present. +- Issue #8933: distutils' PKG-INFO files and packaging's METADATA files will + now correctly report Metadata-Version: 1.1 instead of 1.0 if a Classifier or + Download-URL field is present. - Issue #12567: Add curses.unget_wch() function. Push a character so the next get_wch() will return it. From c8f9c81cfa7fb46d3c0be9e9e5f18bfda9247984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Araujo?= Date: Sat, 10 Sep 2011 18:10:23 +0200 Subject: [PATCH 05/10] Fix usage of dry-run in packaging bdist_wininst and install_distinfo. In dry-run mode, packaging commands should log the same info as in real operation and should collect the same files in self.outputs, so that users can run a command in verbose and dry-run mode to see exactly what operations will be done in the real run. --- Lib/packaging/command/bdist_wininst.py | 5 +- Lib/packaging/command/install_distinfo.py | 72 +++++++++++------------ 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/Lib/packaging/command/bdist_wininst.py b/Lib/packaging/command/bdist_wininst.py index 7dbb39b3117..4ba2b2db17f 100644 --- a/Lib/packaging/command/bdist_wininst.py +++ b/Lib/packaging/command/bdist_wininst.py @@ -186,9 +186,8 @@ def run(self): os.remove(arcname) if not self.keep_temp: - if self.dry_run: - logger.info('removing %s', self.bdist_dir) - else: + logger.info('removing %s', self.bdist_dir) + if not self.dry_run: rmtree(self.bdist_dir) def get_inidata(self): diff --git a/Lib/packaging/command/install_distinfo.py b/Lib/packaging/command/install_distinfo.py index 0e1577e7dc2..1f48eedab73 100644 --- a/Lib/packaging/command/install_distinfo.py +++ b/Lib/packaging/command/install_distinfo.py @@ -28,7 +28,7 @@ class install_distinfo(Command): ('no-record', None, "do not generate a RECORD file"), ('no-resources', None, - "do not generate a RESSOURCES list installed file") + "do not generate a RESSOURCES list installed file"), ] boolean_options = ['requested', 'no-record', 'no-resources'] @@ -70,56 +70,56 @@ def finalize_options(self): self.distinfo_dir = os.path.join(self.distinfo_dir, basename) def run(self): - # FIXME dry-run should be used at a finer level, so that people get - # useful logging output and can have an idea of what the command would - # have done - if not self.dry_run: - target = self.distinfo_dir + target = self.distinfo_dir - if os.path.isdir(target) and not os.path.islink(target): + if os.path.isdir(target) and not os.path.islink(target): + if not self.dry_run: rmtree(target) - elif os.path.exists(target): - self.execute(os.unlink, (self.distinfo_dir,), - "removing " + target) + elif os.path.exists(target): + self.execute(os.unlink, (self.distinfo_dir,), + "removing " + target) - self.execute(os.makedirs, (target,), "creating " + target) + self.execute(os.makedirs, (target,), "creating " + target) - metadata_path = os.path.join(self.distinfo_dir, 'METADATA') - logger.info('creating %s', metadata_path) - self.distribution.metadata.write(metadata_path) - self.outfiles.append(metadata_path) + metadata_path = os.path.join(self.distinfo_dir, 'METADATA') + self.execute(self.distribution.metadata.write, (metadata_path,), + "creating " + metadata_path) + self.outfiles.append(metadata_path) - installer_path = os.path.join(self.distinfo_dir, 'INSTALLER') - logger.info('creating %s', installer_path) + installer_path = os.path.join(self.distinfo_dir, 'INSTALLER') + logger.info('creating %s', installer_path) + if not self.dry_run: with open(installer_path, 'w') as f: f.write(self.installer) - self.outfiles.append(installer_path) + self.outfiles.append(installer_path) - if self.requested: - requested_path = os.path.join(self.distinfo_dir, 'REQUESTED') - logger.info('creating %s', requested_path) + if self.requested: + requested_path = os.path.join(self.distinfo_dir, 'REQUESTED') + logger.info('creating %s', requested_path) + if not self.dry_run: open(requested_path, 'wb').close() - self.outfiles.append(requested_path) + self.outfiles.append(requested_path) - - if not self.no_resources: - install_data = self.get_finalized_command('install_data') - if install_data.get_resources_out() != []: - resources_path = os.path.join(self.distinfo_dir, - 'RESOURCES') - logger.info('creating %s', resources_path) + if not self.no_resources: + install_data = self.get_finalized_command('install_data') + if install_data.get_resources_out() != []: + resources_path = os.path.join(self.distinfo_dir, + 'RESOURCES') + logger.info('creating %s', resources_path) + if not self.dry_run: with open(resources_path, 'wb') as f: writer = csv.writer(f, delimiter=',', lineterminator='\n', quotechar='"') - for tuple in install_data.get_resources_out(): - writer.writerow(tuple) + for row in install_data.get_resources_out(): + writer.writerow(row) - self.outfiles.append(resources_path) + self.outfiles.append(resources_path) - if not self.no_record: - record_path = os.path.join(self.distinfo_dir, 'RECORD') - logger.info('creating %s', record_path) + if not self.no_record: + record_path = os.path.join(self.distinfo_dir, 'RECORD') + logger.info('creating %s', record_path) + if not self.dry_run: with open(record_path, 'w', encoding='utf-8') as f: writer = csv.writer(f, delimiter=',', lineterminator='\n', @@ -141,7 +141,7 @@ def run(self): # add the RECORD file itself writer.writerow((record_path, '', '')) - self.outfiles.append(record_path) + self.outfiles.append(record_path) def get_outputs(self): return self.outfiles From 030cfe26a336826d08362cd60c8ea4be7775844b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Araujo?= Date: Sat, 10 Sep 2011 18:10:58 +0200 Subject: [PATCH 06/10] Use bytes regex instead of decoding whole pages --- Lib/packaging/pypi/simple.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Lib/packaging/pypi/simple.py b/Lib/packaging/pypi/simple.py index 710355d6480..76aad02416d 100644 --- a/Lib/packaging/pypi/simple.py +++ b/Lib/packaging/pypi/simple.py @@ -159,22 +159,20 @@ def search_projects(self, name=None, **kwargs): Return a list of names. """ - with self._open_url(self.index_url) as index: - if '*' in name: - name.replace('*', '.*') - else: - name = "%s%s%s" % ('*.?', name, '*.?') - name = name.replace('*', '[^<]*') # avoid matching end tag - projectname = re.compile(']*>(%s)' % name, re.I) - matching_projects = [] + if '*' in name: + name.replace('*', '.*') + else: + name = "%s%s%s" % ('*.?', name, '*.?') + name = name.replace('*', '[^<]*') # avoid matching end tag + pattern = (']*>(%s)' % name).encode('utf-8') + projectname = re.compile(pattern, re.I) + matching_projects = [] + with self._open_url(self.index_url) as index: index_content = index.read() - # FIXME should use bytes I/O and regexes instead of decoding - index_content = index_content.decode() - for match in projectname.finditer(index_content): - project_name = match.group(1) + project_name = match.group(1).decode('utf-8') matching_projects.append(self._get_project(project_name)) return matching_projects From c6d52eddaaa5fb8b8091480103188beda35a9f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Araujo?= Date: Sat, 10 Sep 2011 18:14:08 +0200 Subject: [PATCH 07/10] Fix usage of bytes in packaging's bdist_wininst. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is copied from the namesake distutils command; there is no automated test, so buildbots won’t call for my head this time, but it should be okay as Python 3 users have tested the distutils command. --- Lib/packaging/command/bdist_wininst.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Lib/packaging/command/bdist_wininst.py b/Lib/packaging/command/bdist_wininst.py index 4ba2b2db17f..6c1e225f9f9 100644 --- a/Lib/packaging/command/bdist_wininst.py +++ b/Lib/packaging/command/bdist_wininst.py @@ -1,7 +1,5 @@ """Create an executable installer for Windows.""" -# FIXME synchronize bytes/str use with same file in distutils - import sys import os @@ -264,14 +262,17 @@ def create_exe(self, arcname, fullname, bitmap=None): cfgdata = cfgdata.encode("mbcs") # Append the pre-install script - cfgdata = cfgdata + "\0" + cfgdata = cfgdata + b"\0" if self.pre_install_script: - with open(self.pre_install_script) as fp: - script_data = fp.read() - cfgdata = cfgdata + script_data + "\n\0" + # We need to normalize newlines, so we open in text mode and + # convert back to bytes. "latin-1" simply avoids any possible + # failures. + with open(self.pre_install_script, encoding="latin-1") as fp: + script_data = fp.read().encode("latin-1") + cfgdata = cfgdata + script_data + b"\n\0" else: # empty pre-install script - cfgdata = cfgdata + "\0" + cfgdata = cfgdata + b"\0" file.write(cfgdata) # The 'magic number' 0x1234567B is used to make sure that the From 1f2bcd35bb49fc17ee7842497ee5e46f9d2f5bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Araujo?= Date: Sat, 10 Sep 2011 18:22:04 +0200 Subject: [PATCH 08/10] =?UTF-8?q?Don=E2=80=99t=20let=20invalid=20line=20in?= =?UTF-8?q?=20setup.cfg=20pass=20silently?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Lib/packaging/config.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Lib/packaging/config.py b/Lib/packaging/config.py index e02800e9605..366faea4ede 100644 --- a/Lib/packaging/config.py +++ b/Lib/packaging/config.py @@ -227,10 +227,11 @@ def _read_setup_cfg(self, parser, cfg_filename): self.dist.scripts = [self.dist.scripts] self.dist.package_data = {} - for data in files.get('package_data', []): - data = data.split('=') + for line in files.get('package_data', []): + data = line.split('=') if len(data) != 2: - continue # FIXME errors should never pass silently + raise ValueError('invalid line for package_data: %s ' + '(misses "=")' % line) key, value = data self.dist.package_data[key.strip()] = value.strip() From fb7d24492f89ed1a3e076cca9f759c5f40ba2905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Araujo?= Date: Sat, 10 Sep 2011 18:22:31 +0200 Subject: [PATCH 09/10] Remove obsolete comment (yes, build_ext supports C++) --- Lib/packaging/command/build_ext.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Lib/packaging/command/build_ext.py b/Lib/packaging/command/build_ext.py index fa8d11f39c4..2bffae38ac2 100644 --- a/Lib/packaging/command/build_ext.py +++ b/Lib/packaging/command/build_ext.py @@ -1,9 +1,5 @@ """Build extension modules.""" -# FIXME Is this module limited to C extensions or do C++ extensions work too? -# The docstring of this module said that C++ was not supported, but other -# comments contradict that. - import os import re import sys From cde65768203618f084592fa5441a4c92618ed7e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Araujo?= Date: Mon, 12 Sep 2011 16:45:38 +0200 Subject: [PATCH 10/10] =?UTF-8?q?Remove=20unneeded=20--all=20option=20of?= =?UTF-8?q?=20=E2=80=9Cpysetup=20list=E2=80=9D.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The command without arguments already prints all installed distributions found. In addition, change “releases” for “projects” in the description of the list action. Strictly speaking, one installed distribution satisfies the requirement for a release (i.e. version) of a project, but as currently only one release per project can be installed at a time, the two are somewhat equivalent, and “project” is more understandable in help texts (which call their argument “dist”, by the way..) --- Doc/install/pysetup.rst | 17 ++++++++-------- Lib/packaging/run.py | 20 ++++++++----------- .../tests/test_command_install_data.py | 1 + 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/Doc/install/pysetup.rst b/Doc/install/pysetup.rst index 08ba08ed2e5..f6f1f837da3 100644 --- a/Doc/install/pysetup.rst +++ b/Doc/install/pysetup.rst @@ -19,13 +19,12 @@ Finding out what's installed Pysetup makes it easy to find out what Python packages are installed:: - $ pysetup search virtualenv - virtualenv 1.6 at /opt/python3.3/lib/python3.3/site-packages/virtualenv-1.6-py3.3.egg-info + $ pysetup list virtualenv + 'virtualenv' 1.6 at '/opt/python3.3/lib/python3.3/site-packages/virtualenv-1.6-py3.3.egg-info' - $ pysetup search --all - pyverify 0.8.1 at /opt/python3.3/lib/python3.3/site-packages/pyverify-0.8.1.dist-info - virtualenv 1.6 at /opt/python3.3/lib/python3.3/site-packages/virtualenv-1.6-py3.3.egg-info - wsgiref 0.1.2 at /opt/python3.3/lib/python3.3/wsgiref.egg-info + $ pysetup list + 'pyverify' 0.8.1 at '/opt/python3.3/lib/python3.3/site-packages/pyverify-0.8.1.dist-info' + 'virtualenv' 1.6 at '/opt/python3.3/lib/python3.3/site-packages/virtualenv-1.6-py3.3.egg-info' ... @@ -146,9 +145,11 @@ Getting a list of all pysetup actions and global options:: metadata: Display the metadata of a project install: Install a project remove: Remove a project - search: Search for a project + search: Search for a project in the indexes + list: List installed projects graph: Display a graph - create: Create a Project + create: Create a project + generate-setup: Generate a backward-comptatible setup.py To get more help on an action, use: diff --git a/Lib/packaging/run.py b/Lib/packaging/run.py index ef20f35865d..5affb17974f 100644 --- a/Lib/packaging/run.py +++ b/Lib/packaging/run.py @@ -290,27 +290,23 @@ def _run(dispatcher, args, **kw): @action_help("""\ -Usage: pysetup list dist [dist ...] +Usage: pysetup list [dist ...] or: pysetup list --help - or: pysetup list --all Print name, version and location for the matching installed distributions. positional arguments: - dist installed distribution name - -optional arguments: - --all list all installed distributions + dist installed distribution name; omit to get all distributions """) def _list(dispatcher, args, **kw): - opts = _parse_args(args[1:], '', ['all']) + opts = _parse_args(args[1:], '', []) dists = get_distributions(use_egg_info=True) - if 'all' in opts or opts['args'] == []: - results = dists - listall = True - else: + if opts['args']: results = (d for d in dists if d.name.lower() in opts['args']) listall = False + else: + results = dists + listall = True number = 0 for dist in results: @@ -368,7 +364,7 @@ def _search(dispatcher, args, **kw): ('install', 'Install a project', _install), ('remove', 'Remove a project', _remove), ('search', 'Search for a project in the indexes', _search), - ('list', 'List installed releases', _list), + ('list', 'List installed projects', _list), ('graph', 'Display a graph', _graph), ('create', 'Create a project', _create), ('generate-setup', 'Generate a backward-comptatible setup.py', _generate), diff --git a/Lib/packaging/tests/test_command_install_data.py b/Lib/packaging/tests/test_command_install_data.py index 0486427a2a8..35ce847f354 100644 --- a/Lib/packaging/tests/test_command_install_data.py +++ b/Lib/packaging/tests/test_command_install_data.py @@ -35,6 +35,7 @@ def restore(): two = os.path.join(pkg_dir, 'two') self.write_file(two, 'xxx') + # FIXME this creates a literal \{inst2\} directory! cmd.data_files = {one: '{inst}/one', two: '{inst2}/two'} self.assertCountEqual(cmd.get_inputs(), [one, two])