urlparser.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
  2. # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
  3. """
  4. WSGI applications that parse the URL and dispatch to on-disk resources
  5. """
  6. import os
  7. import sys
  8. import imp
  9. import mimetypes
  10. try:
  11. import pkg_resources
  12. except ImportError:
  13. pkg_resources = None
  14. from paste import request
  15. from paste import fileapp
  16. from paste.util import import_string
  17. from paste import httpexceptions
  18. from httpheaders import ETAG
  19. from paste.util import converters
  20. class NoDefault(object):
  21. pass
  22. __all__ = ['URLParser', 'StaticURLParser', 'PkgResourcesParser']
  23. class URLParser(object):
  24. """
  25. WSGI middleware
  26. Application dispatching, based on URL. An instance of `URLParser` is
  27. an application that loads and delegates to other applications. It
  28. looks for files in its directory that match the first part of
  29. PATH_INFO; these may have an extension, but are not required to have
  30. one, in which case the available files are searched to find the
  31. appropriate file. If it is ambiguous, a 404 is returned and an error
  32. logged.
  33. By default there is a constructor for .py files that loads the module,
  34. and looks for an attribute ``application``, which is a ready
  35. application object, or an attribute that matches the module name,
  36. which is a factory for building applications, and is called with no
  37. arguments.
  38. URLParser will also look in __init__.py for special overrides.
  39. These overrides are:
  40. ``urlparser_hook(environ)``
  41. This can modify the environment. Its return value is ignored,
  42. and it cannot be used to change the response in any way. You
  43. *can* use this, for example, to manipulate SCRIPT_NAME/PATH_INFO
  44. (try to keep them consistent with the original URL -- but
  45. consuming PATH_INFO and moving that to SCRIPT_NAME is ok).
  46. ``urlparser_wrap(environ, start_response, app)``:
  47. After URLParser finds the application, it calls this function
  48. (if present). If this function doesn't call
  49. ``app(environ, start_response)`` then the application won't be
  50. called at all! This can be used to allocate resources (with
  51. ``try:finally:``) or otherwise filter the output of the
  52. application.
  53. ``not_found_hook(environ, start_response)``:
  54. If no file can be found (*in this directory*) to match the
  55. request, then this WSGI application will be called. You can
  56. use this to change the URL and pass the request back to
  57. URLParser again, or on to some other application. This
  58. doesn't catch all ``404 Not Found`` responses, just missing
  59. files.
  60. ``application(environ, start_response)``:
  61. This basically overrides URLParser completely, and the given
  62. application is used for all requests. ``urlparser_wrap`` and
  63. ``urlparser_hook`` are still called, but the filesystem isn't
  64. searched in any way.
  65. """
  66. parsers_by_directory = {}
  67. # This is lazily initialized
  68. init_module = NoDefault
  69. global_constructors = {}
  70. def __init__(self, global_conf,
  71. directory, base_python_name,
  72. index_names=NoDefault,
  73. hide_extensions=NoDefault,
  74. ignore_extensions=NoDefault,
  75. constructors=None,
  76. **constructor_conf):
  77. """
  78. Create a URLParser object that looks at `directory`.
  79. `base_python_name` is the package that this directory
  80. represents, thus any Python modules in this directory will
  81. be given names under this package.
  82. """
  83. if global_conf:
  84. import warnings
  85. warnings.warn(
  86. 'The global_conf argument to URLParser is deprecated; '
  87. 'either pass in None or {}, or use make_url_parser',
  88. DeprecationWarning)
  89. else:
  90. global_conf = {}
  91. if os.path.sep != '/':
  92. directory = directory.replace(os.path.sep, '/')
  93. self.directory = directory
  94. self.base_python_name = base_python_name
  95. # This logic here should be deprecated since it is in
  96. # make_url_parser
  97. if index_names is NoDefault:
  98. index_names = global_conf.get(
  99. 'index_names', ('index', 'Index', 'main', 'Main'))
  100. self.index_names = converters.aslist(index_names)
  101. if hide_extensions is NoDefault:
  102. hide_extensions = global_conf.get(
  103. 'hide_extensions', ('.pyc', '.bak', '.py~', '.pyo'))
  104. self.hide_extensions = converters.aslist(hide_extensions)
  105. if ignore_extensions is NoDefault:
  106. ignore_extensions = global_conf.get(
  107. 'ignore_extensions', ())
  108. self.ignore_extensions = converters.aslist(ignore_extensions)
  109. self.constructors = self.global_constructors.copy()
  110. if constructors:
  111. self.constructors.update(constructors)
  112. # @@: Should we also check the global options for constructors?
  113. for name, value in constructor_conf.items():
  114. if not name.startswith('constructor '):
  115. raise ValueError(
  116. "Only extra configuration keys allowed are "
  117. "'constructor .ext = import_expr'; you gave %r "
  118. "(=%r)" % (name, value))
  119. ext = name[len('constructor '):].strip()
  120. if isinstance(value, (str, unicode)):
  121. value = import_string.eval_import(value)
  122. self.constructors[ext] = value
  123. def __call__(self, environ, start_response):
  124. environ['paste.urlparser.base_python_name'] = self.base_python_name
  125. if self.init_module is NoDefault:
  126. self.init_module = self.find_init_module(environ)
  127. path_info = environ.get('PATH_INFO', '')
  128. if not path_info:
  129. return self.add_slash(environ, start_response)
  130. if (self.init_module
  131. and getattr(self.init_module, 'urlparser_hook', None)):
  132. self.init_module.urlparser_hook(environ)
  133. orig_path_info = environ['PATH_INFO']
  134. orig_script_name = environ['SCRIPT_NAME']
  135. application, filename = self.find_application(environ)
  136. if not application:
  137. if (self.init_module
  138. and getattr(self.init_module, 'not_found_hook', None)
  139. and environ.get('paste.urlparser.not_found_parser') is not self):
  140. not_found_hook = self.init_module.not_found_hook
  141. environ['paste.urlparser.not_found_parser'] = self
  142. environ['PATH_INFO'] = orig_path_info
  143. environ['SCRIPT_NAME'] = orig_script_name
  144. return not_found_hook(environ, start_response)
  145. if filename is None:
  146. name, rest_of_path = request.path_info_split(environ['PATH_INFO'])
  147. if not name:
  148. name = 'one of %s' % ', '.join(
  149. self.index_names or
  150. ['(no index_names defined)'])
  151. return self.not_found(
  152. environ, start_response,
  153. 'Tried to load %s from directory %s'
  154. % (name, self.directory))
  155. else:
  156. environ['wsgi.errors'].write(
  157. 'Found resource %s, but could not construct application\n'
  158. % filename)
  159. return self.not_found(
  160. environ, start_response,
  161. 'Tried to load %s from directory %s'
  162. % (filename, self.directory))
  163. if (self.init_module
  164. and getattr(self.init_module, 'urlparser_wrap', None)):
  165. return self.init_module.urlparser_wrap(
  166. environ, start_response, application)
  167. else:
  168. return application(environ, start_response)
  169. def find_application(self, environ):
  170. if (self.init_module
  171. and getattr(self.init_module, 'application', None)
  172. and not environ.get('paste.urlparser.init_application') == environ['SCRIPT_NAME']):
  173. environ['paste.urlparser.init_application'] = environ['SCRIPT_NAME']
  174. return self.init_module.application, None
  175. name, rest_of_path = request.path_info_split(environ['PATH_INFO'])
  176. environ['PATH_INFO'] = rest_of_path
  177. if name is not None:
  178. environ['SCRIPT_NAME'] = environ.get('SCRIPT_NAME', '') + '/' + name
  179. if not name:
  180. names = self.index_names
  181. for index_name in names:
  182. filename = self.find_file(environ, index_name)
  183. if filename:
  184. break
  185. else:
  186. # None of the index files found
  187. filename = None
  188. else:
  189. filename = self.find_file(environ, name)
  190. if filename is None:
  191. return None, filename
  192. else:
  193. return self.get_application(environ, filename), filename
  194. def not_found(self, environ, start_response, debug_message=None):
  195. exc = httpexceptions.HTTPNotFound(
  196. 'The resource at %s could not be found'
  197. % request.construct_url(environ),
  198. comment='SCRIPT_NAME=%r; PATH_INFO=%r; looking in %r; debug: %s'
  199. % (environ.get('SCRIPT_NAME'), environ.get('PATH_INFO'),
  200. self.directory, debug_message or '(none)'))
  201. return exc.wsgi_application(environ, start_response)
  202. def add_slash(self, environ, start_response):
  203. """
  204. This happens when you try to get to a directory
  205. without a trailing /
  206. """
  207. url = request.construct_url(environ, with_query_string=False)
  208. url += '/'
  209. if environ.get('QUERY_STRING'):
  210. url += '?' + environ['QUERY_STRING']
  211. exc = httpexceptions.HTTPMovedPermanently(
  212. 'The resource has moved to %s - you should be redirected '
  213. 'automatically.' % url,
  214. headers=[('location', url)])
  215. return exc.wsgi_application(environ, start_response)
  216. def find_file(self, environ, base_filename):
  217. possible = []
  218. """Cache a few values to reduce function call overhead"""
  219. for filename in os.listdir(self.directory):
  220. base, ext = os.path.splitext(filename)
  221. full_filename = os.path.join(self.directory, filename)
  222. if (ext in self.hide_extensions
  223. or not base):
  224. continue
  225. if filename == base_filename:
  226. possible.append(full_filename)
  227. continue
  228. if ext in self.ignore_extensions:
  229. continue
  230. if base == base_filename:
  231. possible.append(full_filename)
  232. if not possible:
  233. #environ['wsgi.errors'].write(
  234. # 'No file found matching %r in %s\n'
  235. # % (base_filename, self.directory))
  236. return None
  237. if len(possible) > 1:
  238. # If there is an exact match, this isn't 'ambiguous'
  239. # per se; it might mean foo.gif and foo.gif.back for
  240. # instance
  241. if full_filename in possible:
  242. return full_filename
  243. else:
  244. environ['wsgi.errors'].write(
  245. 'Ambiguous URL: %s; matches files %s\n'
  246. % (request.construct_url(environ),
  247. ', '.join(possible)))
  248. return None
  249. return possible[0]
  250. def get_application(self, environ, filename):
  251. if os.path.isdir(filename):
  252. t = 'dir'
  253. else:
  254. t = os.path.splitext(filename)[1]
  255. constructor = self.constructors.get(t, self.constructors.get('*'))
  256. if constructor is None:
  257. #environ['wsgi.errors'].write(
  258. # 'No constructor found for %s\n' % t)
  259. return constructor
  260. app = constructor(self, environ, filename)
  261. if app is None:
  262. #environ['wsgi.errors'].write(
  263. # 'Constructor %s return None for %s\n' %
  264. # (constructor, filename))
  265. pass
  266. return app
  267. def register_constructor(cls, extension, constructor):
  268. """
  269. Register a function as a constructor. Registered constructors
  270. apply to all instances of `URLParser`.
  271. The extension should have a leading ``.``, or the special
  272. extensions ``dir`` (for directories) and ``*`` (a catch-all).
  273. `constructor` must be a callable that takes two arguments:
  274. ``environ`` and ``filename``, and returns a WSGI application.
  275. """
  276. d = cls.global_constructors
  277. assert not d.has_key(extension), (
  278. "A constructor already exists for the extension %r (%r) "
  279. "when attemption to register constructor %r"
  280. % (extension, d[extension], constructor))
  281. d[extension] = constructor
  282. register_constructor = classmethod(register_constructor)
  283. def get_parser(self, directory, base_python_name):
  284. """
  285. Get a parser for the given directory, or create one if
  286. necessary. This way parsers can be cached and reused.
  287. # @@: settings are inherited from the first caller
  288. """
  289. try:
  290. return self.parsers_by_directory[(directory, base_python_name)]
  291. except KeyError:
  292. parser = self.__class__(
  293. {},
  294. directory, base_python_name,
  295. index_names=self.index_names,
  296. hide_extensions=self.hide_extensions,
  297. ignore_extensions=self.ignore_extensions,
  298. constructors=self.constructors)
  299. self.parsers_by_directory[(directory, base_python_name)] = parser
  300. return parser
  301. def find_init_module(self, environ):
  302. filename = os.path.join(self.directory, '__init__.py')
  303. if not os.path.exists(filename):
  304. return None
  305. return load_module(environ, filename)
  306. def __repr__(self):
  307. return '<%s directory=%r; module=%s at %s>' % (
  308. self.__class__.__name__,
  309. self.directory,
  310. self.base_python_name,
  311. hex(abs(id(self))))
  312. def make_directory(parser, environ, filename):
  313. base_python_name = environ['paste.urlparser.base_python_name']
  314. if base_python_name:
  315. base_python_name += "." + os.path.basename(filename)
  316. else:
  317. base_python_name = os.path.basename(filename)
  318. return parser.get_parser(filename, base_python_name)
  319. URLParser.register_constructor('dir', make_directory)
  320. def make_unknown(parser, environ, filename):
  321. return fileapp.FileApp(filename)
  322. URLParser.register_constructor('*', make_unknown)
  323. def load_module(environ, filename):
  324. base_python_name = environ['paste.urlparser.base_python_name']
  325. module_name = os.path.splitext(os.path.basename(filename))[0]
  326. if base_python_name:
  327. module_name = base_python_name + '.' + module_name
  328. return load_module_from_name(environ, filename, module_name,
  329. environ['wsgi.errors'])
  330. def load_module_from_name(environ, filename, module_name, errors):
  331. if sys.modules.has_key(module_name):
  332. return sys.modules[module_name]
  333. init_filename = os.path.join(os.path.dirname(filename), '__init__.py')
  334. if not os.path.exists(init_filename):
  335. try:
  336. f = open(init_filename, 'w')
  337. except (OSError, IOError), e:
  338. errors.write(
  339. 'Cannot write __init__.py file into directory %s (%s)\n'
  340. % (os.path.dirname(filename), e))
  341. return None
  342. f.write('#\n')
  343. f.close()
  344. fp = None
  345. if sys.modules.has_key(module_name):
  346. return sys.modules[module_name]
  347. if '.' in module_name:
  348. parent_name = '.'.join(module_name.split('.')[:-1])
  349. base_name = module_name.split('.')[-1]
  350. parent = load_module_from_name(environ, os.path.dirname(filename),
  351. parent_name, errors)
  352. else:
  353. base_name = module_name
  354. fp = None
  355. try:
  356. fp, pathname, stuff = imp.find_module(
  357. base_name, [os.path.dirname(filename)])
  358. module = imp.load_module(module_name, fp, pathname, stuff)
  359. finally:
  360. if fp is not None:
  361. fp.close()
  362. return module
  363. def make_py(parser, environ, filename):
  364. module = load_module(environ, filename)
  365. if not module:
  366. return None
  367. if hasattr(module, 'application') and module.application:
  368. return getattr(module.application, 'wsgi_application', module.application)
  369. base_name = module.__name__.split('.')[-1]
  370. if hasattr(module, base_name):
  371. obj = getattr(module, base_name)
  372. if hasattr(obj, 'wsgi_application'):
  373. return obj.wsgi_application
  374. else:
  375. # @@: Old behavior; should probably be deprecated eventually:
  376. return getattr(module, base_name)()
  377. environ['wsgi.errors'].write(
  378. "Cound not find application or %s in %s\n"
  379. % (base_name, module))
  380. return None
  381. URLParser.register_constructor('.py', make_py)
  382. class StaticURLParser(object):
  383. """
  384. Like ``URLParser`` but only serves static files.
  385. ``cache_max_age``:
  386. integer specifies Cache-Control max_age in seconds
  387. """
  388. # @@: Should URLParser subclass from this?
  389. def __init__(self, directory, root_directory=None,
  390. cache_max_age=None):
  391. if os.path.sep != '/':
  392. directory = directory.replace(os.path.sep, '/')
  393. self.directory = os.path.normcase(os.path.abspath(directory))
  394. self.root_directory = root_directory
  395. if root_directory is not None:
  396. self.root_directory = os.path.normpath(self.root_directory)
  397. else:
  398. self.root_directory = directory
  399. self.root_directory = os.path.normcase(os.path.normpath(
  400. os.path.abspath(self.root_directory)))
  401. self.cache_max_age = cache_max_age
  402. if os.path.sep != '/':
  403. directory = directory.replace('/', os.path.sep)
  404. self.root_directory = self.root_directory.replace('/', os.path.sep)
  405. def __call__(self, environ, start_response):
  406. path_info = environ.get('PATH_INFO', '')
  407. if not path_info:
  408. return self.add_slash(environ, start_response)
  409. if path_info == '/':
  410. # @@: This should obviously be configurable
  411. filename = 'index.html'
  412. else:
  413. filename = request.path_info_pop(environ)
  414. full = os.path.normcase(os.path.normpath(
  415. os.path.join(self.directory, filename)))
  416. if os.path.sep != '/':
  417. full = full.replace('/', os.path.sep)
  418. if self.root_directory is not None and not full.startswith(self.root_directory):
  419. # Out of bounds
  420. return self.not_found(environ, start_response)
  421. if not os.path.exists(full):
  422. return self.not_found(environ, start_response)
  423. if os.path.isdir(full):
  424. # @@: Cache?
  425. child_root = self.root_directory is not None and \
  426. self.root_directory or self.directory
  427. return self.__class__(full, root_directory=child_root,
  428. cache_max_age=self.cache_max_age)(environ,
  429. start_response)
  430. if environ.get('PATH_INFO') and environ.get('PATH_INFO') != '/':
  431. return self.error_extra_path(environ, start_response)
  432. if_none_match = environ.get('HTTP_IF_NONE_MATCH')
  433. if if_none_match:
  434. mytime = os.stat(full).st_mtime
  435. if str(mytime) == if_none_match:
  436. headers = []
  437. ETAG.update(headers, mytime)
  438. start_response('304 Not Modified', headers)
  439. return [''] # empty body
  440. fa = self.make_app(full)
  441. if self.cache_max_age:
  442. fa.cache_control(max_age=self.cache_max_age)
  443. return fa(environ, start_response)
  444. def make_app(self, filename):
  445. return fileapp.FileApp(filename)
  446. def add_slash(self, environ, start_response):
  447. """
  448. This happens when you try to get to a directory
  449. without a trailing /
  450. """
  451. url = request.construct_url(environ, with_query_string=False)
  452. url += '/'
  453. if environ.get('QUERY_STRING'):
  454. url += '?' + environ['QUERY_STRING']
  455. exc = httpexceptions.HTTPMovedPermanently(
  456. 'The resource has moved to %s - you should be redirected '
  457. 'automatically.' % url,
  458. headers=[('location', url)])
  459. return exc.wsgi_application(environ, start_response)
  460. def not_found(self, environ, start_response, debug_message=None):
  461. exc = httpexceptions.HTTPNotFound(
  462. 'The resource at %s could not be found'
  463. % request.construct_url(environ),
  464. comment='SCRIPT_NAME=%r; PATH_INFO=%r; looking in %r; debug: %s'
  465. % (environ.get('SCRIPT_NAME'), environ.get('PATH_INFO'),
  466. self.directory, debug_message or '(none)'))
  467. return exc.wsgi_application(environ, start_response)
  468. def error_extra_path(self, environ, start_response):
  469. exc = httpexceptions.HTTPNotFound(
  470. 'The trailing path %r is not allowed' % environ['PATH_INFO'])
  471. return exc.wsgi_application(environ, start_response)
  472. def __repr__(self):
  473. return '<%s %r>' % (self.__class__.__name__, self.directory)
  474. def make_static(global_conf, document_root, cache_max_age=None):
  475. """
  476. Return a WSGI application that serves a directory (configured
  477. with document_root)
  478. cache_max_age - integer specifies CACHE_CONTROL max_age in seconds
  479. """
  480. if cache_max_age is not None:
  481. cache_max_age = int(cache_max_age)
  482. return StaticURLParser(
  483. document_root, cache_max_age=cache_max_age)
  484. class PkgResourcesParser(StaticURLParser):
  485. def __init__(self, egg_or_spec, resource_name, manager=None, root_resource=None):
  486. if pkg_resources is None:
  487. raise NotImplementedError("This class requires pkg_resources.")
  488. if isinstance(egg_or_spec, (str, unicode)):
  489. self.egg = pkg_resources.get_distribution(egg_or_spec)
  490. else:
  491. self.egg = egg_or_spec
  492. self.resource_name = resource_name
  493. if manager is None:
  494. manager = pkg_resources.ResourceManager()
  495. self.manager = manager
  496. if root_resource is None:
  497. root_resource = resource_name
  498. self.root_resource = os.path.normpath(root_resource)
  499. def __repr__(self):
  500. return '<%s for %s:%r>' % (
  501. self.__class__.__name__,
  502. self.egg.project_name,
  503. self.resource_name)
  504. def __call__(self, environ, start_response):
  505. path_info = environ.get('PATH_INFO', '')
  506. if not path_info:
  507. return self.add_slash(environ, start_response)
  508. if path_info == '/':
  509. # @@: This should obviously be configurable
  510. filename = 'index.html'
  511. else:
  512. filename = request.path_info_pop(environ)
  513. resource = os.path.normcase(os.path.normpath(
  514. self.resource_name + '/' + filename))
  515. if self.root_resource is not None and not resource.startswith(self.root_resource):
  516. # Out of bounds
  517. return self.not_found(environ, start_response)
  518. if not self.egg.has_resource(resource):
  519. return self.not_found(environ, start_response)
  520. if self.egg.resource_isdir(resource):
  521. # @@: Cache?
  522. child_root = self.root_resource is not None and self.root_resource or \
  523. self.resource_name
  524. return self.__class__(self.egg, resource, self.manager,
  525. root_resource=child_root)(environ, start_response)
  526. if environ.get('PATH_INFO') and environ.get('PATH_INFO') != '/':
  527. return self.error_extra_path(environ, start_response)
  528. type, encoding = mimetypes.guess_type(resource)
  529. if not type:
  530. type = 'application/octet-stream'
  531. # @@: I don't know what to do with the encoding.
  532. try:
  533. file = self.egg.get_resource_stream(self.manager, resource)
  534. except (IOError, OSError), e:
  535. exc = httpexceptions.HTTPForbidden(
  536. 'You are not permitted to view this file (%s)' % e)
  537. return exc.wsgi_application(environ, start_response)
  538. start_response('200 OK',
  539. [('content-type', type)])
  540. return fileapp._FileIter(file)
  541. def not_found(self, environ, start_response, debug_message=None):
  542. exc = httpexceptions.HTTPNotFound(
  543. 'The resource at %s could not be found'
  544. % request.construct_url(environ),
  545. comment='SCRIPT_NAME=%r; PATH_INFO=%r; looking in egg:%s#%r; debug: %s'
  546. % (environ.get('SCRIPT_NAME'), environ.get('PATH_INFO'),
  547. self.egg, self.resource_name, debug_message or '(none)'))
  548. return exc.wsgi_application(environ, start_response)
  549. def make_pkg_resources(global_conf, egg, resource_name=''):
  550. """
  551. A static file parser that loads data from an egg using
  552. ``pkg_resources``. Takes a configuration value ``egg``, which is
  553. an egg spec, and a base ``resource_name`` (default empty string)
  554. which is the path in the egg that this starts at.
  555. """
  556. if pkg_resources is None:
  557. raise NotImplementedError("This function requires pkg_resources.")
  558. return PkgResourcesParser(egg, resource_name)
  559. def make_url_parser(global_conf, directory, base_python_name,
  560. index_names=None, hide_extensions=None,
  561. ignore_extensions=None,
  562. **constructor_conf):
  563. """
  564. Create a URLParser application that looks in ``directory``, which
  565. should be the directory for the Python package named in
  566. ``base_python_name``. ``index_names`` are used when viewing the
  567. directory (like ``'index'`` for ``'index.html'``).
  568. ``hide_extensions`` are extensions that are not viewable (like
  569. ``'.pyc'``) and ``ignore_extensions`` are viewable but only if an
  570. explicit extension is given.
  571. """
  572. if index_names is None:
  573. index_names = global_conf.get(
  574. 'index_names', ('index', 'Index', 'main', 'Main'))
  575. index_names = converters.aslist(index_names)
  576. if hide_extensions is None:
  577. hide_extensions = global_conf.get(
  578. 'hide_extensions', ('.pyc', 'bak', 'py~'))
  579. hide_extensions = converters.aslist(hide_extensions)
  580. if ignore_extensions is None:
  581. ignore_extensions = global_conf.get(
  582. 'ignore_extensions', ())
  583. ignore_extensions = converters.aslist(ignore_extensions)
  584. # There's no real way to set constructors currently...
  585. return URLParser({}, directory, base_python_name,
  586. index_names=index_names,
  587. hide_extensions=hide_extensions,
  588. ignore_extensions=ignore_extensions,
  589. **constructor_conf)