Skip to content

Commit

Permalink
Don't create metadata.json anymore
Browse files Browse the repository at this point in the history
This was a thing in the now-withdrawn PEP 426.
Fixes #195.
  • Loading branch information
agronholm committed Mar 30, 2018
1 parent 7c3d3c5 commit 595e4a8
Show file tree
Hide file tree
Showing 6 changed files with 3 additions and 268 deletions.
2 changes: 1 addition & 1 deletion CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Unreleased
- Fixed single digit versions in wheel files not being properly recognized
- Enabled Zip64 support in wheels by default
- Metadata-Version is now 2.1
- Don't create a DESCRIPTION.RST anymore.
- Dropped DESCRIPTION.rst and metadata.json from the list of generated files

0.30.0
======
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
'signatures:sys_platform!="win32"': ['pyxdg'],
'faster-signatures': ['ed25519ll'],
'tool': [],
'test': ['jsonschema', 'pytest >= 3.0.0', 'pytest-cov']
'test': ['pytest >= 3.0.0', 'pytest-cov']
},
include_package_data=True,
zip_safe=False,
Expand Down
24 changes: 0 additions & 24 deletions tests/test_bdist_wheel.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,8 @@
import json
import os.path
from zipfile import ZipFile

import jsonschema
import pytest

THISDIR = os.path.dirname(__file__)


def test_no_scripts(wheel_paths):
"""Make sure entry point scripts are not generated."""
path = next(path for path in wheel_paths if 'complex-dist' in path)
for entry in ZipFile(path).infolist():
assert '.data/scripts/' not in entry.filename


def test_pydist(wheel_paths):
"""Make sure metadata.json exists and validates against our schema."""
# XXX this test may need manual cleanup of older wheels
with open(os.path.join(THISDIR, 'pydist-schema.json')) as f:
pymeta_schema = json.load(f)

for wheel_path in wheel_paths:
with ZipFile(wheel_path) as whl:
for entry in ZipFile(wheel_path).infolist():
if entry.filename.endswith('/metadata.json'):
pymeta = json.loads(whl.read(entry).decode('utf-8'))
jsonschema.validate(pymeta, pymeta_schema)
break
else:
pytest.fail('No metadata.json found in %s' % wheel_path)
15 changes: 0 additions & 15 deletions wheel/bdist_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import subprocess
import warnings
import shutil
import json
import sys
import re
from email.generator import Generator
Expand All @@ -25,7 +24,6 @@
from .util import native, open_for_csv
from .archive import archive_wheelfile
from .pkginfo import read_pkg_info, write_pkg_info
from .metadata import pkginfo_to_dict
from . import pep425tags, metadata
from . import __version__ as wheel_version

Expand Down Expand Up @@ -420,24 +418,11 @@ def adios(p):
metadata_path = os.path.join(distinfo_path, 'METADATA')
self.add_requirements(metadata_path)

# XXX intentionally a different path than the PEP.
metadata_json_path = os.path.join(distinfo_path, 'metadata.json')
pymeta = pkginfo_to_dict(metadata_path,
distribution=self.distribution)

if 'description' in pymeta:
# Don't include `description` in JSON metadata
pymeta.pop('description')

# XXX heuristically copy any LICENSE/LICENSE.txt?
license = self.license_file()
if license:
license_filename = 'LICENSE.txt'
shutil.copy(license, os.path.join(self.distinfo_dir, license_filename))
pymeta['extensions']['python.details']['document_names']['license'] = license_filename

with open(metadata_json_path, "w") as metadata_json:
json.dump(pymeta, metadata_json, sort_keys=True)

adios(egginfo_path)

