1# coding: utf-8
2
3"""
4This module provides a Locator class for finding template files.
5
6"""
7
8import os
9import re
10import sys
11
12from pystache.common import TemplateNotFoundError
13from pystache import defaults
14
15
16class Locator(object):
17    def __init__(self, extension=None):
18        """
19        Construct a template locator.
20
21        Arguments:
22
23          extension: the template file extension, without the leading dot.
24            Pass False for no extension (e.g. to use extensionless template
25            files).  Defaults to the package default.
26
27        """
28        if extension is None:
29            extension = defaults.TEMPLATE_EXTENSION
30
31        self.template_extension = extension
32
33    def get_object_directory(self, obj):
34        """
35        Return the directory containing an object's defining class.
36
37        Returns None if there is no such directory, for example if the
38        class was defined in an interactive Python session, or in a
39        doctest that appears in a text file (rather than a Python file).
40
41        """
42        if not hasattr(obj, '__module__'):
43            return None
44
45        module = sys.modules[obj.__module__]
46
47        if not hasattr(module, '__file__'):
48            # TODO: add a unit test for this case.
49            return None
50
51        path = module.__file__
52
53        return os.path.dirname(path)
54
55    def make_template_name(self, obj):
56        """
57        Return the canonical template name for an object instance.
58
59        This method converts Python-style class names (PEP 8's recommended
60        CamelCase, aka CapWords) to lower_case_with_underscords.  Here
61        is an example with code:
62
63        >>> class HelloWorld(object):
64        ...     pass
65        >>> hi = HelloWorld()
66        >>>
67        >>> locator = Locator()
68        >>> locator.make_template_name(hi)
69        'hello_world'
70
71        """
72        template_name = obj.__class__.__name__
73
74        def repl(match):
75            return '_' + match.group(0).lower()
76
77        return re.sub('[A-Z]', repl, template_name)[1:]
78
79    def make_file_name(self, template_name, template_extension=None):
80        """
81        Generate and return the file name for the given template name.
82
83        Arguments:
84
85          template_extension: defaults to the instance's extension.
86
87        """
88        file_name = template_name
89
90        if template_extension is None:
91            template_extension = self.template_extension
92
93        if template_extension is not False:
94            file_name += os.path.extsep + template_extension
95
96        return file_name
97
98    def _find_path(self, search_dirs, file_name):
99        """
100        Search for the given file, and return the path.
101
102        Returns None if the file is not found.
103
104        """
105        for dir_path in search_dirs:
106            file_path = os.path.join(dir_path, file_name)
107            if os.path.exists(file_path):
108                return file_path
109
110        return None
111
112    def _find_path_required(self, search_dirs, file_name):
113        """
114        Return the path to a template with the given file name.
115
116        """
117        path = self._find_path(search_dirs, file_name)
118
119        if path is None:
120            raise TemplateNotFoundError(
121                'File %s not found in dirs: %s' % (repr(file_name), repr(search_dirs))
122            )
123
124        return path
125
126    def find_file(self, file_name, search_dirs):
127        """
128        Return the path to a template with the given file name.
129
130        Arguments:
131
132          file_name: the file name of the template.
133
134          search_dirs: the list of directories in which to search.
135
136        """
137        return self._find_path_required(search_dirs, file_name)
138
139    def find_name(self, template_name, search_dirs):
140        """
141        Return the path to a template with the given name.
142
143        Arguments:
144
145          template_name: the name of the template.
146
147          search_dirs: the list of directories in which to search.
148
149        """
150        file_name = self.make_file_name(template_name)
151
152        return self._find_path_required(search_dirs, file_name)
153
154    def find_object(self, obj, search_dirs, file_name=None):
155        """
156        Return the path to a template associated with the given object.
157
158        """
159        if file_name is None:
160            # TODO: should we define a make_file_name() method?
161            template_name = self.make_template_name(obj)
162            file_name = self.make_file_name(template_name)
163
164        dir_path = self.get_object_directory(obj)
165
166        if dir_path is not None:
167            search_dirs = [dir_path] + search_dirs
168
169        path = self._find_path_required(search_dirs, file_name)
170
171        return path
172