Skip to content

Commit 7494253

Browse files
committed
fix: stabilize pii filter stub generation
Signed-off-by: lucarlig <luca.carlig@ibm.com>
1 parent 1e83765 commit 7494253

2 files changed

Lines changed: 39 additions & 17 deletions

File tree

plugins/rust/python-package/pii_filter/cpex_pii_filter/pii_filter_rust/__init__.pyi

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,29 @@ __all__ = [
1212
class PIIDetectorRust:
1313
r"""
1414
Main PII detector exposed to Python
15-
15+
1616
# Example (Python)
1717
```python
1818
from cpex_pii_filter import PIIDetectorRust
19-
19+
2020
config = {"detect_ssn": True, "detect_email": True}
2121
detector = PIIDetectorRust(config)
22-
22+
2323
text = "My SSN is 123-45-6789 and email is john@example.com"
2424
detections = detector.detect(text)
2525
print(detections) # {"ssn": [...], "email": [...]}
26-
26+
2727
masked = detector.mask(text, detections)
2828
print(masked) # "My SSN is [REDACTED] and email is [REDACTED]"
2929
```
3030
"""
3131
def __new__(cls, config: typing.Any) -> PIIDetectorRust:
3232
r"""
3333
Create a new PII detector
34-
34+
3535
# Arguments
3636
* `config` - Python dictionary or Pydantic model with configuration
37-
37+
3838
# Configuration Keys
3939
* `detect_ssn` (bool): Detect Social Security Numbers
4040
* `detect_bsn` (bool): Detect Dutch citizen service numbers
@@ -55,16 +55,16 @@ class PIIDetectorRust:
5555
* `max_text_bytes` (int): Maximum text payload size to inspect
5656
* `max_nested_depth` (int): Maximum nested container depth to inspect
5757
* `max_collection_items` (int): Maximum items to inspect per collection
58-
* `custom_patterns` (list[dict]): Additional regex-based PII patterns
59-
* `whitelist_patterns` (list[str]): Regex patterns to exclude from detection
58+
* `custom_patterns` (`list[dict]`): Additional regex-based PII patterns
59+
* `whitelist_patterns` (`list[str]`): Regex patterns to exclude from detection
6060
"""
6161
def detect(self, text: builtins.str) -> typing.Any:
6262
r"""
6363
Detect PII in text
64-
64+
6565
# Arguments
6666
* `text` - Text to scan for PII
67-
67+
6868
# Returns
6969
Dictionary mapping PII type to list of detections:
7070
```python
@@ -81,22 +81,22 @@ class PIIDetectorRust:
8181
def mask(self, text: builtins.str, detections: typing.Any) -> builtins.str:
8282
r"""
8383
Mask detected PII in text
84-
84+
8585
# Arguments
8686
* `text` - Original text
8787
* `detections` - Detection results from detect()
88-
88+
8989
# Returns
9090
Masked text with PII replaced
9191
"""
9292
def process_nested(self, data: typing.Any, path: builtins.str) -> tuple[builtins.bool, typing.Any, typing.Any]:
9393
r"""
9494
Process nested data structures (dicts, lists, strings)
95-
95+
9696
# Arguments
9797
* `data` - Python object (dict, list, str, or other)
9898
* `path` - Current path in the structure (for logging)
99-
99+
100100
# Returns
101101
Tuple of (modified: bool, new_data: Any, detections: dict)
102102
"""
@@ -108,4 +108,3 @@ class PIIFilterPluginCore:
108108
def prompt_post_fetch(self, payload: typing.Any, context: typing.Any) -> typing.Any: ...
109109
def tool_pre_invoke(self, payload: typing.Any, context: typing.Any) -> typing.Any: ...
110110
def tool_post_invoke(self, payload: typing.Any, context: typing.Any) -> typing.Any: ...
111-

plugins/rust/python-package/pii_filter/src/bin/stub_gen.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@ const CURATED_ALL_ENTRY: &str = "\"PIIDetectorRust\",\n \"PIIFilterPluginCore
1818
const PLUGIN_CORE_CLASS_MARKER: &str = "class PIIFilterPluginCore:";
1919
const PLUGIN_CORE_CLASS_DEF: &str = "\n\n@typing.final\nclass PIIFilterPluginCore:\n def __new__(cls, config: typing.Any) -> PIIFilterPluginCore: ...\n def prompt_pre_fetch(self, payload: typing.Any, context: typing.Any) -> typing.Any: ...\n def prompt_post_fetch(self, payload: typing.Any, context: typing.Any) -> typing.Any: ...\n def tool_pre_invoke(self, payload: typing.Any, context: typing.Any) -> typing.Any: ...\n def tool_post_invoke(self, payload: typing.Any, context: typing.Any) -> typing.Any: ...\n";
2020

21+
fn normalize_stub_content(content: &str) -> String {
22+
let mut lines = content.lines().map(str::trim_end).collect::<Vec<_>>();
23+
while matches!(lines.last(), Some(line) if line.is_empty()) {
24+
lines.pop();
25+
}
26+
27+
let mut normalized = lines.join("\n");
28+
normalized.push('\n');
29+
normalized
30+
}
31+
2132
fn curate_extension_stub_content(content: &str) -> String {
2233
let mut curated = content.replace(GENERATED_ALL_MARKER, CURATED_ALL_ENTRY);
2334
if !curated.contains(PLUGIN_CORE_CLASS_MARKER) {
@@ -33,7 +44,7 @@ fn curate_extension_stub_content(content: &str) -> String {
3344
"curated extension stub is missing PIIFilterPluginCore class definition",
3445
);
3546

36-
curated
47+
normalize_stub_content(&curated)
3748
}
3849

3950
fn curate_extension_stub() {
@@ -53,7 +64,8 @@ fn remove_orphan_extension_stub() {
5364
fn curate_top_level_stub() {
5465
let stub_path = Path::new("cpex_pii_filter/__init__.pyi");
5566
let content = "# This file is automatically generated by pyo3_stub_gen\n# ruff: noqa: E501, F401, F403, F405\n\nfrom .pii_filter import PIIFilterPlugin\nfrom .pii_filter_rust import PIIDetectorRust\n\n__all__ = [\n \"PIIDetectorRust\",\n \"PIIFilterPlugin\",\n]\n";
56-
fs::write(stub_path, content).expect("Failed to write curated top-level stub file");
67+
fs::write(stub_path, normalize_stub_content(content))
68+
.expect("Failed to write curated top-level stub file");
5769
}
5870

5971
fn main() {
@@ -84,6 +96,17 @@ mod tests {
8496
assert!(curated.contains(CURATED_ALL_ENTRY));
8597
}
8698

99+
#[test]
100+
fn test_curate_extension_stub_normalizes_trailing_whitespace() {
101+
let generated = "# This file is automatically generated by pyo3_stub_gen\n# ruff: noqa: E501, F401, F403, F405\n\nimport typing\n\n__all__ = [\n \"PIIDetectorRust\",\n]\n\nclass PIIDetectorRust:\n r\"\"\"\n Example text\n \n More text\n \"\"\"\n";
102+
103+
let curated = curate_extension_stub_content(generated);
104+
105+
assert!(!curated.contains(" \n"));
106+
assert!(curated.ends_with('\n'));
107+
assert!(!curated.ends_with("\n\n"));
108+
}
109+
87110
#[test]
88111
#[should_panic(expected = "missing PIIFilterPluginCore in __all__")]
89112
fn test_curate_extension_stub_panics_when_expected_export_injection_fails() {

0 commit comments

Comments
 (0)