22 |
22 |
23 def set_relative_directory(): |
23 def set_relative_directory(): |
24 """Set the directory that `relative_filename` will be relative to.""" |
24 """Set the directory that `relative_filename` will be relative to.""" |
25 global RELATIVE_DIR, CANONICAL_FILENAME_CACHE |
25 global RELATIVE_DIR, CANONICAL_FILENAME_CACHE |
26 |
26 |
|
27 # The current directory |
|
28 abs_curdir = abs_file(os.curdir) |
|
29 if not abs_curdir.endswith(os.sep): |
|
30 # Suffix with separator only if not at the system root |
|
31 abs_curdir = abs_curdir + os.sep |
|
32 |
27 # The absolute path to our current directory. |
33 # The absolute path to our current directory. |
28 RELATIVE_DIR = os.path.normcase(abs_file(os.curdir) + os.sep) |
34 RELATIVE_DIR = os.path.normcase(abs_curdir) |
29 |
35 |
30 # Cache of results of calling the canonical_filename() method, to |
36 # Cache of results of calling the canonical_filename() method, to |
31 # avoid duplicating work. |
37 # avoid duplicating work. |
32 CANONICAL_FILENAME_CACHE = {} |
38 CANONICAL_FILENAME_CACHE = {} |
33 |
39 |
318 |
324 |
319 A `PathAliases` object tracks a list of pattern/result pairs, and can |
325 A `PathAliases` object tracks a list of pattern/result pairs, and can |
320 map a path through those aliases to produce a unified path. |
326 map a path through those aliases to produce a unified path. |
321 |
327 |
322 """ |
328 """ |
323 def __init__(self, relative=False): |
329 def __init__(self, debugfn=None, relative=False): |
324 self.aliases = [] |
330 self.aliases = [] # A list of (original_pattern, regex, result) |
|
331 self.debugfn = debugfn or (lambda msg: 0) |
325 self.relative = relative |
332 self.relative = relative |
326 |
333 self.pprinted = False |
327 def pprint(self): # pragma: debugging |
334 |
|
335 def pprint(self): |
328 """Dump the important parts of the PathAliases, for debugging.""" |
336 """Dump the important parts of the PathAliases, for debugging.""" |
329 print(f"Aliases (relative={self.relative}):") |
337 self.debugfn(f"Aliases (relative={self.relative}):") |
330 for regex, result in self.aliases: |
338 for original_pattern, regex, result in self.aliases: |
331 print(f"{regex.pattern!r} --> {result!r}") |
339 self.debugfn(f" Rule: {original_pattern!r} -> {result!r} using regex {regex.pattern!r}") |
332 |
340 |
333 def add(self, pattern, result): |
341 def add(self, pattern, result): |
334 """Add the `pattern`/`result` pair to the list of aliases. |
342 """Add the `pattern`/`result` pair to the list of aliases. |
335 |
343 |
336 `pattern` is an `fnmatch`-style pattern. `result` is a simple |
344 `pattern` is an `fnmatch`-style pattern. `result` is a simple |
341 |
349 |
342 `pattern` can't end with a wildcard component, since that would |
350 `pattern` can't end with a wildcard component, since that would |
343 match an entire tree, and not just its root. |
351 match an entire tree, and not just its root. |
344 |
352 |
345 """ |
353 """ |
|
354 original_pattern = pattern |
346 pattern_sep = sep(pattern) |
355 pattern_sep = sep(pattern) |
347 |
356 |
348 if len(pattern) > 1: |
357 if len(pattern) > 1: |
349 pattern = pattern.rstrip(r"\/") |
358 pattern = pattern.rstrip(r"\/") |
350 |
359 |
352 if pattern.endswith("*"): |
361 if pattern.endswith("*"): |
353 raise ConfigError("Pattern must not end with wildcards.") |
362 raise ConfigError("Pattern must not end with wildcards.") |
354 |
363 |
355 # The pattern is meant to match a filepath. Let's make it absolute |
364 # The pattern is meant to match a filepath. Let's make it absolute |
356 # unless it already is, or is meant to match any prefix. |
365 # unless it already is, or is meant to match any prefix. |
357 if not pattern.startswith('*') and not isabs_anywhere(pattern + |
366 if not pattern.startswith('*') and not isabs_anywhere(pattern + pattern_sep): |
358 pattern_sep): |
|
359 pattern = abs_file(pattern) |
367 pattern = abs_file(pattern) |
360 if not pattern.endswith(pattern_sep): |
368 if not pattern.endswith(pattern_sep): |
361 pattern += pattern_sep |
369 pattern += pattern_sep |
362 |
370 |
363 # Make a regex from the pattern. |
371 # Make a regex from the pattern. |
364 regex = fnmatches_to_regex([pattern], case_insensitive=True, partial=True) |
372 regex = fnmatches_to_regex([pattern], case_insensitive=True, partial=True) |
365 |
373 |
366 # Normalize the result: it must end with a path separator. |
374 # Normalize the result: it must end with a path separator. |
367 result_sep = sep(result) |
375 result_sep = sep(result) |
368 result = result.rstrip(r"\/") + result_sep |
376 result = result.rstrip(r"\/") + result_sep |
369 self.aliases.append((regex, result)) |
377 self.aliases.append((original_pattern, regex, result)) |
370 |
378 |
371 def map(self, path): |
379 def map(self, path): |
372 """Map `path` through the aliases. |
380 """Map `path` through the aliases. |
373 |
381 |
374 `path` is checked against all of the patterns. The first pattern to |
382 `path` is checked against all of the patterns. The first pattern to |
382 Returns the mapped path. If a mapping has happened, this is a |
390 Returns the mapped path. If a mapping has happened, this is a |
383 canonical path. If no mapping has happened, it is the original value |
391 canonical path. If no mapping has happened, it is the original value |
384 of `path` unchanged. |
392 of `path` unchanged. |
385 |
393 |
386 """ |
394 """ |
387 for regex, result in self.aliases: |
395 if not self.pprinted: |
|
396 self.pprint() |
|
397 self.pprinted = True |
|
398 |
|
399 for original_pattern, regex, result in self.aliases: |
388 m = regex.match(path) |
400 m = regex.match(path) |
389 if m: |
401 if m: |
390 new = path.replace(m[0], result) |
402 new = path.replace(m[0], result) |
391 new = new.replace(sep(path), sep(result)) |
403 new = new.replace(sep(path), sep(result)) |
392 if not self.relative: |
404 if not self.relative: |
393 new = canonical_filename(new) |
405 new = canonical_filename(new) |
|
406 self.debugfn( |
|
407 f"Matched path {path!r} to rule {original_pattern!r} -> {result!r}, " + |
|
408 f"producing {new!r}" |
|
409 ) |
394 return new |
410 return new |
|
411 self.debugfn(f"No rules match, path {path!r} is unchanged") |
395 return path |
412 return path |
396 |
413 |
397 |
414 |
398 def find_python_files(dirname): |
415 def find_python_files(dirname): |
399 """Yield all of the importable Python files in `dirname`, recursively. |
416 """Yield all of the importable Python files in `dirname`, recursively. |