Skip to content

Implement rwt as pip run #3979

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pip/_vendor/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ vendor:
@# Install vendored libraries
pip install -t . -r vendor.txt

# remove the tests
rm -rf rwt/tests

@# Cleanup .egg-info directories
rm -rf *.egg-info
rm -rf *.dist-info
1 change: 1 addition & 0 deletions pip/_vendor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,4 @@ def vendored(modulename):
vendored("requests.packages.urllib3.util.ssl_")
vendored("requests.packages.urllib3.util.timeout")
vendored("requests.packages.urllib3.util.url")
vendored("rwt")
2 changes: 1 addition & 1 deletion pip/_vendor/re-vendor.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def clean():
os.unlink(os.path.join(here, 'six.py'))

def vendor():
pip.main(['install', '-t', here, '-r', 'vendor.txt'])
pip.main(['install', '-t', here, '-r', os.path.join(here, 'vendor.txt')])
for dirname in glob.glob('*.egg-info'):
shutil.rmtree(dirname)

Expand Down
16 changes: 16 additions & 0 deletions pip/_vendor/rwt/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import sys

from . import deps
from . import commands
from . import launch
from . import scripts


def run(args=None):
if args is None:
args = sys.argv[1:]
pip_args, params = commands.parse_script_args(args)
commands.intercept(pip_args)
pip_args.extend(scripts.DepsReader.search(params))
with deps.load(*pip_args) as home:
launch.with_path(home, params)
4 changes: 4 additions & 0 deletions pip/_vendor/rwt/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import run


__name__ == '__main__' and run()
55 changes: 55 additions & 0 deletions pip/_vendor/rwt/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import textwrap


def parse_script_args(args):
"""
Separate the command line arguments into arguments for pip
and arguments to Python.

>>> parse_script_args(['foo', '--', 'bar'])
(['foo'], ['bar'])

>>> parse_script_args(['foo', 'bar'])
(['foo', 'bar'], [])
"""
try:
pivot = args.index('--')
except ValueError:
pivot = len(args)
return args[:pivot], args[pivot+1:]


help_doc = textwrap.dedent("""
Usage:

Arguments to rwt prior to `--` are used to specify the requirements
to make available, just as arguments to pip install. For example,

rwt -r requirements.txt "requests>=2.0"

That will launch python after installing the deps in requirements.txt
and also a late requests. Packages are always installed to a temporary
location and cleaned up when the process exits.

Arguments after `--` are passed to the Python interpreter. So to launch
`script.py`:

rwt -- script.py

If the `--` is ommitted or nothing is passed, the python interpreter
will be launched in interactive mode:

rwt
>>>

For more examples and details, see https://pypi.org/project/rwt.
""").lstrip()


def intercept(args):
"""
Detect certain args and intercept them.
"""
if '--help' in args or '-h' in args:
print(help_doc)
raise SystemExit(0)
95 changes: 95 additions & 0 deletions pip/_vendor/rwt/deps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from __future__ import print_function

import sys
import contextlib
import subprocess
import tempfile
import shutil


@contextlib.contextmanager
def _update_working_set():
"""
Update the master working_set to include these new packages.

TODO: would be better to use an officially-supported API,
but no suitable API is apparent.
"""
try:
pkg_resources = sys.modules['pkg_resources']
if not hasattr(pkg_resources, '_initialize_master_working_set'):
exec(_init_ws_patch, vars(pkg_resources))
pkg_resources._initialize_master_working_set()
except KeyError:
# it's unnecessary to re-initialize when it hasn't
# yet been initialized.
pass
yield


@contextlib.contextmanager
def load(*args):
target = tempfile.mkdtemp(prefix='rwt-')
cmdline = subprocess.list2cmdline(args)
print("Loading requirements using", cmdline)
cmd = (
sys.executable,
'-m', 'pip',
'install',
'-q',
'-t', target,
) + args
subprocess.check_call(cmd)
try:
yield target
finally:
shutil.rmtree(target)


@contextlib.contextmanager
def on_sys_path(*args):
"""
Install dependencies via args to pip and ensure they have precedence
on sys.path.
"""
with load(*args) as target:
sys.path.insert(0, target)
try:
with _update_working_set():
yield target
finally:
sys.path.remove(target)


