Coverage for src/pylint_sort_functions/utils/decorators.py: 100%

50 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2025-08-12 16:06 +0200

1"""Decorator analysis and exclusion logic for framework-aware sorting. 

2 

3This module provides functionality to analyze function decorators and determine 

4whether functions should be excluded from sorting requirements based on their 

5decorators (e.g., framework-specific decorators that create ordering dependencies). 

6""" 

7 

8import re 

9 

10from astroid import nodes # type: ignore[import-untyped] 

11 

12 

13def decorator_matches_pattern(decorator_str: str, pattern: str) -> bool: 

14 """Check if a decorator string matches an ignore pattern. 

15 

16 Supports exact matches and simple wildcard patterns. This allows users to 

17 exclude functions with specific decorators from sorting requirements when 

18 the decorators create ordering dependencies. 

19 

20 Examples: 

21 - "@app.route" matches both @app.route and @app.route("/path") 

22 - "@*.command" matches @main.command(), @cli.command(), etc. 

23 

24 :param decorator_str: Decorator string to check (e.g., "@main.command()") 

25 :type decorator_str: str 

26 :param pattern: Pattern to match against (e.g., "@main.command", "@*.command") 

27 :type pattern: str 

28 :returns: True if decorator matches the pattern 

29 :rtype: bool 

30 """ 

31 # Normalize patterns by ensuring they start with @ 

32 if not pattern.startswith("@"): 

33 pattern = f"@{pattern}" 

34 

35 # Exact match 

36 if decorator_str == pattern: 

37 return True 

38 

39 # Remove parentheses for pattern matching (treat @main.command() as @main.command) 

40 decorator_base = decorator_str.rstrip("()") 

41 pattern_base = pattern.rstrip("()") 

42 

43 if decorator_base == pattern_base: 

44 return True 

45 

46 # Simple wildcard support: @*.command matches @main.command, @app.command, etc. 

47 if "*" in pattern_base: 

48 # Convert simple wildcard pattern to regex 

49 # First escape the pattern, then replace escaped wildcards with regex 

50 regex_pattern = re.escape(pattern_base) 

51 regex_pattern = regex_pattern.replace(r"\*", r"[^.]+") 

52 regex_pattern = f"^{regex_pattern}$" 

53 if re.match(regex_pattern, decorator_base): 

54 return True 

55 

56 return False 

57 

58 

59def function_has_excluded_decorator( 

60 func: nodes.FunctionDef, ignore_decorators: list[str] | None 

61) -> bool: 

62 """Check if a function should be excluded from sorting due to its decorators. 

63 

64 Some decorators create dependencies that make alphabetical sorting inappropriate. 

65 For example, Click commands or Flask routes may need specific ordering for proper 

66 framework behavior. 

67 

68 :param func: Function definition node to check 

69 :type func: nodes.FunctionDef 

70 :param ignore_decorators: List of decorator patterns to match against 

71 :type ignore_decorators: list[str] | None 

72 :returns: True if function should be excluded from sorting requirements 

73 :rtype: bool 

74 """ 

75 if not ignore_decorators or not func.decorators: 

76 return False 

77 

78 # Get string representations of all decorators on this function 

79 function_decorators = get_decorator_strings(func) 

80 

81 # Check if any decorator matches any ignore pattern 

82 for decorator_str in function_decorators: 

83 for ignore_pattern in ignore_decorators: 

84 if decorator_matches_pattern(decorator_str, ignore_pattern): 

85 return True 

86 

87 return False 

88 

89 

90def get_decorator_strings(func: nodes.FunctionDef) -> list[str]: 

91 """Extract string representations of all decorators on a function. 

92 

93 :param func: Function definition node 

94 :type func: nodes.FunctionDef 

95 :returns: List of decorator strings (e.g., ["@main.command()", "@app.route()"]) 

96 :rtype: list[str] 

97 """ 

98 if not func.decorators: 

99 return [] 

100 

101 decorator_strings = [] 

102 for decorator in func.decorators.nodes: 

103 decorator_str = _decorator_node_to_string(decorator) 

104 if decorator_str: 

105 decorator_strings.append(f"@{decorator_str}") 

106 

107 return decorator_strings 

108 

109 

110def _decorator_node_to_string(decorator: nodes.NodeNG) -> str: 

111 """Convert a decorator AST node to its string representation. 

112 

113 :param decorator: Decorator AST node 

114 :type decorator: nodes.NodeNG 

115 :returns: String representation of the decorator (without @ prefix) 

116 :rtype: str 

117 """ 

118 if isinstance(decorator, nodes.Name): 

119 # Simple decorator: @decorator_name 

120 return str(decorator.name) 

121 

122 if isinstance(decorator, nodes.Attribute): 

123 # Attribute decorator: @obj.method 

124 if isinstance(decorator.expr, nodes.Name): 

125 return f"{decorator.expr.name}.{decorator.attrname}" 

126 # Handle nested attributes: @obj.nested.method 

127 base = _decorator_node_to_string(decorator.expr) 

128 if base: 

129 return f"{base}.{decorator.attrname}" 

130 

131 if isinstance(decorator, nodes.Call): 

132 # Function call decorator: @decorator() or @obj.method(args) 

133 func_str = _decorator_node_to_string(decorator.func) 

134 if func_str: 

135 return f"{func_str}()" 

136 

137 # Fallback for complex decorators - return empty string to skip 

138 return ""