Commit abed75c8 authored by PJ Eby's avatar PJ Eby

EasyInstall can now download bare ``.py`` files and wrap them in an egg,

as long as you include an ``#egg=name-version`` suffix on the URL, or if
the ``.py`` file is listed as the "Download URL" on the project's PyPI
page.  This allows third parties to "package" trivial Python modules
just by linking to them (e.g. from within their own PyPI page or
download links page).

--HG--
branch : setuptools
extra : convert_revision : svn%3A6015fed2-1504-0410-9fe1-9d1591cc4771/sandbox/trunk/setuptools%4041995
parent 51d68aa5
...@@ -968,6 +968,13 @@ Known Issues ...@@ -968,6 +968,13 @@ Known Issues
normalized form of the name was used, which could lead to unnecessary normalized form of the name was used, which could lead to unnecessary
full-index searches when a project's name had an underscore (``_``) in it. full-index searches when a project's name had an underscore (``_``) in it.
* EasyInstall can now download bare ``.py`` files and wrap them in an egg,
as long as you include an ``#egg=name-version`` suffix on the URL, or if
the ``.py`` file is listed as the "Download URL" on the project's PyPI page.
This allows third parties to "package" trivial Python modules just by
linking to them (e.g. from within their own PyPI page or download links
page).
0.6a9 0.6a9
* Fixed ``.pth`` file processing picking up nested eggs (i.e. ones inside * Fixed ``.pth`` file processing picking up nested eggs (i.e. ones inside
......
...@@ -499,7 +499,7 @@ class easy_install(Command): ...@@ -499,7 +499,7 @@ class easy_install(Command):
# Anything else, try to extract and build # Anything else, try to extract and build
setup_base = tmpdir setup_base = tmpdir
if os.path.isfile(dist_filename): if os.path.isfile(dist_filename) and not dist_filename.endswith('.py'):
unpack_archive(dist_filename, tmpdir, self.unpack_progress) unpack_archive(dist_filename, tmpdir, self.unpack_progress)
elif os.path.isdir(dist_filename): elif os.path.isdir(dist_filename):
setup_base = os.path.abspath(dist_filename) setup_base = os.path.abspath(dist_filename)
......
...@@ -12,7 +12,7 @@ from distutils import file_util ...@@ -12,7 +12,7 @@ from distutils import file_util
from distutils.util import convert_path from distutils.util import convert_path
from distutils.filelist import FileList from distutils.filelist import FileList
from pkg_resources import parse_requirements, safe_name, parse_version, \ from pkg_resources import parse_requirements, safe_name, parse_version, \
safe_version, yield_lines, EntryPoint, iter_entry_points safe_version, yield_lines, EntryPoint, iter_entry_points, to_filename
from sdist import walk_revctrl from sdist import walk_revctrl
class egg_info(Command): class egg_info(Command):
...@@ -58,7 +58,7 @@ class egg_info(Command): ...@@ -58,7 +58,7 @@ class egg_info(Command):
self.egg_base = (dirs or {}).get('',os.curdir) self.egg_base = (dirs or {}).get('',os.curdir)
self.ensure_dirname('egg_base') self.ensure_dirname('egg_base')
self.egg_info = self.egg_name.replace('-','_')+'.egg-info' self.egg_info = to_filename(self.egg_name)+'.egg-info'
if self.egg_base != os.curdir: if self.egg_base != os.curdir:
self.egg_info = os.path.join(self.egg_base, self.egg_info) self.egg_info = os.path.join(self.egg_base, self.egg_info)
if '-' in self.egg_name: self.check_broken_egg_info() if '-' in self.egg_name: self.check_broken_egg_info()
......
"""PyPI and direct package downloading""" """PyPI and direct package downloading"""
import sys, os.path, re, urlparse, urllib2 import sys, os.path, re, urlparse, urllib2, shutil
from pkg_resources import * from pkg_resources import *
from distutils import log from distutils import log
from distutils.errors import DistutilsError from distutils.errors import DistutilsError
...@@ -39,12 +39,15 @@ def parse_bdist_wininst(name): ...@@ -39,12 +39,15 @@ def parse_bdist_wininst(name):
return base,py_ver return base,py_ver
def distros_for_url(url, metadata=None): def egg_info_for_url(url):
"""Yield egg or source distribution objects that might be found at a URL"""
scheme, server, path, parameters, query, fragment = urlparse.urlparse(url) scheme, server, path, parameters, query, fragment = urlparse.urlparse(url)
base = urllib2.unquote(path.split('/')[-1]) base = urllib2.unquote(path.split('/')[-1])
if '#' in base: base, fragment = base.split('#',1) if '#' in base: base, fragment = base.split('#',1)
return base,fragment
def distros_for_url(url, metadata=None):
"""Yield egg or source distribution objects that might be found at a URL"""
base, fragment = egg_info_for_url(url)
dists = distros_for_location(url, base, metadata) dists = distros_for_location(url, base, metadata)
if fragment and not dists: if fragment and not dists:
match = EGG_FRAGMENT.match(fragment) match = EGG_FRAGMENT.match(fragment)
...@@ -54,12 +57,10 @@ def distros_for_url(url, metadata=None): ...@@ -54,12 +57,10 @@ def distros_for_url(url, metadata=None):
) )
return dists return dists
def distros_for_location(location, basename, metadata=None): def distros_for_location(location, basename, metadata=None):
"""Yield egg or source distribution objects based on basename""" """Yield egg or source distribution objects based on basename"""
if basename.endswith('.egg.zip'): if basename.endswith('.egg.zip'):
basename = basename[:-4] # strip the .zip basename = basename[:-4] # strip the .zip
if basename.endswith('.egg'): # only one, unambiguous interpretation if basename.endswith('.egg'): # only one, unambiguous interpretation
return [Distribution.from_location(location, basename, metadata)] return [Distribution.from_location(location, basename, metadata)]
...@@ -76,7 +77,6 @@ def distros_for_location(location, basename, metadata=None): ...@@ -76,7 +77,6 @@ def distros_for_location(location, basename, metadata=None):
if basename.endswith(ext): if basename.endswith(ext):
basename = basename[:-len(ext)] basename = basename[:-len(ext)]
return interpret_distro_name(location, basename, metadata) return interpret_distro_name(location, basename, metadata)
return [] # no extension matched return [] # no extension matched
...@@ -205,7 +205,6 @@ class PackageIndex(Environment): ...@@ -205,7 +205,6 @@ class PackageIndex(Environment):
def process_index(self,url,page): def process_index(self,url,page):
"""Process the contents of a PyPI page""" """Process the contents of a PyPI page"""
def scan(link): def scan(link):
# Process a URL to see if it's for a package page # Process a URL to see if it's for a package page
if link.startswith(self.index_url): if link.startswith(self.index_url):
...@@ -217,14 +216,15 @@ class PackageIndex(Environment): ...@@ -217,14 +216,15 @@ class PackageIndex(Environment):
pkg = safe_name(parts[0]) pkg = safe_name(parts[0])
ver = safe_version(parts[1]) ver = safe_version(parts[1])
self.package_pages.setdefault(pkg.lower(),{})[link] = True self.package_pages.setdefault(pkg.lower(),{})[link] = True
return to_filename(pkg), to_filename(ver)
return None, None
if url==self.index_url or 'Index of Packages</title>' in page: if url==self.index_url or 'Index of Packages</title>' in page:
# process an index page into the package-page index # process an index page into the package-page index
for match in HREF.finditer(page): for match in HREF.finditer(page):
scan( urlparse.urljoin(url, match.group(1)) ) scan( urlparse.urljoin(url, match.group(1)) )
else: else:
scan(url) # ensure this page is in the page index pkg,ver = scan(url) # ensure this page is in the page index
# process individual package page # process individual package page
for tag in ("<th>Home Page", "<th>Download URL"): for tag in ("<th>Home Page", "<th>Download URL"):
pos = page.find(tag) pos = page.find(tag)
...@@ -232,35 +232,44 @@ class PackageIndex(Environment): ...@@ -232,35 +232,44 @@ class PackageIndex(Environment):
match = HREF.search(page,pos) match = HREF.search(page,pos)
if match: if match:
# Process the found URL # Process the found URL
self.scan_url(urlparse.urljoin(url, match.group(1))) new_url = urlparse.urljoin(url, match.group(1))
base, frag = egg_info_for_url(new_url)
if base.endswith('.py') and not frag:
if pkg and ver:
new_url+='#egg=%s-%s' % (pkg,ver)
else:
self.need_version_info(url)
self.scan_url(new_url)
return PYPI_MD5.sub( return PYPI_MD5.sub(
lambda m: '<a href="%s#md5=%s">%s</a>' % m.group(1,3,2), page lambda m: '<a href="%s#md5=%s">%s</a>' % m.group(1,3,2), page
) )
def need_version_info(self, url):
self.scan_all(
"Page at %s links to .py file(s) without version info; an index "
"scan is required.", url
)
def scan_all(self, msg, *args):
if self.index_url not in self.fetched_urls:
if msg: self.warn(msg,*args)
self.warn(
"Scanning index of all packages (this may take a while)"
)
self.scan_url(self.index_url)
def find_packages(self, requirement): def find_packages(self, requirement):
self.scan_url(self.index_url + requirement.unsafe_name+'/') self.scan_url(self.index_url + requirement.unsafe_name+'/')
if not self.package_pages.get(requirement.key): if not self.package_pages.get(requirement.key):
# Fall back to safe version of the name # Fall back to safe version of the name
self.scan_url(self.index_url + requirement.project_name+'/') self.scan_url(self.index_url + requirement.project_name+'/')
if not self.package_pages.get(requirement.key): if not self.package_pages.get(requirement.key):
# We couldn't find the target package, so search the index page too # We couldn't find the target package, so search the index page too
self.warn( self.warn(
"Couldn't find index page for %r (maybe misspelled?)", "Couldn't find index page for %r (maybe misspelled?)",
requirement.unsafe_name requirement.unsafe_name
) )
if self.index_url not in self.fetched_urls: self.scan_all()
self.warn(
"Scanning index of all packages (this may take a while)"
)
self.scan_url(self.index_url)
for url in self.package_pages.get(requirement.key,()): for url in self.package_pages.get(requirement.key,()):
# scan each page that might be related to the desired package # scan each page that might be related to the desired package
...@@ -274,6 +283,8 @@ class PackageIndex(Environment): ...@@ -274,6 +283,8 @@ class PackageIndex(Environment):
self.debug("%s does not match %s", requirement, dist) self.debug("%s does not match %s", requirement, dist)
return super(PackageIndex, self).obtain(requirement,installer) return super(PackageIndex, self).obtain(requirement,installer)
def check_md5(self, cs, info, filename, tfp): def check_md5(self, cs, info, filename, tfp):
if re.match('md5=[0-9a-f]{32}$', info): if re.match('md5=[0-9a-f]{32}$', info):
self.debug("Validating md5 checksum for %s", filename) self.debug("Validating md5 checksum for %s", filename)
...@@ -290,7 +301,10 @@ class PackageIndex(Environment): ...@@ -290,7 +301,10 @@ class PackageIndex(Environment):
`spec` may be a ``Requirement`` object, or a string containing a URL, `spec` may be a ``Requirement`` object, or a string containing a URL,
an existing local filename, or a project/version requirement spec an existing local filename, or a project/version requirement spec
(i.e. the string form of a ``Requirement`` object). (i.e. the string form of a ``Requirement`` object). If it is the URL
of a .py file with an unambiguous ``#egg=name-version`` tag (i.e., one
that escapes ``-`` as ``_`` throughout), a trivial ``setup.py`` is
automatically created alongside the downloaded file.
If `spec` is a ``Requirement`` object or a string containing a If `spec` is a ``Requirement`` object or a string containing a
project/version requirement spec, this method is equivalent to project/version requirement spec, this method is equivalent to
...@@ -304,8 +318,11 @@ class PackageIndex(Environment): ...@@ -304,8 +318,11 @@ class PackageIndex(Environment):
scheme = URL_SCHEME(spec) scheme = URL_SCHEME(spec)
if scheme: if scheme:
# It's a url, download it to tmpdir # It's a url, download it to tmpdir
return self._download_url(scheme.group(1), spec, tmpdir) found = self._download_url(scheme.group(1), spec, tmpdir)
base, fragment = egg_info_for_url(spec)
if base.endswith('.py'):
found = self.gen_setup(found,fragment,tmpdir)
return found
elif os.path.exists(spec): elif os.path.exists(spec):
# Existing file or directory, just return it # Existing file or directory, just return it
return spec return spec
...@@ -317,15 +334,9 @@ class PackageIndex(Environment): ...@@ -317,15 +334,9 @@ class PackageIndex(Environment):
"Not a URL, existing file, or requirement spec: %r" % "Not a URL, existing file, or requirement spec: %r" %
(spec,) (spec,)
) )
return self.fetch(spec, tmpdir) return self.fetch(spec, tmpdir)
def fetch(self, requirement, tmpdir, force_scan=False, source=False): def fetch(self, requirement, tmpdir, force_scan=False, source=False):
"""Obtain a file suitable for fulfilling `requirement` """Obtain a file suitable for fulfilling `requirement`
...@@ -367,6 +378,47 @@ class PackageIndex(Environment): ...@@ -367,6 +378,47 @@ class PackageIndex(Environment):
return dist return dist
def gen_setup(self, filename, fragment, tmpdir):
match = EGG_FRAGMENT.match(fragment); #import pdb; pdb.set_trace()
dists = match and [d for d in
interpret_distro_name(filename, match.group(1), None) if d.version
] or []
if len(dists)==1: # unambiguous ``#egg`` fragment
basename = os.path.basename(filename)
# Make sure the file has been downloaded to the temp dir.
if os.path.dirname(filename) != tmpdir:
dst = os.path.join(tmpdir, basename)
from setuptools.command.easy_install import samefile
if not samefile(filename, dst):
shutil.copy2(filename, dst)
filename=dst
file = open(os.path.join(tmpdir, 'setup.py'), 'w')
file.write(
"from setuptools import setup\n"
"setup(name=%r, version=%r, py_modules=[%r])\n"
% (
dists[0].project_name, dists[0].version,
os.path.splitext(basename)[0]
)
)
file.close()
return filename
elif match:
raise DistutilsError(
"Can't unambiguously interpret project/version identifier %r; "
"any dashes in the name or version should be escaped using "
"underscores. %r" % (fragment,dists)
)
else:
raise DistutilsError(
"Can't process plain .py files without an '#egg=name-version'"
" suffix to enable automatic setup script generation."
)
dl_blocksize = 8192 dl_blocksize = 8192
def _download_to(self, url, filename): def _download_to(self, url, filename):
self.url_ok(url,True) # raises error if not allowed self.url_ok(url,True) # raises error if not allowed
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment