Skip to content

Commit d49e2c5

Browse files
committed
0.9.4
1 parent c240cdd commit d49e2c5

File tree

11 files changed

+1308
-13
lines changed

11 files changed

+1308
-13
lines changed

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
# Changelog
22

3+
## [0.9.4] - 2025-12-08
4+
5+
### Added
6+
- **Python Enum Support**: Full support for Python `Enum` types as dropdown menus.
7+
- Use standard Python enums as type hints: `def func(theme: Theme)`
8+
- Supports `str`, `int`, and `float` enum values
9+
- Automatic conversion from form values back to Enum members
10+
- Your function receives the actual Enum member (e.g., `Theme.LIGHT`), not just the string value
11+
- Access both `.name` and `.value` properties in your function
12+
- Optional enums with `Theme | None` syntax
13+
- Compatible with all enum features (methods, properties, iteration)
14+
- Add tests covering enum handling, conversion, and edge cases
15+
16+
### Benefits
17+
- **Type Safety**: Full IDE autocomplete and type checking
18+
- **Reusability**: Define enum once, use across multiple functions
19+
- **Rich Semantics**: Access both enum name and value, add custom methods
20+
- **Clean Code**: No repetition of `Literal['option1', 'option2']` in every function signature
21+
322
## [0.9.3] - 2025-11-30
423

524
### Fixed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# Func To Web 0.9.3
1+
# Func To Web 0.9.4
22

