Skip to content

Commit 5de2ddb

Browse files
authored
Merge pull request #44 from vertti/release/1.0.0
Release 1.0.0 - First stable release
2 parents fe37e64 + 5181d7d commit 5de2ddb

16 files changed

+335
-175
lines changed

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,34 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## 1.0.0
6+
7+
### Stable Release
8+
9+
Daffy 1.0.0 marks the first stable release. The public API (`df_in`, `df_out`, `df_log`) is now considered stable and follows semantic versioning.
10+
11+
### Changed
12+
13+
- Development status upgraded from Beta to Production/Stable
14+
- Updated documentation to reflect current tooling (uv, ruff, pyrefly)
15+
16+
### Fixed
17+
18+
- Improved error handling for invalid regex patterns in column specifications
19+
- Better error messages when parameter extraction fails
20+
21+
### Internal
22+
23+
- Extracted duplicate row validation logic into shared helper function
24+
- Added docstrings to public-facing utility functions
25+
26+
### API Stability
27+
28+
As of 1.0.0, Daffy follows semantic versioning:
29+
- Major versions (2.0, 3.0) may contain breaking changes
30+
- Minor versions (1.1, 1.2) add features without breaking changes
31+
- Patch versions (1.0.1, 1.0.2) contain bug fixes only
32+
533
## 0.19.0
634

735
### Performance Improvements

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ Install with your favorite Python dependency manager:
5252
pip install daffy
5353
```
5454

55+
Daffy works with **pandas**, **polars**, or both - install whichever you need:
56+
57+
```sh
58+
pip install pandas # for pandas support
59+
pip install polars # for polars support
60+
```
61+
62+
**Python version support:** 3.9 - 3.14
63+
5564
## Quick Start
5665

5766
### Column Validation
@@ -69,7 +78,7 @@ def apply_discount(cars_df):
6978

7079
### Row Validation
7180

72-
For validating actual data values (requires `pip install pydantic`):
81+
For validating actual data values (requires `pip install 'pydantic>=2.4.0'`):
7382

7483
```python
7584
from pydantic import BaseModel, Field

daffy/config.py

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
"""Configuration handling for DAFFY."""
22

33
import os
4-
from typing import Any, Dict, Optional
4+
from functools import lru_cache
5+
from typing import Any, Optional
56

67
import tomli
78

89

