diff --git a/CHANGES.rst b/CHANGES.rst index 991fb8d8b..042ea953e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,18 @@ CHANGES ======= +---------- +1.0.0.dev1 +---------- + +* Adds ``-m`` and ``--entry-point`` alias to the existing ``-e`` option for entry points in + the pex tool to evoke the similarity to ``python -m``. + +* Adds console script support via ``-c/--script/--console-script`` and ``PEX_SCRIPT``. This allows + you to reference the named entry point instead of the exact ``module:name`` pair. Also supports + scripts defined in the ``scripts`` section of setup.py. + `#59 `_. + ---------- 1.0.0.dev0 ---------- diff --git a/pex/bin/pex.py b/pex/bin/pex.py index 337dfac47..efab03650 100644 --- a/pex/bin/pex.py +++ b/pex/bin/pex.py @@ -16,7 +16,7 @@ from pex.archiver import Archiver from pex.base import maybe_requirement -from pex.common import safe_delete, safe_mkdir, safe_mkdtemp +from pex.common import die, safe_delete, safe_mkdir, safe_mkdtemp from pex.crawler import Crawler from pex.fetcher import Fetcher, PyPIFetcher from pex.http import Context @@ -32,15 +32,12 @@ from pex.resolver import CachingResolver, Resolver from pex.resolver_options import ResolverOptionsBuilder from pex.tracer import TRACER, TraceLogger -from pex.version import __setuptools_requirement, __version__, __wheel_requirement +from pex.version import SETUPTOOLS_REQUIREMENT, WHEEL_REQUIREMENT, __version__ CANNOT_DISTILL = 101 CANNOT_SETUP_INTERPRETER = 102 - - -def die(msg, error_code=1): - print(msg, file=sys.stderr) - sys.exit(error_code) +INVALID_OPTIONS = 103 +INVALID_ENTRY_POINT = 104 def log(msg, v=False): @@ -247,6 +244,32 @@ def configure_clp_pex_environment(parser): parser.add_option_group(group) +def configure_clp_pex_entry_points(parser): + group = OptionGroup( + parser, + 'PEX entry point options', + 'Specify what target/module the PEX should invoke if any.') + + group.add_option( + '-m', '-e', '--entry-point', + dest='entry_point', + metavar='MODULE[:SYMBOL]', + default=None, + help='Set the entry point to module or module:symbol. If just specifying module, pex ' + 'behaves like python -m, e.g. python -m SimpleHTTPServer. If specifying ' + 'module:symbol, pex imports that symbol and invokes it as if it were main.') + + group.add_option( + '-c', '--script', '--console-script', + dest='script', + default=None, + metavar='SCRIPT_NAME', + help='Set the entry point as to the script or console_script as defined by a any of the ' + 'distributions in the pex. For example: "pex -c fab fabric" or "pex -c mturk boto".') + + parser.add_option_group(group) + + def configure_clp(): usage = ( '%prog [-o OUTPUT.PEX] [options] [-- arg1 arg2 ...]\n\n' @@ -258,6 +281,7 @@ def configure_clp(): configure_clp_pex_resolution(parser, resolver_options_builder) configure_clp_pex_options(parser) configure_clp_pex_environment(parser) + configure_clp_pex_entry_points(parser) parser.add_option( '-o', '--output-file', @@ -266,14 +290,6 @@ def configure_clp(): help='The name of the generated .pex file: Omiting this will run PEX ' 'immediately and not save it to a file.') - parser.add_option( - '-e', '--entry-point', - dest='entry_point', - default=None, - help='The entry point for this pex; Omiting this will enter the python ' - 'REPL with sources and requirements available for import. Can be ' - 'either a module or EntryPoint (module:function) format.') - parser.add_option( '-r', '--requirement', dest='requirement_files', @@ -387,11 +403,11 @@ def interpreter_from_options(options): resolve = functools.partial(resolve_interpreter, options.interpreter_cache_dir, options.repos) # resolve setuptools - interpreter = resolve(interpreter, __setuptools_requirement) + interpreter = resolve(interpreter, SETUPTOOLS_REQUIREMENT) # possibly resolve wheel if interpreter and options.use_wheel: - interpreter = resolve(interpreter, __wheel_requirement) + interpreter = resolve(interpreter, WHEEL_REQUIREMENT) return interpreter @@ -440,11 +456,13 @@ def build_pex(args, options, resolver_option_builder): pex_builder.add_distribution(dist) pex_builder.add_requirement(dist.as_requirement()) - if options.entry_point is not None: - log('Setting entry point to %s' % options.entry_point, v=options.verbosity) - pex_builder.info.entry_point = options.entry_point - else: - log('Creating environment PEX.', v=options.verbosity) + if options.entry_point and options.script: + die('Must specify at most one entry point or script.', INVALID_OPTIONS) + + if options.entry_point: + pex_builder.set_entry_point(options.entry_point) + elif options.script: + pex_builder.set_script(options.script) return pex_builder diff --git a/pex/common.py b/pex/common.py index 4dfa56f9f..b928ce5ed 100644 --- a/pex/common.py +++ b/pex/common.py @@ -17,6 +17,11 @@ from uuid import uuid4 +def die(msg, exit_code=1): + print(msg, file=sys.stderr) + sys.exit(exit_code) + + def safe_copy(source, dest, overwrite=False): def do_copy(): temp_dest = dest + uuid4().hex diff --git a/pex/finders.py b/pex/finders.py index 6dd039f71..108c1d684 100644 --- a/pex/finders.py +++ b/pex/finders.py @@ -234,3 +234,40 @@ def unregister_finders(): _remove_finder(importlib_bootstrap.FileFinder, find_wheels_on_path) __PREVIOUS_FINDER = None + + +def get_script_from_egg(name, dist): + """Returns location, content of script in distribution or (None, None) if not there.""" + if name in dist.metadata_listdir('scripts'): + return ( + os.path.join(dist.egg_info, 'scripts', name), + dist.get_metadata('scripts/%s' % name).replace('\r\n', '\n').replace('\r', '\n')) + return None, None + + +def get_script_from_whl(name, dist): + # This is true as of at least wheel==0.24. Might need to take into account the + # metadata version bundled with the wheel. + wheel_scripts_dir = '%s-%s.data/scripts' % (dist.key, dist.version) + if dist.resource_isdir(wheel_scripts_dir) and name in dist.resource_listdir(wheel_scripts_dir): + script_path = os.path.join(wheel_scripts_dir, name) + return ( + os.path.join(dist.egg_info, script_path), + dist.get_resource_string('', script_path).replace('\r\n', '\n').replace('\r', '\n')) + return None, None + + +def get_script_from_distribution(name, dist): + if isinstance(dist._provider, FixedEggMetadata): + return get_script_from_egg(name, dist) + elif isinstance(dist._provider, (WheelMetadata, pkg_resources.PathMetadata)): + return get_script_from_whl(name, dist) + return None, None + + +def get_script_from_distributions(name, dists): + for dist in dists: + script_path, script_content = get_script_from_distribution(name, dist) + if script_path: + return dist, script_path, script_content + return None, None, None diff --git a/pex/installer.py b/pex/installer.py index b48a7e0a9..38958a6b4 100644 --- a/pex/installer.py +++ b/pex/installer.py @@ -13,6 +13,7 @@ from .common import safe_mkdtemp, safe_rmtree from .interpreter import PythonInterpreter from .tracer import TRACER +from .version import SETUPTOOLS_REQUIREMENT, WHEEL_REQUIREMENT __all__ = ( 'Installer', @@ -232,8 +233,8 @@ class WheelInstaller(DistributionPackager): Create a source distribution from an unpacked setup.py-based project. """ MIXINS = { - 'setuptools': 'setuptools>=2', - 'wheel': 'wheel>=0.17', + 'setuptools': SETUPTOOLS_REQUIREMENT, + 'wheel': WHEEL_REQUIREMENT, } def mixins(self): diff --git a/pex/pex.py b/pex/pex.py index 7aeb2decf..e08bb1c39 100644 --- a/pex/pex.py +++ b/pex/pex.py @@ -13,9 +13,10 @@ import pkg_resources from pkg_resources import EntryPoint, find_distributions -from .common import safe_mkdir +from .common import die, safe_mkdir from .compatibility import exec_function from .environment import PEXEnvironment +from .finders import get_script_from_distributions from .interpreter import PythonInterpreter from .orderedset import OrderedSet from .pex_info import PexInfo @@ -59,27 +60,12 @@ def clean_environment(cls, forking=False): def __init__(self, pex=sys.argv[0], interpreter=None): self._pex = pex - self._pex_info = PexInfo.from_pex(self._pex) - self._env = PEXEnvironment(self._pex, self._pex_info) self._interpreter = interpreter or PythonInterpreter.get() - - @property - def info(self): - return self._pex_info - - def entry(self): - """Return the module spec of the entry point of this PEX. - - :returns: The entry point for this environment as a string, otherwise - ``None`` if there is no specific entry point. - """ - if 'PEX_MODULE' in os.environ: - TRACER.log('PEX_MODULE override detected: %s' % os.environ['PEX_MODULE']) - return os.environ['PEX_MODULE'] - entry_point = self._pex_info.entry_point - if entry_point: - TRACER.log('Using prescribed entry point: %s' % entry_point) - return str(entry_point) + self._pex_info = PexInfo.from_pex(self._pex) + self._pex_info_overrides = PexInfo.from_env() + env_pex_info = self._pex_info.copy() + env_pex_info.update(self._pex_info_overrides) + self._env = PEXEnvironment(self._pex, env_pex_info) @classmethod def _extras_paths(cls): @@ -248,15 +234,12 @@ def patch_all(path, path_importer_cache, modules): finally: patch_all(old_sys_path, old_sys_path_importer_cache, old_sys_modules) - def execute(self, args=()): + def execute(self): """Execute the PEX. This function makes assumptions that it is the last function called by the interpreter. """ - - entry_point = self.entry() - try: with self.patch_sys(): working_set = self._env.activate() @@ -267,10 +250,7 @@ def execute(self, args=()): TRACER.log(' %c %s' % (' ' if os.path.exists(element) else '*', element)) TRACER.log(' * - paths that do not exist or will be imported via zipimport') with self.patch_pkg_resources(working_set): - if entry_point and 'PEX_INTERPRETER' not in os.environ: - self.execute_entry(entry_point, args) - else: - self.execute_interpreter() + self._execute() except Exception: # Allow the current sys.excepthook to handle this app exception before we tear things down in # finally, then reraise so that the exit status is reflected correctly. @@ -285,6 +265,27 @@ def execute(self, args=()): sys.stderr = DevNull() sys.excepthook = lambda *a, **kw: None + def _execute(self): + if 'PEX_INTERPRETER' in os.environ: + return self.execute_interpreter() + + if self._pex_info_overrides.script and self._pex_info_overrides.entry_point: + die('Cannot specify both script and entry_point for a PEX!') + + if self._pex_info.script and self._pex_info.entry_point: + die('Cannot specify both script and entry_point for a PEX!') + + if self._pex_info_overrides.script: + return self.execute_script(self._pex_info_overrides.script) + elif self._pex_info_overrides.entry_point: + return self.execute_entry(self._pex_info_overrides.entry_point) + elif self._pex_info.script: + return self.execute_script(self._pex_info.script) + elif self._pex_info.entry_point: + return self.execute_entry(self._pex_info.entry_point) + else: + return self.execute_interpreter() + @classmethod def execute_interpreter(cls): force_interpreter = 'PEX_INTERPRETER' in os.environ @@ -295,26 +296,53 @@ def execute_interpreter(cls): if sys.argv[1:]: try: with open(sys.argv[1]) as fp: - ast = compile(fp.read(), fp.name, 'exec', flags=0, dont_inherit=1) + name, content = sys.argv[1], fp.read() except IOError as e: - print("Could not open %s in the environment [%s]: %s" % (sys.argv[1], sys.argv[0], e)) - sys.exit(1) + die("Could not open %s in the environment [%s]: %s" % (sys.argv[1], sys.argv[0], e)) sys.argv = sys.argv[1:] - old_name = globals()['__name__'] - try: - globals()['__name__'] = '__main__' - exec_function(ast, globals()) - finally: - globals()['__name__'] = old_name + cls.execute_content(name, content) else: import code code.interact() + def execute_script(self, script_name): + # TODO(wickman) PEXEnvironment should probably have a working_set property + # or possibly just __iter__. + dist, script_path, script_content = get_script_from_distributions( + script_name, self._env.activate()) + if not dist: + raise self.NotFound('Could not find script %s in pex!' % script_name) + if not script_content.startswith('#!/usr/bin/env python'): + die('Cannot execute non-Python script within PEX environment!') + TRACER.log('Found script %s in %s' % (script_name, dist)) + self.execute_content(script_path, script_content, argv0=script_name) + + @classmethod + def execute_content(cls, name, content, argv0=None): + argv0 = argv0 or name + ast = compile(content, name, 'exec', flags=0, dont_inherit=1) + old_name, old_file = globals().get('__name__'), globals().get('__file__') + try: + old_argv0 = sys.argv[0] + sys.argv[0] = argv0 + globals()['__name__'] = '__main__' + globals()['__file__'] = name + exec_function(ast, globals()) + finally: + if old_name: + globals()['__name__'] = old_name + else: + globals().pop('__name__') + if old_file: + globals()['__file__'] = old_file + else: + globals().pop('__file__') + sys.argv[0] = old_argv0 + + # TODO(wickman) Find a way to make PEX_PROFILE work with all execute_* @classmethod - def execute_entry(cls, entry_point, args=None): - if args: - sys.argv = args - runner = cls.execute_pkg_resources if ":" in entry_point else cls.execute_module + def execute_entry(cls, entry_point): + runner = cls.execute_pkg_resources if ':' in entry_point else cls.execute_module if 'PEX_PROFILE' not in os.environ: runner(entry_point) diff --git a/pex/pex_builder.py b/pex/pex_builder.py index 8b9911d40..89b5bde3d 100644 --- a/pex/pex_builder.py +++ b/pex/pex_builder.py @@ -10,6 +10,7 @@ from .common import Chroot, chmod_plus_x, open_zip, safe_mkdir, safe_mkdtemp from .compatibility import to_bytes +from .finders import get_script_from_distributions from .interpreter import PythonInterpreter from .marshaller import CodeMarshaller from .pex_info import PexInfo @@ -77,6 +78,7 @@ def __init__(self, path=None, interpreter=None, chroot=None, pex_info=None, prea self._interpreter = interpreter or PythonInterpreter.get() self._logger = logging.getLogger(__name__) self._preamble = to_bytes(preamble or '') + self._distributions = set() def _ensure_unfrozen(self, name='Operation'): if self._frozen: @@ -157,6 +159,71 @@ def add_requirement(self, req): self._ensure_unfrozen('Adding a requirement') self._pex_info.add_requirement(req) + def set_executable(self, filename, env_filename=None): + """Set the executable for this environment. + + :param filename: The file that should be executed within the PEX environment when the PEX is + invoked. + :keyword env_filename: (optional) The name that the executable file should be stored as within + the PEX. By default this will be the base name of the given filename. + + The entry point of the PEX may also be specified via ``PEXBuilder.set_entry_point``. + """ + self._ensure_unfrozen('Setting the executable') + if self._pex_info.script: + raise self.InvalidExecutableSpecification('Cannot set both entry point and script of PEX!') + if env_filename is None: + env_filename = os.path.basename(filename) + if self._chroot.get("executable"): + raise self.InvalidExecutableSpecification( + "Setting executable on a PEXBuilder that already has one!") + self._chroot.link(filename, env_filename, "executable") + entry_point = env_filename + entry_point.replace(os.path.sep, '.') + self._pex_info.entry_point = entry_point.rpartition('.')[0] + + @classmethod + def _get_entry_point_from_console_script(cls, script, distributions): + # check all distributions for the console_script "script" + entries = frozenset(filter(None, ( + dist.get_entry_map().get('console_scripts', {}).get(script) for dist in distributions))) + + # if multiple matches, freak out + if len(entries) > 1: + raise cls.InvalidExecutableSpecification( + 'Ambiguous script specification %s matches multiple entry points:%s' % ( + script, ' '.join(map(str, entries)))) + + if entries: + entry_point = next(iter(entries)) + # entry points are of the form 'foo = bar', we just want the 'bar' part: + return str(entry_point).split('=')[1].strip() + + def set_script(self, script): + """Set the entry point of this PEX environment based upon a distribution script. + + :param script: The script name as defined either by a console script or ordinary + script within the setup.py of one of the distributions added to the PEX. + :raises: :class:`PEXBuilder.InvalidExecutableSpecification` if the script is not found + in any distribution added to the PEX. + """ + + # check if 'script' is a console_script + entry_point = self._get_entry_point_from_console_script(script, self._distributions) + if entry_point: + self.set_entry_point(entry_point) + return + + # check if 'script' is an ordinary script + script_path, _, _ = get_script_from_distributions(script, self._distributions) + if script_path: + if self._pex_info.entry_point: + raise self.InvalidExecutableSpecification('Cannot set both entry point and script of PEX!') + self._pex_info.script = script + return + + raise self.InvalidExecutableSpecification('Could not find script %s in PEX!' % script) + def set_entry_point(self, entry_point): """Set the entry point of this PEX environment. @@ -204,6 +271,7 @@ def add_distribution(self, dist, dist_name=None): """ self._ensure_unfrozen('Adding a distribution') dist_name = dist_name or os.path.basename(dist.location) + self._distributions.add(dist) if os.path.isdir(dist.location): dist_hash = self._add_dist_dir(dist.location, dist_name) @@ -237,29 +305,6 @@ def add_egg(self, egg): self._ensure_unfrozen('Adding an egg') return self.add_dist_location(egg) - # TODO(wickman) Consider warning/erroring should set_executable and set_entry_point both be - # specified, or each specified more than once. - def set_executable(self, filename, env_filename=None): - """Set the executable for this environment. - - :param filename: The file that should be executed within the PEX environment when the PEX is - invoked. - :keyword env_filename: (optional) The name that the executable file should be stored as within - the PEX. By default this will be the base name of the given filename. - - The entry point of the PEX may also be specified via ``PEXBuilder.set_entry_point``. - """ - self._ensure_unfrozen('Setting the executable') - if env_filename is None: - env_filename = os.path.basename(filename) - if self._chroot.get("executable"): - raise self.InvalidExecutableSpecification( - "Setting executable on a PEXBuilder that already has one!") - self._chroot.link(filename, env_filename, "executable") - entry_point = env_filename - entry_point.replace(os.path.sep, '.') - self._pex_info.entry_point = entry_point.rpartition('.')[0] - # TODO(wickman) Consider changing this behavior to put the onus on the consumer # of pex to write the pex sources correctly. def _prepare_inits(self): diff --git a/pex/pex_info.py b/pex/pex_info.py index 9112c4a0d..8f13695d4 100644 --- a/pex/pex_info.py +++ b/pex/pex_info.py @@ -11,11 +11,29 @@ from .common import open_zip from .compatibility import string as compatibility_string +from .compatibility import PY2 from .orderedset import OrderedSet PexPlatform = namedtuple('PexPlatform', 'interpreter version strict') +def process_bool(value, negate=False): + def apply_bool(pex_info, env_variable): + if env_variable.strip().lower() in ('0', 'false'): + pex_info[value] = True if negate else False + elif env_variable.strip().lower() in ('1', 'true'): + pex_info[value] = False if negate else True + else: + raise ValueError('Unknown value for %s: %r' % (env_variable, value)) + + +def process_string(value): + def apply_string(pex_info, env_variable): + pex_info[value] = env_variable + return apply_string + + +# TODO(wickman) Split this into a PexInfoBuilder/PexInfo to ensure immutability. class PexInfo(object): """PEX metadata. @@ -24,18 +42,32 @@ class PexInfo(object): code_hash: str # sha1 hash of all names/code in the archive distributions: {dist_name: str} # map from distribution name (i.e. path in # the internal cache) to its cache key (sha1) + requirements: list # list of requirements for this environment # Environment options pex_root: ~/.pex # root of all pex-related files + # PEX_ROOT + entry_point: string # entry point into this pex + # PEX_MODULE + + script: string # script to execute in this pex environment + # at most one of script/entry_point can be specified + # PEX_SCRIPT + zip_safe: True, default False # is this pex zip safe? + # PEX_FORCE_LOCAL + inherit_path: True, default False # should this pex inherit site-packages + PYTHONPATH? + # PEX_INHERIT_PATH + ignore_errors: True, default False # should we ignore inability to resolve dependencies? + # PEX_IGNORE_ERRORS + always_write_cache: False # should we always write the internal cache to disk first? # this is useful if you have very large dependencies that # do not fit in RAM constrained environments - requirements: list # list of requirements for this environment - + # PEX_ALWAYS_CACHE .. versionchanged:: 0.8 Removed the ``repositories`` and ``indices`` information, as they were never @@ -44,6 +76,15 @@ class PexInfo(object): PATH = 'PEX-INFO' INTERNAL_CACHE = '.deps' + ENVIRONMENT_VARIABLES = { + 'PEX_ROOT': process_string('pex_root'), + 'PEX_MODULE': process_string('entry_point'), + 'PEX_SCRIPT': process_string('script'), + 'PEX_FORCE_LOCAL': process_bool('zip_safe', negate=True), + 'PEX_INHERIT_PATH': process_string('inherit_path'), + 'PEX_IGNORE_ERRORS': process_bool('ignore_errors'), + 'PEX_ALWAYS_CACHE': process_bool('always_write_cache'), + } @classmethod def make_build_properties(cls): @@ -62,7 +103,6 @@ def default(cls): pex_info = { 'requirements': [], 'distributions': {}, - 'always_write_cache': False, 'build_properties': cls.make_build_properties(), } return cls(info=pex_info) @@ -81,7 +121,16 @@ def from_pex(cls, pex): def from_json(cls, content): if isinstance(content, bytes): content = content.decode('utf-8') - return PexInfo(info=json.loads(content)) + return cls(info=json.loads(content)) + + @classmethod + def from_env(cls): + pex_info = {} + for variable, processor in cls.ENVIRONMENT_VARIABLES.items(): + if variable in os.environ: + cls.debug('processing %s = %s' % (variable, os.environ[variable])) + processor(pex_info, os.environ[variable]) + return cls(info=pex_info) @classmethod def _parse_requirement_tuple(cls, requirement_tuple): @@ -113,6 +162,12 @@ def __init__(self, info=None): raise ValueError('Expected requirements to be a list, got %s' % type(requirements)) self._requirements = OrderedSet(self._parse_requirement_tuple(req) for req in requirements) + def _get_safe(self, key): + if key not in self._pex_info: + return None + value = self._pex_info[key] + return value.encode('utf-8') if PY2 else value + @property def build_properties(self): """Information about the system on which this PEX was generated. @@ -139,9 +194,6 @@ def zip_safe(self): By default zip_safe is True. May be overridden at runtime by the $PEX_FORCE_LOCAL environment variable. """ - if 'PEX_FORCE_LOCAL' in os.environ: - self.debug('PEX_FORCE_LOCAL forcing zip_safe to False') - return False return self._pex_info.get('zip_safe', True) @zip_safe.setter @@ -158,11 +210,7 @@ def inherit_path(self): By default inherit_path is False. This may be overridden at runtime by the $PEX_INHERIT_PATH environment variable. """ - if 'PEX_INHERIT_PATH' in os.environ: - self.debug('PEX_INHERIT_PATH override detected') - return True - else: - return self._pex_info.get('inherit_path', False) + return self._pex_info.get('inherit_path', False) @inherit_path.setter def inherit_path(self, value): @@ -186,15 +234,20 @@ def code_hash(self, value): @property def entry_point(self): - if 'PEX_MODULE' in os.environ: - self.debug('PEX_MODULE override detected: %s' % os.environ['PEX_MODULE']) - return os.environ['PEX_MODULE'] - return self._pex_info.get('entry_point') + return self._get_safe('entry_point') @entry_point.setter def entry_point(self, value): self._pex_info['entry_point'] = value + @property + def script(self): + return self._get_safe('script') + + @script.setter + def script(self, value): + self._pex_info['script'] = value + def add_requirement(self, requirement): self._requirements.add(str(requirement)) @@ -211,9 +264,6 @@ def distributions(self): @property def always_write_cache(self): - if 'PEX_ALWAYS_CACHE' in os.environ: - self.debug('PEX_ALWAYS_CACHE override detected: %s' % os.environ['PEX_ALWAYS_CACHE']) - return True return self._pex_info.get('always_write_cache', False) @always_write_cache.setter @@ -241,6 +291,13 @@ def install_cache(self): def zip_unsafe_cache(self): return os.path.join(self.pex_root, 'code') + def update(self, other): + if not isinstance(other, PexInfo): + raise TypeError('Cannot merge a %r with PexInfo' % type(other)) + self._pex_info.update(other._pex_info) + self._distributions.update(other.distributions) + self._requirements.update(other.requirements) + def dump(self): pex_info_copy = self._pex_info.copy() pex_info_copy['requirements'] = list(self._requirements) diff --git a/pex/version.py b/pex/version.py index 2bc0a6788..47c5cf188 100644 --- a/pex/version.py +++ b/pex/version.py @@ -1,7 +1,7 @@ # Copyright 2015 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). -__version__ = '1.0.0.dev0' +__version__ = '1.0.0.dev1' -__setuptools_requirement = 'setuptools>=2.2,<16' -__wheel_requirement = 'wheel>=0.24.0,<0.25.0' +SETUPTOOLS_REQUIREMENT = 'setuptools>=2.2,<16' +WHEEL_REQUIREMENT = 'wheel>=0.24.0,<0.25.0' diff --git a/setup.py b/setup.py index fe7512a6a..b50c313e2 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ 'pex.bin', ], install_requires = [ - __setuptools_requirement, + SETUPTOOLS_REQUIREMENT, ], tests_require = [ 'mock',