Expand Down
209 changes: 1 addition & 208 deletions wheel/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,222 +2,22 @@
Tools for converting old- to new-style metadata.
"""

import email.parser
import os.path
import re
import textwrap
from collections import namedtuple, OrderedDict
from collections import namedtuple

import pkg_resources

from . import __version__ as wheel_version
from .pkginfo import read_pkg_info
from .util import OrderedDefaultDict

METADATA_VERSION = "2.1"

PLURAL_FIELDS = {"classifier": "classifiers",
"provides_dist": "provides",
"provides_extra": "extras"}

SKIP_FIELDS = set()

CONTACT_FIELDS = (({"email": "author_email", "name": "author"},
"author"),
({"email": "maintainer_email", "name": "maintainer"},
"maintainer"))

# commonly filled out as "UNKNOWN" by distutils:
UNKNOWN_FIELDS = {"author", "author_email", "platform", "home_page", "license"}

# 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<package>.*?)(;\s*(?P<condition>.*?)(extra == '(?P<extra>.*?)')?)$""")
KEYWORDS_RE = re.compile("[\0-,]+")

MayRequiresKey = namedtuple('MayRequiresKey', ('condition', 'extra'))


def unique(iterable):
"""
Yield unique values in iterable, preserving order.
"""
seen = set()
for value in iterable:
if value not in seen:
seen.add(value)
yield value


def handle_requires(metadata, pkg_info, key):
"""
Place the runtime requirements from pkg_info into metadata.
"""
may_requires = OrderedDefaultDict(list)
for value in sorted(pkg_info.get_all(key)):
extra_match = EXTRA_RE.search(value)
if extra_match:
groupdict = extra_match.groupdict()
condition = groupdict['condition']
extra = groupdict['extra']
package = groupdict['package']
if condition.endswith(' and '):
condition = condition[:-5]
else:
condition, extra = None, None
package = value
key = MayRequiresKey(condition, extra)
may_requires[key].append(package)

if may_requires:
metadata['run_requires'] = []

def sort_key(item):
# Both condition and extra could be None, which can't be compared
# against strings in Python 3.
key, value = item
if key.condition is None:
return ''
return key.condition

for key, value in sorted(may_requires.items(), key=sort_key):
may_requirement = OrderedDict((('requires', value),))
if key.extra:
may_requirement['extra'] = key.extra
if key.condition:
may_requirement['environment'] = key.condition
metadata['run_requires'].append(may_requirement)

if 'extras' not in metadata:
metadata['extras'] = []
metadata['extras'].extend([key.extra for key in may_requires.keys() if key.extra])


def pkginfo_to_dict(path, distribution=None):
"""
Convert PKG-INFO to a prototype Metadata 2.0 (PEP 426) dict.
The description is included under the key ['description'] rather than
being written to a separate file.
path: path to PKG-INFO file
distribution: optional distutils Distribution()
"""

metadata = OrderedDefaultDict(
lambda: OrderedDefaultDict(lambda: OrderedDefaultDict(OrderedDict)))
metadata["generator"] = "bdist_wheel (" + wheel_version + ")"
try:
unicode
pkg_info = read_pkg_info(path)
except NameError:
with open(path, 'rb') as pkg_info_file:
pkg_info = email.parser.Parser().parsestr(pkg_info_file.read().decode('utf-8'))
description = None

if pkg_info['Summary']:
metadata['summary'] = pkginfo_unicode(pkg_info, 'Summary')
del pkg_info['Summary']

if pkg_info['Description']:
description = dedent_description(pkg_info)
del pkg_info['Description']
else:
payload = pkg_info.get_payload()
if isinstance(payload, bytes):
# Avoid a Python 2 Unicode error.
# We still suffer ? glyphs on Python 3.
payload = payload.decode('utf-8')
if payload:
description = payload

if description:
pkg_info['description'] = description

for key in sorted(unique(k.lower() for k in pkg_info.keys())):
low_key = key.replace('-', '_')

if low_key in SKIP_FIELDS:
continue

if low_key in UNKNOWN_FIELDS and pkg_info.get(key) == 'UNKNOWN':
continue