9-
def load_config() -> Dict[str, Any]:
10+
def load_config() -> dict[str, Any]:
1011
"""
1112
Load daffy configuration from pyproject.toml.
1213
@@ -45,41 +46,20 @@ def load_config() -> Dict[str, Any]:
4546

4647

4748
def find_config_file() -> Optional[str]:
48-
"""
49-
Find pyproject.toml in the user's project directory.
50-
51-
This searches only in the current working directory (where the user's code is running),
52-
not in daffy's installation directory.
53-
54-
Returns:
55-
str or None: Path to pyproject.toml if found, None otherwise
56-
"""
57-
# Only look for pyproject.toml in the current working directory,
58-
# which should be the user's project directory, not daffy's installation directory
59-
current_dir = os.getcwd()
60-
config_path = os.path.join(current_dir, "pyproject.toml")
61-
62-
if os.path.isfile(config_path):
63-
return config_path
49+
"""Find pyproject.toml in the current working directory."""
50+
path = os.path.join(os.getcwd(), "pyproject.toml")
51+
return path if os.path.isfile(path) else None
6452

65-
return None
6653

54+
@lru_cache(maxsize=1)
55+
def get_config() -> dict[str, Any]:
56+
"""Get the daffy configuration, cached after first load."""
57+
return load_config()
6758

68-
# Cache config to avoid reading the file multiple times
69-
_config_cache = None
7059

71-
72-
def get_config() -> Dict[str, Any]:
73-
"""
74-
Get the daffy configuration, loading it if necessary.
75-
76-
Returns:
77-
dict: Configuration dictionary with daffy settings
78-
"""
79-
global _config_cache
80-
if _config_cache is None:
81-
_config_cache = load_config()
82-
return _config_cache
60+
def clear_config_cache() -> None:
61+
"""Clear the configuration cache. Primarily for testing."""
62+
get_config.cache_clear()
8363

8464

8565
def get_strict(strict_param: Optional[bool] = None) -> bool:
@@ -97,7 +77,7 @@ def get_strict(strict_param: Optional[bool] = None) -> bool:
9777
return bool(get_config()["strict"])
9878

9979

100-
def get_row_validation_config() -> Dict[str, Any]:
80+
def get_row_validation_config() -> dict[str, Any]:
10181
"""Get all row validation configuration options."""
10282
config = get_config()
10383
return {

daffy/dataframe_types.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""DataFrame type handling for DAFFY - supports Pandas and Polars."""
22

3-
from typing import TYPE_CHECKING, Any, List, Union
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING, Any, Union
46

57
# Lazy imports - only import what's available
68
try:
@@ -26,6 +28,8 @@
2628
# Build DataFrame type dynamically based on what's available
2729
if TYPE_CHECKING:
2830
# For static type checking, assume both are available
31+
from typing import TypeGuard
32+
2933
from pandas import DataFrame as PandasDataFrame
3034
from polars import DataFrame as PolarsDataFrame
3135

@@ -61,12 +65,12 @@ def get_dataframe_types() -> tuple[Any, ...]:
6165
return tuple(dataframe_types)
6266

6367

64-
def get_available_library_names() -> List[str]:
68+
def get_available_library_names() -> list[str]:
6569
"""
6670
Get list of available DataFrame library names for error messages.
6771
6872
Returns:
69-
List[str]: List of available library names (e.g., ["Pandas", "Polars"])
73+
list[str]: List of available library names (e.g., ["Pandas", "Polars"])
7074
"""
7175
available_libs = []
7276
if HAS_PANDAS:
@@ -76,11 +80,11 @@ def get_available_library_names() -> List[str]:
7680
return available_libs
7781

7882

79-
def is_pandas_dataframe(df: Any) -> bool:
83+
def is_pandas_dataframe(df: Any) -> TypeGuard[PandasDataFrame]:
8084
return HAS_PANDAS and pd is not None and isinstance(df, pd.DataFrame)
8185

8286

83-
def is_polars_dataframe(df: Any) -> bool:
87+
def is_polars_dataframe(df: Any) -> TypeGuard[PolarsDataFrame]:
8488
return HAS_POLARS and pl is not None and isinstance(df, pl.DataFrame)
8589

8690

daffy/decorators.py

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
"""Decorators for DAFFY DataFrame Column Validator."""
22

3+
from __future__ import annotations
4+
35
import logging
6+
from collections.abc import Callable
47
from functools import wraps
5-
from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union
8+
from typing import TYPE_CHECKING, Any, TypeVar
69

710
if TYPE_CHECKING:
811
# For static type checking, assume both are available
@@ -14,10 +17,12 @@
1417
PandasDataFrame = None
1518
PolarsDataFrame = None
1619

17-
from daffy.config import get_strict
20+
from daffy.config import get_row_validation_config, get_strict
1821
from daffy.dataframe_types import DataFrameType
22+
from daffy.row_validation import validate_dataframe_rows
1923
from daffy.utils import (
2024
assert_is_dataframe,
25+
format_param_context,
2126
get_parameter,
2227
get_parameter_name,
2328
log_dataframe_input,
@@ -28,16 +33,46 @@
2833
# Type variables for preserving return types
2934
T = TypeVar("T") # Generic type var for df_log
3035
if TYPE_CHECKING:
31-
DF = TypeVar("DF", bound=Union[PandasDataFrame, PolarsDataFrame])
36+
DF = TypeVar("DF", bound=PandasDataFrame | PolarsDataFrame)
3237
else:
3338
DF = TypeVar("DF", bound=DataFrameType)
3439
R = TypeVar("R") # Return type for df_in
3540

3641

42+
def _validate_rows_with_context(
43+
df: Any,
44+
row_validator: "type[BaseModel]",
45+
func_name: str,
46+
param_name: str | None,
47+
is_return_value: bool,
48+
) -> None:
49+
"""Validate DataFrame rows with Pydantic model and add context to errors.
50+
51+
Args:
52+
df: DataFrame to validate
53+
row_validator: Pydantic model class for row validation
54+
func_name: Name of the decorated function
55+
param_name: Name of the parameter being validated (None for return values)
56+
is_return_value: True if validating a return value
57+
"""
58+
config = get_row_validation_config()
59+
60+
try:
61+
validate_dataframe_rows(
62+
df,
63+
row_validator,
64+
max_errors=config["max_errors"],
65+
convert_nans=config["convert_nans"],
66+
)
67+
except AssertionError as e:
68+
context = format_param_context(param_name, func_name, is_return_value)
69+
raise AssertionError(f"{str(e)}{context}") from e
70+
71+
3772
def df_out(
3873
columns: ColumnsDef = None,
39-
strict: Optional[bool] = None,
40-
row_validator: Optional["type[BaseModel]"] = None,
74+
strict: bool | None = None,
75+
row_validator: "type[BaseModel] | None" = None,
4176
) -> Callable[[Callable[..., DF]], Callable[..., DF]]:
4277
"""Decorate a function that returns a Pandas or Polars DataFrame.
4378
@@ -67,22 +102,7 @@ def wrapper(*args: Any, **kwargs: Any) -> DF:
67102
validate_dataframe(result, columns, get_strict(strict), None, func.__name__, True)
68103

69104
if row_validator is not None:
70-
from daffy.config import get_row_validation_config
71-
from daffy.row_validation import validate_dataframe_rows
72-
from daffy.utils import format_param_context
73-
74-
config = get_row_validation_config()
75-
76-
try:
77-
validate_dataframe_rows(
78-
result,
79-
row_validator,
80-
max_errors=config["max_errors"],
81-
convert_nans=config["convert_nans"],
82-
)
83-
except AssertionError as e:
84-
context = format_param_context(None, func.__name__, True)
85-
raise AssertionError(f"{str(e)}{context}") from e
105+
_validate_rows_with_context(result, row_validator, func.__name__, None, True)
86106

87107
return result
88108

@@ -92,10 +112,10 @@ def wrapper(*args: Any, **kwargs: Any) -> DF:
92112

93113

94114
def df_in(
95-
name: Optional[str] = None,
115+
name: str | None = None,
96116
columns: ColumnsDef = None,
97-
strict: Optional[bool] = None,
98-
row_validator: Optional["type[BaseModel]"] = None,
117+
strict: bool | None = None,
118+
row_validator: "type[BaseModel] | None" = None,
99119
) -> Callable[[Callable[..., R]], Callable[..., R]]:
100120
"""Decorate a function parameter that is a Pandas or Polars DataFrame.
101121
@@ -127,22 +147,7 @@ def wrapper(*args: Any, **kwargs: Any) -> R:
127147
validate_dataframe(df, columns, get_strict(strict), param_name, func.__name__)
128148

129149
if row_validator is not None:
130-
from daffy.config import get_row_validation_config
131-
from daffy.row_validation import validate_dataframe_rows
132-
from daffy.utils import format_param_context
133-
134-
config = get_row_validation_config()
135-
136-
try:
137-
validate_dataframe_rows(
138-
df,
139-
row_validator,
140-
max_errors=config["max_errors"],
141-
convert_nans=config["convert_nans"],
142-
)
143-
except AssertionError as e:
144-
context = format_param_context(param_name, func.__name__, False)
145-
raise AssertionError(f"{str(e)}{context}") from e
150+
_validate_rows_with_context(df, row_validator, func.__name__, param_name, False)
146151

147152
return func(*args, **kwargs)
148153

0 commit comments

Comments
 (0)