# from setuptools 19.6.2
_init_ws_patch = '''
def _initialize_master_working_set():
"""
Prepare the master working set and make the ``require()``
API available.

This function has explicit effects on the global state
of pkg_resources. It is intended to be invoked once at
the initialization of this module.

Invocation by other packages is unsupported and done
at their own risk.
"""
working_set = WorkingSet._build_master()
_declare_state('object', working_set=working_set)

require = working_set.require
iter_entry_points = working_set.iter_entry_points
add_activation_listener = working_set.subscribe
run_script = working_set.run_script
# backward compatibility
run_main = run_script
# Activate all distributions already on sys.path, and ensure that
# all distributions added to the working set in the future (e.g. by
# calling ``require()``) will get activated as well.
add_activation_listener(lambda dist: dist.activate())
working_set.entries=[]
# match order
list(map(working_set.add_entry, sys.path))
globals().update(locals())
'''
91 changes: 91 additions & 0 deletions pip/_vendor/rwt/launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import textwrap
import os
import subprocess
import sys
import signal
import glob
import itertools


class PathReader:
@staticmethod
def _read_file(filename):
root = os.path.dirname(filename)
return (
os.path.join(root, path.rstrip())
for path in open(filename)
if path.strip()
and not path.startswith('#')
and not path.startswith('import ')
)

@classmethod
def _read(cls, target):
"""
As .pth files aren't honored except in site dirs,
read the paths indicated by them.
"""
pth_files = glob.glob(os.path.join(target, '*.pth'))
file_items = map(cls._read_file, pth_files)
return itertools.chain.from_iterable(file_items)


def _inject_sitecustomize(target):
"""
Create a sitecustomize file in the target that will install
the target as a sitedir.

Only needed on Python 3.2 and earlier to workaround #1.
"""
if sys.version_info > (3, 3):
return

hook = textwrap.dedent("""
import site
site.addsitedir({target!r})
""").lstrip().format(**locals())
sc_fn = os.path.join(target, 'sitecustomize.py')
with open(sc_fn, 'w') as strm:
strm.write(hook)


def _build_env(target):
"""
Prepend target and .pth references in target to PYTHONPATH
"""
env = dict(os.environ)
suffix = env.get('PYTHONPATH')
prefix = target,
items = itertools.chain(
prefix,
PathReader._read(target),
(suffix,) if suffix else (),
)
joined = os.pathsep.join(items)
env['PYTHONPATH'] = joined
return env


def _setup_env(target):
_inject_sitecustomize(target)
return _build_env(target)


def with_path(target, params):
"""
Launch Python with target on the path and params
"""
def null_handler(signum, frame):
pass

signal.signal(signal.SIGINT, null_handler)
cmd = [sys.executable] + params
subprocess.Popen(cmd, env=_setup_env(target)).wait()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically this is an issue with rwt rather than with the vendoring or pip run, but this doesn't pass the exit code of the script back to the caller of rwt. It probably should.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I've filed jaraco/pip-run#10



def with_path_overlay(target, params):
"""
Overlay Python with target on the path and params
"""
cmd = [sys.executable] + params
os.execve(sys.executable, cmd, _setup_env(target))
81 changes: 81 additions & 0 deletions pip/_vendor/rwt/scripts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import os
import sys
import ast
import tokenize
import itertools


if sys.version_info < (3,):
filter = itertools.ifilter


class DepsReader:
"""
Given a Python script, read the dependencies from the
indicated variable (default __requires__). Does not
execute the script, so expects the var_name to be
assigned a static list of strings.
"""
def __init__(self, script):
self.script = script

@classmethod
def load(cls, script_path):
with open(script_path) as stream:
return cls(stream.read())

@classmethod
def try_read(cls, script_path):
"""
Attempt to load the dependencies from the script,
but return an empty list if unsuccessful.
"""
try:
reader = cls.load(script_path)
return reader.read()
except Exception:
return []

@classmethod
def search(cls, params):
"""
Given a (possibly-empty) series of parameters to a
Python interpreter, return any dependencies discovered
in a script indicated in the parameters. Only honor the
first file found.
"""
files = filter(os.path.isfile, params)
return cls.try_read(next(files, None))

def read(self, var_name='__requires__'):
"""
>>> DepsReader("__requires__=['foo']").read()
['foo']
"""
mod = ast.parse(self.script)
node, = (
node
for node in mod.body
if isinstance(node, ast.Assign)
and len(node.targets) == 1
and isinstance(node.targets[0], ast.Name)
and node.targets[0].id == var_name
)
return ast.literal_eval(node.value)


def run(cmdline):
"""
Execute the script as if it had been invoked naturally.
"""
namespace = dict()
filename = cmdline[0]
namespace['__file__'] = filename
namespace['__name__'] = '__main__'
sys.argv[:] = cmdline

open_ = getattr(tokenize, 'open', open)
script = open_(filename).read()
norm_script = script.replace('\\r\\n', '\\n')
code = compile(norm_script, filename, 'exec')
exec(code, namespace)
1 change: 1 addition & 0 deletions pip/_vendor/vendor.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ ipaddress==1.0.16 # Only needed on 2.6 and 2.7
packaging==16.7
pyparsing==2.1.1
retrying==1.3.3
rwt==2.12
Loading