source: graphvizplugin/trunk/graphviz/graphviz.py

Last change on this file was 18525, checked in by Jun Omae, 7 months ago

Make compatible with Trac 1.5.4 and Python 3, and remove Genshi from dependencies (closes #14173)

  • Property svn:keywords set to Id HeadURL LastChangedRevision
File size: 27.3 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (c) 2005, 2006, 2008 Peter Kropf. All rights reserved.
4#
5# Redistribution and use in source and binary forms, with or without
6# modification, are permitted provided that the following conditions
7# are met:
8#
9#  1. Redistributions of source code must retain the above copyright
10#     notice, this list of conditions and the following disclaimer.
11#  2. Redistributions in binary form must reproduce the above copyright
12#     notice, this list of conditions and the following disclaimer in
13#     the documentation and/or other materials provided with the
14#     distribution.
15#  3. The name of the author may not be used to endorse or promote
16#     products derived from this software without specific prior
17#     written permission.
18#
19# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
20# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
23# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
25# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
27# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
28# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
29# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31import inspect
32import os
33import re
34import hashlib
35import subprocess
36import sys
37
38from trac.config import BoolOption, IntOption, Option
39from trac.core import Component, implements
40from trac.mimeview.api import RenderingContext, IHTMLPreviewRenderer, MIME_MAP
41from trac.util.compat import close_fds
42from trac.util.html import Markup, TracHTMLSanitizer, find_element, tag
43from trac.util.translation import _
44from trac.web.api import IRequestHandler
45from trac.wiki.api import IWikiMacroProvider, WikiSystem
46from trac.wiki.formatter import extract_link
47
48
49try:
50    unicode = unicode
51except NameError:
52    unicode = str
53
54
55if hasattr(subprocess.Popen, '__enter__'):
56    Popen = subprocess.Popen
57else:
58    class Popen(subprocess.Popen):
59        """From trac/util/compat.py in 1.4-stable"""
60
61        def __enter__(self):
62            return self
63
64        def __exit__(self, type, value, traceback):
65            if self.stdout:
66                self.stdout.close()
67            if self.stderr:
68                self.stderr.close()
69            try:
70                if self.stdin:
71                    self.stdin.close()
72            finally:
73                self.wait()
74
75
76try:
77    hashlib.sha1(usedforsecurity=False)
78except TypeError:
79    sha1 = hashlib.sha1
80else:
81    sha1 = lambda string: hashlib.sha1(string, usedforsecurity=False)
82
83
84class Graphviz(Component):
85    """
86    The GraphvizPlugin (http://trac-hacks.org/wiki/GraphvizPlugin)
87    provides a plugin for Trac to render graphviz
88    (http://www.graphviz.org/) graph layouts within a Trac wiki page.
89    """
90    implements(IWikiMacroProvider, IHTMLPreviewRenderer, IRequestHandler)
91
92    # Available formats and processors, default first (dot/png)
93    Processors = ['dot', 'neato', 'twopi', 'circo', 'fdp']
94    Bitmap_Formats = ['png', 'jpg', 'gif']
95    Vector_Formats = ['svg', 'svgz']
96    Formats = Bitmap_Formats + Vector_Formats
97    Cmd_Paths = {
98        'linux':    ['/usr/bin',
99                     '/usr/local/bin',],
100
101        'win32':    ['c:\\Program Files\\Graphviz\\bin',
102                     'c:\\Program Files\\ATT\\Graphviz\\bin',
103                     ],
104
105        'freebsd6': ['/usr/local/bin',
106                     ],
107
108        'freebsd5': ['/usr/local/bin',
109                     ],
110
111        'darwin':   ['/opt/local/bin',
112                     '/sw/bin',
113                     '/usr/local/bin'],
114
115        }
116    Cmd_Paths['linux2'] = Cmd_Paths['linux']  # Python 2 on Linux
117
118    # Note: the following options named "..._option" are those which need
119    #       some additional processing, see `_load_config()` below.
120
121    DEFAULT_CACHE_DIR = 'gvcache'
122
123    cache_dir_option = Option("graphviz", "cache_dir", DEFAULT_CACHE_DIR,
124            """The directory that will be used to cache the generated
125            images.  Note that if different than the default (`%s`),
126            this directory must exist.  If not given as an absolute
127            path, the path will be relative to the `files` directory
128            within the Trac environment's directory.
129            """ % DEFAULT_CACHE_DIR)
130
131    encoding = Option("graphviz", "encoding", 'utf-8',
132            """The encoding which should be used for communicating
133            with Graphviz (should match `-Gcharset` if given).
134            """)
135
136    cmd_path = Option("graphviz", "cmd_path", '',
137            r"""Full path to the directory where the graphviz programs
138            are located. If not specified, the default is `/usr/bin`
139            on Linux, `C:\Program Files\ATT\Graphviz\bin` on Windows
140            and `/usr/local/bin` on FreeBSD 6.
141            """)
142
143    out_format = Option("graphviz", "out_format", Formats[0],
144            """Graph output format. Valid formats are: png, jpg, svg,
145            svgz, gif. If not specified, the default is png. This
146            setting can be overrided on a per-graph basis.
147            """)
148
149    processor = Option("graphviz", "processor", Processors[0],
150            """Graphviz default processor. Valid processors are: dot,
151            neato, twopi, fdp, circo. If not specified, the default is
152            dot. This setting can be overrided on a per-graph basis.
153
154            !GraphvizMacro will verify that the default processor is
155            installed and will not work if it is missing. All other
156            processors are optional.  If any of the other processors
157            are missing, a warning message will be sent to the trac
158            log and !GraphvizMacro will continue to work.
159            """)
160
161    png_anti_alias = BoolOption("graphviz", "png_antialias", False,
162            """If this entry exists in the configuration file, then
163            PNG outputs will be antialiased.  Note that this requires
164            `rsvg` to be installed.
165            """)
166
167    rsvg_path_option = Option("graphviz", "rsvg_path", "",
168            """Full path to the rsvg program (including the filename).
169            The default is `<cmd_path>/rsvg`.
170            """)
171
172    cache_manager = BoolOption("graphviz", "cache_manager", False,
173            """If this entry exists and set to true in the
174            configuration file, then the cache management logic will
175            be invoked and the cache_max_size, cache_min_size,
176            cache_max_count and cache_min_count must be defined.
177            """)
178
179    cache_max_size = IntOption("graphviz", "cache_max_size", 1024*1024*10,
180            """The maximum size in bytes that the cache should
181            consume. This is the high watermark for disk space used.
182            """)
183
184    cache_min_size = IntOption("graphviz", "cache_min_size", 1024*1024*5,
185            """When cleaning out the cache, remove files until this
186            size in bytes is used by the cache. This is the low
187            watermark for disk space used.
188            """)
189
190    cache_max_count = IntOption("graphviz", "cache_max_count", 2000,
191            """The maximum number of files that the cache should
192            contain. This is the high watermark for the directory
193            entry count.
194            """)
195
196    cache_min_count = IntOption("graphviz", "cache_min_count", 1500,
197            """The minimum number of files that the cache should
198            contain. This is the low watermark for the directory entry
199            count.
200            """)
201
202    dpi = IntOption('graphviz', 'default_graph_dpi', 96,
203            """Default dpi setting for graphviz, used during SVG to
204            PNG rasterization.
205            """)
206
207
208    sanitizer = None
209
210    def __init__(self):
211        wiki = WikiSystem(self.env)
212        if not wiki.render_unsafe_content:
213            self.sanitizer = TracHTMLSanitizer(wiki.safe_schemes)
214
215
216    # IHTMLPreviewRenderer methods
217
218    MIME_TYPES = ('application/graphviz',)
219
220    def get_quality_ratio(self, mimetype):
221        if mimetype in self.MIME_TYPES:
222            return 2
223        return 0
224
225    def render(self, context, mimetype, content, filename=None, url=None):
226        ext = filename.split('.')[1] if filename else None
227        name = 'graphviz' if not ext or ext == 'graphviz' \
228                else 'graphviz.%s' % ext
229        text = content.read() if hasattr(content, 'read') else content
230        return self.expand_macro(context, name, text)
231
232
233    # IRequestHandler methods
234
235    def match_request(self, req):
236        return req.path_info.startswith('/graphviz')
237
238    def process_request(self, req):
239        # check and load the configuration
240        errmsg = self._load_config()
241        if errmsg:
242            return self._error_div(errmsg)
243
244        pieces = [item for item in req.path_info.split('/graphviz') if item]
245
246        if pieces:
247            pieces = [item for item in pieces[0].split('/') if item]
248
249            if pieces:
250                name = pieces[0]
251                img_path = os.path.join(self.cache_dir, name)
252                return req.send_file(img_path)
253
254    # IWikiMacroProvider methods
255
256    def get_macros(self):
257        """Return an iterable that provides the names of the provided macros.
258        """
259        self._load_config()
260        for p in ['.' + p for p in Graphviz.Processors] + ['']:
261            for f in ['/' + f for f in Graphviz.Formats] + ['']:
262                yield 'graphviz%s%s' % (p, f)
263
264
265    def get_macro_description(self, name):
266        """
267        Return a plain text description of the macro with the
268        specified name. Only return a description for the base
269        graphviz macro. All the other variants (graphviz/png,
270        graphviz/svg, etc.) will have no description. This will
271        cleanup the WikiMacros page a bit.
272        """
273        if name == 'graphviz':
274            return inspect.getdoc(Graphviz)
275        else:
276            return None
277
278
279    def expand_macro(self, formatter_or_context, name, content):
280        """Return the HTML output of the macro.
281
282        :param formatter_or_context: a Formatter when called as a macro,
283               a RenderingContext when called by `GraphvizPlugin.render`
284
285        :param name: Wiki macro command that resulted in this method being
286               called. In this case, it should be 'graphviz', followed
287               (or not) by the processor name, then by an output
288               format, as following: graphviz.<processor>/<format>
289
290               Valid processor names are: dot, neato, twopi, circo,
291               and fdp.  The default is dot.
292
293               Valid output formats are: jpg, png, gif, svg and svgz.
294               The default is the value specified in the out_format
295               configuration parameter. If out_format is not specified
296               in the configuration, then the default is png.
297
298               examples: graphviz.dot/png   -> dot    png
299                         graphviz.neato/jpg -> neato  jpg
300                         graphviz.circo     -> circo  png
301                         graphviz/svg       -> dot    svg
302
303        :param content: The text the user entered for the macro to process.
304        """
305        # check and load the configuration
306        errmsg = self._load_config()
307        if errmsg:
308            return self._error_div(errmsg)
309
310        ## Extract processor and format from name
311        processor = out_format = None
312
313        # first try with the RegExp engine
314        try:
315            m = re.match('graphviz\.?([a-z]*)\/?([a-z]*)', name)
316            (processor, out_format) = m.group(1, 2)
317
318        # or use the string.split method
319        except:
320            (d_sp, s_sp) = (name.split('.'), name.split('/'))
321            if len(d_sp) > 1:
322                s_sp = d_sp[1].split('/')
323                if len(s_sp) > 1:
324                    out_format = s_sp[1]
325                processor = s_sp[0]
326            elif len(s_sp) > 1:
327                out_format = s_sp[1]
328
329        # assign default values, if instance ones are empty
330        if not out_format:
331            out_format = self.out_format
332        if not processor:
333            processor = self.processor
334
335        if processor in Graphviz.Processors:
336            proc_cmd = self.cmds[processor]
337
338        else:
339            self.log.error('render_macro: requested processor (%s) not found.',
340                           processor)
341            return self._error_div('requested processor (%s) not found.' %
342                                   processor)
343
344        if out_format not in Graphviz.Formats:
345            self.log.error('render_macro: requested format (%s) not found.',
346                           out_format)
347            return self._error_div(
348                    tag.p(_("Graphviz macro processor error: "
349                            "requested format (%(fmt)s) not valid.",
350                            fmt=out_format)))
351
352        encoded_content = content.encode(self.encoding)
353        sha_key = self._build_cache_key(processor, self.processor_options,
354                                        content, 'S' if self.sanitizer else '')
355        img_name = '%s.%s.%s' % (sha_key, processor, out_format)
356        # cache: hash.<dot>.<png>
357        img_path = os.path.join(self.cache_dir, img_name)
358        map_name = '%s.%s.map' % (sha_key, processor)
359        # cache: hash.<dot>.map
360        map_path = os.path.join(self.cache_dir, map_name)
361
362        # Check for URL="" presence in graph code
363        URL_in_graph = 'URL=' in content or 'href=' in content
364
365        # Create image if not in cache
366        if not os.path.exists(img_path):
367            self._clean_cache()
368
369            if self.sanitizer:
370                content = self._sanitize_html_labels(content)
371
372            if URL_in_graph: # translate wiki TracLinks in URL
373                if isinstance(formatter_or_context, RenderingContext):
374                    context = formatter_or_context
375                else:
376                    context = formatter_or_context.context
377                content = self._expand_wiki_links(context, out_format, content)
378                encoded_content = content.encode(self.encoding)
379
380            # Antialias PNGs with rsvg, if requested
381            if out_format == 'png' and self.png_anti_alias == True:
382                # 1. SVG output
383                failure, errmsg = self._launch(
384                        encoded_content, proc_cmd, '-Tsvg',
385                        '-o%s.svg' % img_path, *self.processor_options)
386                if failure:
387                    return self._error_div(errmsg)
388
389                # 2. SVG to PNG rasterization
390                failure, errmsg = self._launch(
391                        None, self.rsvg_path, '--dpi-x=%d' % self.dpi,
392                        '--dpi-y=%d' % self.dpi, '%s.svg' % img_path, img_path)
393                if failure:
394                    return self._error_div(errmsg)
395
396            else: # Render other image formats
397                failure, errmsg = self._launch(
398                        encoded_content, proc_cmd, '-T%s' % out_format,
399                        '-o%s' % img_path, *self.processor_options)
400                if failure:
401                    return self._error_div(errmsg)
402
403            # Generate a map file for binary formats
404            if URL_in_graph and out_format in Graphviz.Bitmap_Formats:
405
406                # Create the map if not in cache
407                if not os.path.exists(map_path):
408                    failure, errmsg = self._launch(
409                            encoded_content, proc_cmd, '-Tcmap',
410                            '-o%s' % map_path, *self.processor_options)
411                    if failure:
412                        return self._error_div(errmsg)
413
414        if errmsg:
415            # there was a warning. Ideally we should be able to use
416            # `add_warning` here, but that's not possible as the warnings
417            # are already emitted at this point in the template processing
418            return self._error_div(errmsg)
419
420        # Generate HTML output
421        img_url = formatter_or_context.href.graphviz(img_name)
422        # for SVG(z)
423        if out_format in Graphviz.Vector_Formats:
424            try: # try to get SVG dimensions
425                f = open(img_path, 'r')
426                svg = f.readlines(1024) # don't read all
427                f.close()
428                svg = "".join(svg).replace('\n', '')
429                w = re.search('width="([0-9]+)(.*?)" ', svg)
430                h = re.search('height="([0-9]+)(.*?)"', svg)
431                (w_val, w_unit) = w.group(1,2)
432                (h_val, h_unit) = h.group(1,2)
433                # Graphviz seems to underestimate height/width for SVG images,
434                # so we have to adjust them.
435                # The correction factor seems to be constant.
436                w_val, h_val = [1.35 * float(x) for x in (w_val, h_val)]
437                width = unicode(w_val) + w_unit
438                height = unicode(h_val) + h_unit
439            except ValueError:
440                width = height = '100%'
441
442            # insert SVG, IE compatibility
443            return tag.object(
444                    tag.embed(src=img_url, type="image/svg+xml",
445                              width=width, height=height),
446                    data=img_url, type="image/svg+xml",
447                    width=width, height=height)
448
449        # for binary formats, add map
450        elif URL_in_graph and os.path.exists(map_path):
451            f = open(map_path, 'r')
452            map = f.readlines()
453            f.close()
454            map = "".join(map).replace('\n', '')
455            return tag(tag.map(Markup(unicode(map)),
456                               id='G' + sha_key, name='G' + sha_key),
457                       tag.img(src=img_url, usemap="#G" + sha_key,
458                               alt=_("GraphViz image")))
459        else:
460            return tag.img(src=img_url, alt=_("GraphViz image"))
461
462
463    # Private methods
464
465    def _expand_wiki_links(self, context, out_format, content):
466        """Expand TracLinks that follow all URL= patterns."""
467        def expand(match):
468            attrib, wiki_text = match.groups() # "URL" or "href", "TracLink"
469            link = extract_link(self.env, context, wiki_text)
470            link = find_element(link, 'href')
471            if link:
472                href = link.attrib.get('href')
473                description = link.attrib.get('title', '')
474            else:
475                href = wiki_text
476                description = None
477                if self.sanitizer:
478                    href = ''
479            attribs = []
480            if href:
481                if out_format == 'svg':
482                    format = '="javascript:window.parent.location.href=\'%s\'"'
483                else:
484                    format = '="%s"'
485                attribs.append(attrib + format % href)
486            if description:
487                attribs.append('tooltip="%s"' % (description.replace('"', '')
488                                                 .replace('\n', '')))
489            return '\n'.join(attribs)
490        return re.sub(r'(URL|href)="(.*?)"', expand, content)
491
492    def _sanitize_html_labels(self, content):
493        def sanitize(match):
494            html = match.group(1)
495            sanitized = self.sanitizer.sanitize(html)
496            return "label=<%s>" % sanitized
497        return re.sub(r'label=<(.*)>', sanitize, content)
498
499    def _load_config(self):
500        """Preprocess the graphviz trac.ini configuration."""
501
502        # if 'graphviz' not in self.config.sections():
503        # ... so what? the defaults might be good enough
504
505        # check for the cache_dir entry
506        self.cache_dir = self.cache_dir_option
507        if not self.cache_dir:
508            return _("The [graphviz] section is missing the cache_dir field.")
509
510        if not os.path.isabs(self.cache_dir):
511            self.cache_dir = os.path.join(self.env.path, 'files',
512                                          self.cache_dir)
513        if not os.path.exists(self.cache_dir):
514            if self.cache_dir_option == self.DEFAULT_CACHE_DIR:
515                os.makedirs(self.cache_dir)
516            else:
517                return _("The cache_dir '%(path)s' doesn't exist, "
518                         "please create it.", path=self.cache_dir)
519
520        # Get optional configuration parameters from trac.ini.
521
522        # check for the cmd_path entry and setup the various command paths
523        cmd_paths = Graphviz.Cmd_Paths.get(sys.platform, [])
524
525        if self.cmd_path:
526            if not os.path.exists(self.cmd_path):
527                return _("The '[graphviz] cmd_path' configuration entry "
528                         "is set to '%(path)s' but that path does not exist.",
529                         path=self.cmd_path)
530            cmd_paths = [self.cmd_path]
531
532        if not cmd_paths:
533            return _("The '[graphviz] cmd_path' configuration entry "
534                     "is not set and there is no default for %(platform)s.",
535                     platform=sys.platform)
536
537        self.cmds = {}
538        pname = self._find_cmd(self.processor, cmd_paths)
539        if not pname:
540            return _("The default processor '%(proc)s' was not found "
541                     "in '%(paths)s'.", proc=self.processor, paths=cmd_paths)
542
543        for name in Graphviz.Processors:
544            pname = self._find_cmd(name, cmd_paths)
545
546            if not pname:
547                self.log.info('The %s program was not found. '
548                              'The graphviz/%s macro will be disabled.',
549                              pname, name)
550                Graphviz.Processors.remove(name)
551
552            self.cmds[name] = pname
553
554        if self.png_anti_alias:
555            self.rsvg_path = (self.rsvg_path_option or
556                              self._find_cmd('rsvg', cmd_paths))
557            if not (self.rsvg_path and os.path.exists(self.rsvg_path)):
558                return _("The rsvg program is set to '%(path)s' but that path "
559                         "does not exist.", path=self.rsvg_path)
560
561        # get default graph/node/edge attributes
562        self.processor_options = []
563        defaults = [opt for opt in self.config.options('graphviz')
564                    if opt[0].startswith('default_')]
565        for name, value in defaults:
566            for prefix, optkey in [
567                    ('default_graph_', '-G'),
568                    ('default_node_', '-N'),
569                    ('default_edge_', '-E')]:
570                if name.startswith(prefix):
571                    self.processor_options.append("%s%s=%s" %
572                            (optkey, name.replace(prefix,''), value))
573
574        # setup mimetypes to support the IHTMLPreviewRenderer interface
575        if 'graphviz' not in MIME_MAP:
576            MIME_MAP['graphviz'] = 'application/graphviz'
577        for processor in Graphviz.Processors:
578            if processor not in MIME_MAP:
579                MIME_MAP[processor] = 'application/graphviz'
580
581    def _launch(self, encoded_input, *args):
582        """Launch a process (cmd), and returns exitcode, stdout + stderr"""
583        # Note: subprocess.Popen doesn't support unicode options arguments
584        # (http://bugs.python.org/issue1759845) so we have to encode them.
585        # Anyway, dot expects utf-8 or the encoding specified with -Gcharset.
586        encoded_cmd = self._launch_args(args)
587        with Popen(encoded_cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
588                   stderr=subprocess.PIPE, close_fds=close_fds) as p:
589            out, err = p.communicate(input=encoded_input)
590            failure = p.wait() != 0
591        if failure or err or out:
592            return (failure, tag.p(tag.br(), _("The command:"),
593                         tag.pre(repr(' '.join(encoded_cmd))),
594                         (_("succeeded but emitted the following output:"),
595                          _("failed with the following output:"))[failure],
596                         out and tag.pre(repr(out)),
597                         err and tag.pre(repr(err))))
598        else:
599            return (False, None)
600
601    if sys.version_info[0] != 2:
602        def _launch_args(self, args):
603            return tuple(args)
604    else:
605        def _launch_args(self, args):
606            encoding = sys.getfilesystemencoding()
607            return tuple(arg.encode(encoding, 'replace') for arg in args)
608
609    def _error_div(self, msg):
610        """Display msg in an error box, using Trac style."""
611        self.log.error(msg)
612        msg = tag.pre(msg)
613        return tag.div(
614                tag.strong(_("Graphviz macro processor has detected an error. "
615                             "Please fix the problem before continuing.")),
616                msg, class_="system-message")
617
618    def _build_cache_key(self, *args):
619        vals = []
620        for arg in args:
621            if isinstance(arg, (list, tuple)):
622                vals.extend(arg)
623            else:
624                vals.append(arg)
625        vals = [val.encode('utf-8') if isinstance(val, unicode) else val
626                for val in vals]
627        key = sha1(b'\0'.join(vals)).hexdigest()
628        if isinstance(key, bytes):
629            key = key.decode('ascii')
630        return key
631
632    def _clean_cache(self):
633        """
634        The cache manager (clean_cache) is an attempt at keeping the
635        cache directory under control. When the cache manager
636        determines that it should clean up the cache, it will delete
637        files based on the file access time. The files that were least
638        accessed will be deleted first.
639
640        The graphviz section of the trac configuration file should
641        have an entry called cache_manager to enable the cache
642        cleaning code. If it does, then the cache_max_size,
643        cache_min_size, cache_max_count and cache_min_count entries
644        must also be there.
645        """
646
647        if self.cache_manager:
648
649            # os.stat gives back a tuple with: st_mode(0), st_ino(1),
650            # st_dev(2), st_nlink(3), st_uid(4), st_gid(5),
651            # st_size(6), st_atime(7), st_mtime(8), st_ctime(9)
652
653            entry_list = {}
654            atime_list = {}
655            size_list = {}
656            count = 0
657            size = 0
658
659            for name in os.listdir(self.cache_dir):
660                #self.log.debug('clean_cache.entry: %s', name)
661                entry_list[name] = os.stat(os.path.join(self.cache_dir, name))
662
663                atime_list.setdefault(entry_list[name][7], []).append(name)
664                count = count + 1
665
666                size_list.setdefault(entry_list[name][6], []).append(name)
667                size = size + entry_list[name][6]
668
669            atime_keys = sorted(atime_list)
670
671            #self.log.debug('clean_cache.atime_keys: %s', atime_keys)
672            #self.log.debug('clean_cache.count: %d', count)
673            #self.log.debug('clean_cache.size: %d', size)
674
675            # In the spirit of keeping the code fairly simple, the
676            # clearing out of files from the cache directory may
677            # result in the count dropping below cache_min_count if
678            # multiple entries are have the same last access
679            # time. Same for cache_min_size.
680            if count > self.cache_max_count or size > self.cache_max_size:
681                while atime_keys and (self.cache_min_count < count or
682                                      self.cache_min_size < size):
683                    key = atime_keys.pop(0)
684                    for file in atime_list[key]:
685                        os.unlink(os.path.join(self.cache_dir, file))
686                        count = count - 1
687                        size = size - entry_list[file][6]
688
689    def _find_cmd(self, cmd, paths):
690        exe_suffix = ''
691        if sys.platform == 'win32':
692            exe_suffix = '.exe'
693
694        for path in paths:
695            p = os.path.join(path, cmd) + exe_suffix
696            if os.path.exists(p):
697                return p
Note: See TracBrowser for help on using the repository browser.