funcname_cache.py 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. """
  2. This module provides functionality for caching and looking up fully qualified function
  3. and class names from Python source files by line number.
  4. It uses Python's tokenize module to parse source files and tracks function/class
  5. definitions along with their nesting to build fully qualified names (e.g. 'class.method'
  6. or 'module.function'). The results are cached in a two-level dictionary mapping:
  7. filename -> (line_number -> fully_qualified_name)
  8. Example usage:
  9. name = get_funcname("myfile.py", 42) # Returns name of function/class at line 42
  10. clearcache() # Clear the cache if file contents have changed
  11. The parsing is done lazily when a file is first accessed. Invalid Python files or
  12. IO errors are handled gracefully by returning empty cache entries.
  13. """
  14. import tokenize
  15. from typing import Optional
  16. cache: dict[str, dict[int, str]] = {}
  17. def clearcache() -> None:
  18. cache.clear()
  19. def _add_file(filename: str) -> None:
  20. try:
  21. with tokenize.open(filename) as f:
  22. tokens = list(tokenize.generate_tokens(f.readline))
  23. except (OSError, tokenize.TokenError):
  24. cache[filename] = {}
  25. return
  26. # NOTE: undefined behavior if file is not valid Python source,
  27. # since tokenize will have undefined behavior.
  28. result: dict[int, str] = {}
  29. # current full funcname, e.g. xxx.yyy.zzz
  30. cur_name = ""
  31. cur_indent = 0
  32. significant_indents: list[int] = []
  33. for i, token in enumerate(tokens):
  34. if token.type == tokenize.INDENT:
  35. cur_indent += 1
  36. elif token.type == tokenize.DEDENT:
  37. cur_indent -= 1
  38. # possible end of function or class
  39. if significant_indents and cur_indent == significant_indents[-1]:
  40. significant_indents.pop()
  41. # pop the last name
  42. cur_name = cur_name.rpartition(".")[0]
  43. elif (
  44. token.type == tokenize.NAME
  45. and i + 1 < len(tokens)
  46. and tokens[i + 1].type == tokenize.NAME
  47. and (token.string == "class" or token.string == "def")
  48. ):
  49. # name of class/function always follows class/def token
  50. significant_indents.append(cur_indent)
  51. if cur_name:
  52. cur_name += "."
  53. cur_name += tokens[i + 1].string
  54. result[token.start[0]] = cur_name
  55. cache[filename] = result
  56. def get_funcname(filename: str, lineno: int) -> Optional[str]:
  57. if filename not in cache:
  58. _add_file(filename)
  59. return cache[filename].get(lineno, None)