""" Tools for converting old- to new-style metadata. """ import os.path import re import textwrap from collections import namedtuple import pkg_resources from .pkginfo import read_pkg_info # Wheel itself is probably the only program that uses non-extras markers # in METADATA/PKG-INFO. Support its syntax with the extra at the end only. EXTRA_RE = re.compile("""^(?P.*?)(;\s*(?P.*?)(extra == '(?P.*?)')?)$""") MayRequiresKey = namedtuple('MayRequiresKey', ('condition', 'extra')) def requires_to_requires_dist(requirement): """Compose the version predicates for requirement in PEP 345 fashion.""" requires_dist = [] for op, ver in requirement.specs: requires_dist.append(op + ver) if not requires_dist: return '' return " (%s)" % ','.join(sorted(requires_dist)) def convert_requirements(requirements): """Yield Requires-Dist: strings for parsed requirements strings.""" for req in requirements: parsed_requirement = pkg_resources.Requirement.parse(req) spec = requires_to_requires_dist(parsed_requirement) extras = ",".join(parsed_requirement.extras) if extras: extras = "[%s]" % extras yield (parsed_requirement.project_name + extras + spec) def generate_requirements(extras_require): """ Convert requirements from a setup()-style dictionary to ('Requires-Dist', 'requirement') and ('Provides-Extra', 'extra') tuples. extras_require is a dictionary of {extra: [requirements]} as passed to setup(), using the empty extra {'': [requirements]} to hold install_requires. """ for extra, depends in extras_require.items(): condition = '' if extra and ':' in extra: # setuptools extra:condition syntax extra, condition = extra.split(':', 1) extra = pkg_resources.safe_extra(extra) if extra: yield ('Provides-Extra', extra) if condition: condition = "(" + condition + ") and " condition += "extra == '%s'" % extra if condition: condition = '; ' + condition for new_req in convert_requirements(depends): yield ('Requires-Dist', new_req + condition) def pkginfo_to_metadata(egg_info_path, pkginfo_path): """ Convert .egg-info directory with PKG-INFO to the Metadata 2.1 format """ pkg_info = read_pkg_info(pkginfo_path) pkg_info.replace_header('Metadata-Version', '2.1') requires_path = os.path.join(egg_info_path, 'requires.txt') if os.path.exists(requires_path): with open(requires_path) as requires_file: requires = requires_file.read() for extra, reqs in sorted(pkg_resources.split_sections(requires), key=lambda x: x[0] or ''): for item in generate_requirements({extra: reqs}): pkg_info[item[0]] = item[1] description = pkg_info['Description'] if description: pkg_info.set_payload(dedent_description(pkg_info)) del pkg_info['Description'] return pkg_info def pkginfo_unicode(pkg_info, field): """Hack to coax Unicode out of an email Message() - Python 3.3+""" text = pkg_info[field] field = field.lower() if not isinstance(text, str): if not hasattr(pkg_info, 'raw_items'): # Python 3.2 return str(text) for item in pkg_info.raw_items(): if item[0].lower() == field: text = item[1].encode('ascii', 'surrogateescape') \ .decode('utf-8') break return text def dedent_description(pkg_info): """ Dedent and convert pkg_info['Description'] to Unicode. """ description = pkg_info['Description'] # Python 3 Unicode handling, sorta. surrogates = False if not isinstance(description, str): surrogates = True description = pkginfo_unicode(pkg_info, 'Description') description_lines = description.splitlines() description_dedent = '\n'.join( # if the first line of long_description is blank, # the first line here will be indented. (description_lines[0].lstrip(), textwrap.dedent('\n'.join(description_lines[1:])), '\n')) if surrogates: description_dedent = description_dedent \ .encode("utf8") \ .decode("ascii", "surrogateescape") return description_dedent