import posixpath
import re
import zipfile2
from ..bundled.traitlets import (
HasTraits, Instance, List, Long, Unicode
)
from ..errors import (
InvalidRequirementString, InvalidEggName, InvalidMetadata,
InvalidMetadataField, MissingMetadata, UnsupportedMetadata
)
from ..platforms import EPDPlatform, PythonImplementation
from ..platforms.legacy import LegacyEPDPlatform
from ..utils import compute_sha256, parse_assignments
from ..utils.py3compat import StringIO, string_types
from ..utils.traitlets import NoneOrInstance, NoneOrUnicode
from ..versions import EnpkgVersion, MetadataVersion
from ._blacklist import (
EGG_PLATFORM_BLACK_LIST, EGG_PYTHON_TAG_BLACK_LIST,
may_be_in_platform_blacklist, may_be_in_python_tag_blacklist,
may_be_in_pkg_info_blacklist
)
from ._package_info import PackageInfo, _keep_position, _read_pkg_info
_EGG_NAME_RE = re.compile("""
(?P<name>[\.\w]+)
-
(?P<version>[^-]+)
-
(?P<build>\d+)
\.egg$""", re.VERBOSE)
_PYVER_RE = re.compile("(?P<major>\d+)\.(?P<minor>\d+)")
EGG_INFO_PREFIX = "EGG-INFO"
# Those may need to be public, depending on how well we can hide their
# locations or not.
_INFO_JSON_LOCATION = posixpath.join(EGG_INFO_PREFIX, "info.json")
_SPEC_DEPEND_LOCATION = posixpath.join(EGG_INFO_PREFIX, "spec", "depend")
_SPEC_LIB_DEPEND_LOCATION = posixpath.join(EGG_INFO_PREFIX, "spec",
"lib-depend")
_SPEC_SUMMARY_LOCATION = posixpath.join(EGG_INFO_PREFIX, "spec", "summary")
_USR_PREFIX_LOCATION = posixpath.join(EGG_INFO_PREFIX, "usr")
_TAG_METADATA_VERSION = "metadata_version"
_TAG_NAME = "name"
_TAG_VERSION = "version"
_TAG_BUILD = "build"
_TAG_ARCH = "arch"
_TAG_OSDIST = "osdist"
_TAG_PLATFORM = "platform"
_TAG_PYTHON = "python"
_TAG_PYTHON_PEP425_TAG = "python_tag"
_TAG_ABI_PEP425_TAG = "abi_tag"
_TAG_PLATFORM_PEP425_TAG = "platform_tag"
_TAG_PACKAGES = "packages"
M = MetadataVersion.from_string
_METADATA_VERSION_TO_KEYS = {
M("1.1"): (
_TAG_METADATA_VERSION, _TAG_NAME, _TAG_VERSION, _TAG_BUILD, _TAG_ARCH,
_TAG_PLATFORM, _TAG_OSDIST, _TAG_PYTHON, _TAG_PACKAGES
),
}
_METADATA_VERSION_TO_KEYS[M("1.2")] = \
_METADATA_VERSION_TO_KEYS[M("1.1")] + (_TAG_PYTHON_PEP425_TAG, )
_METADATA_VERSION_TO_KEYS[M("1.3")] = (
_METADATA_VERSION_TO_KEYS[M("1.2")] +
(_TAG_ABI_PEP425_TAG, _TAG_PLATFORM_PEP425_TAG)
)
_UNSUPPORTED = "unsupported"
_PYVER_RE = re.compile("(?P<major>\d).(?P<minor>\d)")
def _are_compatible(left, right):
"""Return True if both arguments are compatible metadata versions.
Parameters
----------
left: MetadataVersion
right: MetadataVersion
"""
return left.major == right.major
def _highest_compatible(metadata_version):
""" Returns the highest metadata version supporting that is compatible with
the given version.
"""
compatible_versions = [
m for m in _METADATA_VERSION_TO_KEYS
if _are_compatible(m, metadata_version)
]
if len(compatible_versions) > 0:
return max(compatible_versions)
else:
raise UnsupportedMetadata(metadata_version)
def split_egg_name(s):
m = _EGG_NAME_RE.match(s)
if m is None:
raise InvalidEggName(s)
else:
name, version, build = m.groups()
return name, version, int(build)
def parse_rawspec(spec_string):
spec = parse_assignments(StringIO(spec_string.replace('\r', '')))
metadata_version_string = spec.get(_TAG_METADATA_VERSION)
if metadata_version_string is not None:
metadata_version = MetadataVersion.from_string(metadata_version_string)
else:
metadata_version = None
if metadata_version is None:
raise InvalidMetadataField('metadata_version', metadata_version_string)
elif metadata_version not in _METADATA_VERSION_TO_KEYS:
metadata_version = _highest_compatible(metadata_version)
res = {}
keys = _METADATA_VERSION_TO_KEYS.get(metadata_version)
for key in keys:
try:
res[key] = spec[key]
except KeyError:
raise InvalidMetadataField(key, InvalidMetadataField.undefined)
return res
def egg_name(name, version, build):
"""
Return the egg filename (including the .egg extension) for the given
arguments
"""
return "{0}-{1}-{2}.egg".format(name, version, build)
def is_egg_name_valid(s):
"""
Return True if the given string is a valid egg name (not including the
.egg, e.g. 'Qt-4.8.5-2')
"""
return _EGG_NAME_RE.match(s) is not None
_INVALID_REQUIREMENTS = {
"numpy-1.8.0": "numpy 1.8.0",
}
def _translate_invalid_requirement(s):
return _INVALID_REQUIREMENTS.get(s, s)
class Requirement(HasTraits):
"""
Model for entries in the package metadata inside EGG-INFO/spec/depend
"""
name = Unicode()
version_string = Unicode()
build_number = Long(-1)
def __init__(self, name="", version_string="", build_number=-1):
self.name = name
self.version_string = version_string
self.build_number = build_number
super(Requirement, self).__init__(self, name, version_string,
build_number)
@property
def strictness(self):
if len(self.version_string) == 0:
return 1
elif self.build_number < 0:
return 2
else:
return 3
@classmethod
def from_string(cls, s, strictness=2):
"""
Create a Requirement from string following a name-version-build
format.
Parameters
----------
s: str
Egg name, e.g. 'Qt-4.8.5-2'.
strictness: int
Control strictness of string representation
"""
name, version, build = split_egg_name("{0}.egg".format(s))
if strictness >= 3:
build_number = build
else:
build_number = -1
if strictness >= 2:
version_string = version
else:
version_string = ""
return cls(name=name, version_string=version_string,
build_number=build_number)
@classmethod
def from_spec_string(cls, s):
"""
Create a Requirement from a spec string (as used in
EGG-INFO/spec/depend).
"""
s = _translate_invalid_requirement(s)
parts = s.split()
if len(parts) == 1:
name = parts[0]
if "-" in name:
raise InvalidRequirementString(name)
return cls(name=name)
elif len(parts) == 2:
name, version = parts
parts = version.split("-")
if len(parts) == 2:
upstream, build_number = parts
build_number = int(build_number)
else:
upstream, build_number = version, -1
return cls(name=name, version_string=upstream,
build_number=build_number)
else:
raise InvalidRequirementString(name)
def __str__(self):
if len(self.version_string) > 0:
if self.build_number > 0:
return "{0} {1}-{2}".format(self.name,
self.version_string,
self.build_number)
else:
return "{0} {1}".format(self.name, self.version_string)
else:
return self.name
@property
def _key(self):
return (self.name, self.version_string, self.build_number)
def __eq__(self, other):
if not isinstance(other, type(self)):
return False
return self._key == other._key
def __ne__(self, other):
return not (self == other)
def __hash__(self):
return hash(self._key)
_METADATA_TEMPLATES = {
M("1.1"): """\
metadata_version = '1.1'
name = {name!r}
version = {version!r}
build = {build}
arch = {arch!r}
platform = {platform!r}
osdist = {osdist!r}
python = {python!r}
packages = {packages}
""",
M("1.2"): """\
metadata_version = '1.2'
name = {name!r}
version = {version!r}
build = {build}
arch = {arch!r}
platform = {platform!r}
osdist = {osdist!r}
python = {python!r}
python_tag = {python_tag!r}
packages = {packages}
""",
M("1.3"): """\
metadata_version = '1.3'
name = {name!r}
version = {version!r}
build = {build}
arch = {arch!r}
platform = {platform!r}
osdist = {osdist!r}
python = {python!r}
python_tag = {python_tag!r}
abi_tag = {abi_tag!r}
platform_tag = {platform_tag!r}
packages = {packages}
"""
}
def _guess_python_tag(pyver):
""" Guess python_tag from the given python string ("MAJOR.MINOR", e.g. "2.7").
None may be returned (for egg that don't depend on python)
"""
if pyver in (None, ""):
return None
else:
m = _PYVER_RE.search(pyver)
if m is None:
msg = "python_tag cannot be guessed for python = {0}"
raise InvalidMetadata(msg.format(pyver))
else:
major = m.groupdict()["major"]
minor = m.groupdict()["minor"]
return "cp" + major + minor
_METADATA_DEFAULT_VERSION_STRING = "1.3"
_METADATA_DEFAULT_VERSION = M(_METADATA_DEFAULT_VERSION_STRING)
def _epd_platform_from_raw_spec(raw_spec):
""" Create an EPDPlatform instance from the metadata info returned by
parse_rawspec.
if no platform is defined ('platform' and 'osdist' set to None), then
None is returned.
"""
arch_string = raw_spec[_TAG_ARCH]
platform_string = raw_spec[_TAG_PLATFORM]
osdist_string = raw_spec[_TAG_OSDIST]
if platform_string is None and osdist_string is None:
return None
else:
return EPDPlatform._from_spec_depend_data(
platform_string, osdist_string, arch_string
)
class LegacySpecDepend(HasTraits):
"""
This models the EGG-INFO/spec/depend content.
"""
# Name is taken from egg path, so may be upper case
name = Unicode()
"""
Egg name
"""
version = Unicode()
"""
Upstream version (as a string).
"""
build = Long()
"""
Build number
"""
python = NoneOrUnicode()
"""
Python version
"""
python_tag = NoneOrUnicode()
"""
Python tag (as defined in PEP 425).
"""
abi_tag = NoneOrUnicode()
"""
ABI tag (as defined in PEP 425), except that 'none' is None.
"""
platform_tag = NoneOrUnicode()
"""
Platform tag (as defined in PEP 425), except that 'any' is None.
"""
packages = List(Instance(Requirement))
"""
List of dependencies for this egg
"""
_epd_legacy_platform = NoneOrInstance(LegacyEPDPlatform)
_metadata_version = Instance(MetadataVersion)
@classmethod
def _from_data(cls, data, epd_platform):
args = data.copy()
args[_TAG_METADATA_VERSION] = M(
args.get(_TAG_METADATA_VERSION, _METADATA_DEFAULT_VERSION_STRING)
)
if epd_platform is None:
_epd_legacy_platform = None
else:
_epd_legacy_platform = LegacyEPDPlatform(epd_platform)
args["_epd_legacy_platform"] = _epd_legacy_platform
args[_TAG_PACKAGES] = [
Requirement.from_spec_string(s)
for s in args.get(_TAG_PACKAGES, [])
]
return cls(**args)
@classmethod
def from_egg(cls, path_or_file):
sha256 = None
if isinstance(path_or_file, string_types):
if (
may_be_in_platform_blacklist(path_or_file)
or may_be_in_python_tag_blacklist(path_or_file)
):
sha256 = compute_sha256(path_or_file)
else:
with _keep_position(path_or_file.fp):
sha256 = compute_sha256(path_or_file.fp)
return cls._from_egg(path_or_file, sha256)
@classmethod
def _from_egg(cls, path_or_file, sha256):
def _create_spec_depend(zp):
epd_platform_string = EGG_PLATFORM_BLACK_LIST.get(sha256)
if epd_platform_string is None:
epd_platform = None
else:
epd_platform = EPDPlatform.from_epd_string(epd_platform_string)
try:
spec_depend_string = zp.read(_SPEC_DEPEND_LOCATION).decode()
except KeyError:
msg = ("File {0!r} is not an Enthought egg (is missing {1})"
.format(path_or_file, _SPEC_DEPEND_LOCATION))
raise MissingMetadata(msg)
else:
data, epd_platform = _normalized_info_from_string(
spec_depend_string, epd_platform, sha256
)
return cls._from_data(data, epd_platform)
if isinstance(path_or_file, string_types):
with zipfile2.ZipFile(path_or_file) as zp:
return _create_spec_depend(zp)
else:
return _create_spec_depend(path_or_file)
@classmethod
def from_string(cls, spec_depend_string):
data, epd_platform = _normalized_info_from_string(spec_depend_string)
return cls._from_data(data, epd_platform)
@property
def arch(self):
"""
Egg architecture.
"""
if self._epd_legacy_platform is None:
return None
else:
return self._epd_legacy_platform.arch._legacy_name
@property
def egg_name(self):
"""
Full egg name (including .egg extension).
"""
return egg_name(self.name, self.version, self.build)
@property
def osdist(self):
if self._epd_legacy_platform is None:
return None
else:
return self._epd_legacy_platform.osdist
@property
def platform(self):
"""
The legacy platform name (sys.platform).
"""
if self._epd_legacy_platform is None:
return None
else:
return self._epd_legacy_platform.platform
@property
def metadata_version(self):
return self._metadata_version
@metadata_version.setter
def metadata_version(self, value):
self._metadata_version = value
def _to_dict(self):
raw_data = {
_TAG_NAME: self.name,
_TAG_VERSION: self.version,
_TAG_BUILD: self.build,
_TAG_ARCH: self.arch,
_TAG_PLATFORM: self.platform,
_TAG_OSDIST: self.osdist,
_TAG_PACKAGES: [str(p) for p in self.packages],
_TAG_PYTHON: self.python,
_TAG_PYTHON_PEP425_TAG: self.python_tag,
_TAG_ABI_PEP425_TAG: self.abi_tag,
_TAG_PLATFORM_PEP425_TAG: self.platform_tag,
_TAG_METADATA_VERSION: self.metadata_version
}
ret = {}
for k, v in raw_data.items():
if isinstance(v, string_types):
v = str(v)
ret[k] = v
return ret
def to_string(self):
"""
Returns a string that is suitable for the depend file inside our
legacy egg.
"""
template = _METADATA_TEMPLATES.get(self.metadata_version, None)
data = self._to_dict()
# This is just to ensure the exact same string as the produced by the
# legacy buildsystem
if len(self.packages) == 0:
data[_TAG_PACKAGES] = "[]"
else:
data[_TAG_PACKAGES] = "[\n{0}\n]". \
format("\n".join(" '{0}',".format(p)
for p in self.packages))
return template.format(**data)
class Dependencies(object):
""" Object storing the various dependencies for an egg.
Each attribute is a tuple of Requirement instances.
"""
def __init__(self, runtime=None, build=None):
self.runtime = runtime or ()
self.build = runtime or ()
_TAG_RE = re.compile("""
(?P<interpreter>(cp|pp|cpython|py))
(?P<version>([\d_]+))
""", flags=re.VERBOSE)
def _python_tag_to_python(python_tag):
# This converts only python version we currently intent to support in
# metadata version 1.x.
if python_tag is None:
return None
generic_exc = InvalidMetadataField('python_tag', python_tag)
m = _TAG_RE.match(python_tag)
if m is None:
raise generic_exc
else:
d = m.groupdict()
version = d["version"]
if len(version) == 1:
if version == "2":
return "2.7"
else:
raise generic_exc
elif len(version) == 2:
return "{0}.{1}".format(version[0], version[1])
else:
raise generic_exc
def _metadata_version_to_tuple(metadata_version):
""" Convert a metadata version string to a tuple for comparison."""
return tuple(int(s) for s in metadata_version.split("."))
def _guess_abi_tag(platform, python_tag):
assert python_tag is not None, "BUG, this function expects a python_tag"
# For legacy (aka legacy spec version info < 1.3), we know that pyver
# can only be one of "2.X" with X in (5, 6, 7).
#
# In those cases, the mapping (platform pyver) -> ABI is unambiguous,
# as we only ever used one ABI for a given python version/platform.
pyver = _python_tag_to_python(python_tag)
return "cp{0}{1}m".format(pyver[0], pyver[2])
def _guess_platform_tag(platform):
if platform is None:
return None
return platform.pep425_tag
def _normalized_info_from_string(spec_depend_string, epd_platform=None,
sha256=None):
""" Return a 'normalized' dictionary from the given spec/depend string.
Note: the name value is NOT lower-cased, so that the egg filename may
rebuilt from the data.
"""
raw_data = parse_rawspec(spec_depend_string)
data = {}
for k in (_TAG_METADATA_VERSION,
_TAG_NAME, _TAG_VERSION, _TAG_BUILD,
_TAG_ARCH, _TAG_OSDIST, _TAG_PLATFORM,
_TAG_PYTHON, _TAG_PACKAGES):
data[k] = raw_data[k]
epd_platform = epd_platform or _epd_platform_from_raw_spec(data)
for k in (_TAG_ARCH, _TAG_PLATFORM, _TAG_OSDIST):
data.pop(k)
metadata_version = MetadataVersion.from_string(data[_TAG_METADATA_VERSION])
python_tag = EGG_PYTHON_TAG_BLACK_LIST.get(sha256)
if python_tag:
data[_TAG_PYTHON_PEP425_TAG] = python_tag
else:
if metadata_version < M("1.2"):
data[_TAG_PYTHON_PEP425_TAG] = _guess_python_tag(
raw_data[_TAG_PYTHON]
)
else:
data[_TAG_PYTHON_PEP425_TAG] = raw_data[_TAG_PYTHON_PEP425_TAG]
if metadata_version < M("1.3"):
python_tag = data[_TAG_PYTHON_PEP425_TAG]
if python_tag is None:
# No python tag, so should only be a "pure binary" egg, i.e.
# an egg containing no python code and no python C extensions.
abi = None
elif epd_platform is None:
# No platform, so should only be a "pure python" egg, i.e.
# an egg containing no C extension.
abi = None
else:
abi = _guess_abi_tag(epd_platform, python_tag)
data[_TAG_ABI_PEP425_TAG] = abi
else:
data[_TAG_ABI_PEP425_TAG] = raw_data[_TAG_ABI_PEP425_TAG]
if metadata_version < M("1.3"):
if epd_platform is None:
platform_tag = None
else:
platform_tag = _guess_platform_tag(epd_platform)
data[_TAG_PLATFORM_PEP425_TAG] = platform_tag
else:
data[_TAG_PLATFORM_PEP425_TAG] = raw_data[_TAG_PLATFORM_PEP425_TAG]
return data, epd_platform