##// END OF EJS Templates
fixes for issue #148
marcink -
r1196:49ea5274 beta
parent child Browse files
Show More
@@ -1,688 +1,688 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.utils
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Utilities library for RhodeCode
7 7
8 8 :created_on: Apr 18, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software; you can redistribute it and/or
14 14 # modify it under the terms of the GNU General Public License
15 15 # as published by the Free Software Foundation; version 2
16 16 # of the License or (at your opinion) any later version of the license.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program; if not, write to the Free Software
25 25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 26 # MA 02110-1301, USA.
27 27
28 28 import os
29 29 import logging
30 30 import datetime
31 31 import traceback
32 32 import paste
33 33 import beaker
34 34
35 35 from paste.script.command import Command, BadCommand
36 36
37 37 from UserDict import DictMixin
38 38
39 39 from mercurial import ui, config, hg
40 40 from mercurial.error import RepoError
41 41
42 42 from webhelpers.text import collapse, remove_formatting, strip_tags
43 43
44 44 from vcs.backends.base import BaseChangeset
45 45 from vcs.utils.lazy import LazyProperty
46 46
47 47 from rhodecode.model import meta
48 48 from rhodecode.model.caching_query import FromCache
49 49 from rhodecode.model.db import Repository, User, RhodeCodeUi, UserLog, Group
50 50 from rhodecode.model.repo import RepoModel
51 51 from rhodecode.model.user import UserModel
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 def recursive_replace(str, replace=' '):
57 57 """Recursive replace of given sign to just one instance
58 58
59 59 :param str: given string
60 60 :param replace: char to find and replace multiple instances
61 61
62 62 Examples::
63 63 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
64 64 'Mighty-Mighty-Bo-sstones'
65 65 """
66 66
67 67 if str.find(replace * 2) == -1:
68 68 return str
69 69 else:
70 70 str = str.replace(replace * 2, replace)
71 71 return recursive_replace(str, replace)
72 72
73 73 def repo_name_slug(value):
74 74 """Return slug of name of repository
75 75 This function is called on each creation/modification
76 76 of repository to prevent bad names in repo
77 77 """
78 78
79 79 slug = remove_formatting(value)
80 80 slug = strip_tags(slug)
81 81
82 82 for c in """=[]\;'"<>,/~!@#$%^&*()+{}|: """:
83 83 slug = slug.replace(c, '-')
84 84 slug = recursive_replace(slug, '-')
85 85 slug = collapse(slug, '-')
86 86 return slug
87 87
88 88 def get_repo_slug(request):
89 89 return request.environ['pylons.routes_dict'].get('repo_name')
90 90
91 91 def action_logger(user, action, repo, ipaddr='', sa=None):
92 92 """
93 93 Action logger for various actions made by users
94 94
95 95 :param user: user that made this action, can be a unique username string or
96 96 object containing user_id attribute
97 97 :param action: action to log, should be on of predefined unique actions for
98 98 easy translations
99 99 :param repo: string name of repository or object containing repo_id,
100 100 that action was made on
101 101 :param ipaddr: optional ip address from what the action was made
102 102 :param sa: optional sqlalchemy session
103 103
104 104 """
105 105
106 106 if not sa:
107 107 sa = meta.Session()
108 108
109 109 try:
110 110 um = UserModel()
111 111 if hasattr(user, 'user_id'):
112 112 user_obj = user
113 113 elif isinstance(user, basestring):
114 114 user_obj = um.get_by_username(user, cache=False)
115 115 else:
116 116 raise Exception('You have to provide user object or username')
117 117
118 118
119 119 rm = RepoModel()
120 120 if hasattr(repo, 'repo_id'):
121 121 repo_obj = rm.get(repo.repo_id, cache=False)
122 122 repo_name = repo_obj.repo_name
123 123 elif isinstance(repo, basestring):
124 124 repo_name = repo.lstrip('/')
125 125 repo_obj = rm.get_by_repo_name(repo_name, cache=False)
126 126 else:
127 127 raise Exception('You have to provide repository to action logger')
128 128
129 129
130 130 user_log = UserLog()
131 131 user_log.user_id = user_obj.user_id
132 132 user_log.action = action
133 133
134 134 user_log.repository_id = repo_obj.repo_id
135 135 user_log.repository_name = repo_name
136 136
137 137 user_log.action_date = datetime.datetime.now()
138 138 user_log.user_ip = ipaddr
139 139 sa.add(user_log)
140 140 sa.commit()
141 141
142 142 log.info('Adding user %s, action %s on %s', user_obj, action, repo)
143 143 except:
144 144 log.error(traceback.format_exc())
145 145 sa.rollback()
146 146
147 147 def get_repos(path, recursive=False):
148 148 """
149 149 Scans given path for repos and return (name,(type,path)) tuple
150 150
151 151 :param path: path to scann for repositories
152 152 :param recursive: recursive search and return names with subdirs in front
153 153 """
154 154 from vcs.utils.helpers import get_scm
155 155 from vcs.exceptions import VCSError
156 156
157 157 if path.endswith('/'):
158 158 #add ending slash for better results
159 159 path = path[:-1]
160 160
161 161 def _get_repos(p):
162 162 for dirpath in os.listdir(p):
163 163 if os.path.isfile(os.path.join(p, dirpath)):
164 164 continue
165 165 cur_path = os.path.join(p, dirpath)
166 166 try:
167 167 scm_info = get_scm(cur_path)
168 yield scm_info[1].split(path)[-1].lstrip('/'), scm_info
168 yield scm_info[1].split(path)[-1].lstrip(os.sep), scm_info
169 169 except VCSError:
170 170 if not recursive:
171 171 continue
172 172 #check if this dir containts other repos for recursive scan
173 173 rec_path = os.path.join(p, dirpath)
174 174 if os.path.isdir(rec_path):
175 175 for inner_scm in _get_repos(rec_path):
176 176 yield inner_scm
177 177
178 178 return _get_repos(path)
179 179
180 180 def check_repo_fast(repo_name, base_path):
181 181 """
182 182 Check given path for existence of directory
183 183 :param repo_name:
184 184 :param base_path:
185 185
186 186 :return False: if this directory is present
187 187 """
188 188 if os.path.isdir(os.path.join(base_path, repo_name)):return False
189 189 return True
190 190
191 191 def check_repo(repo_name, base_path, verify=True):
192 192
193 193 repo_path = os.path.join(base_path, repo_name)
194 194
195 195 try:
196 196 if not check_repo_fast(repo_name, base_path):
197 197 return False
198 198 r = hg.repository(ui.ui(), repo_path)
199 199 if verify:
200 200 hg.verify(r)
201 201 #here we hnow that repo exists it was verified
202 202 log.info('%s repo is already created', repo_name)
203 203 return False
204 204 except RepoError:
205 205 #it means that there is no valid repo there...
206 206 log.info('%s repo is free for creation', repo_name)
207 207 return True
208 208
209 209 def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
210 210 while True:
211 211 ok = raw_input(prompt)
212 212 if ok in ('y', 'ye', 'yes'): return True
213 213 if ok in ('n', 'no', 'nop', 'nope'): return False
214 214 retries = retries - 1
215 215 if retries < 0: raise IOError
216 216 print complaint
217 217
218 218 #propagated from mercurial documentation
219 219 ui_sections = ['alias', 'auth',
220 220 'decode/encode', 'defaults',
221 221 'diff', 'email',
222 222 'extensions', 'format',
223 223 'merge-patterns', 'merge-tools',
224 224 'hooks', 'http_proxy',
225 225 'smtp', 'patch',
226 226 'paths', 'profiling',
227 227 'server', 'trusted',
228 228 'ui', 'web', ]
229 229
230 230 def make_ui(read_from='file', path=None, checkpaths=True):
231 231 """A function that will read python rc files or database
232 232 and make an mercurial ui object from read options
233 233
234 234 :param path: path to mercurial config file
235 235 :param checkpaths: check the path
236 236 :param read_from: read from 'file' or 'db'
237 237 """
238 238
239 239 baseui = ui.ui()
240 240
241 241 #clean the baseui object
242 242 baseui._ocfg = config.config()
243 243 baseui._ucfg = config.config()
244 244 baseui._tcfg = config.config()
245 245
246 246 if read_from == 'file':
247 247 if not os.path.isfile(path):
248 248 log.warning('Unable to read config file %s' % path)
249 249 return False
250 250 log.debug('reading hgrc from %s', path)
251 251 cfg = config.config()
252 252 cfg.read(path)
253 253 for section in ui_sections:
254 254 for k, v in cfg.items(section):
255 255 log.debug('settings ui from file[%s]%s:%s', section, k, v)
256 256 baseui.setconfig(section, k, v)
257 257
258 258
259 259 elif read_from == 'db':
260 260 sa = meta.Session()
261 261 ret = sa.query(RhodeCodeUi)\
262 262 .options(FromCache("sql_cache_short",
263 263 "get_hg_ui_settings")).all()
264 264
265 265 hg_ui = ret
266 266 for ui_ in hg_ui:
267 267 if ui_.ui_active:
268 268 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
269 269 ui_.ui_key, ui_.ui_value)
270 270 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
271 271
272 272 meta.Session.remove()
273 273 return baseui
274 274
275 275
276 276 def set_rhodecode_config(config):
277 277 """Updates pylons config with new settings from database
278 278
279 279 :param config:
280 280 """
281 281 from rhodecode.model.settings import SettingsModel
282 282 hgsettings = SettingsModel().get_app_settings()
283 283
284 284 for k, v in hgsettings.items():
285 285 config[k] = v
286 286
287 287 def invalidate_cache(cache_key, *args):
288 288 """Puts cache invalidation task into db for
289 289 further global cache invalidation
290 290 """
291 291
292 292 from rhodecode.model.scm import ScmModel
293 293
294 294 if cache_key.startswith('get_repo_cached_'):
295 295 name = cache_key.split('get_repo_cached_')[-1]
296 296 ScmModel().mark_for_invalidation(name)
297 297
298 298 class EmptyChangeset(BaseChangeset):
299 299 """
300 300 An dummy empty changeset. It's possible to pass hash when creating
301 301 an EmptyChangeset
302 302 """
303 303
304 304 def __init__(self, cs='0' * 40):
305 305 self._empty_cs = cs
306 306 self.revision = -1
307 307 self.message = ''
308 308 self.author = ''
309 309 self.date = ''
310 310
311 311 @LazyProperty
312 312 def raw_id(self):
313 313 """Returns raw string identifying this changeset, useful for web
314 314 representation.
315 315 """
316 316
317 317 return self._empty_cs
318 318
319 319 @LazyProperty
320 320 def short_id(self):
321 321 return self.raw_id[:12]
322 322
323 323 def get_file_changeset(self, path):
324 324 return self
325 325
326 326 def get_file_content(self, path):
327 327 return u''
328 328
329 329 def get_file_size(self, path):
330 330 return 0
331 331
332 332 def map_groups(groups):
333 333 """Checks for groups existence, and creates groups structures.
334 334 It returns last group in structure
335 335
336 336 :param groups: list of groups structure
337 337 """
338 338 sa = meta.Session()
339 339
340 340 parent = None
341 341 group = None
342 342 for lvl, group_name in enumerate(groups[:-1]):
343 343 group = sa.query(Group).filter(Group.group_name == group_name).scalar()
344 344
345 345 if group is None:
346 346 group = Group(group_name, parent)
347 347 sa.add(group)
348 348 sa.commit()
349 349
350 350 parent = group
351 351
352 352 return group
353 353
354 354 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
355 355 """maps all repos given in initial_repo_list, non existing repositories
356 356 are created, if remove_obsolete is True it also check for db entries
357 357 that are not in initial_repo_list and removes them.
358 358
359 359 :param initial_repo_list: list of repositories found by scanning methods
360 360 :param remove_obsolete: check for obsolete entries in database
361 361 """
362 362
363 363 sa = meta.Session()
364 364 rm = RepoModel()
365 365 user = sa.query(User).filter(User.admin == True).first()
366 366 added = []
367 367 for name, repo in initial_repo_list.items():
368 368 group = map_groups(name.split('/'))
369 369 if not rm.get_by_repo_name(name, cache=False):
370 370 log.info('repository %s not found creating default', name)
371 371 added.append(name)
372 372 form_data = {
373 373 'repo_name':name,
374 374 'repo_type':repo.alias,
375 375 'description':repo.description \
376 376 if repo.description != 'unknown' else \
377 377 '%s repository' % name,
378 378 'private':False,
379 379 'group_id':getattr(group, 'group_id', None)
380 380 }
381 381 rm.create(form_data, user, just_db=True)
382 382
383 383 removed = []
384 384 if remove_obsolete:
385 385 #remove from database those repositories that are not in the filesystem
386 386 for repo in sa.query(Repository).all():
387 387 if repo.repo_name not in initial_repo_list.keys():
388 388 removed.append(repo.repo_name)
389 389 sa.delete(repo)
390 390 sa.commit()
391 391
392 392 return added, removed
393 393 class OrderedDict(dict, DictMixin):
394 394
395 395 def __init__(self, *args, **kwds):
396 396 if len(args) > 1:
397 397 raise TypeError('expected at most 1 arguments, got %d' % len(args))
398 398 try:
399 399 self.__end
400 400 except AttributeError:
401 401 self.clear()
402 402 self.update(*args, **kwds)
403 403
404 404 def clear(self):
405 405 self.__end = end = []
406 406 end += [None, end, end] # sentinel node for doubly linked list
407 407 self.__map = {} # key --> [key, prev, next]
408 408 dict.clear(self)
409 409
410 410 def __setitem__(self, key, value):
411 411 if key not in self:
412 412 end = self.__end
413 413 curr = end[1]
414 414 curr[2] = end[1] = self.__map[key] = [key, curr, end]
415 415 dict.__setitem__(self, key, value)
416 416
417 417 def __delitem__(self, key):
418 418 dict.__delitem__(self, key)
419 419 key, prev, next = self.__map.pop(key)
420 420 prev[2] = next
421 421 next[1] = prev
422 422
423 423 def __iter__(self):
424 424 end = self.__end
425 425 curr = end[2]
426 426 while curr is not end:
427 427 yield curr[0]
428 428 curr = curr[2]
429 429
430 430 def __reversed__(self):
431 431 end = self.__end
432 432 curr = end[1]
433 433 while curr is not end:
434 434 yield curr[0]
435 435 curr = curr[1]
436 436
437 437 def popitem(self, last=True):
438 438 if not self:
439 439 raise KeyError('dictionary is empty')
440 440 if last:
441 441 key = reversed(self).next()
442 442 else:
443 443 key = iter(self).next()
444 444 value = self.pop(key)
445 445 return key, value
446 446
447 447 def __reduce__(self):
448 448 items = [[k, self[k]] for k in self]
449 449 tmp = self.__map, self.__end
450 450 del self.__map, self.__end
451 451 inst_dict = vars(self).copy()
452 452 self.__map, self.__end = tmp
453 453 if inst_dict:
454 454 return (self.__class__, (items,), inst_dict)
455 455 return self.__class__, (items,)
456 456
457 457 def keys(self):
458 458 return list(self)
459 459
460 460 setdefault = DictMixin.setdefault
461 461 update = DictMixin.update
462 462 pop = DictMixin.pop
463 463 values = DictMixin.values
464 464 items = DictMixin.items
465 465 iterkeys = DictMixin.iterkeys
466 466 itervalues = DictMixin.itervalues
467 467 iteritems = DictMixin.iteritems
468 468
469 469 def __repr__(self):
470 470 if not self:
471 471 return '%s()' % (self.__class__.__name__,)
472 472 return '%s(%r)' % (self.__class__.__name__, self.items())
473 473
474 474 def copy(self):
475 475 return self.__class__(self)
476 476
477 477 @classmethod
478 478 def fromkeys(cls, iterable, value=None):
479 479 d = cls()
480 480 for key in iterable:
481 481 d[key] = value
482 482 return d
483 483
484 484 def __eq__(self, other):
485 485 if isinstance(other, OrderedDict):
486 486 return len(self) == len(other) and self.items() == other.items()
487 487 return dict.__eq__(self, other)
488 488
489 489 def __ne__(self, other):
490 490 return not self == other
491 491
492 492
493 493 #set cache regions for beaker so celery can utilise it
494 494 def add_cache(settings):
495 495 cache_settings = {'regions':None}
496 496 for key in settings.keys():
497 497 for prefix in ['beaker.cache.', 'cache.']:
498 498 if key.startswith(prefix):
499 499 name = key.split(prefix)[1].strip()
500 500 cache_settings[name] = settings[key].strip()
501 501 if cache_settings['regions']:
502 502 for region in cache_settings['regions'].split(','):
503 503 region = region.strip()
504 504 region_settings = {}
505 505 for key, value in cache_settings.items():
506 506 if key.startswith(region):
507 507 region_settings[key.split('.')[1]] = value
508 508 region_settings['expire'] = int(region_settings.get('expire',
509 509 60))
510 510 region_settings.setdefault('lock_dir',
511 511 cache_settings.get('lock_dir'))
512 512 region_settings.setdefault('data_dir',
513 513 cache_settings.get('data_dir'))
514 514
515 515 if 'type' not in region_settings:
516 516 region_settings['type'] = cache_settings.get('type',
517 517 'memory')
518 518 beaker.cache.cache_regions[region] = region_settings
519 519
520 520 def get_current_revision():
521 521 """Returns tuple of (number, id) from repository containing this package
522 522 or None if repository could not be found.
523 523 """
524 524
525 525 try:
526 526 from vcs import get_repo
527 527 from vcs.utils.helpers import get_scm
528 528 from vcs.exceptions import RepositoryError, VCSError
529 529 repopath = os.path.join(os.path.dirname(__file__), '..', '..')
530 530 scm = get_scm(repopath)[0]
531 531 repo = get_repo(path=repopath, alias=scm)
532 532 tip = repo.get_changeset()
533 533 return (tip.revision, tip.short_id)
534 534 except (ImportError, RepositoryError, VCSError), err:
535 535 logging.debug("Cannot retrieve rhodecode's revision. Original error "
536 536 "was: %s" % err)
537 537 return None
538 538
539 539 #===============================================================================
540 540 # TEST FUNCTIONS AND CREATORS
541 541 #===============================================================================
542 542 def create_test_index(repo_location, full_index):
543 543 """Makes default test index
544 544 :param repo_location:
545 545 :param full_index:
546 546 """
547 547 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
548 548 from rhodecode.lib.pidlock import DaemonLock, LockHeld
549 549 import shutil
550 550
551 551 index_location = os.path.join(repo_location, 'index')
552 552 if os.path.exists(index_location):
553 553 shutil.rmtree(index_location)
554 554
555 555 try:
556 556 l = DaemonLock()
557 557 WhooshIndexingDaemon(index_location=index_location,
558 558 repo_location=repo_location)\
559 559 .run(full_index=full_index)
560 560 l.release()
561 561 except LockHeld:
562 562 pass
563 563
564 564 def create_test_env(repos_test_path, config):
565 565 """Makes a fresh database and
566 566 install test repository into tmp dir
567 567 """
568 568 from rhodecode.lib.db_manage import DbManage
569 569 from rhodecode.tests import HG_REPO, GIT_REPO, NEW_HG_REPO, NEW_GIT_REPO, \
570 570 HG_FORK, GIT_FORK, TESTS_TMP_PATH
571 571 import tarfile
572 572 import shutil
573 573 from os.path import dirname as dn, join as jn, abspath
574 574
575 575 log = logging.getLogger('TestEnvCreator')
576 576 # create logger
577 577 log.setLevel(logging.DEBUG)
578 578 log.propagate = True
579 579 # create console handler and set level to debug
580 580 ch = logging.StreamHandler()
581 581 ch.setLevel(logging.DEBUG)
582 582
583 583 # create formatter
584 584 formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
585 585
586 586 # add formatter to ch
587 587 ch.setFormatter(formatter)
588 588
589 589 # add ch to logger
590 590 log.addHandler(ch)
591 591
592 592 #PART ONE create db
593 593 dbconf = config['sqlalchemy.db1.url']
594 594 log.debug('making test db %s', dbconf)
595 595
596 596 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
597 597 tests=True)
598 598 dbmanage.create_tables(override=True)
599 599 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
600 600 dbmanage.create_default_user()
601 601 dbmanage.admin_prompt()
602 602 dbmanage.create_permissions()
603 603 dbmanage.populate_default_permissions()
604 604
605 605 #PART TWO make test repo
606 606 log.debug('making test vcs repositories')
607 607
608 608 #remove old one from previos tests
609 609 for r in [HG_REPO, GIT_REPO, NEW_HG_REPO, NEW_GIT_REPO, HG_FORK, GIT_FORK]:
610 610
611 611 if os.path.isdir(jn(TESTS_TMP_PATH, r)):
612 612 log.debug('removing %s', r)
613 613 shutil.rmtree(jn(TESTS_TMP_PATH, r))
614 614
615 615 #CREATE DEFAULT HG REPOSITORY
616 616 cur_dir = dn(dn(abspath(__file__)))
617 617 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz"))
618 618 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
619 619 tar.close()
620 620
621 621
622 622 #==============================================================================
623 623 # PASTER COMMANDS
624 624 #==============================================================================
625 625
626 626 class BasePasterCommand(Command):
627 627 """
628 628 Abstract Base Class for paster commands.
629 629
630 630 The celery commands are somewhat aggressive about loading
631 631 celery.conf, and since our module sets the `CELERY_LOADER`
632 632 environment variable to our loader, we have to bootstrap a bit and
633 633 make sure we've had a chance to load the pylons config off of the
634 634 command line, otherwise everything fails.
635 635 """
636 636 min_args = 1
637 637 min_args_error = "Please provide a paster config file as an argument."
638 638 takes_config_file = 1
639 639 requires_config_file = True
640 640
641 641 def notify_msg(self, msg, log=False):
642 642 """Make a notification to user, additionally if logger is passed
643 643 it logs this action using given logger
644 644
645 645 :param msg: message that will be printed to user
646 646 :param log: logging instance, to use to additionally log this message
647 647
648 648 """
649 649 if log and isinstance(log, logging):
650 650 log(msg)
651 651
652 652
653 653 def run(self, args):
654 654 """
655 655 Overrides Command.run
656 656
657 657 Checks for a config file argument and loads it.
658 658 """
659 659 if len(args) < self.min_args:
660 660 raise BadCommand(
661 661 self.min_args_error % {'min_args': self.min_args,
662 662 'actual_args': len(args)})
663 663
664 664 # Decrement because we're going to lob off the first argument.
665 665 # @@ This is hacky
666 666 self.min_args -= 1
667 667 self.bootstrap_config(args[0])
668 668 self.update_parser()
669 669 return super(BasePasterCommand, self).run(args[1:])
670 670
671 671 def update_parser(self):
672 672 """
673 673 Abstract method. Allows for the class's parser to be updated
674 674 before the superclass's `run` method is called. Necessary to
675 675 allow options/arguments to be passed through to the underlying
676 676 celery command.
677 677 """
678 678 raise NotImplementedError("Abstract Method.")
679 679
680 680 def bootstrap_config(self, conf):
681 681 """
682 682 Loads the pylons configuration.
683 683 """
684 684 from pylons import config as pylonsconfig
685 685
686 686 path_to_ini_file = os.path.realpath(conf)
687 687 conf = paste.deploy.appconfig('config:' + path_to_ini_file)
688 688 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
General Comments 0
You need to be logged in to leave comments. Login now