if low_key in sorted(PLURAL_FIELDS):
metadata[PLURAL_FIELDS[low_key]] = pkg_info.get_all(key)

elif low_key == "requires_dist":
handle_requires(metadata, pkg_info, key)

elif low_key == 'provides_extra':
if 'extras' not in metadata:
metadata['extras'] = []
metadata['extras'].extend(pkg_info.get_all(key))

elif low_key == 'home_page':
metadata['extensions']['python.details']['project_urls'] = {'Home': pkg_info[key]}

elif low_key == 'keywords':
metadata['keywords'] = KEYWORDS_RE.split(pkg_info[key])

else:
metadata[low_key] = pkg_info[key]

metadata['metadata_version'] = METADATA_VERSION

if 'extras' in metadata:
metadata['extras'] = sorted(set(metadata['extras']))

# include more information if distribution is available
if distribution:
for requires, attr in (('test_requires', 'tests_require'),):
try:
requirements = getattr(distribution, attr)
if isinstance(requirements, list):
new_requirements = sorted(convert_requirements(requirements))
metadata[requires] = [{'requires': new_requirements}]
except AttributeError:
pass

# handle contacts
contacts = []
for contact_type, role in CONTACT_FIELDS:
contact = OrderedDict()
for key in sorted(contact_type):
if contact_type[key] in metadata:
contact[key] = metadata.pop(contact_type[key])
if contact:
contact['role'] = role
contacts.append(contact)
if contacts:
metadata['extensions']['python.details']['contacts'] = contacts

# convert entry points to exports
try:
with open(os.path.join(os.path.dirname(path), "entry_points.txt"), "r") as ep_file:
ep_map = pkg_resources.EntryPoint.parse_map(ep_file.read())
exports = OrderedDict()
for group, items in sorted(ep_map.items()):
exports[group] = OrderedDict()
for item in sorted(map(str, items.values())):
name, export = item.split(' = ', 1)
exports[group][name] = export
if exports:
metadata['extensions']['python.exports'] = exports
except IOError:
pass

# copy console_scripts entry points to commands
if 'python.exports' in metadata['extensions']:
for (ep_script, wrap_script) in (('console_scripts', 'wrap_console'),
('gui_scripts', 'wrap_gui')):
if ep_script in metadata['extensions']['python.exports']:
metadata['extensions']['python.commands'][wrap_script] = \
metadata['extensions']['python.exports'][ep_script]

return metadata


def requires_to_requires_dist(requirement):
"""Compose the version predicates for requirement in PEP 345 fashion."""
requires_dist = []
Expand Down Expand Up @@ -328,10 +128,3 @@ def dedent_description(pkg_info):
.decode("ascii", "surrogateescape")

return description_dedent


if __name__ == "__main__":
import sys
import pprint

pprint.pprint(pkginfo_to_dict(sys.argv[1]))
19 changes: 0 additions & 19 deletions wheel/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import json
import os
import sys
from collections import OrderedDict

__all__ = ['urlsafe_b64encode', 'urlsafe_b64decode', 'utf8',
'to_json', 'from_json', 'matches_requirement']
Expand Down Expand Up @@ -100,24 +99,6 @@ def __exit__(self, exc_type, exc_val, exc_tb):
self.fd.close()


class OrderedDefaultDict(OrderedDict):
def __init__(self, *args, **kwargs):
if not args:
self.default_factory = None
else:
if not (args[0] is None or callable(args[0])):
raise TypeError('first argument must be callable or None')
self.default_factory = args[0]
args = args[1:]
super(OrderedDefaultDict, self).__init__(*args, **kwargs)

def __missing__(self, key):
if self.default_factory is None:
raise KeyError(key)
self[key] = default = self.default_factory()
return default


if sys.platform == 'win32':
import ctypes.wintypes
# CSIDL_APPDATA for reference - not used here for compatibility with
Expand Down

0 comments on commit 595e4a8

Please sign in to comment.