| 1 | n/a | """Implementation of the Metadata for Python packages PEPs. |
|---|
| 2 | n/a | |
|---|
| 3 | n/a | Supports all metadata formats (1.0, 1.1, 1.2). |
|---|
| 4 | n/a | """ |
|---|
| 5 | n/a | |
|---|
| 6 | n/a | import re |
|---|
| 7 | n/a | import logging |
|---|
| 8 | n/a | |
|---|
| 9 | n/a | from io import StringIO |
|---|
| 10 | n/a | from email import message_from_file |
|---|
| 11 | n/a | from packaging import logger |
|---|
| 12 | n/a | from packaging.markers import interpret |
|---|
| 13 | n/a | from packaging.version import (is_valid_predicate, is_valid_version, |
|---|
| 14 | n/a | is_valid_versions) |
|---|
| 15 | n/a | from packaging.errors import (MetadataMissingError, |
|---|
| 16 | n/a | MetadataConflictError, |
|---|
| 17 | n/a | MetadataUnrecognizedVersionError) |
|---|
| 18 | n/a | |
|---|
| 19 | n/a | try: |
|---|
| 20 | n/a | # docutils is installed |
|---|
| 21 | n/a | from docutils.utils import Reporter |
|---|
| 22 | n/a | from docutils.parsers.rst import Parser |
|---|
| 23 | n/a | from docutils import frontend |
|---|
| 24 | n/a | from docutils import nodes |
|---|
| 25 | n/a | |
|---|
| 26 | n/a | class SilentReporter(Reporter): |
|---|
| 27 | n/a | |
|---|
| 28 | n/a | def __init__(self, source, report_level, halt_level, stream=None, |
|---|
| 29 | n/a | debug=0, encoding='ascii', error_handler='replace'): |
|---|
| 30 | n/a | self.messages = [] |
|---|
| 31 | n/a | super(SilentReporter, self).__init__( |
|---|
| 32 | n/a | source, report_level, halt_level, stream, |
|---|
| 33 | n/a | debug, encoding, error_handler) |
|---|
| 34 | n/a | |
|---|
| 35 | n/a | def system_message(self, level, message, *children, **kwargs): |
|---|
| 36 | n/a | self.messages.append((level, message, children, kwargs)) |
|---|
| 37 | n/a | |
|---|
| 38 | n/a | _HAS_DOCUTILS = True |
|---|
| 39 | n/a | except ImportError: |
|---|
| 40 | n/a | # docutils is not installed |
|---|
| 41 | n/a | _HAS_DOCUTILS = False |
|---|
| 42 | n/a | |
|---|
| 43 | n/a | # public API of this module |
|---|
| 44 | n/a | __all__ = ['Metadata', 'PKG_INFO_ENCODING', 'PKG_INFO_PREFERRED_VERSION'] |
|---|
| 45 | n/a | |
|---|
| 46 | n/a | # Encoding used for the PKG-INFO files |
|---|
| 47 | n/a | PKG_INFO_ENCODING = 'utf-8' |
|---|
| 48 | n/a | |
|---|
| 49 | n/a | # preferred version. Hopefully will be changed |
|---|
| 50 | n/a | # to 1.2 once PEP 345 is supported everywhere |
|---|
| 51 | n/a | PKG_INFO_PREFERRED_VERSION = '1.0' |
|---|
| 52 | n/a | |
|---|
| 53 | n/a | _LINE_PREFIX = re.compile('\n \|') |
|---|
| 54 | n/a | _241_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform', |
|---|
| 55 | n/a | 'Summary', 'Description', |
|---|
| 56 | n/a | 'Keywords', 'Home-page', 'Author', 'Author-email', |
|---|
| 57 | n/a | 'License') |
|---|
| 58 | n/a | |
|---|
| 59 | n/a | _314_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform', |
|---|
| 60 | n/a | 'Supported-Platform', 'Summary', 'Description', |
|---|
| 61 | n/a | 'Keywords', 'Home-page', 'Author', 'Author-email', |
|---|
| 62 | n/a | 'License', 'Classifier', 'Download-URL', 'Obsoletes', |
|---|
| 63 | n/a | 'Provides', 'Requires') |
|---|
| 64 | n/a | |
|---|
| 65 | n/a | _314_MARKERS = ('Obsoletes', 'Provides', 'Requires', 'Classifier', |
|---|
| 66 | n/a | 'Download-URL') |
|---|
| 67 | n/a | |
|---|
| 68 | n/a | _345_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform', |
|---|
| 69 | n/a | 'Supported-Platform', 'Summary', 'Description', |
|---|
| 70 | n/a | 'Keywords', 'Home-page', 'Author', 'Author-email', |
|---|
| 71 | n/a | 'Maintainer', 'Maintainer-email', 'License', |
|---|
| 72 | n/a | 'Classifier', 'Download-URL', 'Obsoletes-Dist', |
|---|
| 73 | n/a | 'Project-URL', 'Provides-Dist', 'Requires-Dist', |
|---|
| 74 | n/a | 'Requires-Python', 'Requires-External') |
|---|
| 75 | n/a | |
|---|
| 76 | n/a | _345_MARKERS = ('Provides-Dist', 'Requires-Dist', 'Requires-Python', |
|---|
| 77 | n/a | 'Obsoletes-Dist', 'Requires-External', 'Maintainer', |
|---|
| 78 | n/a | 'Maintainer-email', 'Project-URL') |
|---|
| 79 | n/a | |
|---|
| 80 | n/a | _ALL_FIELDS = set() |
|---|
| 81 | n/a | _ALL_FIELDS.update(_241_FIELDS) |
|---|
| 82 | n/a | _ALL_FIELDS.update(_314_FIELDS) |
|---|
| 83 | n/a | _ALL_FIELDS.update(_345_FIELDS) |
|---|
| 84 | n/a | |
|---|
| 85 | n/a | |
|---|
| 86 | n/a | def _version2fieldlist(version): |
|---|
| 87 | n/a | if version == '1.0': |
|---|
| 88 | n/a | return _241_FIELDS |
|---|
| 89 | n/a | elif version == '1.1': |
|---|
| 90 | n/a | return _314_FIELDS |
|---|
| 91 | n/a | elif version == '1.2': |
|---|
| 92 | n/a | return _345_FIELDS |
|---|
| 93 | n/a | raise MetadataUnrecognizedVersionError(version) |
|---|
| 94 | n/a | |
|---|
| 95 | n/a | |
|---|
| 96 | n/a | def _best_version(fields): |
|---|
| 97 | n/a | """Detect the best version depending on the fields used.""" |
|---|
| 98 | n/a | def _has_marker(keys, markers): |
|---|
| 99 | n/a | for marker in markers: |
|---|
| 100 | n/a | if marker in keys: |
|---|
| 101 | n/a | return True |
|---|
| 102 | n/a | return False |
|---|
| 103 | n/a | |
|---|
| 104 | n/a | keys = list(fields) |
|---|
| 105 | n/a | possible_versions = ['1.0', '1.1', '1.2'] |
|---|
| 106 | n/a | |
|---|
| 107 | n/a | # first let's try to see if a field is not part of one of the version |
|---|
| 108 | n/a | for key in keys: |
|---|
| 109 | n/a | if key not in _241_FIELDS and '1.0' in possible_versions: |
|---|
| 110 | n/a | possible_versions.remove('1.0') |
|---|
| 111 | n/a | if key not in _314_FIELDS and '1.1' in possible_versions: |
|---|
| 112 | n/a | possible_versions.remove('1.1') |
|---|
| 113 | n/a | if key not in _345_FIELDS and '1.2' in possible_versions: |
|---|
| 114 | n/a | possible_versions.remove('1.2') |
|---|
| 115 | n/a | |
|---|
| 116 | n/a | # possible_version contains qualified versions |
|---|
| 117 | n/a | if len(possible_versions) == 1: |
|---|
| 118 | n/a | return possible_versions[0] # found ! |
|---|
| 119 | n/a | elif len(possible_versions) == 0: |
|---|
| 120 | n/a | raise MetadataConflictError('Unknown metadata set') |
|---|
| 121 | n/a | |
|---|
| 122 | n/a | # let's see if one unique marker is found |
|---|
| 123 | n/a | is_1_1 = '1.1' in possible_versions and _has_marker(keys, _314_MARKERS) |
|---|
| 124 | n/a | is_1_2 = '1.2' in possible_versions and _has_marker(keys, _345_MARKERS) |
|---|
| 125 | n/a | if is_1_1 and is_1_2: |
|---|
| 126 | n/a | raise MetadataConflictError('You used incompatible 1.1 and 1.2 fields') |
|---|
| 127 | n/a | |
|---|
| 128 | n/a | # we have the choice, either 1.0, or 1.2 |
|---|
| 129 | n/a | # - 1.0 has a broken Summary field but works with all tools |
|---|
| 130 | n/a | # - 1.1 is to avoid |
|---|
| 131 | n/a | # - 1.2 fixes Summary but is not widespread yet |
|---|
| 132 | n/a | if not is_1_1 and not is_1_2: |
|---|
| 133 | n/a | # we couldn't find any specific marker |
|---|
| 134 | n/a | if PKG_INFO_PREFERRED_VERSION in possible_versions: |
|---|
| 135 | n/a | return PKG_INFO_PREFERRED_VERSION |
|---|
| 136 | n/a | if is_1_1: |
|---|
| 137 | n/a | return '1.1' |
|---|
| 138 | n/a | |
|---|
| 139 | n/a | # default marker when 1.0 is disqualified |
|---|
| 140 | n/a | return '1.2' |
|---|
| 141 | n/a | |
|---|
| 142 | n/a | |
|---|
| 143 | n/a | _ATTR2FIELD = { |
|---|
| 144 | n/a | 'metadata_version': 'Metadata-Version', |
|---|
| 145 | n/a | 'name': 'Name', |
|---|
| 146 | n/a | 'version': 'Version', |
|---|
| 147 | n/a | 'platform': 'Platform', |
|---|
| 148 | n/a | 'supported_platform': 'Supported-Platform', |
|---|
| 149 | n/a | 'summary': 'Summary', |
|---|
| 150 | n/a | 'description': 'Description', |
|---|
| 151 | n/a | 'keywords': 'Keywords', |
|---|
| 152 | n/a | 'home_page': 'Home-page', |
|---|
| 153 | n/a | 'author': 'Author', |
|---|
| 154 | n/a | 'author_email': 'Author-email', |
|---|
| 155 | n/a | 'maintainer': 'Maintainer', |
|---|
| 156 | n/a | 'maintainer_email': 'Maintainer-email', |
|---|
| 157 | n/a | 'license': 'License', |
|---|
| 158 | n/a | 'classifier': 'Classifier', |
|---|
| 159 | n/a | 'download_url': 'Download-URL', |
|---|
| 160 | n/a | 'obsoletes_dist': 'Obsoletes-Dist', |
|---|
| 161 | n/a | 'provides_dist': 'Provides-Dist', |
|---|
| 162 | n/a | 'requires_dist': 'Requires-Dist', |
|---|
| 163 | n/a | 'requires_python': 'Requires-Python', |
|---|
| 164 | n/a | 'requires_external': 'Requires-External', |
|---|
| 165 | n/a | 'requires': 'Requires', |
|---|
| 166 | n/a | 'provides': 'Provides', |
|---|
| 167 | n/a | 'obsoletes': 'Obsoletes', |
|---|
| 168 | n/a | 'project_url': 'Project-URL', |
|---|
| 169 | n/a | } |
|---|
| 170 | n/a | |
|---|
| 171 | n/a | _PREDICATE_FIELDS = ('Requires-Dist', 'Obsoletes-Dist', 'Provides-Dist') |
|---|
| 172 | n/a | _VERSIONS_FIELDS = ('Requires-Python',) |
|---|
| 173 | n/a | _VERSION_FIELDS = ('Version',) |
|---|
| 174 | n/a | _LISTFIELDS = ('Platform', 'Classifier', 'Obsoletes', |
|---|
| 175 | n/a | 'Requires', 'Provides', 'Obsoletes-Dist', |
|---|
| 176 | n/a | 'Provides-Dist', 'Requires-Dist', 'Requires-External', |
|---|
| 177 | n/a | 'Project-URL', 'Supported-Platform') |
|---|
| 178 | n/a | _LISTTUPLEFIELDS = ('Project-URL',) |
|---|
| 179 | n/a | |
|---|
| 180 | n/a | _ELEMENTSFIELD = ('Keywords',) |
|---|
| 181 | n/a | |
|---|
| 182 | n/a | _UNICODEFIELDS = ('Author', 'Maintainer', 'Summary', 'Description') |
|---|
| 183 | n/a | |
|---|
| 184 | n/a | _MISSING = object() |
|---|
| 185 | n/a | |
|---|
| 186 | n/a | _FILESAFE = re.compile('[^A-Za-z0-9.]+') |
|---|
| 187 | n/a | |
|---|
| 188 | n/a | |
|---|
| 189 | n/a | class Metadata: |
|---|
| 190 | n/a | """The metadata of a release. |
|---|
| 191 | n/a | |
|---|
| 192 | n/a | Supports versions 1.0, 1.1 and 1.2 (auto-detected). You can |
|---|
| 193 | n/a | instantiate the class with one of these arguments (or none): |
|---|
| 194 | n/a | - *path*, the path to a METADATA file |
|---|
| 195 | n/a | - *fileobj* give a file-like object with METADATA as content |
|---|
| 196 | n/a | - *mapping* is a dict-like object |
|---|
| 197 | n/a | """ |
|---|
| 198 | n/a | # TODO document that execution_context and platform_dependent are used |
|---|
| 199 | n/a | # to filter on query, not when setting a key |
|---|
| 200 | n/a | # also document the mapping API and UNKNOWN default key |
|---|
| 201 | n/a | |
|---|
| 202 | n/a | def __init__(self, path=None, platform_dependent=False, |
|---|
| 203 | n/a | execution_context=None, fileobj=None, mapping=None): |
|---|
| 204 | n/a | self._fields = {} |
|---|
| 205 | n/a | self.requires_files = [] |
|---|
| 206 | n/a | self.docutils_support = _HAS_DOCUTILS |
|---|
| 207 | n/a | self.platform_dependent = platform_dependent |
|---|
| 208 | n/a | self.execution_context = execution_context |
|---|
| 209 | n/a | if [path, fileobj, mapping].count(None) < 2: |
|---|
| 210 | n/a | raise TypeError('path, fileobj and mapping are exclusive') |
|---|
| 211 | n/a | if path is not None: |
|---|
| 212 | n/a | self.read(path) |
|---|
| 213 | n/a | elif fileobj is not None: |
|---|
| 214 | n/a | self.read_file(fileobj) |
|---|
| 215 | n/a | elif mapping is not None: |
|---|
| 216 | n/a | self.update(mapping) |
|---|
| 217 | n/a | |
|---|
| 218 | n/a | def _set_best_version(self): |
|---|
| 219 | n/a | self._fields['Metadata-Version'] = _best_version(self._fields) |
|---|
| 220 | n/a | |
|---|
| 221 | n/a | def _write_field(self, file, name, value): |
|---|
| 222 | n/a | file.write('%s: %s\n' % (name, value)) |
|---|
| 223 | n/a | |
|---|
| 224 | n/a | def __getitem__(self, name): |
|---|
| 225 | n/a | return self.get(name) |
|---|
| 226 | n/a | |
|---|
| 227 | n/a | def __setitem__(self, name, value): |
|---|
| 228 | n/a | return self.set(name, value) |
|---|
| 229 | n/a | |
|---|
| 230 | n/a | def __delitem__(self, name): |
|---|
| 231 | n/a | field_name = self._convert_name(name) |
|---|
| 232 | n/a | try: |
|---|
| 233 | n/a | del self._fields[field_name] |
|---|
| 234 | n/a | except KeyError: |
|---|
| 235 | n/a | raise KeyError(name) |
|---|
| 236 | n/a | self._set_best_version() |
|---|
| 237 | n/a | |
|---|
| 238 | n/a | def __contains__(self, name): |
|---|
| 239 | n/a | return (name in self._fields or |
|---|
| 240 | n/a | self._convert_name(name) in self._fields) |
|---|
| 241 | n/a | |
|---|
| 242 | n/a | def _convert_name(self, name): |
|---|
| 243 | n/a | if name in _ALL_FIELDS: |
|---|
| 244 | n/a | return name |
|---|
| 245 | n/a | name = name.replace('-', '_').lower() |
|---|
| 246 | n/a | return _ATTR2FIELD.get(name, name) |
|---|
| 247 | n/a | |
|---|
| 248 | n/a | def _default_value(self, name): |
|---|
| 249 | n/a | if name in _LISTFIELDS or name in _ELEMENTSFIELD: |
|---|
| 250 | n/a | return [] |
|---|
| 251 | n/a | return 'UNKNOWN' |
|---|
| 252 | n/a | |
|---|
| 253 | n/a | def _check_rst_data(self, data): |
|---|
| 254 | n/a | """Return warnings when the provided data has syntax errors.""" |
|---|
| 255 | n/a | source_path = StringIO() |
|---|
| 256 | n/a | parser = Parser() |
|---|
| 257 | n/a | settings = frontend.OptionParser().get_default_values() |
|---|
| 258 | n/a | settings.tab_width = 4 |
|---|
| 259 | n/a | settings.pep_references = None |
|---|
| 260 | n/a | settings.rfc_references = None |
|---|
| 261 | n/a | reporter = SilentReporter(source_path, |
|---|
| 262 | n/a | settings.report_level, |
|---|
| 263 | n/a | settings.halt_level, |
|---|
| 264 | n/a | stream=settings.warning_stream, |
|---|
| 265 | n/a | debug=settings.debug, |
|---|
| 266 | n/a | encoding=settings.error_encoding, |
|---|
| 267 | n/a | error_handler=settings.error_encoding_error_handler) |
|---|
| 268 | n/a | |
|---|
| 269 | n/a | document = nodes.document(settings, reporter, source=source_path) |
|---|
| 270 | n/a | document.note_source(source_path, -1) |
|---|
| 271 | n/a | try: |
|---|
| 272 | n/a | parser.parse(data, document) |
|---|
| 273 | n/a | except AttributeError: |
|---|
| 274 | n/a | reporter.messages.append((-1, 'Could not finish the parsing.', |
|---|
| 275 | n/a | '', {})) |
|---|
| 276 | n/a | |
|---|
| 277 | n/a | return reporter.messages |
|---|
| 278 | n/a | |
|---|
| 279 | n/a | def _platform(self, value): |
|---|
| 280 | n/a | if not self.platform_dependent or ';' not in value: |
|---|
| 281 | n/a | return True, value |
|---|
| 282 | n/a | value, marker = value.split(';') |
|---|
| 283 | n/a | return interpret(marker, self.execution_context), value |
|---|
| 284 | n/a | |
|---|
| 285 | n/a | def _remove_line_prefix(self, value): |
|---|
| 286 | n/a | return _LINE_PREFIX.sub('\n', value) |
|---|
| 287 | n/a | |
|---|
| 288 | n/a | # |
|---|
| 289 | n/a | # Public API |
|---|
| 290 | n/a | # |
|---|
| 291 | n/a | def get_fullname(self, filesafe=False): |
|---|
| 292 | n/a | """Return the distribution name with version. |
|---|
| 293 | n/a | |
|---|
| 294 | n/a | If filesafe is true, return a filename-escaped form.""" |
|---|
| 295 | n/a | name, version = self['Name'], self['Version'] |
|---|
| 296 | n/a | if filesafe: |
|---|
| 297 | n/a | # For both name and version any runs of non-alphanumeric or '.' |
|---|
| 298 | n/a | # characters are replaced with a single '-'. Additionally any |
|---|
| 299 | n/a | # spaces in the version string become '.' |
|---|
| 300 | n/a | name = _FILESAFE.sub('-', name) |
|---|
| 301 | n/a | version = _FILESAFE.sub('-', version.replace(' ', '.')) |
|---|
| 302 | n/a | return '%s-%s' % (name, version) |
|---|
| 303 | n/a | |
|---|
| 304 | n/a | def is_metadata_field(self, name): |
|---|
| 305 | n/a | """return True if name is a valid metadata key""" |
|---|
| 306 | n/a | name = self._convert_name(name) |
|---|
| 307 | n/a | return name in _ALL_FIELDS |
|---|
| 308 | n/a | |
|---|
| 309 | n/a | def is_multi_field(self, name): |
|---|
| 310 | n/a | name = self._convert_name(name) |
|---|
| 311 | n/a | return name in _LISTFIELDS |
|---|
| 312 | n/a | |
|---|
| 313 | n/a | def read(self, filepath): |
|---|
| 314 | n/a | """Read the metadata values from a file path.""" |
|---|
| 315 | n/a | with open(filepath, 'r', encoding='utf-8') as fp: |
|---|
| 316 | n/a | self.read_file(fp) |
|---|
| 317 | n/a | |
|---|
| 318 | n/a | def read_file(self, fileob): |
|---|
| 319 | n/a | """Read the metadata values from a file object.""" |
|---|
| 320 | n/a | msg = message_from_file(fileob) |
|---|
| 321 | n/a | self._fields['Metadata-Version'] = msg['metadata-version'] |
|---|
| 322 | n/a | |
|---|
| 323 | n/a | for field in _version2fieldlist(self['Metadata-Version']): |
|---|
| 324 | n/a | if field in _LISTFIELDS: |
|---|
| 325 | n/a | # we can have multiple lines |
|---|
| 326 | n/a | values = msg.get_all(field) |
|---|
| 327 | n/a | if field in _LISTTUPLEFIELDS and values is not None: |
|---|
| 328 | n/a | values = [tuple(value.split(',')) for value in values] |
|---|
| 329 | n/a | self.set(field, values) |
|---|
| 330 | n/a | else: |
|---|
| 331 | n/a | # single line |
|---|
| 332 | n/a | value = msg[field] |
|---|
| 333 | n/a | if value is not None and value != 'UNKNOWN': |
|---|
| 334 | n/a | self.set(field, value) |
|---|
| 335 | n/a | |
|---|
| 336 | n/a | def write(self, filepath): |
|---|
| 337 | n/a | """Write the metadata fields to filepath.""" |
|---|
| 338 | n/a | with open(filepath, 'w', encoding='utf-8') as fp: |
|---|
| 339 | n/a | self.write_file(fp) |
|---|
| 340 | n/a | |
|---|
| 341 | n/a | def write_file(self, fileobject): |
|---|
| 342 | n/a | """Write the PKG-INFO format data to a file object.""" |
|---|
| 343 | n/a | self._set_best_version() |
|---|
| 344 | n/a | for field in _version2fieldlist(self['Metadata-Version']): |
|---|
| 345 | n/a | values = self.get(field) |
|---|
| 346 | n/a | if field in _ELEMENTSFIELD: |
|---|
| 347 | n/a | self._write_field(fileobject, field, ','.join(values)) |
|---|
| 348 | n/a | continue |
|---|
| 349 | n/a | if field not in _LISTFIELDS: |
|---|
| 350 | n/a | if field == 'Description': |
|---|
| 351 | n/a | values = values.replace('\n', '\n |') |
|---|
| 352 | n/a | values = [values] |
|---|
| 353 | n/a | |
|---|
| 354 | n/a | if field in _LISTTUPLEFIELDS: |
|---|
| 355 | n/a | values = [','.join(value) for value in values] |
|---|
| 356 | n/a | |
|---|
| 357 | n/a | for value in values: |
|---|
| 358 | n/a | self._write_field(fileobject, field, value) |
|---|
| 359 | n/a | |
|---|
| 360 | n/a | def update(self, other=None, **kwargs): |
|---|
| 361 | n/a | """Set metadata values from the given iterable `other` and kwargs. |
|---|
| 362 | n/a | |
|---|
| 363 | n/a | Behavior is like `dict.update`: If `other` has a ``keys`` method, |
|---|
| 364 | n/a | they are looped over and ``self[key]`` is assigned ``other[key]``. |
|---|
| 365 | n/a | Else, ``other`` is an iterable of ``(key, value)`` iterables. |
|---|
| 366 | n/a | |
|---|
| 367 | n/a | Keys that don't match a metadata field or that have an empty value are |
|---|
| 368 | n/a | dropped. |
|---|
| 369 | n/a | """ |
|---|
| 370 | n/a | # XXX the code should just use self.set, which does tbe same checks and |
|---|
| 371 | n/a | # conversions already, but that would break packaging.pypi: it uses the |
|---|
| 372 | n/a | # update method, which does not call _set_best_version (which set |
|---|
| 373 | n/a | # does), and thus allows having a Metadata object (as long as you don't |
|---|
| 374 | n/a | # modify or write it) with extra fields from PyPI that are not fields |
|---|
| 375 | n/a | # defined in Metadata PEPs. to solve it, the best_version system |
|---|
| 376 | n/a | # should be reworked so that it's called only for writing, or in a new |
|---|
| 377 | n/a | # strict mode, or with a new, more lax Metadata subclass in p7g.pypi |
|---|
| 378 | n/a | def _set(key, value): |
|---|
| 379 | n/a | if key in _ATTR2FIELD and value: |
|---|
| 380 | n/a | self.set(self._convert_name(key), value) |
|---|
| 381 | n/a | |
|---|
| 382 | n/a | if not other: |
|---|
| 383 | n/a | # other is None or empty container |
|---|
| 384 | n/a | pass |
|---|
| 385 | n/a | elif hasattr(other, 'keys'): |
|---|
| 386 | n/a | for k in other.keys(): |
|---|
| 387 | n/a | _set(k, other[k]) |
|---|
| 388 | n/a | else: |
|---|
| 389 | n/a | for k, v in other: |
|---|
| 390 | n/a | _set(k, v) |
|---|
| 391 | n/a | |
|---|
| 392 | n/a | if kwargs: |
|---|
| 393 | n/a | for k, v in kwargs.items(): |
|---|
| 394 | n/a | _set(k, v) |
|---|
| 395 | n/a | |
|---|
| 396 | n/a | def set(self, name, value): |
|---|
| 397 | n/a | """Control then set a metadata field.""" |
|---|
| 398 | n/a | name = self._convert_name(name) |
|---|
| 399 | n/a | |
|---|
| 400 | n/a | if ((name in _ELEMENTSFIELD or name == 'Platform') and |
|---|
| 401 | n/a | not isinstance(value, (list, tuple))): |
|---|
| 402 | n/a | if isinstance(value, str): |
|---|
| 403 | n/a | value = [v.strip() for v in value.split(',')] |
|---|
| 404 | n/a | else: |
|---|
| 405 | n/a | value = [] |
|---|
| 406 | n/a | elif (name in _LISTFIELDS and |
|---|
| 407 | n/a | not isinstance(value, (list, tuple))): |
|---|
| 408 | n/a | if isinstance(value, str): |
|---|
| 409 | n/a | value = [value] |
|---|
| 410 | n/a | else: |
|---|
| 411 | n/a | value = [] |
|---|
| 412 | n/a | |
|---|
| 413 | n/a | if logger.isEnabledFor(logging.WARNING): |
|---|
| 414 | n/a | project_name = self['Name'] |
|---|
| 415 | n/a | |
|---|
| 416 | n/a | if name in _PREDICATE_FIELDS and value is not None: |
|---|
| 417 | n/a | for v in value: |
|---|
| 418 | n/a | # check that the values are valid predicates |
|---|
| 419 | n/a | if not is_valid_predicate(v.split(';')[0]): |
|---|
| 420 | n/a | logger.warning( |
|---|
| 421 | n/a | '%r: %r is not a valid predicate (field %r)', |
|---|
| 422 | n/a | project_name, v, name) |
|---|
| 423 | n/a | # FIXME this rejects UNKNOWN, is that right? |
|---|
| 424 | n/a | elif name in _VERSIONS_FIELDS and value is not None: |
|---|
| 425 | n/a | if not is_valid_versions(value): |
|---|
| 426 | n/a | logger.warning('%r: %r is not a valid version (field %r)', |
|---|
| 427 | n/a | project_name, value, name) |
|---|
| 428 | n/a | elif name in _VERSION_FIELDS and value is not None: |
|---|
| 429 | n/a | if not is_valid_version(value): |
|---|
| 430 | n/a | logger.warning('%r: %r is not a valid version (field %r)', |
|---|
| 431 | n/a | project_name, value, name) |
|---|
| 432 | n/a | |
|---|
| 433 | n/a | if name in _UNICODEFIELDS: |
|---|
| 434 | n/a | if name == 'Description': |
|---|
| 435 | n/a | value = self._remove_line_prefix(value) |
|---|
| 436 | n/a | |
|---|
| 437 | n/a | self._fields[name] = value |
|---|
| 438 | n/a | self._set_best_version() |
|---|
| 439 | n/a | |
|---|
| 440 | n/a | def get(self, name, default=_MISSING): |
|---|
| 441 | n/a | """Get a metadata field.""" |
|---|
| 442 | n/a | name = self._convert_name(name) |
|---|
| 443 | n/a | if name not in self._fields: |
|---|
| 444 | n/a | if default is _MISSING: |
|---|
| 445 | n/a | default = self._default_value(name) |
|---|
| 446 | n/a | return default |
|---|
| 447 | n/a | if name in _UNICODEFIELDS: |
|---|
| 448 | n/a | value = self._fields[name] |
|---|
| 449 | n/a | return value |
|---|
| 450 | n/a | elif name in _LISTFIELDS: |
|---|
| 451 | n/a | value = self._fields[name] |
|---|
| 452 | n/a | if value is None: |
|---|
| 453 | n/a | return [] |
|---|
| 454 | n/a | res = [] |
|---|
| 455 | n/a | for val in value: |
|---|
| 456 | n/a | valid, val = self._platform(val) |
|---|
| 457 | n/a | if not valid: |
|---|
| 458 | n/a | continue |
|---|
| 459 | n/a | if name not in _LISTTUPLEFIELDS: |
|---|
| 460 | n/a | res.append(val) |
|---|
| 461 | n/a | else: |
|---|
| 462 | n/a | # That's for Project-URL |
|---|
| 463 | n/a | res.append((val[0], val[1])) |
|---|
| 464 | n/a | return res |
|---|
| 465 | n/a | |
|---|
| 466 | n/a | elif name in _ELEMENTSFIELD: |
|---|
| 467 | n/a | valid, value = self._platform(self._fields[name]) |
|---|
| 468 | n/a | if not valid: |
|---|
| 469 | n/a | return [] |
|---|
| 470 | n/a | if isinstance(value, str): |
|---|
| 471 | n/a | return value.split(',') |
|---|
| 472 | n/a | valid, value = self._platform(self._fields[name]) |
|---|
| 473 | n/a | if not valid: |
|---|
| 474 | n/a | return None |
|---|
| 475 | n/a | return value |
|---|
| 476 | n/a | |
|---|
| 477 | n/a | def check(self, strict=False, restructuredtext=False): |
|---|
| 478 | n/a | """Check if the metadata is compliant. If strict is False then raise if |
|---|
| 479 | n/a | no Name or Version are provided""" |
|---|
| 480 | n/a | # XXX should check the versions (if the file was loaded) |
|---|
| 481 | n/a | missing, warnings = [], [] |
|---|
| 482 | n/a | |
|---|
| 483 | n/a | for attr in ('Name', 'Version'): # required by PEP 345 |
|---|
| 484 | n/a | if attr not in self: |
|---|
| 485 | n/a | missing.append(attr) |
|---|
| 486 | n/a | |
|---|
| 487 | n/a | if strict and missing != []: |
|---|
| 488 | n/a | msg = 'missing required metadata: %s' % ', '.join(missing) |
|---|
| 489 | n/a | raise MetadataMissingError(msg) |
|---|
| 490 | n/a | |
|---|
| 491 | n/a | for attr in ('Home-page', 'Author'): |
|---|
| 492 | n/a | if attr not in self: |
|---|
| 493 | n/a | missing.append(attr) |
|---|
| 494 | n/a | |
|---|
| 495 | n/a | if _HAS_DOCUTILS and restructuredtext: |
|---|
| 496 | n/a | warnings.extend(self._check_rst_data(self['Description'])) |
|---|
| 497 | n/a | |
|---|
| 498 | n/a | # checking metadata 1.2 (XXX needs to check 1.1, 1.0) |
|---|
| 499 | n/a | if self['Metadata-Version'] != '1.2': |
|---|
| 500 | n/a | return missing, warnings |
|---|
| 501 | n/a | |
|---|
| 502 | n/a | def is_valid_predicates(value): |
|---|
| 503 | n/a | for v in value: |
|---|
| 504 | n/a | if not is_valid_predicate(v.split(';')[0]): |
|---|
| 505 | n/a | return False |
|---|
| 506 | n/a | return True |
|---|
| 507 | n/a | |
|---|
| 508 | n/a | for fields, controller in ((_PREDICATE_FIELDS, is_valid_predicates), |
|---|
| 509 | n/a | (_VERSIONS_FIELDS, is_valid_versions), |
|---|
| 510 | n/a | (_VERSION_FIELDS, is_valid_version)): |
|---|
| 511 | n/a | for field in fields: |
|---|
| 512 | n/a | value = self.get(field, None) |
|---|
| 513 | n/a | if value is not None and not controller(value): |
|---|
| 514 | n/a | warnings.append('Wrong value for %r: %s' % (field, value)) |
|---|
| 515 | n/a | |
|---|
| 516 | n/a | return missing, warnings |
|---|
| 517 | n/a | |
|---|
| 518 | n/a | def todict(self): |
|---|
| 519 | n/a | """Return fields as a dict. |
|---|
| 520 | n/a | |
|---|
| 521 | n/a | Field names will be converted to use the underscore-lowercase style |
|---|
| 522 | n/a | instead of hyphen-mixed case (i.e. home_page instead of Home-page). |
|---|
| 523 | n/a | """ |
|---|
| 524 | n/a | data = { |
|---|
| 525 | n/a | 'metadata_version': self['Metadata-Version'], |
|---|
| 526 | n/a | 'name': self['Name'], |
|---|
| 527 | n/a | 'version': self['Version'], |
|---|
| 528 | n/a | 'summary': self['Summary'], |
|---|
| 529 | n/a | 'home_page': self['Home-page'], |
|---|
| 530 | n/a | 'author': self['Author'], |
|---|
| 531 | n/a | 'author_email': self['Author-email'], |
|---|
| 532 | n/a | 'license': self['License'], |
|---|
| 533 | n/a | 'description': self['Description'], |
|---|
| 534 | n/a | 'keywords': self['Keywords'], |
|---|
| 535 | n/a | 'platform': self['Platform'], |
|---|
| 536 | n/a | 'classifier': self['Classifier'], |
|---|
| 537 | n/a | 'download_url': self['Download-URL'], |
|---|
| 538 | n/a | } |
|---|
| 539 | n/a | |
|---|
| 540 | n/a | if self['Metadata-Version'] == '1.2': |
|---|
| 541 | n/a | data['requires_dist'] = self['Requires-Dist'] |
|---|
| 542 | n/a | data['requires_python'] = self['Requires-Python'] |
|---|
| 543 | n/a | data['requires_external'] = self['Requires-External'] |
|---|
| 544 | n/a | data['provides_dist'] = self['Provides-Dist'] |
|---|
| 545 | n/a | data['obsoletes_dist'] = self['Obsoletes-Dist'] |
|---|
| 546 | n/a | data['project_url'] = [','.join(url) for url in |
|---|
| 547 | n/a | self['Project-URL']] |
|---|
| 548 | n/a | |
|---|
| 549 | n/a | elif self['Metadata-Version'] == '1.1': |
|---|
| 550 | n/a | data['provides'] = self['Provides'] |
|---|
| 551 | n/a | data['requires'] = self['Requires'] |
|---|
| 552 | n/a | data['obsoletes'] = self['Obsoletes'] |
|---|
| 553 | n/a | |
|---|
| 554 | n/a | return data |
|---|
| 555 | n/a | |
|---|
| 556 | n/a | # Mapping API |
|---|
| 557 | n/a | # XXX these methods should return views or sets in 3.x |
|---|
| 558 | n/a | |
|---|
| 559 | n/a | def keys(self): |
|---|
| 560 | n/a | return list(_version2fieldlist(self['Metadata-Version'])) |
|---|
| 561 | n/a | |
|---|
| 562 | n/a | def __iter__(self): |
|---|
| 563 | n/a | for key in self.keys(): |
|---|
| 564 | n/a | yield key |
|---|
| 565 | n/a | |
|---|
| 566 | n/a | def values(self): |
|---|
| 567 | n/a | return [self[key] for key in self.keys()] |
|---|
| 568 | n/a | |
|---|
| 569 | n/a | def items(self): |
|---|
| 570 | n/a | return [(key, self[key]) for key in self.keys()] |
|---|