modgud 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
modgud/__init__.py ADDED
@@ -0,0 +1,28 @@
1
+ """Modgud - Modern Guard Clauses for Python.
2
+
3
+ A library for implementing guard clause decorators with single return point architecture.
4
+
5
+ Primary API:
6
+ - guarded_expression: Unified decorator combining guards + implicit return
7
+ - CommonGuards: Pre-built guard validators
8
+ """
9
+
10
+ from .guarded_expression import CommonGuards, guarded_expression
11
+ from .shared.errors import (
12
+ ExplicitReturnDisallowedError,
13
+ GuardClauseError,
14
+ ImplicitReturnError,
15
+ MissingImplicitReturnError,
16
+ UnsupportedConstructError,
17
+ )
18
+
19
+ __version__ = '0.2.0'
20
+ __all__ = [
21
+ 'guarded_expression',
22
+ 'CommonGuards',
23
+ 'GuardClauseError',
24
+ 'ImplicitReturnError',
25
+ 'ExplicitReturnDisallowedError',
26
+ 'MissingImplicitReturnError',
27
+ 'UnsupportedConstructError',
28
+ ]
@@ -0,0 +1,13 @@
1
+ """Guarded Expression - Unified decorator combining guard clauses with implicit return.
2
+
3
+ This package provides the primary interface for the modgud library, unifying
4
+ guard clause validation and implicit return transformation into a single decorator.
5
+ """
6
+
7
+ from .common_guards import CommonGuards
8
+ from .decorator import guarded_expression
9
+
10
+ __all__ = [
11
+ 'guarded_expression',
12
+ 'CommonGuards',
13
+ ]
@@ -0,0 +1,230 @@
1
+ """Pure AST transformation logic for implicit return functionality.
2
+
3
+ Extracts the AST rewriting logic from implicit_return into a composable,
4
+ testable pure function that transforms function nodes to enforce implicit
5
+ return semantics.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import ast
11
+ from typing import List, Optional, Tuple
12
+
13
+ from ..shared.errors import (
14
+ ExplicitReturnDisallowedError,
15
+ MissingImplicitReturnError,
16
+ UnsupportedConstructError,
17
+ )
18
+
19
+
20
+ class _NoExplicitReturnChecker(ast.NodeVisitor):
21
+
22
+ """Check for explicit return statements in top-level function body.
23
+
24
+ Ensures no explicit `return` appears in the *top-level* body of the decorated
25
+ function. We deliberately do NOT descend into nested function/async def/lambda
26
+ bodies so those can use normal Python semantics independently.
27
+ """
28
+
29
+ def __init__(self) -> None:
30
+ self.found: Optional[Tuple[int, int]] = None # (lineno, col)
31
+
32
+ def visit_Return(self, node: ast.Return) -> None: # type: ignore[override]
33
+ # If we are called, it means we're at top-level (we never recurse into nested defs)
34
+ self.found = (getattr(node, 'lineno', 0), getattr(node, 'col_offset', 0))
35
+
36
+ # Block traversal into nested defs/lambdas
37
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> None: # type: ignore[override]
38
+ return
39
+
40
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: # type: ignore[override]
41
+ return
42
+
43
+ def visit_Lambda(self, node: ast.Lambda) -> None: # type: ignore[override]
44
+ return
45
+
46
+
47
+ class _TailRewriter:
48
+
49
+ """Rewrite tail positions to assign to implicit result variable.
50
+
51
+ Rewrites *tail positions* (the final statement of a block that determines the
52
+ branch's return value) by replacing a final expression with an assignment to a
53
+ hidden result variable. After transforming the top-level body, we append a single
54
+ `return __implicit_result` to the function.
55
+
56
+ Supported tail forms:
57
+ - Expr -> assign to result
58
+ - If -> both body and orelse must set result via their own tails
59
+ - Try -> body and each except must set result; else (if present) also sets it
60
+ - Match -> each case body must set result via its tail
61
+ """
62
+
63
+ def __init__(self, result_name: str) -> None:
64
+ self.result_name = result_name
65
+
66
+ def _assign(self, value: ast.expr) -> ast.Assign:
67
+ return ast.Assign(targets=[ast.Name(id=self.result_name, ctx=ast.Store())], value=value)
68
+
69
+ def rewrite_tail_stmt(self, stmt: ast.stmt) -> List[ast.stmt]:
70
+ """Rewrite tail statement to assign to result variable.
71
+
72
+ Return a list of statements that replace the given tail statement,
73
+ ensuring the result variable is set on all runtime paths.
74
+ """
75
+ if isinstance(stmt, ast.Expr):
76
+ return [self._assign(stmt.value)]
77
+
78
+ if isinstance(stmt, ast.If):
79
+ if not stmt.orelse:
80
+ raise MissingImplicitReturnError(
81
+ 'If without else at tail position must have an else clause.',
82
+ getattr(stmt, 'lineno', None),
83
+ getattr(stmt, 'col_offset', None),
84
+ )
85
+ stmt.body = self.rewrite_block(stmt.body)
86
+ stmt.orelse = self.rewrite_block(stmt.orelse)
87
+ return [stmt]
88
+
89
+ if isinstance(stmt, ast.Try):
90
+ # Body must produce a value
91
+ stmt.body = self.rewrite_block(stmt.body)
92
+ # Each except must produce a value
93
+ for h in stmt.handlers:
94
+ h.body = self.rewrite_block(h.body)
95
+ # Else (if present) also produces a value (it runs on success)
96
+ if stmt.orelse:
97
+ stmt.orelse = self.rewrite_block(stmt.orelse)
98
+ # finally is allowed, but it shouldn't need to assign result; it runs after
99
+ # the above assignments. We leave it unchanged.
100
+ return [stmt]
101
+
102
+ if isinstance(stmt, ast.Match):
103
+ # All cases must set the result
104
+ for case in stmt.cases:
105
+ if not case.body:
106
+ raise MissingImplicitReturnError(
107
+ 'Empty match case body cannot yield a value.',
108
+ getattr(stmt, 'lineno', None),
109
+ getattr(stmt, 'col_offset', None),
110
+ )
111
+ case.body = self.rewrite_block(case.body)
112
+ return [stmt]
113
+
114
+ if isinstance(stmt, ast.Pass):
115
+ raise MissingImplicitReturnError(
116
+ 'Pass statement cannot yield a value.',
117
+ getattr(stmt, 'lineno', None),
118
+ getattr(stmt, 'col_offset', None),
119
+ )
120
+
121
+ raise UnsupportedConstructError(
122
+ f'Unsupported tail construct: {type(stmt).__name__}',
123
+ getattr(stmt, 'lineno', None),
124
+ getattr(stmt, 'col_offset', None),
125
+ )
126
+
127
+ def rewrite_block(self, body: List[ast.stmt]) -> List[ast.stmt]:
128
+ if not body:
129
+ raise MissingImplicitReturnError('Empty block where a value is required.')
130
+ *init, last = body
131
+ new_last = self.rewrite_tail_stmt(last)
132
+ return [*init, *new_last]
133
+
134
+
135
+ def transform_function_ast(fn_node: ast.AST, func_name: str) -> ast.AST:
136
+ """Transform function AST to enforce implicit return semantics.
137
+
138
+ Pure function that transforms a FunctionDef/AsyncFunctionDef AST node to enforce
139
+ implicit return semantics.
140
+
141
+ Steps:
142
+ 1. Verify no explicit `return` at top-level
143
+ 2. Rewrite tail of the function body to assign to a hidden result var
144
+ 3. Append a single `return __implicit_result`
145
+
146
+ Args:
147
+ fn_node: The function AST node to transform
148
+ func_name: Name of the function (for error messages)
149
+
150
+ Returns:
151
+ The transformed AST node with implicit return semantics
152
+
153
+ Raises:
154
+ ExplicitReturnDisallowedError: If explicit return found at top level
155
+ MissingImplicitReturnError: If a block cannot yield a value
156
+ UnsupportedConstructError: If an unsupported construct is found
157
+
158
+ """
159
+ assert isinstance(fn_node, (ast.FunctionDef, ast.AsyncFunctionDef))
160
+ body = fn_node.body
161
+
162
+ # Check for explicit return at top-level
163
+ checker = _NoExplicitReturnChecker()
164
+ # Visit only top-level statements
165
+ for stmt in body:
166
+ checker.visit(stmt)
167
+ if checker.found is not None:
168
+ line, col = checker.found
169
+ raise ExplicitReturnDisallowedError(
170
+ f"Explicit `return` is disallowed in '@guarded_expression' function '{func_name}' with implicit_return=True.",
171
+ line,
172
+ col,
173
+ )
174
+
175
+ result_name = '__implicit_result'
176
+ rewriter = _TailRewriter(result_name)
177
+ new_body = rewriter.rewrite_block(body)
178
+ # Append the single return
179
+ new_body.append(ast.Return(value=ast.Name(id=result_name, ctx=ast.Load())))
180
+ fn_node.body = new_body
181
+ return fn_node
182
+
183
+
184
+ class _TopLevelTransformer(ast.NodeTransformer):
185
+
186
+ """Transform only the target function definition.
187
+
188
+ Applies transformation only to the *decorated* function definition that we parsed.
189
+ We rely on inspect.getsource(func) returning just that function (common in modules).
190
+ Strips all decorators to prevent re-application during exec.
191
+ """
192
+
193
+ def __init__(self, target_name: str) -> None:
194
+ self.target_name = target_name
195
+ super().__init__()
196
+
197
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.AST: # type: ignore[override]
198
+ if node.name == self.target_name:
199
+ node.decorator_list = []
200
+ return transform_function_ast(node, node.name)
201
+ return node
202
+
203
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> ast.AST: # type: ignore[override]
204
+ if node.name == self.target_name:
205
+ node.decorator_list = []
206
+ return transform_function_ast(node, node.name)
207
+ return node
208
+
209
+
210
+ def apply_implicit_return_transform(func_source: str, func_name: str) -> Tuple[ast.Module, str]:
211
+ """Pure function that applies implicit return transformation to function source code.
212
+
213
+ Args:
214
+ func_source: The source code of the function (dedented)
215
+ func_name: The name of the function to transform
216
+
217
+ Returns:
218
+ Tuple of (transformed_ast, compiled_code_object)
219
+
220
+ Raises:
221
+ ExplicitReturnDisallowedError: If explicit return found
222
+ MissingImplicitReturnError: If a block cannot yield a value
223
+ UnsupportedConstructError: If an unsupported construct is found
224
+
225
+ """
226
+ tree = ast.parse(func_source)
227
+ transformer = _TopLevelTransformer(func_name)
228
+ new_tree = transformer.visit(tree)
229
+ ast.fix_missing_locations(new_tree)
230
+ return new_tree, f'<{func_name}-implicit>'
@@ -0,0 +1,164 @@
1
+ """Common guard validators for typical validation scenarios.
2
+
3
+ Provides pre-built guard functions through the CommonGuards class for
4
+ common validation patterns like not_none, positive, in_range, etc.
5
+ """
6
+
7
+ import re
8
+ from typing import Any, Optional, Union
9
+
10
+ from ..shared.types import GuardFunction
11
+
12
+
13
+ class CommonGuards:
14
+
15
+ """Pre-defined common guard clauses.
16
+
17
+ Usage:
18
+ @guarded_expression(
19
+ CommonGuards.not_empty("username"),
20
+ log=True
21
+ )
22
+ def create_user(username):
23
+ return {"username": username}
24
+ """
25
+
26
+ @staticmethod
27
+ def _extract_param(
28
+ param_name: str, position: Optional[int], args: tuple, kwargs: dict, default: Any = None
29
+ ) -> Any:
30
+ """Extract parameter value from args or kwargs.
31
+
32
+ Args:
33
+ param_name: Name of the parameter in kwargs
34
+ position: Position in args (None means first arg)
35
+ args: Positional arguments tuple
36
+ kwargs: Keyword arguments dict
37
+ default: Default value if not found
38
+
39
+ Returns:
40
+ Parameter value or default
41
+
42
+ """
43
+ if param_name in kwargs:
44
+ return kwargs[param_name]
45
+
46
+ # Use explicit position if provided, else default to first arg
47
+ pos = position if position is not None else 0
48
+ if 0 <= pos < len(args):
49
+ return args[pos]
50
+
51
+ return default
52
+
53
+ @staticmethod
54
+ def not_empty(param_name: str = 'parameter', position: Optional[int] = None) -> GuardFunction:
55
+ """Guard ensuring collection parameter is not empty.
56
+
57
+ Args:
58
+ param_name: Name of the parameter to check
59
+ position: Optional explicit position for positional args (0-based)
60
+
61
+ """
62
+
63
+ def check_not_empty(*args: Any, **kwargs: Any) -> Union[bool, str]:
64
+ value = CommonGuards._extract_param(param_name, position, args, kwargs, default='')
65
+
66
+ # Check if value is empty (works for strings and collections)
67
+ if hasattr(value, '__len__'):
68
+ return len(value) > 0 or f'{param_name} cannot be empty'
69
+
70
+ return bool(value) or f'{param_name} cannot be empty'
71
+
72
+ return check_not_empty
73
+
74
+ @staticmethod
75
+ def not_none(param_name: str = 'parameter', position: int = 0) -> GuardFunction:
76
+ """Guard ensuring parameter is not None.
77
+
78
+ Args:
79
+ param_name: Name of the parameter to check
80
+ position: Position for positional args (default: 0)
81
+
82
+ """
83
+
84
+ def check_not_none(*args: Any, **kwargs: Any) -> Union[bool, str]:
85
+ value = CommonGuards._extract_param(param_name, position, args, kwargs, default=None)
86
+ return value is not None or f'{param_name} cannot be None'
87
+
88
+ return check_not_none
89
+
90
+ @staticmethod
91
+ def positive(param_name: str = 'parameter', position: int = 0) -> GuardFunction:
92
+ """Guard ensuring parameter is positive.
93
+
94
+ Args:
95
+ param_name: Name of the parameter to check
96
+ position: Position for positional args (default: 0)
97
+
98
+ """
99
+
100
+ def check_positive(*args: Any, **kwargs: Any) -> Union[bool, str]:
101
+ value = CommonGuards._extract_param(param_name, position, args, kwargs, default=0)
102
+ return value > 0 or f'{param_name} must be positive'
103
+
104
+ return check_positive
105
+
106
+ @staticmethod
107
+ def in_range(
108
+ min_val: float, max_val: float, param_name: str = 'parameter', position: int = 0
109
+ ) -> GuardFunction:
110
+ """Guard ensuring parameter is within range [min_val, max_val].
111
+
112
+ Args:
113
+ min_val: Minimum value (inclusive)
114
+ max_val: Maximum value (inclusive)
115
+ param_name: Name of the parameter to check
116
+ position: Position for positional args (default: 0)
117
+
118
+ """
119
+
120
+ def check_in_range(*args: Any, **kwargs: Any) -> Union[bool, str]:
121
+ value = CommonGuards._extract_param(param_name, position, args, kwargs, default=min_val - 1)
122
+ return min_val <= value <= max_val or f'{param_name} must be between {min_val} and {max_val}'
123
+
124
+ return check_in_range
125
+
126
+ @staticmethod
127
+ def type_check(
128
+ expected_type: type, param_name: str = 'parameter', position: int = 0
129
+ ) -> GuardFunction:
130
+ """Guard ensuring parameter matches expected type.
131
+
132
+ Args:
133
+ expected_type: Expected type for the parameter
134
+ param_name: Name of the parameter to check
135
+ position: Position for positional args (default: 0)
136
+
137
+ """
138
+
139
+ def check_type(*args: Any, **kwargs: Any) -> Union[bool, str]:
140
+ value = CommonGuards._extract_param(param_name, position, args, kwargs, default=None)
141
+ return (
142
+ isinstance(value, expected_type) or f'{param_name} must be of type {expected_type.__name__}'
143
+ )
144
+
145
+ return check_type
146
+
147
+ @staticmethod
148
+ def matches_pattern(
149
+ pattern: str, param_name: str = 'parameter', position: int = 0
150
+ ) -> GuardFunction:
151
+ """Guard ensuring string parameter matches regex pattern.
152
+
153
+ Args:
154
+ pattern: Regular expression pattern to match
155
+ param_name: Name of the parameter to check
156
+ position: Position for positional args (default: 0)
157
+
158
+ """
159
+
160
+ def check_pattern(*args: Any, **kwargs: Any) -> Union[bool, str]:
161
+ value = str(CommonGuards._extract_param(param_name, position, args, kwargs, default=''))
162
+ return re.match(pattern, value) is not None or f'{param_name} must match pattern {pattern}'
163
+
164
+ return check_pattern
@@ -0,0 +1,130 @@
1
+ """Unified guarded_expression decorator combining guards and implicit returns.
2
+
3
+ Unified guarded_expression decorator that combines guard clause validation
4
+ with optional implicit return transformation.
5
+
6
+ This is the primary decorator for the modgud library, unifying the functionality
7
+ of guard_clause and implicit_return into a single, composable decorator.
8
+ """
9
+
10
+ import functools
11
+ import inspect
12
+ from textwrap import dedent
13
+ from typing import Any, Callable, Optional
14
+
15
+ from ..shared.errors import GuardClauseError, UnsupportedConstructError
16
+ from ..shared.types import FailureBehavior, GuardFunction
17
+ from .ast_transform import apply_implicit_return_transform
18
+ from .guard_runtime import check_guards, handle_failure
19
+
20
+
21
+ class guarded_expression:
22
+
23
+ """Unified decorator combining guard clauses with optional implicit return.
24
+
25
+ Guards are callables that return True (pass) or a string error message (fail).
26
+ On failure, behavior is determined by the `on_error` parameter.
27
+
28
+ Default behavior: Raises GuardClauseError on guard failure.
29
+
30
+ Args:
31
+ *guards: Guard functions returning True or error message string
32
+ implicit_return: Enable implicit return transformation (default: True)
33
+ on_error: Failure behavior (default: GuardClauseError) - can be:
34
+ - Value (str, int, None, etc.): Returned on guard failure
35
+ - Callable: Invoked with (error_msg, *args, **kwargs), return value used
36
+ - Exception class: Instantiated with error message and raised
37
+ log: If True, log guard failures at INFO level (default: False)
38
+
39
+ Usage:
40
+ @guarded_expression(
41
+ lambda x: x > 0 or "Must be positive",
42
+ implicit_return=True,
43
+ on_error=GuardClauseError
44
+ )
45
+ def safe_divide(x):
46
+ result = 100 / x
47
+ # No explicit return needed when implicit_return=True
48
+
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ *guards: GuardFunction,
54
+ implicit_return: bool = True,
55
+ on_error: FailureBehavior = GuardClauseError,
56
+ log: bool = False,
57
+ ):
58
+ """Initialize the guarded_expression decorator.
59
+
60
+ Args:
61
+ *guards: Variable number of guard functions
62
+ implicit_return: Enable implicit return transformation
63
+ on_error: Behavior on guard failure
64
+ log: Enable logging of guard failures
65
+
66
+ """
67
+ self.guards = guards
68
+ self.implicit_return_enabled = implicit_return
69
+ self.on_error = on_error
70
+ self.log = log
71
+
72
+ def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
73
+ """Apply guard wrapping and optional implicit return transformation."""
74
+ return (
75
+ self._apply_implicit_return(func)
76
+ if self.implicit_return_enabled
77
+ else self._wrap_with_guards(func)
78
+ )
79
+
80
+ def _apply_implicit_return(self, func: Callable[..., Any]) -> Callable[..., Any]:
81
+ """Apply implicit return transformation to the function."""
82
+ # Extract and parse source
83
+ try:
84
+ source = dedent(inspect.getsource(func))
85
+ except OSError as e:
86
+ raise UnsupportedConstructError(
87
+ 'Source unavailable — guarded_expression with implicit_return=True requires importable source code.'
88
+ ) from e
89
+
90
+ # Transform the AST
91
+ new_tree, filename = apply_implicit_return_transform(source, func.__name__)
92
+
93
+ # Compile the transformed code in the original global scope
94
+ env = func.__globals__.copy()
95
+ code = compile(new_tree, filename=filename, mode='exec')
96
+ exec(code, env)
97
+
98
+ transformed = env[func.__name__]
99
+
100
+ # Wrap with guards and return
101
+ return self._wrap_with_guards(transformed, preserve_metadata_from=func)
102
+
103
+ def _wrap_with_guards(
104
+ self, func: Callable[..., Any], preserve_metadata_from: Optional[Callable[..., Any]] = None
105
+ ) -> Callable[..., Any]:
106
+ """Wrap the function with guard checking logic."""
107
+ metadata_source = preserve_metadata_from if preserve_metadata_from is not None else func
108
+
109
+ @functools.wraps(metadata_source)
110
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
111
+ # Check guards if any are defined
112
+ if self.guards:
113
+ error_msg = check_guards(self.guards, args, kwargs)
114
+ if error_msg is not None:
115
+ # Handle failure
116
+ result, exception_to_raise = handle_failure(
117
+ error_msg, self.on_error, func.__name__, args, kwargs, self.log
118
+ )
119
+ # Raise exception if configured
120
+ if exception_to_raise:
121
+ raise exception_to_raise
122
+ return result
123
+
124
+ # All guards passed - execute the function
125
+ return func(*args, **kwargs)
126
+
127
+ # Preserve explicit annotations for typing/IDE help
128
+ wrapper.__signature__ = inspect.signature(metadata_source) # type: ignore[attr-defined]
129
+ wrapper.__annotations__ = getattr(metadata_source, '__annotations__', {})
130
+ return wrapper
@@ -0,0 +1,73 @@
1
+ """Pure guard checking logic extracted from guard_clause.
2
+
3
+ Provides composable functions for evaluating guards and handling failures
4
+ without decorator-specific concerns.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Optional, Tuple
9
+
10
+ from ..shared.types import FailureBehavior, GuardFunction
11
+
12
+ # Module-level logger for guard failures
13
+ _logger = logging.getLogger(__name__)
14
+
15
+
16
+ def check_guards(
17
+ guards: Tuple[GuardFunction, ...], args: Tuple[Any, ...], kwargs: dict
18
+ ) -> Optional[str]:
19
+ """Pure function that evaluates all guards sequentially.
20
+
21
+ Args:
22
+ guards: Tuple of guard functions to evaluate
23
+ args: Positional arguments passed to the decorated function
24
+ kwargs: Keyword arguments passed to the decorated function
25
+
26
+ Returns:
27
+ None if all guards pass, or error message string if any guard fails
28
+
29
+ """
30
+ for guard in guards:
31
+ guard_result = guard(*args, **kwargs)
32
+ # Handle guard failure
33
+ if guard_result is not True:
34
+ return guard_result if isinstance(guard_result, str) else 'Guard clause failed'
35
+ return None
36
+
37
+
38
+ def handle_failure(
39
+ error_msg: str,
40
+ on_error: FailureBehavior,
41
+ func_name: str,
42
+ args: Tuple[Any, ...],
43
+ kwargs: dict,
44
+ log_enabled: bool,
45
+ ) -> Tuple[Any, Optional[BaseException]]:
46
+ """Pure function that handles guard failure based on on_error configuration.
47
+
48
+ Args:
49
+ error_msg: The error message from the failed guard
50
+ on_error: The failure behavior configuration
51
+ func_name: Name of the decorated function (for logging)
52
+ args: Positional arguments passed to the decorated function
53
+ kwargs: Keyword arguments passed to the decorated function
54
+ log_enabled: Whether to log the failure
55
+
56
+ Returns:
57
+ Tuple of (return_value, exception_to_raise)
58
+ - If exception should be raised: (None, exception_instance)
59
+ - If value should be returned: (value, None)
60
+
61
+ """
62
+ # Log if enabled
63
+ if log_enabled:
64
+ _logger.info(f'Guard clause failed in {func_name}: {error_msg}')
65
+
66
+ # Handle failure based on on_error type
67
+ if isinstance(on_error, type) and issubclass(on_error, BaseException):
68
+ return None, on_error(error_msg)
69
+
70
+ if callable(on_error):
71
+ return on_error(error_msg, *args, **kwargs), None # type: ignore[call-arg]
72
+
73
+ return on_error, None
modgud/py.typed ADDED
File without changes
@@ -0,0 +1,21 @@
1
+ """Shared types and errors for the modgud library."""
2
+
3
+ from .errors import (
4
+ ExplicitReturnDisallowedError,
5
+ GuardClauseError,
6
+ ImplicitReturnError,
7
+ MissingImplicitReturnError,
8
+ UnsupportedConstructError,
9
+ )
10
+ from .types import FailureBehavior, FailureTypes, GuardFunction
11
+
12
+ __all__ = [
13
+ 'GuardFunction',
14
+ 'FailureBehavior',
15
+ 'FailureTypes',
16
+ 'GuardClauseError',
17
+ 'ImplicitReturnError',
18
+ 'ExplicitReturnDisallowedError',
19
+ 'MissingImplicitReturnError',
20
+ 'UnsupportedConstructError',
21
+ ]
@@ -0,0 +1,51 @@
1
+ """Shared error classes for the modgud library."""
2
+
3
+ from typing import Optional
4
+
5
+
6
+ class GuardClauseError(Exception):
7
+
8
+ """Exception raised when a guard clause fails."""
9
+
10
+ pass
11
+
12
+
13
+ class ImplicitReturnError(SyntaxError):
14
+
15
+ """Base class for implicit-return related transformation errors."""
16
+
17
+ def __init__(
18
+ self, message: str, lineno: Optional[int] = None, col_offset: Optional[int] = None
19
+ ) -> None:
20
+ """Initialize the ImplicitReturnError with location information.
21
+
22
+ Args:
23
+ message: Error message describing the issue
24
+ lineno: Line number where the error occurred
25
+ col_offset: Column offset where the error occurred
26
+
27
+ """
28
+ super().__init__(message)
29
+ if lineno is not None:
30
+ self.lineno = lineno # type: ignore[attr-defined]
31
+ if col_offset is not None:
32
+ self.offset = col_offset # type: ignore[attr-defined]
33
+
34
+
35
+ class ExplicitReturnDisallowedError(ImplicitReturnError):
36
+
37
+ """Raised when an explicit `return` statement is found in a decorated function."""
38
+
39
+
40
+ class MissingImplicitReturnError(ImplicitReturnError):
41
+
42
+ """Raised when block cannot yield a value.
43
+
44
+ Raised when a block is required to yield a value but does not end with
45
+ a (convertible) final expression or a supported branching structure.
46
+ """
47
+
48
+
49
+ class UnsupportedConstructError(ImplicitReturnError):
50
+
51
+ """Raised when an unsupported AST construct appears at a required return boundary."""
modgud/shared/types.py ADDED
@@ -0,0 +1,10 @@
1
+ """Shared type definitions for the modgud library."""
2
+
3
+ from typing import Callable, Union
4
+
5
+ # Guard function signature: (*args, **kwargs) -> True | str
6
+ GuardFunction = Callable[..., Union[bool, str]]
7
+
8
+ # Failure behavior types
9
+ FailureTypes = Union[bool, str, int, float, None, dict, list, tuple]
10
+ FailureBehavior = Union[FailureTypes, Callable, type]
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: modgud
3
+ Version: 0.2.1
4
+ Summary: Guard clauses are validation checks at the beginning of functions that exit early when preconditions aren't met. They prevent deeply nested conditional logic and make the "happy path" of your code more readable.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: guards,validation,decorators,defensive-programming,single-return-point,implicit-return
8
+ Author: steven.miers@gmail.com
9
+ Requires-Python: >=3.13
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: System :: Systems Administration
18
+ Classifier: Topic :: Utilities
@@ -0,0 +1,14 @@
1
+ modgud/__init__.py,sha256=CJijIcli216dyVuAcWeC1usSRukjV4gw3DwAbAOHFu0,732
2
+ modgud/guarded_expression/__init__.py,sha256=jIANzo9NyimaB6bBwjZg268OYMlem1SrX5gmVaXzXnY,394
3
+ modgud/guarded_expression/ast_transform.py,sha256=gVxqi6N35dXotIrBlT80xvxkhdXVlmMf4KejQiylsOs,8089
4
+ modgud/guarded_expression/common_guards.py,sha256=MPNoLJmZWu0DdEjTL0BJIdGCzoB0y33ziZ8NqbtGigg,5158
5
+ modgud/guarded_expression/decorator.py,sha256=C07gRB3V32pFeI0aMx3-RTzxTdtMs0vGdnshqzAND-g,4727
6
+ modgud/guarded_expression/guard_runtime.py,sha256=SWgjINSMpIu462z4xECKuyRhgGwxfhxjPLIuAW5FHf8,2252
7
+ modgud/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ modgud/shared/__init__.py,sha256=Fss9heMohcSIBCT2fSrKjLlaOza6gzVnd1X4C_dkR70,496
9
+ modgud/shared/errors.py,sha256=zOHWXfW6UP06vIbORXRN2_V9VoTDO2nkAQo5_7MTNMc,1412
10
+ modgud/shared/types.py,sha256=FUgAN3Erj1mGVat7WSpbYdO0RiGVPzy9rZFgEveCyqQ,348
11
+ modgud-0.2.1.dist-info/METADATA,sha256=roIylBHHzy_5k_Rf8gYooDo1hL5h9NmQ-5c7k_dUHKA,900
12
+ modgud-0.2.1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
13
+ modgud-0.2.1.dist-info/licenses/LICENSE,sha256=T3FjasFS3bH2RCA7LAvTcSWlKWYmswCyLrMgj5IsUGc,1073
14
+ modgud-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.2.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2025] [Steven Miers]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.