diff --git a/.gitignore b/.gitignore index db4561ea..0717c7b6 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ htmlcov/ .cache nosetests.xml coverage.xml +cover/ # Translations *.mo diff --git a/common/pulp_python/common/constants.py b/common/pulp_python/common/constants.py index d258c001..d6af03a4 100644 --- a/common/pulp_python/common/constants.py +++ b/common/pulp_python/common/constants.py @@ -1,22 +1,19 @@ - -REPOSITORY_TYPE_ID = 'python_repository' +PACKAGE_TYPE_ID = 'python_package' REPO_NOTE_PYTHON = 'PYTHON' -WEB_IMPORTER_TYPE_ID = 'python_web_importer' -WEB_DISTRIBUTOR_TYPE_ID = 'python_web_distributor' - -CLI_WEB_DISTRIBUTOR_ID = 'python_web_distributor_name_cli' +IMPORTER_TYPE_ID = 'python_importer' +DISTRIBUTOR_TYPE_ID = 'python_distributor' -IMPORTER_CONFIG_KEY_BRANCHES = 'branches' +CLI_DISTRIBUTOR_ID = 'cli_python_distributor' DISTRIBUTOR_CONFIG_FILE_NAME = 'server/plugins.conf.d/python_distributor.json' # Config keys for the distributor plugin conf -CONFIG_KEY_PYTHON_PUBLISH_DIRECTORY = 'python_publish_directory' -CONFIG_VALUE_PYTHON_PUBLISH_DIRECTORY = '/var/lib/pulp/published/python' +CONFIG_KEY_PUBLISH_DIRECTORY = 'python_publish_directory' +CONFIG_VALUE_PUBLISH_DIRECTORY = '/var/lib/pulp/published/python' # STEP_ID -PUBLISH_STEP_WEB_PUBLISHER = 'python_publish_step_web' +PUBLISH_STEP_PUBLISHER = 'python_publish_step' PUBLISH_STEP_CONTENT = 'python_publish_content' PUBLISH_STEP_METADATA = 'python_publish_metadata' PUBLISH_STEP_OVER_HTTP = 'python_publish_over_http' diff --git a/common/pulp_python/common/model.py b/common/pulp_python/common/model.py deleted file mode 100644 index e69de29b..00000000 diff --git a/extensions_admin/pulp_python/extensions/admin/cudl.py b/extensions_admin/pulp_python/extensions/admin/cudl.py index bf455d7d..e986189f 100644 --- a/extensions_admin/pulp_python/extensions/admin/cudl.py +++ b/extensions_admin/pulp_python/extensions/admin/cudl.py @@ -30,7 +30,7 @@ class CreatePythonRepositoryCommand(CreateAndConfigureRepositoryCommand, ImporterConfigMixin): default_notes = {REPO_NOTE_TYPE_KEY: constants.REPO_NOTE_PYTHON} - IMPORTER_TYPE_ID = constants.WEB_IMPORTER_TYPE_ID + IMPORTER_TYPE_ID = constants.IMPORTER_TYPE_ID def __init__(self, context): CreateAndConfigureRepositoryCommand.__init__(self, context) @@ -53,10 +53,10 @@ def _describe_distributors(self, user_input): config = {} auto_publish = user_input.get('auto-publish', True) data = [ - dict(distributor_type_id=constants.WEB_DISTRIBUTOR_TYPE_ID, + dict(distributor_type_id=constants.DISTRIBUTOR_TYPE_ID, distributor_config=config, auto_publish=auto_publish, - distributor_id=constants.CLI_WEB_DISTRIBUTOR_ID), + distributor_id=constants.CLI_DISTRIBUTOR_ID), ] return data @@ -104,7 +104,7 @@ def run(self, **kwargs): if web_config: kwargs['distributor_configs'] = {} - kwargs['distributor_configs'][constants.CLI_WEB_DISTRIBUTOR_ID] = web_config + kwargs['distributor_configs'][constants.CLI_DISTRIBUTOR_ID] = web_config super(UpdatePythonRepositoryCommand, self).run(**kwargs) diff --git a/extensions_admin/pulp_python/extensions/admin/pulp_cli.py b/extensions_admin/pulp_python/extensions/admin/pulp_cli.py index 63dc2f95..f0964916 100644 --- a/extensions_admin/pulp_python/extensions/admin/pulp_cli.py +++ b/extensions_admin/pulp_python/extensions/admin/pulp_cli.py @@ -4,9 +4,9 @@ from pulp.client.extensions.decorator import priority from pulp_python.common import constants -from pulp_python.extensions.admin.cudl import CreatePythonRepositoryCommand -from pulp_python.extensions.admin.cudl import UpdatePythonRepositoryCommand -from pulp_python.extensions.admin.cudl import ListPythonRepositoriesCommand +from pulp_python.extensions.admin.cudl import ( + CreatePythonRepositoryCommand, UpdatePythonRepositoryCommand, ListPythonRepositoriesCommand) +from pulp_python.extensions.admin import upload SECTION_ROOT = 'python' @@ -31,12 +31,10 @@ def initialize(context): :type context: pulp.client.extensions.core.ClientContext """ root_section = context.cli.create_section(SECTION_ROOT, DESC_ROOT) - repo_section = add_repo_section(context, root_section) - add_publish_section(context, repo_section) - add_sync_section(context, repo_section) + _add_repo_section(context, root_section) -def add_repo_section(context, parent_section): +def _add_repo_section(context, parent_section): """ add a repo section to the python section @@ -53,10 +51,13 @@ def add_repo_section(context, parent_section): repo_section.add_command(cudl.DeleteRepositoryCommand(context)) repo_section.add_command(ListPythonRepositoriesCommand(context)) - return repo_section + _add_publish_section(context, repo_section) + _add_sync_section(context, repo_section) + repo_section.add_command(upload.UploadPackageCommand(context)) -def add_publish_section(context, parent_section): + +def _add_publish_section(context, parent_section): """ add a publish section to the repo section @@ -71,14 +72,12 @@ def add_publish_section(context, parent_section): section.add_command( sync_publish.RunPublishRepositoryCommand(context, renderer, - constants.CLI_WEB_DISTRIBUTOR_ID)) + constants.CLI_DISTRIBUTOR_ID)) section.add_command( sync_publish.PublishStatusCommand(context, renderer)) - return section - -def add_sync_section(context, parent_section): +def _add_sync_section(context, parent_section): """ add a sync section @@ -94,5 +93,3 @@ def add_sync_section(context, parent_section): sync_section = parent_section.create_subsection(SECTION_SYNC, DESC_SYNC) sync_section.add_command(sync_publish.RunSyncRepositoryCommand(context, renderer)) - - return sync_section diff --git a/extensions_admin/pulp_python/extensions/admin/upload.py b/extensions_admin/pulp_python/extensions/admin/upload.py new file mode 100644 index 00000000..3e0f60cd --- /dev/null +++ b/extensions_admin/pulp_python/extensions/admin/upload.py @@ -0,0 +1,35 @@ +from pulp.client.commands.repo import upload + +from pulp_python.common import constants + + +class UploadPackageCommand(upload.UploadCommand): + """ + The command used to upload Python packages. + """ + def determine_type_id(self, filename, **kwargs): + """ + Return pulp_python.common.constants.PACKAGE_TYPE_ID. + + :param filename: Unused + :type filename: basestring + :param kwargs: Unused + :type kwargs: dict + :returns: pulp_python.common.constants.PACKAGE_TYPE_ID + :rtype: basestring + """ + return constants.PACKAGE_TYPE_ID + + def generate_unit_key(self, *args, **kwargs): + """ + We don't need to generate the unit key client-side, but the superclass requires us to define + this method. It returns the empty dictionary. + + :param args: Unused + :type args: list + :param kwargs: Unused + :type kwargs: dict + :returns: An empty dictionary + :rtype: dict + """ + return {} diff --git a/extensions_admin/test/unit/admin/test_cudl.py b/extensions_admin/test/unit/admin/test_cudl.py index 0664ddcf..ae123257 100644 --- a/extensions_admin/test/unit/admin/test_cudl.py +++ b/extensions_admin/test/unit/admin/test_cudl.py @@ -18,14 +18,14 @@ def test_default_notes(self): def test_importer_id(self): # this value is required to be set, so just make sure it's correct self.assertEqual(cudl.CreatePythonRepositoryCommand.IMPORTER_TYPE_ID, - constants.WEB_IMPORTER_TYPE_ID) + constants.IMPORTER_TYPE_ID) def test_describe_distributors(self): command = cudl.CreatePythonRepositoryCommand(Mock()) user_input = {} result = command._describe_distributors(user_input) - target_result = {'distributor_id': constants.CLI_WEB_DISTRIBUTOR_ID, - 'distributor_type_id': constants.WEB_DISTRIBUTOR_TYPE_ID, + target_result = {'distributor_id': constants.CLI_DISTRIBUTOR_ID, + 'distributor_type_id': constants.DISTRIBUTOR_TYPE_ID, 'distributor_config': {}, 'auto_publish': True} compare_dict(result[0], target_result) @@ -76,7 +76,7 @@ def test_repo_update_distributors(self): self.command.run(**user_input) repo_config = {} - dist_config = {constants.CLI_WEB_DISTRIBUTOR_ID: {'auto_publish': False}} + dist_config = {constants.CLI_DISTRIBUTOR_ID: {'auto_publish': False}} self.context.server.repo.update.assert_called_once_with('foo-repo', repo_config, None, dist_config) @@ -121,7 +121,7 @@ def test_get_repositories(self): {'config': {}} ], 'distributors': [ - {'id': constants.CLI_WEB_DISTRIBUTOR_ID} + {'id': constants.CLI_DISTRIBUTOR_ID} ] }, {'id': 'non-rpm-repo', @@ -165,7 +165,7 @@ def test_get_other_repositories(self): 'repo_id': 'matching', 'notes': {REPO_NOTE_TYPE_KEY: constants.REPO_NOTE_PYTHON, }, 'distributors': [ - {'id': constants.CLI_WEB_DISTRIBUTOR_ID} + {'id': constants.CLI_DISTRIBUTOR_ID} ] }, { diff --git a/extensions_admin/test/unit/admin/test_pulp_cli.py b/extensions_admin/test/unit/admin/test_pulp_cli.py index 1dcf4eec..852fff32 100644 --- a/extensions_admin/test/unit/admin/test_pulp_cli.py +++ b/extensions_admin/test/unit/admin/test_pulp_cli.py @@ -8,7 +8,7 @@ RunPublishRepositoryCommand, RunSyncRepositoryCommand from pulp.client.extensions.core import PulpCli -from pulp_python.extensions.admin import pulp_cli +from pulp_python.extensions.admin import pulp_cli, upload class TestInitialize(unittest.TestCase): @@ -31,6 +31,7 @@ def test_structure(self): self.assertTrue(isinstance(repo_section.commands['delete'], DeleteRepositoryCommand)) self.assertTrue(isinstance(repo_section.commands['update'], UpdateRepositoryCommand)) self.assertTrue(isinstance(repo_section.commands['list'], ListRepositoriesCommand)) + self.assertTrue(isinstance(repo_section.commands['upload'], upload.UploadPackageCommand)) section = repo_section.subsections['sync'] self.assertTrue(isinstance(section.commands['run'], RunSyncRepositoryCommand)) diff --git a/extensions_admin/test/unit/admin/test_upload.py b/extensions_admin/test/unit/admin/test_upload.py new file mode 100644 index 00000000..9cd810d1 --- /dev/null +++ b/extensions_admin/test/unit/admin/test_upload.py @@ -0,0 +1,35 @@ +""" +This module contains tests for the pulp_python.extensions.admin.upload module. +""" +import unittest + +import mock + +from pulp_python.common import constants +from pulp_python.extensions.admin import upload + + +@mock.patch('pulp.client.upload.manager.UploadManager.init_with_defaults', mock.MagicMock()) +class TestUploadPackageCommand(unittest.TestCase): + """ + This class contains tests for the UploadPackageCommand class. + """ + def test_determine_type_id(self): + """ + Assert that determine_type_id() returns the correct type. + """ + command = upload.UploadPackageCommand(mock.MagicMock()) + + type_id = command.determine_type_id('some_file_name', some='kwargs') + + self.assertEqual(type_id, constants.PACKAGE_TYPE_ID) + + def test_generate_unit_key(self): + """ + Assert that generate_unit_key() returns the empty dictionary. + """ + command = upload.UploadPackageCommand(mock.MagicMock()) + + key = command.generate_unit_key('some', 'args', and_some='kwargs') + + self.assertEqual(key, {}) diff --git a/plugins/etc/httpd/conf.d/pulp_ostree.conf b/plugins/etc/httpd/conf.d/pulp_python.conf similarity index 100% rename from plugins/etc/httpd/conf.d/pulp_ostree.conf rename to plugins/etc/httpd/conf.d/pulp_python.conf diff --git a/plugins/pulp_python/plugins/distributors/configuration.py b/plugins/pulp_python/plugins/distributors/configuration.py index b3482071..3a3517f5 100644 --- a/plugins/pulp_python/plugins/distributors/configuration.py +++ b/plugins/pulp_python/plugins/distributors/configuration.py @@ -27,7 +27,7 @@ def get_root_publish_directory(config): :return: The publish directory for the python plugin :rtype: str """ - return config.get(constants.CONFIG_KEY_PYTHON_PUBLISH_DIRECTORY) + return config.get(constants.CONFIG_KEY_PUBLISH_DIRECTORY) def get_master_publish_dir(repo, config): diff --git a/plugins/pulp_python/plugins/distributors/steps.py b/plugins/pulp_python/plugins/distributors/steps.py index 1e7c028b..059fe48b 100644 --- a/plugins/pulp_python/plugins/distributors/steps.py +++ b/plugins/pulp_python/plugins/distributors/steps.py @@ -11,10 +11,10 @@ logger = logging.getLogger(__name__) -class WebPublisher(PluginStep): +class PythonPublisher(PluginStep): """ - Web publisher class that is responsible for the actual publishing - of a repository via a web server + Publisher class that is responsible for the actual publishing + of a repository via a web server. """ def __init__(self, repo, publish_conduit, config): """ @@ -25,8 +25,8 @@ def __init__(self, repo, publish_conduit, config): :param config: Pulp configuration for the distributor :type config: pulp.plugins.config.PluginCallConfiguration """ - super(WebPublisher, self).__init__(constants.PUBLISH_STEP_WEB_PUBLISHER, - repo, publish_conduit, config) + super(PythonPublisher, self).__init__(constants.PUBLISH_STEP_PUBLISHER, + repo, publish_conduit, config) publish_dir = configuration.get_web_publish_dir(repo, config) os.makedirs(self.get_working_dir()) diff --git a/plugins/pulp_python/plugins/distributors/web_distributor.py b/plugins/pulp_python/plugins/distributors/web.py similarity index 91% rename from plugins/pulp_python/plugins/distributors/web_distributor.py rename to plugins/pulp_python/plugins/distributors/web.py index 080abede..932e9bfc 100644 --- a/plugins/pulp_python/plugins/distributors/web_distributor.py +++ b/plugins/pulp_python/plugins/distributors/web.py @@ -8,11 +8,11 @@ from pulp_python.common import constants from pulp_python.plugins.distributors import configuration -from pulp_python.plugins.distributors.steps import WebPublisher +from pulp_python.plugins.distributors.steps import PythonPublisher PLUGIN_DEFAULT_CONFIG = { - constants.CONFIG_KEY_PYTHON_PUBLISH_DIRECTORY: constants.CONFIG_VALUE_PYTHON_PUBLISH_DIRECTORY + constants.CONFIG_KEY_PUBLISH_DIRECTORY: constants.CONFIG_VALUE_PUBLISH_DIRECTORY } _logger = logging.getLogger(__name__) @@ -30,10 +30,10 @@ def entry_point(): plugin_config.update(edited_config) - return WebDistributor, plugin_config + return PythonDistributor, plugin_config -class WebDistributor(Distributor): +class PythonDistributor(Distributor): @classmethod def metadata(cls): @@ -51,16 +51,16 @@ def metadata(cls): :rtype: dict """ return { - 'id': constants.WEB_DISTRIBUTOR_TYPE_ID, - 'display_name': _('Python Web Distributor'), - 'types': [constants.REPOSITORY_TYPE_ID] + 'id': constants.DISTRIBUTOR_TYPE_ID, + 'display_name': _('Python Python Distributor'), + 'types': [constants.PACKAGE_TYPE_ID] } def __init__(self): """ - Initialize the WebDistributor. + Initialize the PythonDistributor. """ - super(WebDistributor, self).__init__() + super(PythonDistributor, self).__init__() self._publisher = None self.canceled = False @@ -86,7 +86,7 @@ def publish_repo(self, repo, publish_conduit, config): :rtype: pulp.plugins.model.PublishReport """ _logger.debug('Publishing Python repository: %s' % repo.id) - self._publisher = WebPublisher(repo, publish_conduit, config) + self._publisher = PythonPublisher(repo, publish_conduit, config) return self._publisher.process_lifecycle() def cancel_publish_repo(self): diff --git a/plugins/pulp_python/plugins/importers/importer.py b/plugins/pulp_python/plugins/importers/importer.py new file mode 100644 index 00000000..4fbb3416 --- /dev/null +++ b/plugins/pulp_python/plugins/importers/importer.py @@ -0,0 +1,109 @@ +from gettext import gettext as _ +import shutil + +from pulp.plugins.importer import Importer + +from pulp_python.common import constants +from pulp_python.plugins import models + + +def entry_point(): + """ + Entry point that pulp platform uses to load the importer + + :return: 2-tuple of the importer class and its config + :rtype: tuple + """ + return PythonImporter, {} + + +class PythonImporter(Importer): + """ + This class is used to import Python modules into Pulp. + """ + @classmethod + def metadata(cls): + """ + Used by Pulp to classify the capabilities of this importer. The + following keys must be present in the returned dictionary: + + * id - Programmatic way to refer to this importer. Must be unique + across all importers. Only letters and underscores are valid. + * display_name - User-friendly identification of the importer. + * types - List of all content type IDs that may be imported using this + importer. + + :return: keys and values listed above + :rtype: dict + """ + return { + 'id': constants.IMPORTER_TYPE_ID, + 'display_name': _('Python Importer'), + 'types': [constants.PACKAGE_TYPE_ID] + } + + def upload_unit(self, repo, type_id, unit_key, metadata, file_path, conduit, config): + """ + Handles a user request to upload a unit into a repository. This call + should use the data provided to add the unit as if it were synchronized + from an external source. This includes: + + * Initializing the unit through the conduit which populates the final + destination of the unit. + * Moving the unit from the provided temporary location into the unit's + final destination. + * Saving the unit in Pulp, which both adds the unit to Pulp's database and + associates it to the repository. + + This call may be invoked for either units that do not already exist as + well as re-uploading an existing unit. + + The metadata parameter is variable in its usage. In some cases, the + unit may be almost exclusively metadata driven in which case the contents + of this parameter will be used directly as the unit's metadata. In others, + it may function to remove the importer's need to derive the unit's metadata + from the uploaded unit file. In still others, it may be extraneous + user-specified information that should be merged in with any derived + unit metadata. + + Depending on the unit type, it is possible that this call will create + multiple units within Pulp. It is also possible that this call will + create one or more relationships between existing units. + + :param repo: metadata describing the repository + :type repo: pulp.plugins.model.Repository + :param type_id: type of unit being uploaded + :type type_id: str + :param unit_key: identifier for the unit, specified by the user + :type unit_key: dict + :param metadata: any user-specified metadata for the unit + :type metadata: dict + :param file_path: path on the Pulp server's filesystem to the temporary location of the + uploaded file; may be None in the event that a unit is comprised entirely + of metadata and has no bits associated + :type file_path: str + :param conduit: provides access to relevant Pulp functionality + :type conduit: pulp.plugins.conduits.unit_add.UnitAddConduit + :param config: plugin configuration for the repository + :type config: pulp.plugins.config.PluginCallConfiguration + :return: A dictionary describing the success or failure of the upload. It must + contain the following keys: + 'success_flag': bool. Indicates whether the upload was successful + 'summary': json-serializable object, providing summary + 'details': json-serializable object, providing details + :rtype: dict + """ + package = models.Package.from_archive(file_path) + package.init_unit(conduit) + + shutil.move(file_path, package.storage_path) + + package.save_unit(conduit) + + return {'success_flag': True, 'summary': {}, 'details': {}} + + def validate_config(self, repo, config): + """ + We don't have a config yet, so it's always valid + """ + return True, '' diff --git a/plugins/pulp_python/plugins/importers/web.py b/plugins/pulp_python/plugins/importers/web.py deleted file mode 100644 index 5b80349b..00000000 --- a/plugins/pulp_python/plugins/importers/web.py +++ /dev/null @@ -1,102 +0,0 @@ -from gettext import gettext as _ - -from pulp.plugins.importer import Importer - -from pulp_python.common import constants - - -def entry_point(): - """ - Entry point that pulp platform uses to load the importer - - :return: importer class and its config - :rtype: Importer, dict - """ - return WebImporter, {} - - -class WebImporter(Importer): - - @classmethod - def metadata(cls): - """ - Used by Pulp to classify the capabilities of this importer. The - following keys must be present in the returned dictionary: - - * id - Programmatic way to refer to this importer. Must be unique - across all importers. Only letters and underscores are valid. - * display_name - User-friendly identification of the importer. - * types - List of all content type IDs that may be imported using this - importer. - - :return: keys and values listed above - :rtype: dict - """ - return { - 'id': constants.WEB_IMPORTER_TYPE_ID, - 'display_name': _('Python Web Importer'), - 'types': [constants.REPOSITORY_TYPE_ID] - } - - def validate_config(self, repo, config): - """ - We don't have a config yet, so it's always valid - """ - return True, '' - - def sync_repo(self, repo, sync_conduit, config): - """ - Synchronizes content into the given repository. This call is responsible - for adding new content units to Pulp as well as associating them to the - given repository. - - While this call may be implemented using multiple threads, its execution - from the Pulp server's standpoint should be synchronous. This call should - not return until the sync is complete. - - It is not expected that this call be atomic. Should an error occur, it - is not the responsibility of the importer to rollback any unit additions - or associations that have been made. - - The returned report object is used to communicate the results of the - sync back to the user. Care should be taken to i18n the free text "log" - attribute in the report if applicable. - - :param repo: metadata describing the repository - :type repo: pulp.plugins.model.Repository - :param sync_conduit: provides access to relevant Pulp functionality - :type sync_conduit: pulp.plugins.conduits.repo_sync.RepoSyncConduit - :param config: plugin configuration - :type config: pulp.plugins.config.PluginCallConfiguration - :return: report of the details of the sync - :rtype: pulp.plugins.model.SyncReport - """ - pass - - def cancel_sync_repo(self): - """ - Cancels an in-progress sync. - - This call is responsible for halting a current sync by stopping any - in-progress downloads and performing any cleanup necessary to get the - system back into a stable state. - """ - pass - - def remove_units(self, repo, units, config): - """ - Removes content units from the given repository. - - This method also removes the tags associated with images in the repository. - - This call will not result in the unit being deleted from Pulp itself. - - :param repo: metadata describing the repository - :type repo: pulp.plugins.model.Repository - :param units: list of objects describing the units to import in - this call - :type units: list of pulp.plugins.model.AssociatedUnit - :param config: plugin configuration - :type config: pulp.plugins.config.PluginCallConfiguration - """ - pass diff --git a/plugins/pulp_python/plugins/models.py b/plugins/pulp_python/plugins/models.py new file mode 100644 index 00000000..1502ac79 --- /dev/null +++ b/plugins/pulp_python/plugins/models.py @@ -0,0 +1,186 @@ +from gettext import gettext as _ +import re +import tarfile + +from pulp_python.common import constants + + +class Package(object): + """ + This class represents a Python package. + """ + TYPE = constants.PACKAGE_TYPE_ID + # The full list of supported attributes. Attributes beginning with underscore are specific to + # this module and are not found in PKG-INFO. + _ATTRS = ('name', 'version', 'summary', 'home_page', 'author', 'author_email', 'license', + 'description', 'platform', '_filename') + + @classmethod + def from_archive(cls, archive_path): + """ + Instantiate a Package using the metadata found inside the Python package found at + archive_path. This tarball should be the build result of running setup.py sdist on the + package, and should contain a PKG-INFO file. This method will read the PKG-INFO to determine + the package's metadata and unit key. + + :param archive_path: A filesystem path to the Python source distribution that this Package + will represent. + :type archive_path: basestring + :return: An instance of Package that represents the package found at + archive_path. + :rtype: pulp.common.models.Package + :raises: ValueError if archive_path does not point to a valid Python tarball + created with setup.py sdist. + :raises: IOError if the archive_path does not exist. + """ + try: + compression_type = cls._compression_type(archive_path) + package_archive = tarfile.open(archive_path) + for member in package_archive.getmembers(): + if re.match('.*/PKG-INFO$|^PKG-INFO$', member.name): + # We have found the metadata! + metadata_file = member + break + if 'metadata_file' not in locals(): + msg = _('The archive at %(path)s does not contain a PKG-INFO file.') + msg = msg % {'path': archive_path} + raise ValueError(msg) + + metadata_file = package_archive.extractfile(metadata_file) + metadata = metadata_file.read() + + # Build a list of tuples of all the attributes found in the metadata. Ignore attributes + # with a leading underscore, as they are not part of the metadata. + try: + required_attrs = [attr for attr in cls._ATTRS if attr[0] != '_'] + attrs = dict() + for attr in required_attrs: + attrs[attr] = re.search('^%s: (?P.*)$' % cls._metadata_label(attr), + metadata, flags=re.MULTILINE).group('field') + except AttributeError: + msg = _('The PKG-INFO file is missing required attributes. Please ensure that the ' + 'following attributes are all present: %(attrs)s') + msg = msg % { + 'attrs': ', '.join([cls._metadata_label(attr) for attr in required_attrs])} + raise ValueError(msg) + + # Add the filename to the attrs + attrs['_filename'] = '%s-%s.tar%s' % (attrs['name'], attrs['version'], compression_type) + package = cls(**attrs) + return package + finally: + if 'package_archive' in locals(): + package_archive.close() + + def init_unit(self, conduit): + """ + Use the given conduit's init_unit() method to initialize this Unit and store the underlying + Pulp unit as self._unit. + + :param conduit: A conduit with a suitable init_unit() to create a Pulp Unit. + :type conduit: pulp.plugins.conduits.mixins.AddUnitMixin + """ + relative_path = self._filename + unit_key = {'name': self.name, 'version': self.version} + metadata = [] + for attr in self._ATTRS: + if attr in unit_key: + continue + metadata.append((attr, getattr(self, attr))) + metadata = dict(metadata) + self._unit = conduit.init_unit(self.TYPE, unit_key, metadata, relative_path) + + def save_unit(self, conduit): + """ + Use the given conduit's save_unit() method to save self._unit. + + :param conduit: A conduit with a suitable save_unit() to save self._unit. + :type conduit: pulp.plugins.conduits.mixins.AddUnitMixin + """ + conduit.save_unit(self._unit) + + @property + def storage_path(self): + """ + Return the storage path for self._unit. + + :return: The Unit storage path. + :rtype: basestring + """ + return self._unit.storage_path + + @staticmethod + def _compression_type(path): + """ + Return the type of compression used in the file at path. Can be '', '.bz2', '.gz', or + '.zip'. '' is returned if the file at path matches none of the magic signatures. This + algorithm is based on http://stackoverflow.com/a/13044946. + + :param path: The path to the file you wish to test for compression type. + :type path: basestring + :return: File extension used to represent the compression type found at path. + :rtype: basestring + """ + magic_dict = { + "\x1f\x8b\x08": ".gz", + "\x42\x5a\x68": ".bz2", + "\x50\x4b\x03\x04": ".zip" + } + # We need to read the first four bytes of the file to compare + with open(path) as the_file: + header = the_file.read(4) + for magic, filetype in magic_dict.items(): + if header.startswith(magic): + return filetype + return '' + + @staticmethod + def _metadata_label(attribute): + """ + Return the label in the PKG-INFO file that corresponds to the given attribute. + + :param attribute: The attribute on a Package for which you wish to know the PKG-INFO label + :type attribute: basestring + :return: The label in the PKG-INFO file that can be used to get the field + :rtype: basestring + """ + label = attribute[0].upper() + attribute[1:] + return label.replace('_', '-') + + def __init__(self, name, version, summary, home_page, author, author_email, license, + description, platform, _filename): + """ + Initialize self with the given parameters as its attributes. + + :param name: The name of the package. + :type name: basestring + :param version: The package's version. + :type version: basestring + :param summary: A paragraph summarizing the package. + :type summary: basestring + :param home_page: A URL for the package's website. + :type home_page: basestring + :param author: The author's name. + :type author: basestring + :param author_email: The author's e-mail address. + :type author_email: basestring + :param license: The package's license. + :type license: basestring + :param description: A description of the package. + :type description: basestring + :param platform: A list of platforms that the package is intended to be used on. + :type platform: basestring + """ + for attr in self._ATTRS: + setattr(self, attr, locals()[attr]) + + self._unit = None + + def __repr__(self): + """ + Return a string representation of self. + + :return: A string representing self. + :rtype: basestring + """ + return 'Python Package: %(name)s-%(version)s' % {'name': self.name, 'version': self.version} diff --git a/plugins/setup.py b/plugins/setup.py index ddfe2b55..57c45a04 100755 --- a/plugins/setup.py +++ b/plugins/setup.py @@ -12,7 +12,7 @@ description='plugins for python support in pulp', entry_points={ 'pulp.importers': [ - 'importer = pulp_python.plugins.importers.web:entry_point', + 'importer = pulp_python.plugins.importers.importer:entry_point', ], 'pulp.distributors': [ 'distributor = pulp_python.plugins.distributors.web:entry_point' diff --git a/plugins/test/unit/plugins/distributors/test_configuration.py b/plugins/test/unit/plugins/distributors/test_configuration.py index 89981ede..540b00ab 100644 --- a/plugins/test/unit/plugins/distributors/test_configuration.py +++ b/plugins/test/unit/plugins/distributors/test_configuration.py @@ -19,7 +19,7 @@ def setUp(self): self.repo_working = os.path.join(self.working_directory, 'work') self.repo = Mock(id='foo', working_dir=self.repo_working) - self.config = PluginCallConfiguration({constants.CONFIG_KEY_PYTHON_PUBLISH_DIRECTORY: + self.config = PluginCallConfiguration({constants.CONFIG_KEY_PUBLISH_DIRECTORY: self.publish_dir}, {}) def tearDown(self): diff --git a/plugins/test/unit/plugins/distributors/test_steps.py b/plugins/test/unit/plugins/distributors/test_steps.py index 39909b75..096aff8e 100644 --- a/plugins/test/unit/plugins/distributors/test_steps.py +++ b/plugins/test/unit/plugins/distributors/test_steps.py @@ -21,7 +21,7 @@ def setUp(self): self.repo_working = os.path.join(self.working_directory, 'work') self.repo = Mock(id='foo', working_dir=self.repo_working) - self.config = PluginCallConfiguration({constants.CONFIG_KEY_PYTHON_PUBLISH_DIRECTORY: + self.config = PluginCallConfiguration({constants.CONFIG_KEY_PUBLISH_DIRECTORY: self.publish_dir}, {}) def tearDown(self): @@ -33,9 +33,9 @@ def tearDown(self): def test_init(self, mock_metadata, mock_content, mock_atomic): mock_conduit = Mock() mock_config = { - constants.CONFIG_KEY_PYTHON_PUBLISH_DIRECTORY: self.publish_dir + constants.CONFIG_KEY_PUBLISH_DIRECTORY: self.publish_dir } - publisher = steps.WebPublisher(self.repo, mock_conduit, mock_config) + publisher = steps.PythonPublisher(self.repo, mock_conduit, mock_config) self.assertEquals(publisher.children, [mock_metadata.return_value, mock_content.return_value, mock_atomic.return_value]) diff --git a/plugins/test/unit/plugins/distributors/test_web_distributor.py b/plugins/test/unit/plugins/distributors/test_web.py similarity index 75% rename from plugins/test/unit/plugins/distributors/test_web_distributor.py rename to plugins/test/unit/plugins/distributors/test_web.py index 9e738f24..cd932ebf 100644 --- a/plugins/test/unit/plugins/distributors/test_web_distributor.py +++ b/plugins/test/unit/plugins/distributors/test_web.py @@ -11,17 +11,17 @@ from pulp.plugins.model import Repository from pulp_python.common import constants -from pulp_python.plugins.distributors import web_distributor +from pulp_python.plugins.distributors import web class TestEntryPoint(unittest.TestCase): def test_returns_importer(self): - distributor, config = web_distributor.entry_point() + distributor, config = web.entry_point() self.assertTrue(issubclass(distributor, Distributor)) def test_returns_config(self): - distributor, config = web_distributor.entry_point() + distributor, config = web.entry_point() # make sure it's at least the correct type self.assertTrue(isinstance(config, dict)) @@ -30,20 +30,20 @@ def test_returns_config(self): class TestBasics(unittest.TestCase): def setUp(self): - self.distributor = web_distributor.WebDistributor() + self.distributor = web.PythonDistributor() self.working_dir = tempfile.mkdtemp() def tearDown(self): shutil.rmtree(self.working_dir) def test_metadata(self): - metadata = web_distributor.WebDistributor.metadata() + metadata = web.PythonDistributor.metadata() - self.assertEqual(metadata['id'], constants.WEB_DISTRIBUTOR_TYPE_ID) + self.assertEqual(metadata['id'], constants.DISTRIBUTOR_TYPE_ID) self.assertTrue(len(metadata['display_name']) > 0) - @patch('pulp_python.plugins.distributors.web_distributor.configuration.get_master_publish_dir') - @patch('pulp_python.plugins.distributors.web_distributor.configuration.get_web_publish_dir') + @patch('pulp_python.plugins.distributors.web.configuration.get_master_publish_dir') + @patch('pulp_python.plugins.distributors.web.configuration.get_web_publish_dir') def test_distributor_removed(self, mock_web, mock_master): mock_web.return_value = os.path.join(self.working_dir, 'web') @@ -57,7 +57,7 @@ def test_distributor_removed(self, mock_web, mock_master): self.assertEquals(0, len(os.listdir(self.working_dir))) - @patch('pulp_python.plugins.distributors.web_distributor.WebPublisher') + @patch('pulp_python.plugins.distributors.web.PythonPublisher') def test_publish_repo(self, mock_publisher): repo = Repository('test') config = PluginCallConfiguration(None, None) @@ -73,7 +73,7 @@ def test_cancel_publish_repo(self): self.distributor._publisher.cancel.assert_called_once() - @patch('pulp_python.plugins.distributors.web_distributor.configuration.validate_config') + @patch('pulp_python.plugins.distributors.web.configuration.validate_config') def test_validate_config(self, mock_validate): value = self.distributor.validate_config(Mock(), 'foo', Mock()) mock_validate.assert_called_once_with('foo') diff --git a/plugins/test/unit/plugins/importers/test_importer.py b/plugins/test/unit/plugins/importers/test_importer.py new file mode 100644 index 00000000..11793a0f --- /dev/null +++ b/plugins/test/unit/plugins/importers/test_importer.py @@ -0,0 +1,91 @@ +""" +Contains tests for pulp_python.plugins.importers.importer. +""" +from gettext import gettext as _ +import unittest + +import mock + +from pulp_python.common import constants +from pulp_python.plugins import models +from pulp_python.plugins.importers import importer + + +class TestEntryPoint(unittest.TestCase): + """ + Tests for the entry_point() function. + """ + def test_return_value(self): + """ + Assert the correct return value for the entry_point() function. + """ + return_value = importer.entry_point() + + expected_value = (importer.PythonImporter, {}) + self.assertEqual(return_value, expected_value) + + +class TestPythonImporter(unittest.TestCase): + """ + This class contains tests for the PythonImporter class. + """ + def test_metadata(self): + """ + Test the metadata class method's return value. + """ + metadata = importer.PythonImporter.metadata() + + expected_value = { + 'id': constants.IMPORTER_TYPE_ID, 'display_name': _('Python Importer'), + 'types': [constants.PACKAGE_TYPE_ID]} + self.assertEqual(metadata, expected_value) + + @mock.patch('pulp_python.plugins.models.Package.from_archive') + @mock.patch('pulp_python.plugins.models.Package.init_unit', autospec=True) + @mock.patch('pulp_python.plugins.models.Package.save_unit', autospec=True) + @mock.patch('shutil.move') + def test_upload_unit(self, move, save_unit, init_unit, from_archive): + """ + Assert correct operation of upload_unit(). + """ + package = models.Package( + 'name', 'version', 'summary', 'home_page', 'author', 'author_email', 'license', + 'description', 'platform', '_filename') + from_archive.return_value = package + storage_path = '/some/path/name-version.tar.bz2' + + def init_unit_side_effect(self, conduit): + class Unit(object): + def __init__(self, *args, **kwargs): + self.storage_path = storage_path + self._unit = Unit() + init_unit.side_effect = init_unit_side_effect + + python_importer = importer.PythonImporter() + repo = mock.MagicMock() + type_id = constants.PACKAGE_TYPE_ID + unit_key = {} + metadata = {} + file_path = '/some/path/1234' + conduit = mock.MagicMock() + config = {} + + report = python_importer.upload_unit(repo, type_id, unit_key, metadata, file_path, conduit, + config) + + self.assertEqual(report, {'success_flag': True, 'summary': {}, 'details': {}}) + from_archive.assert_called_once_with(file_path) + init_unit.assert_called_once_with(package, conduit) + save_unit.assert_called_once_with(package, conduit) + move.assert_called_once_with(file_path, storage_path) + + def test_validate_config(self): + """ + There is no config, so we'll just assert that validation passes. + """ + python_importer = importer.PythonImporter() + + return_value = python_importer.validate_config(mock.MagicMock(), {}) + + expected_value = (True, '') + self.assertEqual(return_value, expected_value) diff --git a/plugins/test/unit/plugins/test_models.py b/plugins/test/unit/plugins/test_models.py new file mode 100644 index 00000000..9d781999 --- /dev/null +++ b/plugins/test/unit/plugins/test_models.py @@ -0,0 +1,531 @@ +""" +This modules contains tests for pulp_python.plugins.models. +""" +from gettext import gettext as _ +import tarfile +import unittest + +import mock + +from pulp_python.common import constants +from pulp_python.plugins import models + + +# The BAD_MANIFEST is missing the Version field. +BAD_MANIFEST = """Metadata-Version: 1.1 +Name: nectar +Summary: Performance tuned network download client library +Home-page: https://github.com/pulp/nectar +Author: Pulp Team +Author-email: pulp-list@redhat.com +License: GPLv2 +Description: UNKNOWN +Platform: UNKNOWN +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Information Technology +Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2) +Classifier: Operating System :: POSIX +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Topic :: Software Development :: Libraries :: Python Modules""" + +GOOD_MANIFEST = """Metadata-Version: 1.1 +Name: nectar +Version: 1.3.1 +Summary: Performance tuned network download client library +Home-page: https://github.com/pulp/nectar +Author: Pulp Team +Author-email: pulp-list@redhat.com +License: GPLv2 +Description: UNKNOWN +Platform: UNKNOWN +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Information Technology +Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2) +Classifier: Operating System :: POSIX +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Topic :: Software Development :: Libraries :: Python Modules""" + + +class TestPackage(unittest.TestCase): + """ + This class contains tests for the Package class. + """ + def test_class_attributes(self): + """ + Assert correct class attributes. + """ + self.assertEqual(models.Package.TYPE, constants.PACKAGE_TYPE_ID) + self.assertEqual( + models.Package._ATTRS, ('name', 'version', 'summary', 'home_page', 'author', + 'author_email', 'license', 'description', 'platform', + '_filename')) + + @mock.patch('pulp_python.plugins.models.Package._compression_type', return_value='.gz') + @mock.patch('pulp_python.plugins.models.tarfile.open') + def test_from_archive_closely_named_metadata(self, tarfile_open, _compression_type): + """ + Test from_archive() with files named very similarly to PKG-INFO to test the regex. This also + coincidentally tests behavior when the archive is missing metadata. + """ + tarfile_open.return_value = mock.MagicMock(spec=tarfile.TarFile) + + class TarInfo(object): + """ + This class fakes being a TarInfo. It just needs a name. + """ + def __init__(self, name): + self.name = name + + members = [TarInfo(name) for name in ['aPKG-INFO', 'PKG-INFO.txt', '/path/to/PKG-INFO.txt' + '/path/to/aPKG-INFO']] + tarfile_open.return_value.getmembers.return_value = members + path = '/some/path.tar.gz' + + try: + models.Package.from_archive(path) + self.fail('The above call should have raised a ValueError!') + except ValueError as e: + self.assertTrue( + _('The archive at %s does not contain a PKG-INFO file.') % path in str(e)) + + tarfile_open.assert_called_once_with(path) + _compression_type.assert_called_once_with(path) + tarfile_open.return_value.close.assert_called_once_with() + + @mock.patch('pulp_python.plugins.models.Package._compression_type', return_value='.gz') + @mock.patch('pulp_python.plugins.models.tarfile.open') + def test_from_archive_empty_metadata(self, tarfile_open, _compression_type): + """ + Test from_archive() when the PKG-INFO file is empty. + """ + tarfile_open.return_value = mock.MagicMock(spec=tarfile.TarFile) + + class TarInfo(object): + """ + This class fakes being a TarInfo. It just needs a name. + """ + def __init__(self, name): + self.name = name + + members = [TarInfo(name) for name in ['package-1.2.3', 'package-1.2.3/PKG-INFO']] + tarfile_open.return_value.getmembers.return_value = members + mock_manifest_file = mock.MagicMock(spec=file) + mock_manifest_file.read.return_value = '' + tarfile_open.return_value.extractfile.return_value = mock_manifest_file + path = '/some/path.tar.gz' + + try: + models.Package.from_archive(path) + self.fail('The above call should have raised a ValueError!') + except ValueError as e: + self.assertTrue('The PKG-INFO file is missing required attributes.' in str(e)) + + tarfile_open.assert_called_once_with(path) + _compression_type.assert_called_once_with(path) + tarfile_open.return_value.extractfile.assert_called_once_with(members[-1]) + tarfile_open.return_value.close.assert_called_once_with() + + def test_from_archive_file_not_found(self): + """ + Test from_archive() when the given path does not exist. + """ + dne = '/some/path/that/doesnt/exist' + + self.assertRaises(IOError, models.Package.from_archive, dne) + + @mock.patch('pulp_python.plugins.models.Package._compression_type', return_value='.gz') + @mock.patch('pulp_python.plugins.models.tarfile.open') + def test_from_archive_good_metadata(self, tarfile_open, _compression_type): + """ + Test from_archive() with good metadata, with PKG-INFO at the typical location as would be + done by setup.py sdist. + """ + tarfile_open.return_value = mock.MagicMock(spec=tarfile.TarFile) + + class TarInfo(object): + """ + This class fakes being a TarInfo. It just needs a name. + """ + def __init__(self, name): + self.name = name + + members = [ + TarInfo(name) for name in ['nectar-1.3.1', 'nectar-1.3.1/nectar', + 'nectar-1.3.1/nectar/config.py', + 'nectar-1.3.1/nectar/downloaders', + 'nectar-1.3.1/nectar/downloaders/threaded.py', + 'nectar-1.3.1/nectar/downloaders/base.py', + 'nectar-1.3.1/nectar/downloaders/__init__.py', + 'nectar-1.3.1/nectar/downloaders/local.py', + 'nectar-1.3.1/nectar/__init__.py', + 'nectar-1.3.1/nectar/exceptions.py', + 'nectar-1.3.1/nectar/listener.py', + 'nectar-1.3.1/nectar/report.py', + 'nectar-1.3.1/nectar/request.py', 'nectar-1.3.1/PKG-INFO']] + tarfile_open.return_value.getmembers.return_value = members + mock_manifest_file = mock.MagicMock(spec=file) + mock_manifest_file.read.return_value = GOOD_MANIFEST + tarfile_open.return_value.extractfile.return_value = mock_manifest_file + path = '/some/path.tar.gz' + + package = models.Package.from_archive(path) + + self.assertEqual(package.name, 'nectar') + self.assertEqual(package.version, '1.3.1') + self.assertEqual(package.summary, 'Performance tuned network download client library') + self.assertEqual(package.home_page, 'https://github.com/pulp/nectar') + self.assertEqual(package.author, 'Pulp Team') + self.assertEqual(package.author_email, 'pulp-list@redhat.com') + self.assertEqual(package.license, 'GPLv2') + self.assertEqual(package.description, 'UNKNOWN') + self.assertEqual(package.platform, 'UNKNOWN') + self.assertEqual(package._filename, 'nectar-1.3.1.tar.gz') + self.assertEqual(package._unit, None) + tarfile_open.assert_called_once_with(path) + _compression_type.assert_called_once_with(path) + tarfile_open.return_value.extractfile.assert_called_once_with(members[-1]) + tarfile_open.return_value.close.assert_called_once_with() + + @mock.patch('pulp_python.plugins.models.Package._compression_type', return_value='.gz') + @mock.patch('pulp_python.plugins.models.tarfile.open') + def test_from_archive_metadata_at_absolute_root(self, tarfile_open, _compression_type): + """ + Test from_archive() with good metadata when the PKG-INFO file is at /. + """ + tarfile_open.return_value = mock.MagicMock(spec=tarfile.TarFile) + + class TarInfo(object): + """ + This class fakes being a TarInfo. It just needs a name. + """ + def __init__(self, name): + self.name = name + + members = [ + TarInfo(name) for name in ['/PKG-INFO', 'nectar-1.3.1', 'nectar-1.3.1/nectar', + 'nectar-1.3.1/nectar/config.py', + 'nectar-1.3.1/nectar/downloaders', + 'nectar-1.3.1/nectar/downloaders/threaded.py', + 'nectar-1.3.1/nectar/downloaders/base.py', + 'nectar-1.3.1/nectar/downloaders/__init__.py', + 'nectar-1.3.1/nectar/downloaders/local.py', + 'nectar-1.3.1/nectar/__init__.py', + 'nectar-1.3.1/nectar/exceptions.py', + 'nectar-1.3.1/nectar/listener.py', + 'nectar-1.3.1/nectar/report.py', + 'nectar-1.3.1/nectar/request.py']] + tarfile_open.return_value.getmembers.return_value = members + mock_manifest_file = mock.MagicMock(spec=file) + mock_manifest_file.read.return_value = GOOD_MANIFEST + tarfile_open.return_value.extractfile.return_value = mock_manifest_file + path = '/some/path.tar.gz' + + package = models.Package.from_archive(path) + + self.assertEqual(package.name, 'nectar') + self.assertEqual(package.version, '1.3.1') + self.assertEqual(package.summary, 'Performance tuned network download client library') + self.assertEqual(package.home_page, 'https://github.com/pulp/nectar') + self.assertEqual(package.author, 'Pulp Team') + self.assertEqual(package.author_email, 'pulp-list@redhat.com') + self.assertEqual(package.license, 'GPLv2') + self.assertEqual(package.description, 'UNKNOWN') + self.assertEqual(package.platform, 'UNKNOWN') + self.assertEqual(package._filename, 'nectar-1.3.1.tar.gz') + self.assertEqual(package._unit, None) + tarfile_open.assert_called_once_with(path) + _compression_type.assert_called_once_with(path) + tarfile_open.return_value.extractfile.assert_called_once_with(members[0]) + tarfile_open.return_value.close.assert_called_once_with() + + @mock.patch('pulp_python.plugins.models.Package._compression_type', return_value='.gz') + @mock.patch('pulp_python.plugins.models.tarfile.open') + def test_from_archive_metadata_at_root(self, tarfile_open, _compression_type): + """ + Test from_archive() when the PKG-INFO file is at the root of the archive. + """ + tarfile_open.return_value = mock.MagicMock(spec=tarfile.TarFile) + + class TarInfo(object): + """ + This class fakes being a TarInfo. It just needs a name. + """ + def __init__(self, name): + self.name = name + + members = [ + TarInfo(name) for name in ['nectar-1.3.1', 'nectar-1.3.1/nectar', + 'nectar-1.3.1/nectar/config.py', + 'nectar-1.3.1/nectar/downloaders', + 'nectar-1.3.1/nectar/downloaders/threaded.py', + 'nectar-1.3.1/nectar/downloaders/base.py', + 'nectar-1.3.1/nectar/downloaders/__init__.py', + 'nectar-1.3.1/nectar/downloaders/local.py', + 'nectar-1.3.1/nectar/__init__.py', + 'nectar-1.3.1/nectar/exceptions.py', + 'nectar-1.3.1/nectar/listener.py', + 'nectar-1.3.1/nectar/report.py', + 'nectar-1.3.1/nectar/request.py', 'PKG-INFO']] + tarfile_open.return_value.getmembers.return_value = members + mock_manifest_file = mock.MagicMock(spec=file) + mock_manifest_file.read.return_value = GOOD_MANIFEST + tarfile_open.return_value.extractfile.return_value = mock_manifest_file + path = '/some/path.tar.gz' + + package = models.Package.from_archive(path) + + self.assertEqual(package.name, 'nectar') + self.assertEqual(package.version, '1.3.1') + self.assertEqual(package.summary, 'Performance tuned network download client library') + self.assertEqual(package.home_page, 'https://github.com/pulp/nectar') + self.assertEqual(package.author, 'Pulp Team') + self.assertEqual(package.author_email, 'pulp-list@redhat.com') + self.assertEqual(package.license, 'GPLv2') + self.assertEqual(package.description, 'UNKNOWN') + self.assertEqual(package.platform, 'UNKNOWN') + self.assertEqual(package._filename, 'nectar-1.3.1.tar.gz') + self.assertEqual(package._unit, None) + tarfile_open.assert_called_once_with(path) + _compression_type.assert_called_once_with(path) + tarfile_open.return_value.extractfile.assert_called_once_with(members[-1]) + tarfile_open.return_value.close.assert_called_once_with() + + @mock.patch('pulp_python.plugins.models.Package._compression_type', return_value='.gz') + @mock.patch('pulp_python.plugins.models.tarfile.open') + def test_from_archive_missing_required_metadata(self, tarfile_open, _compression_type): + """ + Test from_archive() when the PKG-INFO file is missing required fields. + """ + tarfile_open.return_value = mock.MagicMock(spec=tarfile.TarFile) + + class TarInfo(object): + """ + This class fakes being a TarInfo. It just needs a name. + """ + def __init__(self, name): + self.name = name + + members = [ + TarInfo(name) for name in ['nectar-1.3.1', 'nectar-1.3.1/nectar', + 'nectar-1.3.1/nectar/config.py', + 'nectar-1.3.1/nectar/downloaders', + 'nectar-1.3.1/nectar/downloaders/threaded.py', + 'nectar-1.3.1/nectar/downloaders/base.py', + 'nectar-1.3.1/nectar/downloaders/__init__.py', + 'nectar-1.3.1/nectar/downloaders/local.py', + 'nectar-1.3.1/nectar/__init__.py', + 'nectar-1.3.1/nectar/exceptions.py', + 'nectar-1.3.1/nectar/listener.py', + 'nectar-1.3.1/nectar/report.py', + 'nectar-1.3.1/nectar/request.py', 'nectar-1.3.1/PKG-INFO']] + tarfile_open.return_value.getmembers.return_value = members + mock_manifest_file = mock.MagicMock(spec=file) + mock_manifest_file.read.return_value = BAD_MANIFEST + tarfile_open.return_value.extractfile.return_value = mock_manifest_file + path = '/some/path.tar.gz' + try: + models.Package.from_archive(path) + self.fail('The above call should have raised a ValueError!') + except ValueError as e: + self.assertTrue(_('The PKG-INFO file is missing required attributes.') in str(e)) + + tarfile_open.assert_called_once_with(path) + _compression_type.assert_called_once_with(path) + tarfile_open.return_value.extractfile.assert_called_once_with(members[-1]) + tarfile_open.return_value.close.assert_called_once_with() + + def test_init_unit(self): + """ + Test the init_unit() method. + """ + pulp_unit = mock.MagicMock() + conduit = mock.MagicMock() + conduit.init_unit.return_value = pulp_unit + name = 'nectar' + version = '1.3.1' + summary = 'a summary' + home_page = 'http://github.com/pulp/nectar' + author = 'The Pulp Team' + author_email = 'pulp-list@redhat.com' + license = 'GPLv2' + description = 'a description' + platform = 'Linux' + _filename = 'nectar-1.3.1.tar.gz' + pp = models.Package(name, version, summary, home_page, author, author_email, license, + description, platform, _filename) + + pp.init_unit(conduit) + + conduit.init_unit.assert_called_once_with( + models.Package.TYPE, {'name': name, 'version': version}, + {'summary': summary, 'home_page': home_page, 'author': author, + 'author_email': author_email, 'license': license, 'description': description, + 'platform': platform, '_filename': _filename}, + _filename) + self.assertEqual(pp._unit, pulp_unit) + + def test_save_unit(self): + """ + Test the save_unit() method. + """ + conduit = mock.MagicMock() + pp = models.Package('name', 'version', 'summary', 'home_page', 'author', 'author_email', + 'license', 'description', 'platform', '_filename') + pp._unit = mock.MagicMock() + + pp.save_unit(conduit) + + conduit.save_unit.assert_called_once_with(pp._unit) + + def test_storage_path(self): + """ + Test the storage_path property. + """ + pp = models.Package('name', 'version', 'summary', 'home_page', 'author', 'author_email', + 'license', 'description', 'platform', '_filename') + pp._unit = mock.MagicMock() + path = '/some/path.tar.gz' + pp._unit.storage_path.return_value = path + + sp = pp.storage_path() + + self.assertEqual(sp, path) + + @mock.patch('pulp_python.plugins.models.open', create=True) + def test__compression_type_empty_file(self, mock_open): + """ + Test that _compression_type() correctly handles empty files. + """ + mock_file = mock.MagicMock(spec=file) + mock_file.__enter__.return_value.read.return_value = '' + mock_open.return_value = mock_file + path = '/some/path/to/hello_world' + + compression_type = models.Package._compression_type(path) + + # Since "" isn't a magic string, the empty string should have been returned. + self.assertEqual(compression_type, '') + mock_open.assert_called_once_with(path) + + @mock.patch('pulp_python.plugins.models.open', create=True) + def test__compression_type_handle_other(self, mock_open): + """ + Test that _compression_type() correctly handles other files. + """ + mock_file = mock.MagicMock(spec=file) + mock_file.__enter__.return_value.read.return_value = 'Hello World!' + mock_open.return_value = mock_file + path = '/some/path/to/hello_world' + + compression_type = models.Package._compression_type(path) + + # Since "Hello World!" isn't a magic string, the empty string should have been returned. + self.assertEqual(compression_type, '') + mock_open.assert_called_once_with(path) + + @mock.patch('pulp_python.plugins.models.open', create=True) + def test__compression_type_match_bz2(self, mock_open): + """ + Test that _compression_type() correctly matches bz2 files. + """ + mock_file = mock.MagicMock(spec=file) + mock_file.__enter__.return_value.read.return_value = '\x42\x5a\x68Hello World!' + mock_open.return_value = mock_file + path = '/some/path/to/hello_world.bz2' + + compression_type = models.Package._compression_type(path) + + self.assertEqual(compression_type, '.bz2') + mock_open.assert_called_once_with(path) + + @mock.patch('pulp_python.plugins.models.open', create=True) + def test__compression_type_match_gz(self, mock_open): + """ + Test that _compression_type() correctly matches gz files. + """ + mock_file = mock.MagicMock(spec=file) + mock_file.__enter__.return_value.read.return_value = '\x1f\x8b\x08Hello World!' + mock_open.return_value = mock_file + path = '/some/path/to/hello_world.gz' + + compression_type = models.Package._compression_type(path) + + self.assertEqual(compression_type, '.gz') + mock_open.assert_called_once_with(path) + + @mock.patch('pulp_python.plugins.models.open', create=True) + def test__compression_type_match_zip(self, mock_open): + """ + Test that _compression_type() correctly matches zip files. + """ + mock_file = mock.MagicMock(spec=file) + mock_file.__enter__.return_value.read.return_value = '\x50\x4b\x03\x04Hello World!' + mock_open.return_value = mock_file + path = '/some/path/to/hello_world.zip' + + compression_type = models.Package._compression_type(path) + + self.assertEqual(compression_type, '.zip') + mock_open.assert_called_once_with(path) + + def test__metadata_label(self): + """ + Test various manipulations of possible metadata attributes with the _metadata_label() + method. + """ + expected_value_map = {'name': 'Name', 'author_email': 'Author-email', 'a': 'A'} + + for key, value in expected_value_map.items(): + self.assertEqual(models.Package._metadata_label(key), value) + + def test___init__(self): + """ + Assert correct behavior with the __init__() method. + """ + name = 'nectar' + version = '1.3.1' + summary = 'a summary' + home_page = 'http://github.com/pulp/nectar' + author = 'The Pulp Team' + author_email = 'pulp-list@redhat.com' + license = 'GPLv2' + description = 'a description' + platform = 'Linux' + _filename = 'nectar-1.3.1.tar.gz' + + pp = models.Package(name, version, summary, home_page, author, author_email, license, + description, platform, _filename) + + self.assertEqual(pp.name, name) + self.assertEqual(pp.version, version) + self.assertEqual(pp.summary, summary) + self.assertEqual(pp.home_page, home_page) + self.assertEqual(pp.author, author) + self.assertEqual(pp.author_email, author_email) + self.assertEqual(pp.license, license) + self.assertEqual(pp.description, description) + self.assertEqual(pp.platform, platform) + self.assertEqual(pp._filename, _filename) + self.assertEqual(pp._unit, None) + + def test___repr__(self): + """ + Assert correct behavior with the __repr__() method. + """ + name = 'nectar' + version = '1.3.1' + summary = 'a summary' + home_page = 'http://github.com/pulp/nectar' + author = 'The Pulp Team' + author_email = 'pulp-list@redhat.com' + license = 'GPLv2' + description = 'a description' + platform = 'Linux' + _filename = 'nectar-1.3.1.tar.gz' + + pp = models.Package(name, version, summary, home_page, author, author_email, license, + description, platform, _filename) + + self.assertEqual(repr(pp), 'Python Package: nectar-1.3.1')