33
[![PyPI version](https://img.shields.io/pypi/v/func-to-web.svg)](https://pypi.org/project/func-to-web/)
44
[![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
5-
[![Tests](https://img.shields.io/badge/tests-454%20passing-brightgreen.svg)](tests/)
5+
[![Tests](https://img.shields.io/badge/tests-514%20passing-brightgreen.svg)](tests/)
66
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
77

88
> **Type hints → Web UI.** Minimal-boilerplate web apps from Python functions.
@@ -53,7 +53,7 @@ Complete documentation with **examples and screenshots** for each feature:
5353
- **[File Uploads](https://offerrall.github.io/FuncToWeb/files/)**: `ImageFile`, `DataFile`, `TextFile`, `DocumentFile`
5454
- **[Dynamic Lists](https://offerrall.github.io/FuncToWeb/lists/)**: `list[Type]` with add/remove buttons
5555
- **[Optional Fields](https://offerrall.github.io/FuncToWeb/optional/)**: `Type | None` with toggle switches
56-
- **[Dropdowns](https://offerrall.github.io/FuncToWeb/dropdowns/)**: Static `Literal['a', 'b']` or Dynamic `Literal[func]`
56+
- **[Dropdowns](https://offerrall.github.io/FuncToWeb/dropdowns/)**: Static (`Literal`, `Enum`) or Dynamic (`Literal[func]`)
5757
- **[Validation](https://offerrall.github.io/FuncToWeb/constraints/)**: Pydantic constraints (min/max, regex, list validation)
5858
</td>
5959
<td width="50%">

docs/dropdowns.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ Use dropdown menus for fixed or dynamic selection options.
99
<div markdown>
1010

1111
Use `Literal` for fixed dropdown options:
12-
1312
```python
1413
from typing import Literal
1514
from func_to_web import run
@@ -35,14 +34,48 @@ All options must be literals (strings, numbers, booleans) and all options must b
3534

3635
</div>
3736

37+
## Enum Dropdowns
38+
39+
<div class="grid" markdown>
40+
41+
<div markdown>
42+
43+
Use Python `Enum` for reusable dropdowns with named constants:
44+
```python
45+
from enum import Enum
46+
from func_to_web import run
47+
48+
class Theme(Enum):
49+
LIGHT = 'light'
50+
DARK = 'dark'
51+
AUTO = 'auto'
52+
53+
def preferences(theme: Theme):
54+
# Receives Theme.LIGHT, not just 'light'
55+
return f"Selected: {theme.name} = {theme.value}"
56+
57+
run(preferences)
58+
```
59+
60+
Your function receives the Enum member with access to both `.name` and `.value`. Useful when the same options are used in multiple functions.
61+
62+
</div>
63+
64+
<div markdown>
65+
66+
![Dropdowns](images/enum_drop.jpg)
67+
68+
</div>
69+
70+
</div>
71+
3872
## Dynamic Dropdowns
3973

4074
<div class="grid" markdown>
4175

4276
<div markdown>
4377

4478
Use functions inside `Literal` to generate options dynamically at runtime:
45-
4679
```python
4780
from typing import Literal
4881
from random import sample

docs/images/enum_drop.jpg

25.6 KB
Loading

examples/enum_dropdowns.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from enum import Enum
2+
from func_to_web import run
3+
4+
class Theme(Enum):
5+
LIGHT = 'light'
6+
DARK = 'dark'
7+
AUTO = 'auto'
8+
9+
class Priority(Enum):
10+
LOW = 1
11+
MEDIUM = 2
12+
HIGH = 3
13+
14+
def create_task(
15+
theme: Theme,
16+
priority: Priority
17+
):
18+
"""Your function receives the Enum member"""
19+
# Access both name and value
20+
return f"Theme: {theme.name} ({theme.value}), Priority: {priority.value}"
21+
22+
run(create_task)

func_to_web/analyze_function.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import inspect
22
from dataclasses import dataclass
33
from typing import Annotated, Literal, get_args, get_origin, Callable, Any
4+
from enum import Enum
45
import types
56

67
from pydantic import TypeAdapter
@@ -67,6 +68,10 @@ class ParamInfo:
6768
- Contains Field constraints for the list container
6869
- None if no list-level constraints
6970
Example: Field(min_items=2, max_items=5)
71+
enum_type: The original Enum type if parameter was an Enum.
72+
- None for non-Enum parameters
73+
- Stored to convert string back to Enum in validation
74+
- Example: For `color: Color`, stores the Color Enum class
7075
"""
7176
type: type
7277
default: Any = None
@@ -76,6 +81,7 @@ class ParamInfo:
7681
optional_enabled: bool = False
7782
is_list: bool = False
7883
list_field_info: Any = None
84+
enum_type: None = None
7985

8086
def analyze(func: Callable[..., Any]) -> dict[str, ParamInfo]:
8187
"""Analyze a function's signature and extract parameter metadata.
@@ -110,6 +116,7 @@ def analyze(func: Callable[..., Any]) -> dict[str, ParamInfo]:
110116
is_optional = False
111117
optional_default_enabled = None # None = auto, True = enabled, False = disabled
112118
is_list = False
119+
enum_type = None
113120

114121
# 1. Extract base type from Annotated (OUTER level)
115122
# This could be constraints for the list itself
@@ -239,6 +246,27 @@ def analyze(func: Callable[..., Any]) -> dict[str, ParamInfo]:
239246
else:
240247
t = type(None)
241248

249+
# 5b. Handle Enum types
250+
elif isinstance(t, type) and issubclass(t, Enum):
251+
opts = tuple(e.value for e in t)
252+
253+
if not opts:
254+
raise ValueError(f"'{name}': Enum must have at least one value")
255+
256+
types_set = {type(v) for v in opts}
257+
if len(types_set) > 1:
258+
raise TypeError(f"'{name}': Enum values must be same type")
259+
260+
if default is not None:
261+
if not isinstance(default, t):
262+
raise TypeError(f"'{name}': default must be {t.__name__} instance")
263+
default = default.value
264+
265+
enum_type = t
266+
267+
f = Literal[opts]
268+
t = types_set.pop()
269+
242270
# 6. Validate base type
243271
if t not in VALID:
244272
raise TypeError(f"'{name}': {t} not supported")
@@ -289,6 +317,6 @@ def analyze(func: Callable[..., Any]) -> dict[str, ParamInfo]:
289317
# No default, start disabled
290318
final_optional_enabled = False
291319

292-
result[name] = ParamInfo(t, default, f, dynamic_func, is_optional, final_optional_enabled, is_list, list_f)
320+
result[name] = ParamInfo(t, default, f, dynamic_func, is_optional, final_optional_enabled, is_list, list_f, enum_type)
293321

294322
return result

func_to_web/validate_params.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,25 @@ def validate_params(form_data: dict, params_info: dict[str, ParamInfo]) -> dict:
8888
value = float(value)
8989

9090
# Only validate against options if Literal is NOT dynamic
91-
# Dynamic literals can change between form render and submit
9291
if info.dynamic_func is None:
93-
# Static literal - validate against fixed options
9492
opts = get_args(info.field_info)
9593
if value not in opts:
9694
raise ValueError(f"'{name}': value '{value}' not in {opts}")
97-
# else: Dynamic literal - skip validation, trust the value from the form
95+
96+
# Convert string → Enum if needed
97+
if info.enum_type is not None:
98+
# Find the Enum member with this value
99+
for member in info.enum_type:
100+
if member.value == value:
101+
value = member
102+
break
103+
else:
104+
# This shouldn't happen if validation passed
105+
raise ValueError(f"'{name}': invalid value for {info.enum_type.__name__}")
98106

99107
validated[name] = value
100108
continue
101-
109+
102110
# Expand shorthand hex colors (#RGB -> #RRGGBB)
103111
if value and isinstance(value, str) and value.startswith('#') and len(value) == 4:
104112
value = '#' + ''.join(c*2 for c in value[1:])

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "func-to-web"
7-
version = "0.9.3"
7+
version = "0.9.4"
88
authors = [
99
{name = "Beltrán Offerrall"}
1010
]

0 commit comments

Comments
 (0)