Mercurial > hg-stable
changeset 14076:924c82157d46
url: move URL parsing functions into util to improve startup time
The introduction of the new URL parsing code has created a startup
time regression. This is mainly due to the use of url.hasscheme() in
the ui class. It ends up importing many libraries that the url module
requires.
This fix helps marginally, but if we can get rid of the urllib import
in the URL parser all together, startup time will go back to normal.
perfstartup time before the URL refactoring (8796fb6af67e):
! wall 0.050692 comb 0.000000 user 0.000000 sys 0.000000 (best of 100)
current startup time (139fb11210bb):
! wall 0.070685 comb 0.000000 user 0.000000 sys 0.000000 (best of 100)
after this change:
! wall 0.064667 comb 0.000000 user 0.000000 sys 0.000000 (best of 100)
author | Brodie Rao <brodie@bitheap.org> |
---|---|
date | Sat, 30 Apr 2011 09:43:20 -0700 |
parents | bc101902a68d |
children | c285bdb0572a |
files | hgext/fetch.py hgext/patchbomb.py hgext/schemes.py mercurial/bundlerepo.py mercurial/commands.py mercurial/hg.py mercurial/hgweb/hgwebdir_mod.py mercurial/httprepo.py mercurial/localrepo.py mercurial/sshrepo.py mercurial/statichttprepo.py mercurial/subrepo.py mercurial/ui.py mercurial/url.py mercurial/util.py tests/test-url.py |
diffstat | 16 files changed, 310 insertions(+), 311 deletions(-) [+] |
line wrap: on
line diff
--- a/hgext/fetch.py Sat Apr 30 16:33:47 2011 +0200 +++ b/hgext/fetch.py Sat Apr 30 09:43:20 2011 -0700 @@ -9,7 +9,7 @@ from mercurial.i18n import _ from mercurial.node import nullid, short -from mercurial import commands, cmdutil, hg, util, url, error +from mercurial import commands, cmdutil, hg, util, error from mercurial.lock import release def fetch(ui, repo, source='default', **opts): @@ -66,7 +66,7 @@ other = hg.repository(hg.remoteui(repo, opts), ui.expandpath(source)) ui.status(_('pulling from %s\n') % - url.hidepassword(ui.expandpath(source))) + util.hidepassword(ui.expandpath(source))) revs = None if opts['rev']: try: @@ -125,7 +125,7 @@ # we don't translate commit messages message = (cmdutil.logmessage(opts) or ('Automated merge with %s' % - url.removeauth(other.url()))) + util.removeauth(other.url()))) editor = cmdutil.commiteditor if opts.get('force_editor') or opts.get('edit'): editor = cmdutil.commitforceeditor
--- a/hgext/patchbomb.py Sat Apr 30 16:33:47 2011 +0200 +++ b/hgext/patchbomb.py Sat Apr 30 09:43:20 2011 -0700 @@ -48,7 +48,7 @@ import os, errno, socket, tempfile, cStringIO, time import email.MIMEMultipart, email.MIMEBase import email.Utils, email.Encoders, email.Generator -from mercurial import cmdutil, commands, hg, mail, patch, util, discovery, url +from mercurial import cmdutil, commands, hg, mail, patch, util, discovery from mercurial.i18n import _ from mercurial.node import bin @@ -239,7 +239,7 @@ dest, branches = hg.parseurl(dest) revs, checkout = hg.addbranchrevs(repo, repo, branches, revs) other = hg.repository(hg.remoteui(repo, opts), dest) - ui.status(_('comparing with %s\n') % url.hidepassword(dest)) + ui.status(_('comparing with %s\n') % util.hidepassword(dest)) common, _anyinc, _heads = discovery.findcommonincoming(repo, other) nodes = revs and map(repo.lookup, revs) or revs o = repo.changelog.findmissing(common, heads=nodes)
--- a/hgext/schemes.py Sat Apr 30 16:33:47 2011 +0200 +++ b/hgext/schemes.py Sat Apr 30 09:43:20 2011 -0700 @@ -41,7 +41,7 @@ """ import os, re -from mercurial import extensions, hg, templater, url as urlmod, util +from mercurial import extensions, hg, templater, util from mercurial.i18n import _ @@ -95,4 +95,4 @@ 'letter %s:\\\n') % (scheme, scheme.upper())) hg.schemes[scheme] = ShortRepository(url, scheme, t) - extensions.wrapfunction(urlmod, 'hasdriveletter', hasdriveletter) + extensions.wrapfunction(util, 'hasdriveletter', hasdriveletter)
--- a/mercurial/bundlerepo.py Sat Apr 30 16:33:47 2011 +0200 +++ b/mercurial/bundlerepo.py Sat Apr 30 09:43:20 2011 -0700 @@ -15,7 +15,7 @@ from i18n import _ import os, struct, tempfile, shutil import changegroup, util, mdiff, discovery -import localrepo, changelog, manifest, filelog, revlog, error, url +import localrepo, changelog, manifest, filelog, revlog, error class bundlerevlog(revlog.revlog): def __init__(self, opener, indexfile, bundle, @@ -274,7 +274,7 @@ cwd = os.path.join(cwd,'') if parentpath.startswith(cwd): parentpath = parentpath[len(cwd):] - u = url.url(path) + u = util.url(path) path = u.localpath() if u.scheme == 'bundle': s = path.split("+", 1)
--- a/mercurial/commands.py Sat Apr 30 16:33:47 2011 +0200 +++ b/mercurial/commands.py Sat Apr 30 09:43:20 2011 -0700 @@ -2607,7 +2607,7 @@ if 'bookmarks' not in other.listkeys('namespaces'): ui.warn(_("remote doesn't support bookmarks\n")) return 0 - ui.status(_('comparing with %s\n') % url.hidepassword(source)) + ui.status(_('comparing with %s\n') % util.hidepassword(source)) return bookmarks.diff(ui, repo, other) ret = hg.incoming(ui, repo, source, opts) @@ -2894,7 +2894,7 @@ if 'bookmarks' not in other.listkeys('namespaces'): ui.warn(_("remote doesn't support bookmarks\n")) return 0 - ui.status(_('comparing with %s\n') % url.hidepassword(dest)) + ui.status(_('comparing with %s\n') % util.hidepassword(dest)) return bookmarks.diff(ui, other, repo) ret = hg.outgoing(ui, repo, dest, opts) @@ -2968,13 +2968,13 @@ if search: for name, path in ui.configitems("paths"): if name == search: - ui.write("%s\n" % url.hidepassword(path)) + ui.write("%s\n" % util.hidepassword(path)) return ui.warn(_("not found!\n")) return 1 else: for name, path in ui.configitems("paths"): - ui.write("%s = %s\n" % (name, url.hidepassword(path))) + ui.write("%s = %s\n" % (name, util.hidepassword(path))) def postincoming(ui, repo, modheads, optupdate, checkout): if modheads == 0: @@ -3017,7 +3017,7 @@ """ source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch')) other = hg.repository(hg.remoteui(repo, opts), source) - ui.status(_('pulling from %s\n') % url.hidepassword(source)) + ui.status(_('pulling from %s\n') % util.hidepassword(source)) revs, checkout = hg.addbranchrevs(repo, other, branches, opts.get('rev')) if opts.get('bookmark'): @@ -3100,7 +3100,7 @@ dest = ui.expandpath(dest or 'default-push', dest or 'default') dest, branches = hg.parseurl(dest, opts.get('branch')) - ui.status(_('pushing to %s\n') % url.hidepassword(dest)) + ui.status(_('pushing to %s\n') % util.hidepassword(dest)) revs, checkout = hg.addbranchrevs(repo, repo, branches, opts.get('rev')) other = hg.repository(hg.remoteui(repo, opts), dest) if revs: @@ -3919,7 +3919,7 @@ source, branches = hg.parseurl(ui.expandpath('default')) other = hg.repository(hg.remoteui(repo, {}), source) revs, checkout = hg.addbranchrevs(repo, other, branches, opts.get('rev')) - ui.debug('comparing with %s\n' % url.hidepassword(source)) + ui.debug('comparing with %s\n' % util.hidepassword(source)) repo.ui.pushbuffer() common, incoming, rheads = discovery.findcommonincoming(repo, other) repo.ui.popbuffer() @@ -3929,7 +3929,7 @@ dest, branches = hg.parseurl(ui.expandpath('default-push', 'default')) revs, checkout = hg.addbranchrevs(repo, repo, branches, None) other = hg.repository(hg.remoteui(repo, {}), dest) - ui.debug('comparing with %s\n' % url.hidepassword(dest)) + ui.debug('comparing with %s\n' % util.hidepassword(dest)) repo.ui.pushbuffer() common, _anyinc, _heads = discovery.findcommonincoming(repo, other) repo.ui.popbuffer()
--- a/mercurial/hg.py Sat Apr 30 16:33:47 2011 +0200 +++ b/mercurial/hg.py Sat Apr 30 09:43:20 2011 -0700 @@ -11,13 +11,13 @@ from node import hex, nullid import localrepo, bundlerepo, httprepo, sshrepo, statichttprepo, bookmarks import lock, util, extensions, error, node -import cmdutil, discovery, url +import cmdutil, discovery import merge as mergemod import verify as verifymod import errno, os, shutil def _local(path): - path = util.expandpath(url.localpath(path)) + path = util.expandpath(util.localpath(path)) return (os.path.isfile(path) and bundlerepo or localrepo) def addbranchrevs(lrepo, repo, branches, revs): @@ -54,7 +54,7 @@ def parseurl(path, branches=None): '''parse url#branch, returning (url, (branch, branches))''' - u = url.url(path) + u = util.url(path) branch = None if u.fragment: branch = u.fragment @@ -71,7 +71,7 @@ } def _lookup(path): - u = url.url(path) + u = util.url(path) scheme = u.scheme or 'file' thing = schemes.get(scheme) or schemes['file'] try: @@ -221,8 +221,8 @@ else: dest = ui.expandpath(dest) - dest = url.localpath(dest) - source = url.localpath(source) + dest = util.localpath(dest) + source = util.localpath(source) if os.path.exists(dest): if not os.path.isdir(dest): @@ -248,7 +248,7 @@ abspath = origsource copy = False if src_repo.cancopy() and islocal(dest): - abspath = os.path.abspath(url.localpath(origsource)) + abspath = os.path.abspath(util.localpath(origsource)) copy = not pull and not rev if copy: @@ -421,7 +421,7 @@ """ source, branches = parseurl(ui.expandpath(source), opts.get('branch')) other = repository(remoteui(repo, opts), source) - ui.status(_('comparing with %s\n') % url.hidepassword(source)) + ui.status(_('comparing with %s\n') % util.hidepassword(source)) revs, checkout = addbranchrevs(repo, other, branches, opts.get('rev')) if revs: @@ -477,7 +477,7 @@ def _outgoing(ui, repo, dest, opts): dest = ui.expandpath(dest or 'default-push', dest or 'default') dest, branches = parseurl(dest, opts.get('branch')) - ui.status(_('comparing with %s\n') % url.hidepassword(dest)) + ui.status(_('comparing with %s\n') % util.hidepassword(dest)) revs, checkout = addbranchrevs(repo, repo, branches, opts.get('rev')) if revs: revs = [repo.lookup(rev) for rev in revs]
--- a/mercurial/hgweb/hgwebdir_mod.py Sat Apr 30 16:33:47 2011 +0200 +++ b/mercurial/hgweb/hgwebdir_mod.py Sat Apr 30 09:43:20 2011 -0700 @@ -9,7 +9,7 @@ import os, re, time from mercurial.i18n import _ from mercurial import ui, hg, scmutil, util, templater -from mercurial import error, encoding, url +from mercurial import error, encoding from common import ErrorResponse, get_mtime, staticfile, paritygen, \ get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR from hgweb_mod import hgweb @@ -364,7 +364,7 @@ def updatereqenv(self, env): if self._baseurl is not None: - u = url.url(self._baseurl) + u = util.url(self._baseurl) env['SERVER_NAME'] = u.host if u.port: env['SERVER_PORT'] = u.port
--- a/mercurial/httprepo.py Sat Apr 30 16:33:47 2011 +0200 +++ b/mercurial/httprepo.py Sat Apr 30 09:43:20 2011 -0700 @@ -28,7 +28,7 @@ self.path = path self.caps = None self.handler = None - u = url.url(path) + u = util.url(path) if u.query or u.fragment: raise util.Abort(_('unsupported URL component: "%s"') % (u.query or u.fragment)) @@ -111,12 +111,12 @@ except AttributeError: proto = resp.headers['content-type'] - safeurl = url.hidepassword(self._url) + safeurl = util.hidepassword(self._url) # accept old "text/plain" and "application/hg-changegroup" for now if not (proto.startswith('application/mercurial-') or proto.startswith('text/plain') or proto.startswith('application/hg-changegroup')): - self.ui.debug("requested URL: '%s'\n" % url.hidepassword(cu)) + self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu)) raise error.RepoError( _("'%s' does not appear to be an hg repository:\n" "---%%<--- (%s)\n%s\n---%%<---\n")
--- a/mercurial/localrepo.py Sat Apr 30 16:33:47 2011 +0200 +++ b/mercurial/localrepo.py Sat Apr 30 09:43:20 2011 -0700 @@ -14,7 +14,6 @@ import match as matchmod import merge as mergemod import tags as tagsmod -import url as urlmod from lock import release import weakref, errno, os, time, inspect propertycache = util.propertycache @@ -1695,7 +1694,7 @@ cl.delayupdate() oldheads = cl.heads() - tr = self.transaction("\n".join([srctype, urlmod.hidepassword(url)])) + tr = self.transaction("\n".join([srctype, util.hidepassword(url)])) try: trp = weakref.proxy(tr) # pull off the changeset group @@ -1937,7 +1936,7 @@ return a def instance(ui, path, create): - return localrepository(ui, urlmod.localpath(path), create) + return localrepository(ui, util.localpath(path), create) def islocal(path): return True
--- a/mercurial/sshrepo.py Sat Apr 30 16:33:47 2011 +0200 +++ b/mercurial/sshrepo.py Sat Apr 30 09:43:20 2011 -0700 @@ -6,7 +6,7 @@ # GNU General Public License version 2 or any later version. from i18n import _ -import util, error, wireproto, url +import util, error, wireproto class remotelock(object): def __init__(self, repo): @@ -23,7 +23,7 @@ self._url = path self.ui = ui - u = url.url(path, parsequery=False, parsefragment=False) + u = util.url(path, parsequery=False, parsefragment=False) if u.scheme != 'ssh' or not u.host or u.path is None: self._abort(error.RepoError(_("couldn't parse location %s") % path))
--- a/mercurial/statichttprepo.py Sat Apr 30 16:33:47 2011 +0200 +++ b/mercurial/statichttprepo.py Sat Apr 30 09:43:20 2011 -0700 @@ -85,7 +85,7 @@ self.ui = ui self.root = path - u = url.url(path.rstrip('/') + "/.hg") + u = util.url(path.rstrip('/') + "/.hg") self.path, authinfo = u.authinfo() opener = build_opener(ui, authinfo)
--- a/mercurial/subrepo.py Sat Apr 30 16:33:47 2011 +0200 +++ b/mercurial/subrepo.py Sat Apr 30 09:43:20 2011 -0700 @@ -8,7 +8,7 @@ import errno, os, re, xml.dom.minidom, shutil, posixpath import stat, subprocess, tarfile from i18n import _ -import config, scmutil, util, node, error, cmdutil, url, bookmarks +import config, scmutil, util, node, error, cmdutil, bookmarks hg = None propertycache = util.propertycache @@ -194,13 +194,13 @@ """return pull/push path of repo - either based on parent repo .hgsub info or on the top repo config. Abort or return None if no source found.""" if hasattr(repo, '_subparent'): - source = url.url(repo._subsource) + source = util.url(repo._subsource) source.path = posixpath.normpath(source.path) if posixpath.isabs(source.path) or source.scheme: return str(source) parent = _abssource(repo._subparent, push, abort=False) if parent: - parent = url.url(parent) + parent = util.url(parent) parent.path = posixpath.join(parent.path, source.path) parent.path = posixpath.normpath(parent.path) return str(parent)
--- a/mercurial/ui.py Sat Apr 30 16:33:47 2011 +0200 +++ b/mercurial/ui.py Sat Apr 30 09:43:20 2011 -0700 @@ -7,7 +7,7 @@ from i18n import _ import errno, getpass, os, socket, sys, tempfile, traceback -import config, scmutil, util, error, url +import config, scmutil, util, error class ui(object): def __init__(self, src=None): @@ -111,7 +111,7 @@ % (n, p, self.configsource('paths', n))) p = p.replace('%%', '%') p = util.expandpath(p) - if not url.hasscheme(p) and not os.path.isabs(p): + if not util.hasscheme(p) and not os.path.isabs(p): p = os.path.normpath(os.path.join(root, p)) c.set("paths", n, p) @@ -332,7 +332,7 @@ def expandpath(self, loc, default=None): """Return repository location relative to cwd or from [paths]""" - if url.hasscheme(loc) or os.path.isdir(os.path.join(loc, '.hg')): + if util.hasscheme(loc) or os.path.isdir(os.path.join(loc, '.hg')): return loc path = self.config('paths', loc)
--- a/mercurial/url.py Sat Apr 30 16:33:47 2011 +0200 +++ b/mercurial/url.py Sat Apr 30 09:43:20 2011 -0700 @@ -7,273 +7,11 @@ # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. -import urllib, urllib2, httplib, os, socket, cStringIO, re +import urllib, urllib2, httplib, os, socket, cStringIO import __builtin__ from i18n import _ import keepalive, util -class url(object): - """Reliable URL parser. - - This parses URLs and provides attributes for the following - components: - - <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment> - - Missing components are set to None. The only exception is - fragment, which is set to '' if present but empty. - - If parsefragment is False, fragment is included in query. If - parsequery is False, query is included in path. If both are - False, both fragment and query are included in path. - - See http://www.ietf.org/rfc/rfc2396.txt for more information. - - Note that for backward compatibility reasons, bundle URLs do not - take host names. That means 'bundle://../' has a path of '../'. - - Examples: - - >>> url('http://www.ietf.org/rfc/rfc2396.txt') - <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'> - >>> url('ssh://[::1]:2200//home/joe/repo') - <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'> - >>> url('file:///home/joe/repo') - <url scheme: 'file', path: '/home/joe/repo'> - >>> url('bundle:foo') - <url scheme: 'bundle', path: 'foo'> - >>> url('bundle://../foo') - <url scheme: 'bundle', path: '../foo'> - >>> url('c:\\\\foo\\\\bar') - <url path: 'c:\\\\foo\\\\bar'> - - Authentication credentials: - - >>> url('ssh://joe:xyz@x/repo') - <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'> - >>> url('ssh://joe@x/repo') - <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'> - - Query strings and fragments: - - >>> url('http://host/a?b#c') - <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'> - >>> url('http://host/a?b#c', parsequery=False, parsefragment=False) - <url scheme: 'http', host: 'host', path: 'a?b#c'> - """ - - _safechars = "!~*'()+" - _safepchars = "/!~*'()+" - _matchscheme = re.compile(r'^[a-zA-Z0-9+.\-]+:').match - - def __init__(self, path, parsequery=True, parsefragment=True): - # We slowly chomp away at path until we have only the path left - self.scheme = self.user = self.passwd = self.host = None - self.port = self.path = self.query = self.fragment = None - self._localpath = True - self._hostport = '' - self._origpath = path - - # special case for Windows drive letters - if hasdriveletter(path): - self.path = path - return - - # For compatibility reasons, we can't handle bundle paths as - # normal URLS - if path.startswith('bundle:'): - self.scheme = 'bundle' - path = path[7:] - if path.startswith('//'): - path = path[2:] - self.path = path - return - - if self._matchscheme(path): - parts = path.split(':', 1) - if parts[0]: - self.scheme, path = parts - self._localpath = False - - if not path: - path = None - if self._localpath: - self.path = '' - return - else: - if parsefragment and '#' in path: - path, self.fragment = path.split('#', 1) - if not path: - path = None - if self._localpath: - self.path = path - return - - if parsequery and '?' in path: - path, self.query = path.split('?', 1) - if not path: - path = None - if not self.query: - self.query = None - - # // is required to specify a host/authority - if path and path.startswith('//'): - parts = path[2:].split('/', 1) - if len(parts) > 1: - self.host, path = parts - path = path - else: - self.host = parts[0] - path = None - if not self.host: - self.host = None - if path: - path = '/' + path - - if self.host and '@' in self.host: - self.user, self.host = self.host.rsplit('@', 1) - if ':' in self.user: - self.user, self.passwd = self.user.split(':', 1) - if not self.host: - self.host = None - - # Don't split on colons in IPv6 addresses without ports - if (self.host and ':' in self.host and - not (self.host.startswith('[') and self.host.endswith(']'))): - self._hostport = self.host - self.host, self.port = self.host.rsplit(':', 1) - if not self.host: - self.host = None - - if (self.host and self.scheme == 'file' and - self.host not in ('localhost', '127.0.0.1', '[::1]')): - raise util.Abort(_('file:// URLs can only refer to localhost')) - - self.path = path - - for a in ('user', 'passwd', 'host', 'port', - 'path', 'query', 'fragment'): - v = getattr(self, a) - if v is not None: - setattr(self, a, urllib.unquote(v)) - - def __repr__(self): - attrs = [] - for a in ('scheme', 'user', 'passwd', 'host', 'port', 'path', - 'query', 'fragment'): - v = getattr(self, a) - if v is not None: - attrs.append('%s: %r' % (a, v)) - return '<url %s>' % ', '.join(attrs) - - def __str__(self): - """Join the URL's components back into a URL string. - - Examples: - - >>> str(url('http://user:pw@host:80/?foo#bar')) - 'http://user:pw@host:80/?foo#bar' - >>> str(url('ssh://user:pw@[::1]:2200//home/joe#')) - 'ssh://user:pw@[::1]:2200//home/joe#' - >>> str(url('http://localhost:80//')) - 'http://localhost:80//' - >>> str(url('http://localhost:80/')) - 'http://localhost:80/' - >>> str(url('http://localhost:80')) - 'http://localhost:80/' - >>> str(url('bundle:foo')) - 'bundle:foo' - >>> str(url('bundle://../foo')) - 'bundle:../foo' - >>> str(url('path')) - 'path' - """ - if self._localpath: - s = self.path - if self.scheme == 'bundle': - s = 'bundle:' + s - if self.fragment: - s += '#' + self.fragment - return s - - s = self.scheme + ':' - if (self.user or self.passwd or self.host or - self.scheme and not self.path): - s += '//' - if self.user: - s += urllib.quote(self.user, safe=self._safechars) - if self.passwd: - s += ':' + urllib.quote(self.passwd, safe=self._safechars) - if self.user or self.passwd: - s += '@' - if self.host: - if not (self.host.startswith('[') and self.host.endswith(']')): - s += urllib.quote(self.host) - else: - s += self.host - if self.port: - s += ':' + urllib.quote(self.port) - if self.host: - s += '/' - if self.path: - s += urllib.quote(self.path, safe=self._safepchars) - if self.query: - s += '?' + urllib.quote(self.query, safe=self._safepchars) - if self.fragment is not None: - s += '#' + urllib.quote(self.fragment, safe=self._safepchars) - return s - - def authinfo(self): - user, passwd = self.user, self.passwd - try: - self.user, self.passwd = None, None - s = str(self) - finally: - self.user, self.passwd = user, passwd - if not self.user: - return (s, None) - return (s, (None, (str(self), self.host), - self.user, self.passwd or '')) - - def localpath(self): - if self.scheme == 'file' or self.scheme == 'bundle': - path = self.path or '/' - # For Windows, we need to promote hosts containing drive - # letters to paths with drive letters. - if hasdriveletter(self._hostport): - path = self._hostport + '/' + self.path - elif self.host is not None and self.path: - path = '/' + path - # We also need to handle the case of file:///C:/, which - # should return C:/, not /C:/. - elif hasdriveletter(path): - # Strip leading slash from paths with drive names - return path[1:] - return path - return self._origpath - -def hasscheme(path): - return bool(url(path).scheme) - -def hasdriveletter(path): - return path[1:2] == ':' and path[0:1].isalpha() - -def localpath(path): - return url(path, parsequery=False, parsefragment=False).localpath() - -def hidepassword(u): - '''hide user credential in a url string''' - u = url(u) - if u.passwd: - u.passwd = '***' - return str(u) - -def removeauth(u): - '''remove all authentication information from a url string''' - u = url(u) - u.user = u.passwd = None - return str(u) - def readauthforuri(ui, uri): # Read configuration config = dict() @@ -357,7 +95,7 @@ if not (proxyurl.startswith('http:') or proxyurl.startswith('https:')): proxyurl = 'http://' + proxyurl + '/' - proxy = url(proxyurl) + proxy = util.url(proxyurl) if not proxy.user: proxy.user = ui.config("http_proxy", "user") proxy.passwd = ui.config("http_proxy", "passwd") @@ -545,7 +283,7 @@ new_tunnel = False if new_tunnel or tunnel_host == req.get_full_url(): # has proxy - u = url(tunnel_host) + u = util.url(tunnel_host) if new_tunnel or u.scheme == 'https': # only use CONNECT for HTTPS h.realhostport = ':'.join([u.host, (u.port or '443')]) h.headers = req.headers.copy() @@ -876,7 +614,7 @@ return opener def open(ui, url_, data=None): - u = url(url_) + u = util.url(url_) if u.scheme: u.scheme = u.scheme.lower() url_, authinfo = u.authinfo()
--- a/mercurial/util.py Sat Apr 30 16:33:47 2011 +0200 +++ b/mercurial/util.py Sat Apr 30 09:43:20 2011 -0700 @@ -17,7 +17,7 @@ import error, osutil, encoding import errno, re, shutil, sys, tempfile, traceback import os, time, calendar, textwrap, unicodedata, signal -import imp, socket +import imp, socket, urllib # Python compatibility @@ -1283,3 +1283,265 @@ If s is not a valid boolean, returns None. """ return _booleans.get(s.lower(), None) + +class url(object): + """Reliable URL parser. + + This parses URLs and provides attributes for the following + components: + + <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment> + + Missing components are set to None. The only exception is + fragment, which is set to '' if present but empty. + + If parsefragment is False, fragment is included in query. If + parsequery is False, query is included in path. If both are + False, both fragment and query are included in path. + + See http://www.ietf.org/rfc/rfc2396.txt for more information. + + Note that for backward compatibility reasons, bundle URLs do not + take host names. That means 'bundle://../' has a path of '../'. + + Examples: + + >>> url('http://www.ietf.org/rfc/rfc2396.txt') + <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'> + >>> url('ssh://[::1]:2200//home/joe/repo') + <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'> + >>> url('file:///home/joe/repo') + <url scheme: 'file', path: '/home/joe/repo'> + >>> url('bundle:foo') + <url scheme: 'bundle', path: 'foo'> + >>> url('bundle://../foo') + <url scheme: 'bundle', path: '../foo'> + >>> url('c:\\\\foo\\\\bar') + <url path: 'c:\\\\foo\\\\bar'> + + Authentication credentials: + + >>> url('ssh://joe:xyz@x/repo') + <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'> + >>> url('ssh://joe@x/repo') + <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'> + + Query strings and fragments: + + >>> url('http://host/a?b#c') + <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'> + >>> url('http://host/a?b#c', parsequery=False, parsefragment=False) + <url scheme: 'http', host: 'host', path: 'a?b#c'> + """ + + _safechars = "!~*'()+" + _safepchars = "/!~*'()+" + _matchscheme = re.compile(r'^[a-zA-Z0-9+.\-]+:').match + + def __init__(self, path, parsequery=True, parsefragment=True): + # We slowly chomp away at path until we have only the path left + self.scheme = self.user = self.passwd = self.host = None + self.port = self.path = self.query = self.fragment = None + self._localpath = True + self._hostport = '' + self._origpath = path + + # special case for Windows drive letters + if hasdriveletter(path): + self.path = path + return + + # For compatibility reasons, we can't handle bundle paths as + # normal URLS + if path.startswith('bundle:'): + self.scheme = 'bundle' + path = path[7:] + if path.startswith('//'): + path = path[2:] + self.path = path + return + + if self._matchscheme(path): + parts = path.split(':', 1) + if parts[0]: + self.scheme, path = parts + self._localpath = False + + if not path: + path = None + if self._localpath: + self.path = '' + return + else: + if parsefragment and '#' in path: + path, self.fragment = path.split('#', 1) + if not path: + path = None + if self._localpath: + self.path = path + return + + if parsequery and '?' in path: + path, self.query = path.split('?', 1) + if not path: + path = None + if not self.query: + self.query = None + + # // is required to specify a host/authority + if path and path.startswith('//'): + parts = path[2:].split('/', 1) + if len(parts) > 1: + self.host, path = parts + path = path + else: + self.host = parts[0] + path = None + if not self.host: + self.host = None + if path: + path = '/' + path + + if self.host and '@' in self.host: + self.user, self.host = self.host.rsplit('@', 1) + if ':' in self.user: + self.user, self.passwd = self.user.split(':', 1) + if not self.host: + self.host = None + + # Don't split on colons in IPv6 addresses without ports + if (self.host and ':' in self.host and + not (self.host.startswith('[') and self.host.endswith(']'))): + self._hostport = self.host + self.host, self.port = self.host.rsplit(':', 1) + if not self.host: + self.host = None + + if (self.host and self.scheme == 'file' and + self.host not in ('localhost', '127.0.0.1', '[::1]')): + raise Abort(_('file:// URLs can only refer to localhost')) + + self.path = path + + for a in ('user', 'passwd', 'host', 'port', + 'path', 'query', 'fragment'): + v = getattr(self, a) + if v is not None: + setattr(self, a, urllib.unquote(v)) + + def __repr__(self): + attrs = [] + for a in ('scheme', 'user', 'passwd', 'host', 'port', 'path', + 'query', 'fragment'): + v = getattr(self, a) + if v is not None: + attrs.append('%s: %r' % (a, v)) + return '<url %s>' % ', '.join(attrs) + + def __str__(self): + """Join the URL's components back into a URL string. + + Examples: + + >>> str(url('http://user:pw@host:80/?foo#bar')) + 'http://user:pw@host:80/?foo#bar' + >>> str(url('ssh://user:pw@[::1]:2200//home/joe#')) + 'ssh://user:pw@[::1]:2200//home/joe#' + >>> str(url('http://localhost:80//')) + 'http://localhost:80//' + >>> str(url('http://localhost:80/')) + 'http://localhost:80/' + >>> str(url('http://localhost:80')) + 'http://localhost:80/' + >>> str(url('bundle:foo')) + 'bundle:foo' + >>> str(url('bundle://../foo')) + 'bundle:../foo' + >>> str(url('path')) + 'path' + """ + if self._localpath: + s = self.path + if self.scheme == 'bundle': + s = 'bundle:' + s + if self.fragment: + s += '#' + self.fragment + return s + + s = self.scheme + ':' + if (self.user or self.passwd or self.host or + self.scheme and not self.path): + s += '//' + if self.user: + s += urllib.quote(self.user, safe=self._safechars) + if self.passwd: + s += ':' + urllib.quote(self.passwd, safe=self._safechars) + if self.user or self.passwd: + s += '@' + if self.host: + if not (self.host.startswith('[') and self.host.endswith(']')): + s += urllib.quote(self.host) + else: + s += self.host + if self.port: + s += ':' + urllib.quote(self.port) + if self.host: + s += '/' + if self.path: + s += urllib.quote(self.path, safe=self._safepchars) + if self.query: + s += '?' + urllib.quote(self.query, safe=self._safepchars) + if self.fragment is not None: + s += '#' + urllib.quote(self.fragment, safe=self._safepchars) + return s + + def authinfo(self): + user, passwd = self.user, self.passwd + try: + self.user, self.passwd = None, None + s = str(self) + finally: + self.user, self.passwd = user, passwd + if not self.user: + return (s, None) + return (s, (None, (str(self), self.host), + self.user, self.passwd or '')) + + def localpath(self): + if self.scheme == 'file' or self.scheme == 'bundle': + path = self.path or '/' + # For Windows, we need to promote hosts containing drive + # letters to paths with drive letters. + if hasdriveletter(self._hostport): + path = self._hostport + '/' + self.path + elif self.host is not None and self.path: + path = '/' + path + # We also need to handle the case of file:///C:/, which + # should return C:/, not /C:/. + elif hasdriveletter(path): + # Strip leading slash from paths with drive names + return path[1:] + return path + return self._origpath + +def hasscheme(path): + return bool(url(path).scheme) + +def hasdriveletter(path): + return path[1:2] == ':' and path[0:1].isalpha() + +def localpath(path): + return url(path, parsequery=False, parsefragment=False).localpath() + +def hidepassword(u): + '''hide user credential in a url string''' + u = url(u) + if u.passwd: + u.passwd = '***' + return str(u) + +def removeauth(u): + '''remove all authentication information from a url string''' + u = url(u) + u.user = u.passwd = None + return str(u)
--- a/tests/test-url.py Sat Apr 30 16:33:47 2011 +0200 +++ b/tests/test-url.py Sat Apr 30 09:43:20 2011 -0700 @@ -53,7 +53,7 @@ def test_url(): """ - >>> from mercurial.url import url + >>> from mercurial.util import url This tests for edge cases in url.URL's parsing algorithm. Most of these aren't useful for documentation purposes, so they aren't