Created
September 2, 2012 15:18
-
-
Save dstufft/3600388 to your computer and use it in GitHub Desktop.
_pep345.py is a monkeypatcher and utility function to bring PEP345 (metadata 1.2) to distutils1/setuptools (Metadata only, no additional functionality)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import copy | |
# ----- START [distutils2.version] ----- # | |
import re | |
_FINAL_MARKER = ('z',) | |
_VERSION_RE = re.compile(r''' | |
^ | |
(?P<version>\d+\.\d+) # minimum 'N.N' | |
(?P<extraversion>(?:\.\d+)*) # any number of extra '.N' segments | |
(?: | |
(?P<prerel>[abc]|rc) # 'a'=alpha, 'b'=beta, 'c'=release candidate | |
# 'rc'= alias for release candidate | |
(?P<prerelversion>\d+(?:\.\d+)*) | |
)? | |
(?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)? | |
$''', re.VERBOSE) | |
class IrrationalVersionError(Exception): | |
"""This is an irrational version.""" | |
pass | |
class HugeMajorVersionNumError(IrrationalVersionError): | |
"""An irrational version because the major version number is huge | |
(often because a year or date was used). | |
See `error_on_huge_major_num` option in `NormalizedVersion` for details. | |
This guard can be disabled by setting that option False. | |
""" | |
pass | |
class NormalizedVersion(object): | |
"""A rational version. | |
Good: | |
1.2 # equivalent to "1.2.0" | |
1.2.0 | |
1.2a1 | |
1.2.3a2 | |
1.2.3b1 | |
1.2.3c1 | |
1.2.3.4 | |
TODO: fill this out | |
Bad: | |
1 # mininum two numbers | |
1.2a # release level must have a release serial | |
1.2.3b | |
""" | |
def __init__(self, s, error_on_huge_major_num=True, | |
drop_trailing_zeros=False): | |
"""Create a NormalizedVersion instance from a version string. | |
@param s {str} The version string. | |
@param error_on_huge_major_num {bool} Whether to consider an | |
apparent use of a year or full date as the major version number | |
an error. Default True. One of the observed patterns on PyPI before | |
the introduction of `NormalizedVersion` was version numbers like | |
this: | |
2009.01.03 | |
20040603 | |
2005.01 | |
This guard is here to strongly encourage the package author to | |
use an alternate version, because a release deployed into PyPI | |
and, e.g. downstream Linux package managers, will forever remove | |
the possibility of using a version number like "1.0" (i.e. | |
where the major number is less than that huge major number). | |
@param drop_trailing_zeros {bool} Whether to drop trailing zeros | |
from the returned list. Default True. | |
""" | |
self.is_final = True # by default, consider a version as final. | |
self.drop_trailing_zeros = drop_trailing_zeros | |
self._parse(s, error_on_huge_major_num) | |
@classmethod | |
def from_parts(cls, version, prerelease=_FINAL_MARKER, devpost=_FINAL_MARKER): | |
return cls(cls.parts_to_str((version, prerelease, devpost))) | |
def _parse(self, s, error_on_huge_major_num=True): | |
"""Parses a string version into parts.""" | |
match = _VERSION_RE.search(s) | |
if not match: | |
raise IrrationalVersionError(s) | |
groups = match.groupdict() | |
parts = [] | |
# main version | |
block = self._parse_numdots(groups['version'], s, 2) | |
extraversion = groups.get('extraversion') | |
if extraversion not in ('', None): | |
block += self._parse_numdots(extraversion[1:], s) | |
parts.append(tuple(block)) | |
# prerelease | |
prerel = groups.get('prerel') | |
if prerel is not None: | |
block = [prerel] | |
block += self._parse_numdots(groups.get('prerelversion'), s, | |
pad_zeros_length=1) | |
parts.append(tuple(block)) | |
self.is_final = False | |
else: | |
parts.append(_FINAL_MARKER) | |
# postdev | |
if groups.get('postdev'): | |
post = groups.get('post') | |
dev = groups.get('dev') | |
postdev = [] | |
if post is not None: | |
postdev.extend((_FINAL_MARKER[0], 'post', int(post))) | |
if dev is None: | |
postdev.append(_FINAL_MARKER[0]) | |
if dev is not None: | |
postdev.extend(('dev', int(dev))) | |
self.is_final = False | |
parts.append(tuple(postdev)) | |
else: | |
parts.append(_FINAL_MARKER) | |
self.parts = tuple(parts) | |
if error_on_huge_major_num and self.parts[0][0] > 1980: | |
raise HugeMajorVersionNumError("huge major version number, %r, " | |
"which might cause future problems: %r" % (self.parts[0][0], s)) | |
def _parse_numdots(self, s, full_ver_str, pad_zeros_length=0): | |
"""Parse 'N.N.N' sequences, return a list of ints. | |
@param s {str} 'N.N.N...' sequence to be parsed | |
@param full_ver_str {str} The full version string from which this | |
comes. Used for error strings. | |
@param pad_zeros_length {int} The length to which to pad the | |
returned list with zeros, if necessary. Default 0. | |
""" | |
nums = [] | |
for n in s.split("."): | |
if len(n) > 1 and n[0] == '0': | |
raise IrrationalVersionError("cannot have leading zero in " | |
"version number segment: '%s' in %r" % (n, full_ver_str)) | |
nums.append(int(n)) | |
if self.drop_trailing_zeros: | |
while nums and nums[-1] == 0: | |
nums.pop() | |
while len(nums) < pad_zeros_length: | |
nums.append(0) | |
return nums | |
def __str__(self): | |
return self.parts_to_str(self.parts) | |
@classmethod | |
def parts_to_str(cls, parts): | |
"""Transforms a version expressed in tuple into its string | |
representation.""" | |
# XXX This doesn't check for invalid tuples | |
main, prerel, postdev = parts | |
s = '.'.join(str(v) for v in main) | |
if prerel is not _FINAL_MARKER: | |
s += prerel[0] | |
s += '.'.join(str(v) for v in prerel[1:]) | |
# XXX clean up: postdev is always true; code is obscure | |
if postdev and postdev is not _FINAL_MARKER: | |
if postdev[0] == _FINAL_MARKER[0]: | |
postdev = postdev[1:] | |
i = 0 | |
while i < len(postdev): | |
if i % 2 == 0: | |
s += '.' | |
s += str(postdev[i]) | |
i += 1 | |
return s | |
def __repr__(self): | |
return "%s('%s')" % (self.__class__.__name__, self) | |
def _cannot_compare(self, other): | |
raise TypeError("cannot compare %s and %s" | |
% (type(self).__name__, type(other).__name__)) | |
def __eq__(self, other): | |
if not isinstance(other, NormalizedVersion): | |
self._cannot_compare(other) | |
return self.parts == other.parts | |
def __lt__(self, other): | |
if not isinstance(other, NormalizedVersion): | |
self._cannot_compare(other) | |
return self.parts < other.parts | |
def __ne__(self, other): | |
return not self.__eq__(other) | |
def __gt__(self, other): | |
return not (self.__lt__(other) or self.__eq__(other)) | |
def __le__(self, other): | |
return self.__eq__(other) or self.__lt__(other) | |
def __ge__(self, other): | |
return self.__eq__(other) or self.__gt__(other) | |
# See http://docs.python.org/reference/datamodel#object.__hash__ | |
def __hash__(self): | |
return hash(self.parts) | |
_PREDICATE = re.compile(r"(?i)^\s*(\w[\s\w-]*(?:\.\w*)*)(.*)") | |
_VERSIONS = re.compile(r"^\s*\((?P<versions>.*)\)\s*$|^\s*(?P<versions2>.*)\s*$") | |
_SPLIT_CMP = re.compile(r"^\s*(<=|>=|<|>|!=|==)\s*([^\s,]+)\s*$") | |
def _split_predicate(predicate): | |
match = _SPLIT_CMP.match(predicate) | |
if match is None: | |
# probably no op, we'll use "==" | |
comp, version = '==', predicate | |
else: | |
comp, version = match.groups() | |
return comp, version # NormalizedVersion(version) | |
class VersionPredicate(object): | |
"""Defines a predicate: ProjectName (>ver1,ver2, ..)""" | |
_operators = {"<": lambda x, y: x < y, | |
">": lambda x, y: x > y, | |
"<=": lambda x, y: str(x).startswith(str(y)) or x < y, | |
">=": lambda x, y: str(x).startswith(str(y)) or x > y, | |
"==": lambda x, y: str(x).startswith(str(y)), | |
"!=": lambda x, y: not str(x).startswith(str(y)), | |
} | |
def __init__(self, predicate): | |
self._string = predicate | |
predicate = predicate.strip() | |
match = _PREDICATE.match(predicate) | |
if match is None: | |
raise ValueError('Bad predicate "%s"' % predicate) | |
name, predicates = match.groups() | |
self.name = name.strip() | |
self.predicates = [] | |
if predicates is None: | |
return | |
predicates = _VERSIONS.match(predicates.strip()) | |
if predicates is None: | |
return | |
predicates = predicates.groupdict() | |
if predicates['versions'] is not None: | |
versions = predicates['versions'] | |
else: | |
versions = predicates.get('versions2') | |
if versions is not None: | |
for version in versions.split(','): | |
if version.strip() == '': | |
continue | |
self.predicates.append(_split_predicate(version)) | |
def match(self, version): | |
"""Check if the provided version matches the predicates.""" | |
# if isinstance(version, basestring): | |
# version = NormalizedVersion(version) | |
for operator, predicate in self.predicates: | |
if not self._operators[operator](version, predicate): | |
return False | |
return True | |
def __repr__(self): | |
return self._string | |
# ----- END [distutils2.version] ----- # | |
# ----- START [distutils.dist] ----- # | |
import distutils.dist | |
from distutils.util import rfc822_escape | |
_DistributionMetadata = copy.deepcopy(distutils.dist.DistributionMetadata) | |
class DistributionMetadata(_DistributionMetadata): | |
_METHOD_BASENAMES = _DistributionMetadata._METHOD_BASENAMES + ( | |
"requires_python", "requires_external", | |
"requires_dist", "provides_dist", "obsoletes_dist", | |
"project_url", | |
) | |
def __init__(self): | |
_DistributionMetadata.__init__(self) | |
# PEP 345 | |
self.requires_python = None | |
self.requires_external = None | |
self.requires_dist = None | |
self.provides_dist = None | |
self.obsoletes_dist = None | |
self.project_url = None | |
def write_pkg_file(self, file): | |
""" | |
Write the PKG-INFO format data to a file object. | |
""" | |
version = "1.0" | |
if (self.provides or self.requires or self.obsoletes or self.classifiers or self.download_url): | |
version = "1.1" | |
if (self.requires_python or self.requires_external or self.requires_dist or self.provides_dist or self.obsoletes_dist or self.project_url): | |
version = "1.2" | |
file.write("Metadata-Version: %s\n" % version) | |
file.write("Name: %s\n" % self.get_name()) | |
file.write("Version: %s\n" % self.get_version()) | |
file.write("Summary: %s\n" % self.get_description()) | |
file.write("Home-page: %s\n" % self.get_url()) | |
file.write("Author: %s\n" % self.get_contact()) | |
file.write("Author-email: %s\n" % self.get_contact_email()) | |
file.write("License: %s\n" % self.get_license()) | |
if self.download_url: | |
file.write("Download-URL: %s\n" % self.download_url) | |
long_desc = rfc822_escape(self.get_long_description()) | |
file.write("Description: %s\n" % long_desc) | |
keywords = ",".join(self.get_keywords()) | |
if keywords: | |
file.write("Keywords: %s\n" % keywords) | |
self._write_list(file, "Platform", self.get_platforms()) | |
self._write_list(file, "Classifier", self.get_classifiers()) | |
# PEP 314 | |
self._write_list(file, "Requires", self.get_requires()) | |
self._write_list(file, "Provides", self.get_provides()) | |
self._write_list(file, "Obsoletes", self.get_obsoletes()) | |
# PEP 345 | |
if self.requires_python: | |
file.write("Requires-Python: %s\n" % self.get_requires_python()) | |
self._write_list(file, "Requires-External", self.get_requires_external()) | |
self._write_list(file, "Requires-Dist", self.get_requires_dist()) | |
self._write_list(file, "Provides-Dist", self.get_provides_dist()) | |
self._write_list(file, "Obsoletes-Dist", self.get_obsoletes_dist()) | |
self._write_list(file, "Project-URL", self.get_project_url()) | |
# PEP 345 | |
def get_requires_python(self): | |
# @@@ Should there be any sort of semantic checks here? | |
return self.requires_python | |
def get_requires_external(self): | |
return self.requires_external or [] | |
def get_requires_dist(self): | |
# @@@ Should there be any sort of semantic checks here? | |
return self.requires_dist or [] | |
def get_provides_dist(self): | |
# @@@ Should there be any sort of semantic checks here? | |
provides = self.provides_dist or [] | |
current = "%s (%s)" % (self.get_name(), self.get_version()) | |
if not current in provides: | |
provides = [current] + provides | |
return provides | |
def get_obsoletes_dist(self): | |
# @@@ Should there be any sort of semantic checks here? | |
return self.obsoletes_dist or [] | |
def get_project_url(self): | |
# @@@ Should there be any sort of semantic checks here? | |
project_urls = self.project_url or {} | |
return [",".join(x) for x in project_urls.items()] | |
distutils.dist.DistributionMetadata = DistributionMetadata | |
# ----- END [distutils.dist] ----- # | |
# ----- START [distutils.command.check] ----- # | |
try: | |
import distutils.command.check | |
except ImportError: | |
has_check = False | |
else: | |
has_check = True | |
import distutils.errors | |
if has_check: | |
_check = copy.deepcopy(distutils.command.check.check) | |
class check(_check): | |
def check_metadata(self): | |
_check.check_metadata(self) | |
metadata = self.distribution.metadata | |
# Check that Version matches PEP 346 | |
try: | |
NormalizedVersion(metadata.get_version()) | |
except IrrationalVersionError: | |
raise distutils.errors.DistutilsSetupError("Version must conform to PEP386 - http://www.python.org/dev/peps/pep-0386/") | |
distutils.command.check.check = check | |
# ----- END [distutils.command.check] ----- # | |
# ----- START [distutils.command.register] ----- # | |
import distutils.command.register | |
_register = copy.deepcopy(distutils.command.register.register) | |
class register(_register): | |
def check_metadata(self): | |
if not has_check: | |
metadata = self.distribution.metadata | |
# Check that Version matches PEP 346 | |
try: | |
NormalizedVersion(metadata.get_version()) | |
except IrrationalVersionError: | |
raise distutils.errors.DistutilsSetupError("Version must conform to PEP386 - http://www.python.org/dev/peps/pep-0386/") | |
return _register.check_metadata(self) | |
def build_post_data(self, action): | |
data = _register.build_post_data(self, action) | |
# PEP 345 | |
meta = self.distribution.metadata | |
if meta.requires_python: | |
data["requires_python"] = meta.get_requires_python() | |
data["requires_external"] = meta.get_requires_external() | |
data["requires_dist"] = meta.get_requires_dist() | |
data["provides_dist"] = meta.get_provides_dist() | |
data["obsoletes_dist"] = meta.get_obsoletes_dist() | |
data["project_url"] = meta.get_project_url() | |
if data["requires_python"] or data["requires_external"] or data["requires_dist"] or data["provides_dist"] or data["obsoletes_dist"] or data["project_url"]: | |
data['metadata_version'] = "1.2" | |
return data | |
distutils.command.register.register = register | |
# ----- END [distutils.command.register] ----- # | |
# ----- START [distutils.command.sdist] ----- # | |
import distutils.command.sdist | |
import os | |
_sdist = copy.deepcopy(distutils.command.sdist.sdist) | |
class sdist(_sdist): | |
def add_defaults(self): | |
_sdist.add_defaults(self) | |
# Add _pep345.py if it exists | |
if os.path.exists("_pep345.py"): | |
self.filelist.append("_pep345.py") | |
distutils.command.sdist.sdist = sdist | |
# ----- END [distutils.command.sdist] ----- # | |
# ----- START [setuptools.command.sdist] ----- # | |
try: | |
import setuptools.command.sdist | |
except ImportError: | |
pass | |
else: | |
import os | |
_setuptools_sdist = copy.deepcopy(setuptools.command.sdist.sdist) | |
class sdist(_setuptools_sdist): | |
def add_defaults(self): | |
_setuptools_sdist.add_defaults(self) | |
# Add _pep345.py if it exists | |
if os.path.exists("_pep345.py"): | |
self.filelist.append("_pep345.py") | |
setuptools.command.sdist.sdist = sdist | |
# ----- END [setuptools.command.sdist] ----- # | |
# ----- START [markerlib] ----- # | |
from ast import Compare, BoolOp, Attribute, Name, Load, Str, cmpop, boolop | |
from ast import parse, copy_location, NodeTransformer | |
import os | |
import platform | |
import sys | |
import weakref | |
from platform import python_implementation | |
# restricted set of variables | |
_VARS = {'sys.platform': sys.platform, | |
'python_version': '%s.%s' % sys.version_info[:2], | |
# FIXME parsing sys.platform is not reliable, but there is no other | |
# way to get e.g. 2.7.2+, and the PEP is defined with sys.version | |
'python_full_version': sys.version.split(' ', 1)[0], | |
'os.name': os.name, | |
'platform.version': platform.version(), | |
'platform.machine': platform.machine(), | |
'platform.python_implementation': python_implementation(), | |
'extra': None # wheel extension | |
} | |
def default_environment(): | |
"""Return copy of default PEP 385 globals dictionary.""" | |
return dict(_VARS) | |
class ASTWhitelist(NodeTransformer): | |
def __init__(self, statement): | |
self.statement = statement # for error messages | |
ALLOWED = (Compare, BoolOp, Attribute, Name, Load, Str, cmpop, boolop) | |
def visit(self, node): | |
"""Ensure statement only contains allowed nodes.""" | |
if not isinstance(node, self.ALLOWED): | |
raise SyntaxError('Not allowed in environment markers.\n%s\n%s' % | |
(self.statement, | |
(' ' * node.col_offset) + '^')) | |
return NodeTransformer.visit(self, node) | |
def visit_Attribute(self, node): | |
"""Flatten one level of attribute access.""" | |
new_node = Name("%s.%s" % (node.value.id, node.attr), node.ctx) | |
return copy_location(new_node, node) | |
def parse_marker(marker): | |
tree = parse(marker, mode='eval') | |
new_tree = ASTWhitelist(marker).generic_visit(tree) | |
return new_tree | |
def compile_marker(parsed_marker): | |
return compile(parsed_marker, '<environment marker>', 'eval', | |
dont_inherit=True) | |
_cache = weakref.WeakValueDictionary() | |
def mcompile(marker): | |
"""Return compiled marker as a function accepting an environment dict.""" | |
try: | |
return _cache[marker] | |
except KeyError: | |
pass | |
if not marker.strip(): | |
def marker_fn(environment=None, override=None): | |
"""""" | |
return True | |
else: | |
compiled_marker = compile_marker(parse_marker(marker)) | |
def marker_fn(environment=None, override=None): | |
"""override updates environment""" | |
if override is None: | |
override = {} | |
if environment is None: | |
environment = default_environment() | |
environment.update(override) | |
return eval(compiled_marker, environment) | |
marker_fn.__doc__ = marker | |
_cache[marker] = marker_fn | |
return _cache[marker] | |
def interpret(marker, environment=None): | |
return mcompile(marker)(environment) | |
# ----- END [markerlib] ----- # | |
# ----- START [MAIN] ----- # | |
def pep345_to_setuptools(requirements): | |
setuptools_requirements = [] | |
for req in requirements: | |
if ";" in req: | |
req, environment = req.split(";") | |
environment = environment.strip() | |
else: | |
environment = "" | |
if interpret(environment): | |
vp = VersionPredicate(req) | |
versions = ",".join(["".join(p) for p in vp.predicates]) | |
setuptools_requirements.append("".join([vp.name, versions])) | |
return setuptools_requirements | |
# ----- END [MAIN] ----- # |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
from setuptools import setup, find_packages | |
from _pep345 import pep345_to_setuptools | |
REQUIREMENTS = [ | |
"warehouse", | |
"Flask (0.9)", | |
"Flask-Script (==0.4.0)", | |
"Flask-Testing (<1.0)", | |
"Flask-Exceptional (>0.1)", | |
"Flask-Mail (<=0.7.0)", | |
"Flask-SQLAlchemy (>=0.15)", | |
"python-dateutil (!=2.0)", | |
"requests (>=0.12,!=0.12.3)", | |
"simplejson; python_version < '2.6'", | |
] | |
PROVIDES = [ | |
"wat (2.1)", | |
] | |
OBSOLETES = [ | |
"urgh (1.0)", | |
] | |
setup( | |
name="crate-test", | |
version="2.2", | |
author="Donald Stufft", | |
author_email="[email protected]", | |
url="https://crate.io/", | |
description="A Test Package :D", | |
long_description="Tests Packaging Features", | |
packages=find_packages(exclude=("tests",)), | |
zip_safe=False, | |
include_package_data=True, | |
# PEP 345 | |
requires_python=">=2.4", | |
requires_external=["C", "libpng (>=1.5)"], | |
requires_dist=REQUIREMENTS, | |
provides_dist=PROVIDES, | |
obsoletes_dist=OBSOLETES, | |
project_url={ | |
"test": "https://crate.io/", | |
}, | |
# Backwards compat for setuptools | |
install_requires=pep345_to_setuptools(REQUIREMENTS), | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment