Source code for friendly_dist_manager.package_formats.wheel.metadata_file

"""Primitives for manipulating distutils metadata files"""
from collections import namedtuple

Person = namedtuple("Person", ["name", "email"])
ProjectURL = namedtuple("ProjectURL", ["label", "url"])
ExtraRequirement = namedtuple("ExtraRequirement", ["label", "req"])


[docs]class MetadataFile: # pylint: disable=too-many-instance-attributes """Abstraction around a distutils metadata file References: * (Latest Spec): https://packaging.python.org/specifications/core-metadata/ * (v2.1 Proposed PEP) https://www.python.org/dev/peps/pep-0566/ * (v1.2 Accepted PEP) https://www.python.org/dev/peps/pep-0345/ * (V1.1 Accepted PEP) https://www.python.org/dev/peps/pep-0314/ * (V1.0 Accepted PEP) https://www.python.org/dev/peps/pep-0241/ """ def __init__(self, dist_name, version): """ Args: dist_name (str): Name of the distribution being described version (str): Version of the distribution being described """ self._file_version = "2.2" self._dist_name = dist_name self._dist_version = version self._summary = None self._homepage = None self._authors = list() self._license = None self._keywords = list() self._classifiers = list() self._download_url = None self._maintainers = list() self._requirements = list() self._python_requirements = list() self._project_urls = list() self._extra_requirements = list() @property def file_version(self): """str: the metadata file version / schema version used by this file""" return self._file_version @property def distribution_name(self): """str: name of the distribution being built""" return self._dist_name @property def distribution_version(self): """str: version of the package being built""" return self._dist_version @property def summary(self): """str: description of the package""" return self._summary or "" @summary.setter def summary(self, value): self._summary = value @property def homepage(self): """str: URL of the project homepage""" return self._homepage or "" @homepage.setter def homepage(self, value): self._homepage = value @property def authors(self): """list (Person): authors of the project""" return self._authors @authors.setter def authors(self, value): self._authors = value @property def maintainers(self): """list (Person): maintainers of the project""" return self._maintainers @maintainers.setter def maintainers(self, value): self._maintainers = value @property def license(self): """str: text describing the licensing terms for the project""" return self._license or "" @license.setter def license(self, value): self._license = value @property def keywords(self): """list (str): labels users can search for when looking for distributions like this one""" return self._keywords @keywords.setter def keywords(self, value): self._keywords = value @property def classifiers(self): """list (str): list of distribution classifiers""" return self._classifiers @classifiers.setter def classifiers(self, value): self._classifiers = value @property def download_url(self): """str: URL where the distribution package can be downloaded""" return self._download_url or "" @download_url.setter def download_url(self, value): self._download_url = value @property def requirements(self): """list(str): package definitions describing other distributions this one depends on""" return self._requirements @requirements.setter def requirements(self, value): self._requirements = value @property def python_requirements(self): """list(str): Python version identifiers describing the supported Python runtime versions supported by this distribution package""" return self._python_requirements @python_requirements.setter def python_requirements(self, value): self._python_requirements = value @property def project_urls(self): """list (ProjectURL): Support URLs associated with the distribution""" return self._project_urls @project_urls.setter def project_urls(self, value): self._project_urls = value @property def extra_requirements(self): """list (ExtraRequirement): list of optional requirements that users of the distribution may select to enable additional features""" return self._extra_requirements @extra_requirements.setter def extra_requirements(self, value): self._extra_requirements = value @staticmethod def _encode_user(user_defs, user_key, email_key): """Helper method for encoding author and maintainer information in a format compatible with the metadata file format Args: user_defs (list (Person)): list of users to be encoded user_key (str): attribute key for the field to be populated with user information (ie: "Author" or "Maintainer") email_key (str): attribute key for the field to be populated with email contact information for the user data (ie: "Author-email" or "Maintainer-email") Returns: list (str): list of encoded user definitions for inclusion in the metadata output, each element representing a single line in the output file """ retval = list() # according to the metadata spec, the Author field is intended to only # contain contact information for a single author, so we arbitrarily # select the first author with a name defined in our list of authors names = [usr.name for usr in user_defs if usr.name] if names: retval.append(f"{user_key}: {names[0]}") # For author emails, they may take the form of '"John Doe" <jdoe@company.com>' # if the author has a valid name defined, otherwise they will take the format # of 'jdoe@company.com'. Multiple emails are then separated by commas emails = list() for usr in user_defs: if not usr.email: continue if usr.name: emails.append(f'"{usr.name}" <{usr.email}>') else: emails.append(usr.email) if emails: retval.append(f"{email_key}: {','.join(emails)}") return retval @staticmethod def _encode_property(prop_key, prop_value): """Formats an optional attribute in a compatible way for storage in a metadata file Args: prop_key (str): the attribute key associated with the property to encode prop_value (str): value for the property to encode. May be empty or None if the property is not defined Returns: list (str): encoded representation of the specified property data in a format compatible with the metadata file format. May return an empty list if the provided property data was empty. """ retval = list() if prop_value: retval.append(f"{prop_key}: {prop_value}") return retval @property def raw(self): """str: the raw text content of the metadata file""" lines = list() # Required fields lines.append(f"Metadata-Version: {self.file_version}") lines.append(f"Name: {self.distribution_name}") lines.append(f"Version: {self.distribution_version}") # Optional fields lines.extend(self._encode_user(self.authors, "Author", "Author-email")) lines.extend(self._encode_user(self.maintainers, "Maintainer", "Maintainer-email")) lines.extend(self._encode_property("Summary", self.summary)) lines.extend(self._encode_property("Home-page", self.homepage)) lines.extend(self._encode_property("License", self.license)) lines.extend(self._encode_property("Keywords", ','.join(self.keywords))) lines.extend(self._encode_property("Download-url", self.download_url)) for cur_proj_url in self.project_urls: if cur_proj_url.label: url_text = f"{cur_proj_url.label}, {cur_proj_url.url}" else: url_text = f"{cur_proj_url.url}" lines.append(f"Project-URL: {url_text}") for cur_classifier in self.classifiers: lines.append(f"Classifier: {cur_classifier}") for cur_req in self.python_requirements: lines.append(f"Requires-Python: {cur_req}") extras = set(extra.label for extra in self.extra_requirements) for cur_extra in extras: lines.append(f"Provides-Extra: {cur_extra}") for cur_extra in self.extra_requirements: lines.append(f"Requires-Dist: {cur_extra.req}; extra == '{cur_extra.label}'") for cur_req in self.requirements: lines.append(f"Requires-Dist: {cur_req}") return "\n".join(lines)