Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
metadata.py1069 linesDownload Raw Back to distlib
1# -*- coding: utf-8 -*-2#3# Copyright (C) 2012 The Python Software Foundation.4# See LICENSE.txt and CONTRIBUTORS.txt.5#6"""Implementation of the Metadata for Python packages PEPs.7 8Supports all metadata formats (1.0, 1.1, 1.2, 1.3/2.1 and 2.2).9"""10from __future__ import unicode_literals11 12import codecs13from email import message_from_file14import json15import logging16import re17 18 19from . import DistlibException, __version__20from .compat import StringIO, string_types, text_type21from .markers import interpret22from .util import extract_by_key, get_extras23from .version import get_scheme, PEP440_VERSION_RE24 25logger = logging.getLogger(__name__)26 27 28class MetadataMissingError(DistlibException):29    """A required metadata is missing"""30 31 32class MetadataConflictError(DistlibException):33    """Attempt to read or write metadata fields that are conflictual."""34 35 36class MetadataUnrecognizedVersionError(DistlibException):37    """Unknown metadata version number."""38 39 40class MetadataInvalidError(DistlibException):41    """A metadata value is invalid"""42 43# public API of this module44__all__ = ['Metadata', 'PKG_INFO_ENCODING', 'PKG_INFO_PREFERRED_VERSION']45 46# Encoding used for the PKG-INFO files47PKG_INFO_ENCODING = 'utf-8'48 49# preferred version. Hopefully will be changed50# to 1.2 once PEP 345 is supported everywhere51PKG_INFO_PREFERRED_VERSION = '1.1'52 53_LINE_PREFIX_1_2 = re.compile('\n       \\|')54_LINE_PREFIX_PRE_1_2 = re.compile('\n        ')55_241_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',56               'Summary', 'Description',57               'Keywords', 'Home-page', 'Author', 'Author-email',58               'License')59 60_314_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',61               'Supported-Platform', 'Summary', 'Description',62               'Keywords', 'Home-page', 'Author', 'Author-email',63               'License', 'Classifier', 'Download-URL', 'Obsoletes',64               'Provides', 'Requires')65 66_314_MARKERS = ('Obsoletes', 'Provides', 'Requires', 'Classifier',67                'Download-URL')68 69_345_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',70               'Supported-Platform', 'Summary', 'Description',71               'Keywords', 'Home-page', 'Author', 'Author-email',72               'Maintainer', 'Maintainer-email', 'License',73               'Classifier', 'Download-URL', 'Obsoletes-Dist',74               'Project-URL', 'Provides-Dist', 'Requires-Dist',75               'Requires-Python', 'Requires-External')76 77_345_MARKERS = ('Provides-Dist', 'Requires-Dist', 'Requires-Python',78                'Obsoletes-Dist', 'Requires-External', 'Maintainer',79                'Maintainer-email', 'Project-URL')80 81_426_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',82               'Supported-Platform', 'Summary', 'Description',83               'Keywords', 'Home-page', 'Author', 'Author-email',84               'Maintainer', 'Maintainer-email', 'License',85               'Classifier', 'Download-URL', 'Obsoletes-Dist',86               'Project-URL', 'Provides-Dist', 'Requires-Dist',87               'Requires-Python', 'Requires-External', 'Private-Version',88               'Obsoleted-By', 'Setup-Requires-Dist', 'Extension',89               'Provides-Extra')90 91_426_MARKERS = ('Private-Version', 'Provides-Extra', 'Obsoleted-By',92                'Setup-Requires-Dist', 'Extension')93 94# See issue #106: Sometimes 'Requires' and 'Provides' occur wrongly in95# the metadata. Include them in the tuple literal below to allow them96# (for now).97# Ditto for Obsoletes - see issue #140.98_566_FIELDS = _426_FIELDS + ('Description-Content-Type',99                             'Requires', 'Provides', 'Obsoletes')100 101_566_MARKERS = ('Description-Content-Type',)102 103_643_MARKERS = ('Dynamic', 'License-File')104 105_643_FIELDS = _566_FIELDS + _643_MARKERS106 107_ALL_FIELDS = set()108_ALL_FIELDS.update(_241_FIELDS)109_ALL_FIELDS.update(_314_FIELDS)110_ALL_FIELDS.update(_345_FIELDS)111_ALL_FIELDS.update(_426_FIELDS)112_ALL_FIELDS.update(_566_FIELDS)113_ALL_FIELDS.update(_643_FIELDS)114 115EXTRA_RE = re.compile(r'''extra\s*==\s*("([^"]+)"|'([^']+)')''')116 117 118def _version2fieldlist(version):119    if version == '1.0':120        return _241_FIELDS121    elif version == '1.1':122        return _314_FIELDS123    elif version == '1.2':124        return _345_FIELDS125    elif version in ('1.3', '2.1'):126        # avoid adding field names if already there127        return _345_FIELDS + tuple(f for f in _566_FIELDS if f not in _345_FIELDS)128    elif version == '2.0':129        raise ValueError('Metadata 2.0 is withdrawn and not supported')130        # return _426_FIELDS131    elif version == '2.2':132        return _643_FIELDS133    raise MetadataUnrecognizedVersionError(version)134 135 136def _best_version(fields):137    """Detect the best version depending on the fields used."""138    def _has_marker(keys, markers):139        return any(marker in keys for marker in markers)140 141    keys = [key for key, value in fields.items() if value not in ([], 'UNKNOWN', None)]142    possible_versions = ['1.0', '1.1', '1.2', '1.3', '2.1', '2.2']  # 2.0 removed143 144    # first let's try to see if a field is not part of one of the version145    for key in keys:146        if key not in _241_FIELDS and '1.0' in possible_versions:147            possible_versions.remove('1.0')148            logger.debug('Removed 1.0 due to %s', key)149        if key not in _314_FIELDS and '1.1' in possible_versions:150            possible_versions.remove('1.1')151            logger.debug('Removed 1.1 due to %s', key)152        if key not in _345_FIELDS and '1.2' in possible_versions:153            possible_versions.remove('1.2')154            logger.debug('Removed 1.2 due to %s', key)155        if key not in _566_FIELDS and '1.3' in possible_versions:156            possible_versions.remove('1.3')157            logger.debug('Removed 1.3 due to %s', key)158        if key not in _566_FIELDS and '2.1' in possible_versions:159            if key != 'Description':  # In 2.1, description allowed after headers160                possible_versions.remove('2.1')161                logger.debug('Removed 2.1 due to %s', key)162        if key not in _643_FIELDS and '2.2' in possible_versions:163            possible_versions.remove('2.2')164            logger.debug('Removed 2.2 due to %s', key)165        # if key not in _426_FIELDS and '2.0' in possible_versions:166            # possible_versions.remove('2.0')167            # logger.debug('Removed 2.0 due to %s', key)168 169    # possible_version contains qualified versions170    if len(possible_versions) == 1:171        return possible_versions[0]   # found !172    elif len(possible_versions) == 0:173        logger.debug('Out of options - unknown metadata set: %s', fields)174        raise MetadataConflictError('Unknown metadata set')175 176    # let's see if one unique marker is found177    is_1_1 = '1.1' in possible_versions and _has_marker(keys, _314_MARKERS)178    is_1_2 = '1.2' in possible_versions and _has_marker(keys, _345_MARKERS)179    is_2_1 = '2.1' in possible_versions and _has_marker(keys, _566_MARKERS)180    # is_2_0 = '2.0' in possible_versions and _has_marker(keys, _426_MARKERS)181    is_2_2 = '2.2' in possible_versions and _has_marker(keys, _643_MARKERS)182    if int(is_1_1) + int(is_1_2) + int(is_2_1) + int(is_2_2) > 1:183        raise MetadataConflictError('You used incompatible 1.1/1.2/2.1/2.2 fields')184 185    # we have the choice, 1.0, or 1.2, 2.1 or 2.2186    #   - 1.0 has a broken Summary field but works with all tools187    #   - 1.1 is to avoid188    #   - 1.2 fixes Summary but has little adoption189    #   - 2.1 adds more features190    #   - 2.2 is the latest191    if not is_1_1 and not is_1_2 and not is_2_1 and not is_2_2:192        # we couldn't find any specific marker193        if PKG_INFO_PREFERRED_VERSION in possible_versions:194            return PKG_INFO_PREFERRED_VERSION195    if is_1_1:196        return '1.1'197    if is_1_2:198        return '1.2'199    if is_2_1:200        return '2.1'201    # if is_2_2:202        # return '2.2'203 204    return '2.2'205 206# This follows the rules about transforming keys as described in207# https://www.python.org/dev/peps/pep-0566/#id17208_ATTR2FIELD = {209    name.lower().replace("-", "_"): name for name in _ALL_FIELDS210}211_FIELD2ATTR = {field: attr for attr, field in _ATTR2FIELD.items()}212 213_PREDICATE_FIELDS = ('Requires-Dist', 'Obsoletes-Dist', 'Provides-Dist')214_VERSIONS_FIELDS = ('Requires-Python',)215_VERSION_FIELDS = ('Version',)216_LISTFIELDS = ('Platform', 'Classifier', 'Obsoletes',217               'Requires', 'Provides', 'Obsoletes-Dist',218               'Provides-Dist', 'Requires-Dist', 'Requires-External',219               'Project-URL', 'Supported-Platform', 'Setup-Requires-Dist',220               'Provides-Extra', 'Extension', 'License-File')221_LISTTUPLEFIELDS = ('Project-URL',)222 223_ELEMENTSFIELD = ('Keywords',)224 225_UNICODEFIELDS = ('Author', 'Maintainer', 'Summary', 'Description')226 227_MISSING = object()228 229_FILESAFE = re.compile('[^A-Za-z0-9.]+')230 231 232def _get_name_and_version(name, version, for_filename=False):233    """Return the distribution name with version.234 235    If for_filename is true, return a filename-escaped form."""236    if for_filename:237        # For both name and version any runs of non-alphanumeric or '.'238        # characters are replaced with a single '-'.  Additionally any239        # spaces in the version string become '.'240        name = _FILESAFE.sub('-', name)241        version = _FILESAFE.sub('-', version.replace(' ', '.'))242    return '%s-%s' % (name, version)243 244 245class LegacyMetadata(object):246    """The legacy metadata of a release.247 248    Supports versions 1.0, 1.1, 1.2, 2.0 and 1.3/2.1 (auto-detected). You can249    instantiate the class with one of these arguments (or none):250    - *path*, the path to a metadata file251    - *fileobj* give a file-like object with metadata as content252    - *mapping* is a dict-like object253    - *scheme* is a version scheme name254    """255    # TODO document the mapping API and UNKNOWN default key256 257    def __init__(self, path=None, fileobj=None, mapping=None,258                 scheme='default'):259        if [path, fileobj, mapping].count(None) < 2:260            raise TypeError('path, fileobj and mapping are exclusive')261        self._fields = {}262        self.requires_files = []263        self._dependencies = None264        self.scheme = scheme265        if path is not None:266            self.read(path)267        elif fileobj is not None:268            self.read_file(fileobj)269        elif mapping is not None:270            self.update(mapping)271            self.set_metadata_version()272 273    def set_metadata_version(self):274        self._fields['Metadata-Version'] = _best_version(self._fields)275 276    def _write_field(self, fileobj, name, value):277        fileobj.write('%s: %s\n' % (name, value))278 279    def __getitem__(self, name):280        return self.get(name)281 282    def __setitem__(self, name, value):283        return self.set(name, value)284 285    def __delitem__(self, name):286        field_name = self._convert_name(name)287        try:288            del self._fields[field_name]289        except KeyError:290            raise KeyError(name)291 292    def __contains__(self, name):293        return (name in self._fields or294                self._convert_name(name) in self._fields)295 296    def _convert_name(self, name):297        if name in _ALL_FIELDS:298            return name299        name = name.replace('-', '_').lower()300        return _ATTR2FIELD.get(name, name)301 302    def _default_value(self, name):303        if name in _LISTFIELDS or name in _ELEMENTSFIELD:304            return []305        return 'UNKNOWN'306 307    def _remove_line_prefix(self, value):308        if self.metadata_version in ('1.0', '1.1'):309            return _LINE_PREFIX_PRE_1_2.sub('\n', value)310        else:311            return _LINE_PREFIX_1_2.sub('\n', value)312 313    def __getattr__(self, name):314        if name in _ATTR2FIELD:315            return self[name]316        raise AttributeError(name)317 318    #319    # Public API320    #321 322#    dependencies = property(_get_dependencies, _set_dependencies)323 324    def get_fullname(self, filesafe=False):325        """Return the distribution name with version.326 327        If filesafe is true, return a filename-escaped form."""328        return _get_name_and_version(self['Name'], self['Version'], filesafe)329 330    def is_field(self, name):331        """return True if name is a valid metadata key"""332        name = self._convert_name(name)333        return name in _ALL_FIELDS334 335    def is_multi_field(self, name):336        name = self._convert_name(name)337        return name in _LISTFIELDS338 339    def read(self, filepath):340        """Read the metadata values from a file path."""341        fp = codecs.open(filepath, 'r', encoding='utf-8')342        try:343            self.read_file(fp)344        finally:345            fp.close()346 347    def read_file(self, fileob):348        """Read the metadata values from a file object."""349        msg = message_from_file(fileob)350        self._fields['Metadata-Version'] = msg['metadata-version']351 352        # When reading, get all the fields we can353        for field in _ALL_FIELDS:354            if field not in msg:355                continue356            if field in _LISTFIELDS:357                # we can have multiple lines358                values = msg.get_all(field)359                if field in _LISTTUPLEFIELDS and values is not None:360                    values = [tuple(value.split(',')) for value in values]361                self.set(field, values)362            else:363                # single line364                value = msg[field]365                if value is not None and value != 'UNKNOWN':366                    self.set(field, value)367 368        # PEP 566 specifies that the body be used for the description, if369        # available370        body = msg.get_payload()371        self["Description"] = body if body else self["Description"]372        # logger.debug('Attempting to set metadata for %s', self)373        # self.set_metadata_version()374 375    def write(self, filepath, skip_unknown=False):376        """Write the metadata fields to filepath."""377        fp = codecs.open(filepath, 'w', encoding='utf-8')378        try:379            self.write_file(fp, skip_unknown)380        finally:381            fp.close()382 383    def write_file(self, fileobject, skip_unknown=False):384        """Write the PKG-INFO format data to a file object."""385        self.set_metadata_version()386 387        for field in _version2fieldlist(self['Metadata-Version']):388            values = self.get(field)389            if skip_unknown and values in ('UNKNOWN', [], ['UNKNOWN']):390                continue391            if field in _ELEMENTSFIELD:392                self._write_field(fileobject, field, ','.join(values))393                continue394            if field not in _LISTFIELDS:395                if field == 'Description':396                    if self.metadata_version in ('1.0', '1.1'):397                        values = values.replace('\n', '\n        ')398                    else:399                        values = values.replace('\n', '\n       |')400                values = [values]401 402            if field in _LISTTUPLEFIELDS:403                values = [','.join(value) for value in values]404 405            for value in values:406                self._write_field(fileobject, field, value)407 408    def update(self, other=None, **kwargs):409        """Set metadata values from the given iterable `other` and kwargs.410 411        Behavior is like `dict.update`: If `other` has a ``keys`` method,412        they are looped over and ``self[key]`` is assigned ``other[key]``.413        Else, ``other`` is an iterable of ``(key, value)`` iterables.414 415        Keys that don't match a metadata field or that have an empty value are416        dropped.417        """418        def _set(key, value):419            if key in _ATTR2FIELD and value:420                self.set(self._convert_name(key), value)421 422        if not other:423            # other is None or empty container424            pass425        elif hasattr(other, 'keys'):426            for k in other.keys():427                _set(k, other[k])428        else:429            for k, v in other:430                _set(k, v)431 432        if kwargs:433            for k, v in kwargs.items():434                _set(k, v)435 436    def set(self, name, value):437        """Control then set a metadata field."""438        name = self._convert_name(name)439 440        if ((name in _ELEMENTSFIELD or name == 'Platform') and441            not isinstance(value, (list, tuple))):442            if isinstance(value, string_types):443                value = [v.strip() for v in value.split(',')]444            else:445                value = []446        elif (name in _LISTFIELDS and447              not isinstance(value, (list, tuple))):448            if isinstance(value, string_types):449                value = [value]450            else:451                value = []452 453        if logger.isEnabledFor(logging.WARNING):454            project_name = self['Name']455 456            scheme = get_scheme(self.scheme)457            if name in _PREDICATE_FIELDS and value is not None:458                for v in value:459                    # check that the values are valid460                    if not scheme.is_valid_matcher(v.split(';')[0]):461                        logger.warning(462                            "'%s': '%s' is not valid (field '%s')",463                            project_name, v, name)464            # FIXME this rejects UNKNOWN, is that right?465            elif name in _VERSIONS_FIELDS and value is not None:466                if not scheme.is_valid_constraint_list(value):467                    logger.warning("'%s': '%s' is not a valid version (field '%s')",468                                   project_name, value, name)469            elif name in _VERSION_FIELDS and value is not None:470                if not scheme.is_valid_version(value):471                    logger.warning("'%s': '%s' is not a valid version (field '%s')",472                                   project_name, value, name)473 474        if name in _UNICODEFIELDS:475            if name == 'Description':476                value = self._remove_line_prefix(value)477 478        self._fields[name] = value479 480    def get(self, name, default=_MISSING):481        """Get a metadata field."""482        name = self._convert_name(name)483        if name not in self._fields:484            if default is _MISSING:485                default = self._default_value(name)486            return default487        if name in _UNICODEFIELDS:488            value = self._fields[name]489            return value490        elif name in _LISTFIELDS:491            value = self._fields[name]492            if value is None:493                return []494            res = []495            for val in value:496                if name not in _LISTTUPLEFIELDS:497                    res.append(val)498                else:499                    # That's for Project-URL500                    res.append((val[0], val[1]))501            return res502 503        elif name in _ELEMENTSFIELD:504            value = self._fields[name]505            if isinstance(value, string_types):506                return value.split(',')507        return self._fields[name]508 509    def check(self, strict=False):510        """Check if the metadata is compliant. If strict is True then raise if511        no Name or Version are provided"""512        self.set_metadata_version()513 514        # XXX should check the versions (if the file was loaded)515        missing, warnings = [], []516 517        for attr in ('Name', 'Version'):  # required by PEP 345518            if attr not in self:519                missing.append(attr)520 521        if strict and missing != []:522            msg = 'missing required metadata: %s' % ', '.join(missing)523            raise MetadataMissingError(msg)524 525        for attr in ('Home-page', 'Author'):526            if attr not in self:527                missing.append(attr)528 529        # checking metadata 1.2 (XXX needs to check 1.1, 1.0)530        if self['Metadata-Version'] != '1.2':531            return missing, warnings532 533        scheme = get_scheme(self.scheme)534 535        def are_valid_constraints(value):536            for v in value:537                if not scheme.is_valid_matcher(v.split(';')[0]):538                    return False539            return True540 541        for fields, controller in ((_PREDICATE_FIELDS, are_valid_constraints),542                                   (_VERSIONS_FIELDS,543                                    scheme.is_valid_constraint_list),544                                   (_VERSION_FIELDS,545                                    scheme.is_valid_version)):546            for field in fields:547                value = self.get(field, None)548                if value is not None and not controller(value):549                    warnings.append("Wrong value for '%s': %s" % (field, value))550 551        return missing, warnings552 553    def todict(self, skip_missing=False):554        """Return fields as a dict.555 556        Field names will be converted to use the underscore-lowercase style557        instead of hyphen-mixed case (i.e. home_page instead of Home-page).558        This is as per https://www.python.org/dev/peps/pep-0566/#id17.559        """560        self.set_metadata_version()561 562        fields = _version2fieldlist(self['Metadata-Version'])563 564        data = {}565 566        for field_name in fields:567            if not skip_missing or field_name in self._fields:568                key = _FIELD2ATTR[field_name]569                if key != 'project_url':570                    data[key] = self[field_name]571                else:572                    data[key] = [','.join(u) for u in self[field_name]]573 574        return data575 576    def add_requirements(self, requirements):577        if self['Metadata-Version'] == '1.1':578            # we can't have 1.1 metadata *and* Setuptools requires579            for field in ('Obsoletes', 'Requires', 'Provides'):580                if field in self:581                    del self[field]582        self['Requires-Dist'] += requirements583 584    # Mapping API585    # TODO could add iter* variants586 587    def keys(self):588        return list(_version2fieldlist(self['Metadata-Version']))589 590    def __iter__(self):591        for key in self.keys():592            yield key593 594    def values(self):595        return [self[key] for key in self.keys()]596 597    def items(self):598        return [(key, self[key]) for key in self.keys()]599 600    def __repr__(self):601        return '<%s %s %s>' % (self.__class__.__name__, self.name,602                               self.version)603 604 605METADATA_FILENAME = 'pydist.json'606WHEEL_METADATA_FILENAME = 'metadata.json'607LEGACY_METADATA_FILENAME = 'METADATA'608 609 610class Metadata(object):611    """612    The metadata of a release. This implementation uses 2.1613    metadata where possible. If not possible, it wraps a LegacyMetadata614    instance which handles the key-value metadata format.615    """616 617    METADATA_VERSION_MATCHER = re.compile(r'^\d+(\.\d+)*$')618 619    NAME_MATCHER = re.compile('^[0-9A-Z]([0-9A-Z_.-]*[0-9A-Z])?$', re.I)620 621    FIELDNAME_MATCHER = re.compile('^[A-Z]([0-9A-Z-]*[0-9A-Z])?$', re.I)622 623    VERSION_MATCHER = PEP440_VERSION_RE624 625    SUMMARY_MATCHER = re.compile('.{1,2047}')626 627    METADATA_VERSION = '2.0'628 629    GENERATOR = 'distlib (%s)' % __version__630 631    MANDATORY_KEYS = {632        'name': (),633        'version': (),634        'summary': ('legacy',),635    }636 637    INDEX_KEYS = ('name version license summary description author '638                  'author_email keywords platform home_page classifiers '639                  'download_url')640 641    DEPENDENCY_KEYS = ('extras run_requires test_requires build_requires '642                       'dev_requires provides meta_requires obsoleted_by '643                       'supports_environments')644 645    SYNTAX_VALIDATORS = {646        'metadata_version': (METADATA_VERSION_MATCHER, ()),647        'name': (NAME_MATCHER, ('legacy',)),648        'version': (VERSION_MATCHER, ('legacy',)),649        'summary': (SUMMARY_MATCHER, ('legacy',)),650        'dynamic': (FIELDNAME_MATCHER, ('legacy',)),651    }652 653    __slots__ = ('_legacy', '_data', 'scheme')654 655    def __init__(self, path=None, fileobj=None, mapping=None,656                 scheme='default'):657        if [path, fileobj, mapping].count(None) < 2:658            raise TypeError('path, fileobj and mapping are exclusive')659        self._legacy = None660        self._data = None661        self.scheme = scheme662        #import pdb; pdb.set_trace()663        if mapping is not None:664            try:665                self._validate_mapping(mapping, scheme)666                self._data = mapping667            except MetadataUnrecognizedVersionError:668                self._legacy = LegacyMetadata(mapping=mapping, scheme=scheme)669                self.validate()670        else:671            data = None672            if path:673                with open(path, 'rb') as f:674                    data = f.read()675            elif fileobj:676                data = fileobj.read()677            if data is None:678                # Initialised with no args - to be added679                self._data = {680                    'metadata_version': self.METADATA_VERSION,681                    'generator': self.GENERATOR,682                }683            else:684                if not isinstance(data, text_type):685                    data = data.decode('utf-8')686                try:687                    self._data = json.loads(data)688                    self._validate_mapping(self._data, scheme)689                except ValueError:690                    # Note: MetadataUnrecognizedVersionError does not691                    # inherit from ValueError (it's a DistlibException,692                    # which should not inherit from ValueError).693                    # The ValueError comes from the json.load - if that694                    # succeeds and we get a validation error, we want695                    # that to propagate696                    self._legacy = LegacyMetadata(fileobj=StringIO(data),697                                                  scheme=scheme)698                    self.validate()699 700    common_keys = set(('name', 'version', 'license', 'keywords', 'summary'))701 702    none_list = (None, list)703    none_dict = (None, dict)704 705    mapped_keys = {706        'run_requires': ('Requires-Dist', list),707        'build_requires': ('Setup-Requires-Dist', list),708        'dev_requires': none_list,709        'test_requires': none_list,710        'meta_requires': none_list,711        'extras': ('Provides-Extra', list),712        'modules': none_list,713        'namespaces': none_list,714        'exports': none_dict,715        'commands': none_dict,716        'classifiers': ('Classifier', list),717        'source_url': ('Download-URL', None),718        'metadata_version': ('Metadata-Version', None),719    }720 721    del none_list, none_dict722 723    def __getattribute__(self, key):724        common = object.__getattribute__(self, 'common_keys')725        mapped = object.__getattribute__(self, 'mapped_keys')726        if key in mapped:727            lk, maker = mapped[key]728            if self._legacy:729                if lk is None:730                    result = None if maker is None else maker()731                else:732                    result = self._legacy.get(lk)733            else:734                value = None if maker is None else maker()735                if key not in ('commands', 'exports', 'modules', 'namespaces',736                               'classifiers'):737                    result = self._data.get(key, value)738                else:739                    # special cases for PEP 459740                    sentinel = object()741                    result = sentinel742                    d = self._data.get('extensions')743                    if d:744                        if key == 'commands':745                            result = d.get('python.commands', value)746                        elif key == 'classifiers':747                            d = d.get('python.details')748                            if d:749                                result = d.get(key, value)750                        else:751                            d = d.get('python.exports')752                            if not d:753                                d = self._data.get('python.exports')754                            if d:755                                result = d.get(key, value)756                    if result is sentinel:757                        result = value758        elif key not in common:759            result = object.__getattribute__(self, key)760        elif self._legacy:761            result = self._legacy.get(key)762        else:763            result = self._data.get(key)764        return result765 766    def _validate_value(self, key, value, scheme=None):767        if key in self.SYNTAX_VALIDATORS:768            pattern, exclusions = self.SYNTAX_VALIDATORS[key]769            if (scheme or self.scheme) not in exclusions:770                m = pattern.match(value)771                if not m:772                    raise MetadataInvalidError("'%s' is an invalid value for "773                                               "the '%s' property" % (value,774                                                                    key))775 776    def __setattr__(self, key, value):777        self._validate_value(key, value)778        common = object.__getattribute__(self, 'common_keys')779        mapped = object.__getattribute__(self, 'mapped_keys')780        if key in mapped:781            lk, _ = mapped[key]782            if self._legacy:783                if lk is None:784                    raise NotImplementedError785                self._legacy[lk] = value786            elif key not in ('commands', 'exports', 'modules', 'namespaces',787                             'classifiers'):788                self._data[key] = value789            else:790                # special cases for PEP 459791                d = self._data.setdefault('extensions', {})792                if key == 'commands':793                    d['python.commands'] = value794                elif key == 'classifiers':795                    d = d.setdefault('python.details', {})796                    d[key] = value797                else:798                    d = d.setdefault('python.exports', {})799                    d[key] = value800        elif key not in common:801            object.__setattr__(self, key, value)802        else:803            if key == 'keywords':804                if isinstance(value, string_types):805                    value = value.strip()806                    if value:807                        value = value.split()808                    else:809                        value = []810            if self._legacy:811                self._legacy[key] = value812            else:813                self._data[key] = value814 815    @property816    def name_and_version(self):817        return _get_name_and_version(self.name, self.version, True)818 819    @property820    def provides(self):821        if self._legacy:822            result = self._legacy['Provides-Dist']823        else:824            result = self._data.setdefault('provides', [])825        s = '%s (%s)' % (self.name, self.version)826        if s not in result:827            result.append(s)828        return result829 830    @provides.setter831    def provides(self, value):832        if self._legacy:833            self._legacy['Provides-Dist'] = value834        else:835            self._data['provides'] = value836 837    def get_requirements(self, reqts, extras=None, env=None):838        """839        Base method to get dependencies, given a set of extras840        to satisfy and an optional environment context.841        :param reqts: A list of sometimes-wanted dependencies,842                      perhaps dependent on extras and environment.843        :param extras: A list of optional components being requested.844        :param env: An optional environment for marker evaluation.845        """846        if self._legacy:847            result = reqts848        else:849            result = []850            extras = get_extras(extras or [], self.extras)851            for d in reqts:852                if 'extra' not in d and 'environment' not in d:853                    # unconditional854                    include = True855                else:856                    if 'extra' not in d:857                        # Not extra-dependent - only environment-dependent858                        include = True859                    else:860                        include = d.get('extra') in extras861                    if include:862                        # Not excluded because of extras, check environment863                        marker = d.get('environment')864                        if marker:865                            include = interpret(marker, env)866                if include:867                    result.extend(d['requires'])868            for key in ('build', 'dev', 'test'):869                e = ':%s:' % key870                if e in extras:871                    extras.remove(e)872                    # A recursive call, but it should terminate since 'test'873                    # has been removed from the extras874                    reqts = self._data.get('%s_requires' % key, [])875                    result.extend(self.get_requirements(reqts, extras=extras,876                                                        env=env))877        return result878 879    @property880    def dictionary(self):881        if self._legacy:882            return self._from_legacy()883        return self._data884 885    @property886    def dependencies(self):887        if self._legacy:888            raise NotImplementedError889        else:890            return extract_by_key(self._data, self.DEPENDENCY_KEYS)891 892    @dependencies.setter893    def dependencies(self, value):894        if self._legacy:895            raise NotImplementedError896        else:897            self._data.update(value)898 899    def _validate_mapping(self, mapping, scheme):900        if mapping.get('metadata_version') != self.METADATA_VERSION:901            raise MetadataUnrecognizedVersionError()902        missing = []903        for key, exclusions in self.MANDATORY_KEYS.items():904            if key not in mapping:905                if scheme not in exclusions:906                    missing.append(key)907        if missing:908            msg = 'Missing metadata items: %s' % ', '.join(missing)909            raise MetadataMissingError(msg)910        for k, v in mapping.items():911            self._validate_value(k, v, scheme)912 913    def validate(self):914        if self._legacy:915            missing, warnings = self._legacy.check(True)916            if missing or warnings:917                logger.warning('Metadata: missing: %s, warnings: %s',918                               missing, warnings)919        else:920            self._validate_mapping(self._data, self.scheme)921 922    def todict(self):923        if self._legacy:924            return self._legacy.todict(True)925        else:926            result = extract_by_key(self._data, self.INDEX_KEYS)927            return result928 929    def _from_legacy(self):930        assert self._legacy and not self._data931        result = {932            'metadata_version': self.METADATA_VERSION,933            'generator': self.GENERATOR,934        }935        lmd = self._legacy.todict(True)     # skip missing ones936        for k in ('name', 'version', 'license', 'summary', 'description',937                  'classifier'):938            if k in lmd:939                if k == 'classifier':940                    nk = 'classifiers'941                else:942                    nk = k943                result[nk] = lmd[k]944        kw = lmd.get('Keywords', [])945        if kw == ['']:946            kw = []947        result['keywords'] = kw948        keys = (('requires_dist', 'run_requires'),949                ('setup_requires_dist', 'build_requires'))950        for ok, nk in keys:951            if ok in lmd and lmd[ok]:952                result[nk] = [{'requires': lmd[ok]}]953        result['provides'] = self.provides954        author = {}955        maintainer = {}956        return result957 958    LEGACY_MAPPING = {959        'name': 'Name',960        'version': 'Version',961        ('extensions', 'python.details', 'license'): 'License',962        'summary': 'Summary',963        'description': 'Description',964        ('extensions', 'python.project', 'project_urls', 'Home'): 'Home-page',965        ('extensions', 'python.project', 'contacts', 0, 'name'): 'Author',966        ('extensions', 'python.project', 'contacts', 0, 'email'): 'Author-email',967        'source_url': 'Download-URL',968        ('extensions', 'python.details', 'classifiers'): 'Classifier',969    }970 971    def _to_legacy(self):972        def process_entries(entries):973            reqts = set()974            for e in entries:975                extra = e.get('extra')976                env = e.get('environment')977                rlist = e['requires']978                for r in rlist:979                    if not env and not extra:980                        reqts.add(r)981                    else:982                        marker = ''983                        if extra:984                            marker = 'extra == "%s"' % extra985                        if env:986                            if marker:987                                marker = '(%s) and %s' % (env, marker)988                            else:989                                marker = env990                        reqts.add(';'.join((r, marker)))991            return reqts992 993        assert self._data and not self._legacy994        result = LegacyMetadata()995        nmd = self._data996        # import pdb; pdb.set_trace()997        for nk, ok in self.LEGACY_MAPPING.items():998            if not isinstance(nk, tuple):999                if nk in nmd:1000                    result[ok] = nmd[nk]1001            else:1002                d = nmd1003                found = True1004                for k in nk:1005                    try:1006                        d = d[k]1007                    except (KeyError, IndexError):1008                        found = False1009                        break1010                if found:1011                    result[ok] = d1012        r1 = process_entries(self.run_requires + self.meta_requires)1013        r2 = process_entries(self.build_requires + self.dev_requires)1014        if self.extras:1015            result['Provides-Extra'] = sorted(self.extras)1016        result['Requires-Dist'] = sorted(r1)1017        result['Setup-Requires-Dist'] = sorted(r2)1018        # TODO: any other fields wanted1019        return result1020 1021    def write(self, path=None, fileobj=None, legacy=False, skip_unknown=True):1022        if [path, fileobj].count(None) != 1:1023            raise ValueError('Exactly one of path and fileobj is needed')1024        self.validate()1025        if legacy:1026            if self._legacy:1027                legacy_md = self._legacy1028            else:1029                legacy_md = self._to_legacy()1030            if path:1031                legacy_md.write(path, skip_unknown=skip_unknown)1032            else:1033                legacy_md.write_file(fileobj, skip_unknown=skip_unknown)1034        else:1035            if self._legacy:1036                d = self._from_legacy()1037            else:1038                d = self._data1039            if fileobj:1040                json.dump(d, fileobj, ensure_ascii=True, indent=2,1041                          sort_keys=True)1042            else:1043                with codecs.open(path, 'w', 'utf-8') as f:1044                    json.dump(d, f, ensure_ascii=True, indent=2,1045                              sort_keys=True)1046 1047    def add_requirements(self, requirements):1048        if self._legacy:1049            self._legacy.add_requirements(requirements)1050        else:1051            run_requires = self._data.setdefault('run_requires', [])1052            always = None1053            for entry in run_requires:1054                if 'environment' not in entry and 'extra' not in entry:1055                    always = entry1056                    break1057            if always is None:1058                always = { 'requires': requirements }1059                run_requires.insert(0, always)1060            else:1061                rset = set(always['requires']) | set(requirements)1062                always['requires'] = sorted(rset)1063 1064    def __repr__(self):1065        name = self.name or '(no name)'1066        version = self.version or 'no version'1067        return '<%s %s %s (%s)>' % (self.__class__.__name__,1068                                    self.metadata_version, name, version)1069 
codekingpro/portable-devtools · Team Ai