urlparser.py 26 KB

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