blob: 34662f0edda61f81739e7d62962dc1837e94dc2f [file] [log] [blame]
pam@chromium.orgf46aed92012-03-08 09:18:17 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
dpranke@chromium.org2a009622011-03-01 02:43:31 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
dpranke@chromium.org17cc2442012-10-17 21:12:09 +00005"""A database of OWNERS files.
6
7OWNERS files indicate who is allowed to approve changes in a specific directory
8(or who is allowed to make changes without needing approval of another OWNER).
9Note that all changes must still be reviewed by someone familiar with the code,
10so you may need approval from both an OWNER and a reviewer in many cases.
11
12The syntax of the OWNERS file is, roughly:
13
14lines := (\s* line? \s* "\n")*
15
16line := directive
dpranke@chromium.orgd16e48b2012-12-03 21:53:49 +000017 | "per-file" \s+ glob \s* "=" \s* directive
dpranke@chromium.org17cc2442012-10-17 21:12:09 +000018 | comment
19
20directive := "set noparent"
peter@chromium.org2ce13132015-04-16 16:42:08 +000021 | "file:" glob
dpranke@chromium.org17cc2442012-10-17 21:12:09 +000022 | email_address
23 | "*"
24
25glob := [a-zA-Z0-9_-*?]+
26
27comment := "#" [^"\n"]*
28
29Email addresses must follow the foo@bar.com short form (exact syntax given
30in BASIC_EMAIL_REGEXP, below). Filename globs follow the simple unix
31shell conventions, and relative and absolute paths are not allowed (i.e.,
32globs only refer to the files in the current directory).
33
34If a user's email is one of the email_addresses in the file, the user is
35considered an "OWNER" for all files in the directory.
36
37If the "per-file" directive is used, the line only applies to files in that
38directory that match the filename glob specified.
39
40If the "set noparent" directive used, then only entries in this OWNERS file
41apply to files in this directory; if the "set noparent" directive is not
42used, then entries in OWNERS files in enclosing (upper) directories also
43apply (up until a "set noparent is encountered").
44
45If "per-file glob=set noparent" is used, then global directives are ignored
46for the glob, and only the "per-file" owners are used for files matching that
47glob.
48
peter@chromium.org2ce13132015-04-16 16:42:08 +000049If the "file:" directive is used, the referred to OWNERS file will be parsed and
50considered when determining the valid set of OWNERS. If the filename starts with
51"//" it is relative to the root of the repository, otherwise it is relative to
52the current file
53
dpranke@chromium.org17cc2442012-10-17 21:12:09 +000054Examples for all of these combinations can be found in tests/owners_unittest.py.
55"""
dpranke@chromium.org2a009622011-03-01 02:43:31 +000056
dpranke@chromium.orgfdecfb72011-03-16 23:27:23 +000057import collections
dtu944b6052016-07-14 14:48:21 -070058import fnmatch
dpranke@chromium.orgc591a702012-12-20 20:14:58 +000059import random
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +000060import re
61
62
63# If this is present by itself on a line, this means that everyone can review.
64EVERYONE = '*'
65
66
67# Recognizes 'X@Y' email addresses. Very simplistic.
68BASIC_EMAIL_REGEXP = r'^[\w\-\+\%\.]+\@[\w\-\+\%\.]+$'
dpranke@chromium.org2a009622011-03-01 02:43:31 +000069
dpranke@chromium.org2a009622011-03-01 02:43:31 +000070
dpranke@chromium.org923950f2011-03-17 23:40:00 +000071def _assert_is_collection(obj):
dpranke@chromium.orge6a4ab32011-03-31 01:23:08 +000072 assert not isinstance(obj, basestring)
maruel@chromium.org725f1c32011-04-01 20:24:54 +000073 # Module 'collections' has no 'Iterable' member
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080074 # pylint: disable=no-member
dpranke@chromium.orge6a4ab32011-03-31 01:23:08 +000075 if hasattr(collections, 'Iterable') and hasattr(collections, 'Sized'):
76 assert (isinstance(obj, collections.Iterable) and
77 isinstance(obj, collections.Sized))
dpranke@chromium.org923950f2011-03-17 23:40:00 +000078
79
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000080class SyntaxErrorInOwnersFile(Exception):
dpranke@chromium.org86bbf192011-03-09 21:37:06 +000081 def __init__(self, path, lineno, msg):
82 super(SyntaxErrorInOwnersFile, self).__init__((path, lineno, msg))
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000083 self.path = path
dpranke@chromium.org86bbf192011-03-09 21:37:06 +000084 self.lineno = lineno
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000085 self.msg = msg
86
87 def __str__(self):
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000088 return '%s:%d syntax error: %s' % (self.path, self.lineno, self.msg)
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000089
90
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000091class Database(object):
92 """A database of OWNERS files for a repository.
93
94 This class allows you to find a suggested set of reviewers for a list
95 of changed files, and see if a list of changed files is covered by a
96 list of reviewers."""
97
dtu944b6052016-07-14 14:48:21 -070098 def __init__(self, root, fopen, os_path):
dpranke@chromium.org898a10e2011-03-04 21:54:43 +000099 """Args:
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000100 root: the path to the root of the Repository
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000101 open: function callback to open a text file for reading
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000102 os_path: module/object callback with fields for 'abspath', 'dirname',
mbjorgef2d73522016-07-14 13:28:59 -0700103 'exists', 'join', and 'relpath'
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000104 """
105 self.root = root
106 self.fopen = fopen
107 self.os_path = os_path
108
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000109 # Pick a default email regexp to use; callers can override as desired.
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000110 self.email_regexp = re.compile(BASIC_EMAIL_REGEXP)
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000111
dtu944b6052016-07-14 14:48:21 -0700112 # Mapping of owners to the paths or globs they own.
113 self._owners_to_paths = {EVERYONE: set()}
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000114
115 # Mapping of paths to authorized owners.
dtu944b6052016-07-14 14:48:21 -0700116 self._paths_to_owners = {}
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000117
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000118 # Mapping reviewers to the preceding comment per file in the OWNERS files.
119 self.comments = {}
120
nick7e16cf32016-09-16 16:05:05 -0700121 # Cache of compiled regexes for _fnmatch()
122 self._fnmatch_cache = {}
123
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000124 # Set of paths that stop us from looking above them for owners.
125 # (This is implicitly true for the root directory).
dtu944b6052016-07-14 14:48:21 -0700126 self._stop_looking = set([''])
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000127
peter@chromium.org2ce13132015-04-16 16:42:08 +0000128 # Set of files which have already been read.
129 self.read_files = set()
130
dpranke@chromium.orgdbf8b4e2013-02-28 19:24:16 +0000131 def reviewers_for(self, files, author):
dpranke@chromium.orgfdecfb72011-03-16 23:27:23 +0000132 """Returns a suggested set of reviewers that will cover the files.
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000133
dpranke@chromium.orgdbf8b4e2013-02-28 19:24:16 +0000134 files is a sequence of paths relative to (and under) self.root.
135 If author is nonempty, we ensure it is not included in the set returned
136 in order avoid suggesting the author as a reviewer for their own changes."""
dpranke@chromium.org7eea2592011-03-09 21:35:46 +0000137 self._check_paths(files)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000138 self.load_data_needed_for(files)
dtu944b6052016-07-14 14:48:21 -0700139
dpranke@chromium.orgdbf8b4e2013-02-28 19:24:16 +0000140 suggested_owners = self._covering_set_of_owners_for(files, author)
dpranke@chromium.org9d66f482013-01-18 02:57:11 +0000141 if EVERYONE in suggested_owners:
142 if len(suggested_owners) > 1:
143 suggested_owners.remove(EVERYONE)
144 else:
145 suggested_owners = set(['<anyone>'])
146 return suggested_owners
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000147
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +0000148 def files_not_covered_by(self, files, reviewers):
149 """Returns the files not owned by one of the reviewers.
dpranke@chromium.orgfdecfb72011-03-16 23:27:23 +0000150
151 Args:
152 files is a sequence of paths relative to (and under) self.root.
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000153 reviewers is a sequence of strings matching self.email_regexp.
154 """
dpranke@chromium.org7eea2592011-03-09 21:35:46 +0000155 self._check_paths(files)
156 self._check_reviewers(reviewers)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000157 self.load_data_needed_for(files)
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000158
dtu944b6052016-07-14 14:48:21 -0700159 return set(f for f in files if not self._is_obj_covered_by(f, reviewers))
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000160
dpranke@chromium.org7eea2592011-03-09 21:35:46 +0000161 def _check_paths(self, files):
162 def _is_under(f, pfx):
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000163 return self.os_path.abspath(self.os_path.join(pfx, f)).startswith(pfx)
dpranke@chromium.org923950f2011-03-17 23:40:00 +0000164 _assert_is_collection(files)
dpranke@chromium.orgb54a78e2012-12-13 23:37:23 +0000165 assert all(not self.os_path.isabs(f) and
166 _is_under(f, self.os_path.abspath(self.root)) for f in files)
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000167
dpranke@chromium.org7eea2592011-03-09 21:35:46 +0000168 def _check_reviewers(self, reviewers):
dpranke@chromium.org923950f2011-03-17 23:40:00 +0000169 _assert_is_collection(reviewers)
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000170 assert all(self.email_regexp.match(r) for r in reviewers)
171
dtu944b6052016-07-14 14:48:21 -0700172 def _is_obj_covered_by(self, objname, reviewers):
173 reviewers = list(reviewers) + [EVERYONE]
174 while True:
175 for reviewer in reviewers:
176 for owned_pattern in self._owners_to_paths.get(reviewer, set()):
177 if fnmatch.fnmatch(objname, owned_pattern):
178 return True
179 if self._should_stop_looking(objname):
180 break
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +0000181 objname = self.os_path.dirname(objname)
dtu944b6052016-07-14 14:48:21 -0700182 return False
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000183
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +0000184 def _enclosing_dir_with_owners(self, objname):
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000185 """Returns the innermost enclosing directory that has an OWNERS file."""
dpranke@chromium.org6b1e3ee2013-02-23 00:06:38 +0000186 dirpath = objname
dtu944b6052016-07-14 14:48:21 -0700187 while not self._owners_for(dirpath):
188 if self._should_stop_looking(dirpath):
pam@chromium.orgf46aed92012-03-08 09:18:17 +0000189 break
190 dirpath = self.os_path.dirname(dirpath)
191 return dirpath
192
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000193 def load_data_needed_for(self, files):
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000194 for f in files:
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000195 dirpath = self.os_path.dirname(f)
dtu944b6052016-07-14 14:48:21 -0700196 while not self._owners_for(dirpath):
peter@chromium.org2ce13132015-04-16 16:42:08 +0000197 self._read_owners(self.os_path.join(dirpath, 'OWNERS'))
dtu944b6052016-07-14 14:48:21 -0700198 if self._should_stop_looking(dirpath):
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000199 break
200 dirpath = self.os_path.dirname(dirpath)
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000201
dtu944b6052016-07-14 14:48:21 -0700202 def _should_stop_looking(self, objname):
nick7e16cf32016-09-16 16:05:05 -0700203 return any(self._fnmatch(objname, stop_looking)
dtu944b6052016-07-14 14:48:21 -0700204 for stop_looking in self._stop_looking)
205
206 def _owners_for(self, objname):
207 obj_owners = set()
208 for owned_path, path_owners in self._paths_to_owners.iteritems():
nick7e16cf32016-09-16 16:05:05 -0700209 if self._fnmatch(objname, owned_path):
dtu944b6052016-07-14 14:48:21 -0700210 obj_owners |= path_owners
211 return obj_owners
212
peter@chromium.org2ce13132015-04-16 16:42:08 +0000213 def _read_owners(self, path):
214 owners_path = self.os_path.join(self.root, path)
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000215 if not self.os_path.exists(owners_path):
216 return
peter@chromium.org2ce13132015-04-16 16:42:08 +0000217
218 if owners_path in self.read_files:
219 return
220
221 self.read_files.add(owners_path)
222
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000223 comment = []
peter@chromium.org2ce13132015-04-16 16:42:08 +0000224 dirpath = self.os_path.dirname(path)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000225 in_comment = False
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000226 lineno = 0
227 for line in self.fopen(owners_path):
228 lineno += 1
229 line = line.strip()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000230 if line.startswith('#'):
231 if not in_comment:
232 comment = []
233 comment.append(line[1:].strip())
234 in_comment = True
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000235 continue
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000236 if line == '':
237 continue
238 in_comment = False
239
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000240 if line == 'set noparent':
dtu944b6052016-07-14 14:48:21 -0700241 self._stop_looking.add(dirpath)
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000242 continue
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000243
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000244 m = re.match('per-file (.+)=(.+)', line)
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000245 if m:
dpranke@chromium.orgd16e48b2012-12-03 21:53:49 +0000246 glob_string = m.group(1).strip()
247 directive = m.group(2).strip()
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000248 full_glob_string = self.os_path.join(self.root, dirpath, glob_string)
dpranke@chromium.org9e227d52012-10-20 23:47:42 +0000249 if '/' in glob_string or '\\' in glob_string:
dpranke@chromium.orge3b1c3d2012-10-20 22:28:14 +0000250 raise SyntaxErrorInOwnersFile(owners_path, lineno,
dpranke@chromium.org9e227d52012-10-20 23:47:42 +0000251 'per-file globs cannot span directories or use escapes: "%s"' %
252 line)
dtu944b6052016-07-14 14:48:21 -0700253 relative_glob_string = self.os_path.relpath(full_glob_string, self.root)
254 self._add_entry(relative_glob_string, directive, 'per-file line',
255 owners_path, lineno, '\n'.join(comment))
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000256 continue
257
dpranke@chromium.org86bbf192011-03-09 21:37:06 +0000258 if line.startswith('set '):
259 raise SyntaxErrorInOwnersFile(owners_path, lineno,
260 'unknown option: "%s"' % line[4:].strip())
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000261
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000262 self._add_entry(dirpath, line, 'line', owners_path, lineno,
263 ' '.join(comment))
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000264
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000265 def _add_entry(self, path, directive,
266 line_type, owners_path, lineno, comment):
267 if directive == 'set noparent':
dtu944b6052016-07-14 14:48:21 -0700268 self._stop_looking.add(path)
peter@chromium.org2ce13132015-04-16 16:42:08 +0000269 elif directive.startswith('file:'):
270 owners_file = self._resolve_include(directive[5:], owners_path)
271 if not owners_file:
272 raise SyntaxErrorInOwnersFile(owners_path, lineno,
273 ('%s does not refer to an existing file.' % directive[5:]))
274
275 self._read_owners(owners_file)
276
277 dirpath = self.os_path.dirname(owners_file)
dtu944b6052016-07-14 14:48:21 -0700278 for key in self._owners_to_paths:
279 if not dirpath in self._owners_to_paths[key]:
peter@chromium.org2ce13132015-04-16 16:42:08 +0000280 continue
dtu944b6052016-07-14 14:48:21 -0700281 self._owners_to_paths[key].add(path)
peter@chromium.org2ce13132015-04-16 16:42:08 +0000282
dtu944b6052016-07-14 14:48:21 -0700283 if dirpath in self._paths_to_owners:
284 self._paths_to_owners.setdefault(path, set()).update(
285 self._paths_to_owners[dirpath])
peter@chromium.org2ce13132015-04-16 16:42:08 +0000286
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000287 elif self.email_regexp.match(directive) or directive == EVERYONE:
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000288 self.comments.setdefault(directive, {})
289 self.comments[directive][path] = comment
dtu944b6052016-07-14 14:48:21 -0700290 self._owners_to_paths.setdefault(directive, set()).add(path)
291 self._paths_to_owners.setdefault(path, set()).add(directive)
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000292 else:
dpranke@chromium.org86bbf192011-03-09 21:37:06 +0000293 raise SyntaxErrorInOwnersFile(owners_path, lineno,
peter@chromium.org2ce13132015-04-16 16:42:08 +0000294 ('%s is not a "set" directive, file include, "*", '
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000295 'or an email address: "%s"' % (line_type, directive)))
296
peter@chromium.org2ce13132015-04-16 16:42:08 +0000297 def _resolve_include(self, path, start):
298 if path.startswith('//'):
299 include_path = path[2:]
300 else:
301 assert start.startswith(self.root)
mbjorgef2d73522016-07-14 13:28:59 -0700302 start = self.os_path.dirname(self.os_path.relpath(start, self.root))
peter@chromium.org2ce13132015-04-16 16:42:08 +0000303 include_path = self.os_path.join(start, path)
304
305 owners_path = self.os_path.join(self.root, include_path)
306 if not self.os_path.exists(owners_path):
307 return None
308
309 return include_path
310
dpranke@chromium.orgdbf8b4e2013-02-28 19:24:16 +0000311 def _covering_set_of_owners_for(self, files, author):
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000312 dirs_remaining = set(self._enclosing_dir_with_owners(f) for f in files)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000313 all_possible_owners = self.all_possible_owners(dirs_remaining, author)
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000314 suggested_owners = set()
315 while dirs_remaining:
316 owner = self.lowest_cost_owner(all_possible_owners, dirs_remaining)
317 suggested_owners.add(owner)
318 dirs_to_remove = set(el[0] for el in all_possible_owners[owner])
319 dirs_remaining -= dirs_to_remove
320 return suggested_owners
dpranke@chromium.org5e5d37b2012-12-19 21:04:58 +0000321
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000322 def all_possible_owners(self, dirs, author):
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000323 """Returns a list of (potential owner, distance-from-dir) tuples; a
324 distance of 1 is the lowest/closest possible distance (which makes the
325 subsequent math easier)."""
326 all_possible_owners = {}
zork@chromium.org046e1752012-05-07 05:56:12 +0000327 for current_dir in dirs:
zork@chromium.org046e1752012-05-07 05:56:12 +0000328 dirname = current_dir
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000329 distance = 1
330 while True:
dtu944b6052016-07-14 14:48:21 -0700331 for owner in self._owners_for(dirname):
dpranke@chromium.orgdbf8b4e2013-02-28 19:24:16 +0000332 if author and owner == author:
333 continue
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000334 all_possible_owners.setdefault(owner, [])
335 # If the same person is in multiple OWNERS files above a given
336 # directory, only count the closest one.
337 if not any(current_dir == el[0] for el in all_possible_owners[owner]):
338 all_possible_owners[owner].append((current_dir, distance))
dtu944b6052016-07-14 14:48:21 -0700339 if self._should_stop_looking(dirname):
dpranke@chromium.org6dada4e2011-03-08 22:32:40 +0000340 break
341 dirname = self.os_path.dirname(dirname)
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000342 distance += 1
343 return all_possible_owners
zork@chromium.org046e1752012-05-07 05:56:12 +0000344
nick7e16cf32016-09-16 16:05:05 -0700345 def _fnmatch(self, filename, pattern):
346 """Same as fnmatch.fnmatch(), but interally caches the compiled regexes."""
347 matcher = self._fnmatch_cache.get(pattern)
348 if matcher is None:
349 matcher = re.compile(fnmatch.translate(pattern)).match
350 self._fnmatch_cache[pattern] = matcher
351 return matcher(filename)
352
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000353 @staticmethod
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000354 def total_costs_by_owner(all_possible_owners, dirs):
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000355 # We want to minimize both the number of reviewers and the distance
356 # from the files/dirs needing reviews. The "pow(X, 1.75)" below is
357 # an arbitrarily-selected scaling factor that seems to work well - it
358 # will select one reviewer in the parent directory over three reviewers
359 # in subdirs, but not one reviewer over just two.
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000360 result = {}
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000361 for owner in all_possible_owners:
362 total_distance = 0
363 num_directories_owned = 0
364 for dirname, distance in all_possible_owners[owner]:
365 if dirname in dirs:
366 total_distance += distance
367 num_directories_owned += 1
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000368 if num_directories_owned:
369 result[owner] = (total_distance /
370 pow(num_directories_owned, 1.75))
371 return result
zork@chromium.org046e1752012-05-07 05:56:12 +0000372
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000373 @staticmethod
374 def lowest_cost_owner(all_possible_owners, dirs):
375 total_costs_by_owner = Database.total_costs_by_owner(all_possible_owners,
376 dirs)
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000377 # Return the lowest cost owner. In the case of a tie, pick one randomly.
378 lowest_cost = min(total_costs_by_owner.itervalues())
379 lowest_cost_owners = filter(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +0000380 lambda owner: total_costs_by_owner[owner] == lowest_cost,
381 total_costs_by_owner)
dpranke@chromium.orgc591a702012-12-20 20:14:58 +0000382 return random.Random().choice(lowest_cost_owners)