diff --git a/CHANGES/689.feature b/CHANGES/689.feature new file mode 100644 index 00000000..324f8edd --- /dev/null +++ b/CHANGES/689.feature @@ -0,0 +1 @@ +Support Python package metadata version 2.3 diff --git a/pulp_python/app/migrations/0014_metadata_22_23.py b/pulp_python/app/migrations/0014_metadata_22_23.py new file mode 100644 index 00000000..4d10f1cf --- /dev/null +++ b/pulp_python/app/migrations/0014_metadata_22_23.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.10 on 2024-06-28 04:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('python', '0013_add_rbac_permissions'), + ] + + operations = [ + migrations.AddField( + model_name='pythonpackagecontent', + name='dynamic', + field=models.JSONField(default=list), + preserve_default=False, + ), + migrations.AddField( + model_name='pythonpackagecontent', + name='provides_extra', + field=models.JSONField(default=list), + preserve_default=False, + ), + ] diff --git a/pulp_python/app/models.py b/pulp_python/app/models.py index e3e0d2c8..12490f33 100644 --- a/pulp_python/app/models.py +++ b/pulp_python/app/models.py @@ -145,6 +145,10 @@ class PythonPackageContent(Content): PROTECTED_FROM_RECLAIM = False + # TODO: it appears we've set the default (usually empty-string) for each of these fields + # manually in the migrations rather than setting them declaratively. That's not ideal. + # At some point we should add proper default values and probably make some fields nullable. + TYPE = "python" repo_key_fields = ("filename",) # Required metadata @@ -177,7 +181,11 @@ class PythonPackageContent(Content): requires_external = models.JSONField(default=list) classifiers = models.JSONField(default=list) project_urls = models.JSONField(default=dict) + # Metadata 2.1 description_content_type = models.TextField() + provides_extra = models.JSONField(default=list) + # Metadata 2.2 + dynamic = models.JSONField(default=list) # Pulp Domains _pulp_domain = models.ForeignKey("core.Domain", default=get_domain_pk, on_delete=models.PROTECT) diff --git a/pulp_python/app/serializers.py b/pulp_python/app/serializers.py index ff789b53..13befdaa 100644 --- a/pulp_python/app/serializers.py +++ b/pulp_python/app/serializers.py @@ -108,11 +108,6 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa required=False, allow_blank=True, help_text=_('A longer description of the package that can run to several paragraphs.') ) - description_content_type = serializers.CharField( - required=False, allow_blank=True, - help_text=_('A string stating the markup syntax (if any) used in the distribution’s' - ' description, so that tools can intelligently render the description.') - ) keywords = serializers.CharField( required=False, allow_blank=True, help_text=_('Additional keywords to be used to assist searching for the ' @@ -195,7 +190,23 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa required=False, default=list, help_text=_('A JSON list containing classification values for a Python package.') ) - + # Metadata 2.1 + description_content_type = serializers.CharField( + required=False, allow_blank=True, + help_text=_('A string stating the markup syntax (if any) used in the distribution’s' + ' description, so that tools can intelligently render the description.') + ) + provides_extra = serializers.JSONField( + required=False, default=list, + help_text=_('A JSON list containing names of optional features provided by the package.') + ) + # Metadata 2.2 + dynamic = serializers.JSONField( + required=False, default=list, + help_text=_('A JSON list containing names of other core metadata fields which are ' + 'permitted to vary between sdist and bdist packages. Fields NOT marked ' + 'dynamic MUST be the same between bdist and sdist.') + ) def deferred_validate(self, data): """ Validate the python package data. @@ -251,10 +262,11 @@ def retrieve(self, validated_data): class Meta: fields = core_serializers.SingleArtifactContentUploadSerializer.Meta.fields + ( 'filename', 'packagetype', 'name', 'version', 'sha256', 'metadata_version', 'summary', - 'description', 'description_content_type', 'keywords', 'home_page', 'download_url', - 'author', 'author_email', 'maintainer', 'maintainer_email', 'license', - 'requires_python', 'project_url', 'project_urls', 'platform', 'supported_platform', - 'requires_dist', 'provides_dist', 'obsoletes_dist', 'requires_external', 'classifiers' + 'description', 'keywords', 'home_page', 'download_url', 'author', 'author_email', + 'maintainer', 'maintainer_email', 'license', 'requires_python', 'project_url', + 'project_urls', 'platform', 'supported_platform', 'requires_dist', 'provides_dist', + 'obsoletes_dist', 'requires_external', 'classifiers', 'description_content_type', + 'provides_extra', 'dynamic', ) model = python_models.PythonPackageContent diff --git a/pulp_python/app/utils.py b/pulp_python/app/utils.py index f66dcbd7..198b7d1c 100644 --- a/pulp_python/app/utils.py +++ b/pulp_python/app/utils.py @@ -91,8 +91,9 @@ def parse_project_metadata(project): package['obsoletes_dist'] = json.dumps(project.get('obsoletes_dist', [])) package['requires_external'] = json.dumps(project.get('requires_external', [])) package['classifiers'] = json.dumps(project.get('classifiers', [])) - package['project_urls'] = json.dumps(project.get('project_urls', {})) package['description_content_type'] = project.get('description_content_type') or "" + package['project_urls'] = json.dumps(project.get('project_urls', {})) + package['dynamic'] = project.get('dynamic') or "" return package @@ -219,7 +220,6 @@ def python_content_to_info(content): "summary": content.summary or "", "keywords": content.keywords or "", "description": content.description or "", - "description_content_type": content.description_content_type or "", "bugtrack_url": None, # These two are basically never used "docs_url": None, "downloads": {"last_day": -1, "last_month": -1, "last_week": -1}, @@ -240,6 +240,9 @@ def python_content_to_info(content): "classifiers": json_to_dict(content.classifiers) or None, "yanked": False, # These are no longer used on PyPI, but are still present "yanked_reason": None, + "description_content_type": content.description_content_type or "", + "provides_extra": content.provides_extra or None + "dynamic", content.dynamic or None }