Skip to content

Commit 6773e74

Browse files
authored
[Python] Add Array.skipWhile support (#4296)
* [Python] Add Array.skipWhile support * Allow IEnumerator_1 as base class and filter out IDisposable (since IEnumerator already implements IDisposable) to fix typing issues. * Fix various minor typing issues with the array module
1 parent 8f17a5c commit 6773e74

File tree

9 files changed

+273
-187
lines changed

9 files changed

+273
-187
lines changed

pyproject.toml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,28 @@ reportUnusedVariable = false
4747
reportUnnecessaryIsInstance = true
4848
reportUnnecessaryComparison = true
4949
reportUnnecessaryCast = true
50-
reportPrivateUsage = true
50+
reportPrivateUsage = false # Getters/setters reference private members from outside the class
5151
reportImportCycles = true
5252
reportDuplicateImport = true
5353
reportConstantRedefinition = true
5454
reportOverlappingOverload = true
5555
reportInconsistentConstructor = true
5656
reportImplicitStringConcatenation = true
57+
5758
pythonVersion = "3.12"
5859
typeCheckingMode = "standard"
5960

6061
[tool.ruff]
61-
# Keep in sync with .pre-commit-config.yaml
6262
line-length = 120
63-
lint.ignore = []
64-
lint.select = ["E", "W", "F", "I", "T", "RUF", "TID", "UP"]
6563
target-version = "py312"
6664
include =["*.py"]
6765

66+
[tool.ruff.lint]
67+
select = ["E", "W", "F", "I", "T", "RUF", "TID", "UP"]
68+
ignore = [
69+
"F841" # Ignore unused variable warnings
70+
]
71+
6872
[tool.ruff.lint.flake8-bugbear]
6973
# These Rust extension module types are immutable (frozen)
7074
extend-immutable-calls = [

src/Fable.Cli/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
* [Python] Add `Array.skipWhile` support (by @dbrattli)
13+
* [Python] Allow `IEnumerator_1` as base class to fix typing issues (by @dbrattli)
1214
* [Python] Add Pythonic import path syntax for relative imports (`.module`, `..parent`, `...grandparent`) (by @dbrattli)
1315
* [Python] Add `[<Py.DecorateTemplate>]` attribute for creating custom decorator attributes (by @dbrattli)
1416
* [Python] Add `[<Py.ClassAttributesTemplate>]` attribute for creating custom class attribute shortcuts (by @dbrattli)

src/Fable.Compiler/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
* [Python] Add `Array.skipWhile` support (by @dbrattli)
13+
* [Python] Allow `IEnumerator_1` as base class to fix typing issues (by @dbrattli)
1214
* [Python] Add Pythonic import path syntax for relative imports (`.module`, `..parent`, `...grandparent`) (by @dbrattli)
1315
* [Python] Add `[<Py.DecorateTemplate>]` attribute for creating custom decorator attributes (by @dbrattli)
1416
* [Python] Add `[<Py.ClassAttributesTemplate>]` attribute for creating custom class attribute shortcuts (by @dbrattli)

src/Fable.Transforms/Python/Fable2Python.Transforms.fs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2606,13 +2606,20 @@ let declareClassType
26062606
let interfaces, stmts =
26072607
// We only use a few interfaces as base classes. The rest is handled as Python protocols (PEP 544) to avoid a massive
26082608
// inheritance tree that will prevent Python of finding a consistent method resolution order.
2609-
let allowedInterfaces = [ "IDisposable" ]
2609+
let allowedInterfaces = [ "IDisposable"; "IEnumerator_1" ]
2610+
2611+
// Check if class implements IEnumerator_1 (which already inherits from IDisposable)
2612+
let hasIEnumerator =
2613+
ent.AllInterfaces
2614+
|> Seq.exists (fun int -> Helpers.removeNamespace (int.Entity.FullName) = "IEnumerator_1")
26102615

26112616
ent.AllInterfaces
26122617
|> List.ofSeq
26132618
|> List.filter (fun int ->
26142619
let name = Helpers.removeNamespace (int.Entity.FullName)
2620+
// Filter out IDisposable if IEnumerator_1 is present to avoid diamond inheritance
26152621
allowedInterfaces |> List.contains name
2622+
&& not (hasIEnumerator && name = "IDisposable")
26162623
)
26172624
|> List.map (fun int ->
26182625
let genericArgs =

src/fable-library-py/fable_library/array_.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
_T = TypeVar("_T")
77

8+
# Define the typed array constructors.
89
Int8Array = array.FSharpCons[int8]("Int8")
910
UInt8Array = array.FSharpCons[uint8]("UInt8")
1011
Int16Array = array.FSharpCons[int16]("Int16")
@@ -115,6 +116,7 @@
115116
copy = array.copy
116117
take = array.take
117118
skip = array.skip
119+
skip_while = array.skip_while
118120
compare_to = array.compare_to
119121
sort_with = array.sort_with
120122
choose = array.choose
@@ -206,6 +208,7 @@
206208
"set_slice",
207209
"singleton",
208210
"skip",
211+
"skip_while",
209212
"sort",
210213
"sort_by",
211214
"sort_by",

src/fable-library-py/fable_library/core/array.pyi

Lines changed: 187 additions & 178 deletions
Large diffs are not rendered by default.

src/fable-library-py/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ python-source = "." # Look at project root
3737
module-name = "fable_library.core._core" # Full module path
3838
features = ["pyo3/extension-module"]
3939

40+
[tool.pyright]
41+
reportPrivateUsage = false
42+
4043
[tool.pytest.ini_options]
4144
minversion = "8.0"
4245
pythonpath = "."

src/fable-library-py/src/array.rs

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,6 +1087,32 @@ impl FSharpArray {
10871087
Ok(FSharpArray { storage: results })
10881088
}
10891089

1090+
#[pyo3(signature = (predicate, cons=None))]
1091+
pub fn skip_while(
1092+
&self,
1093+
py: Python<'_>,
1094+
predicate: &Bound<'_, PyAny>,
1095+
cons: Option<&Bound<'_, PyAny>>,
1096+
) -> PyResult<FSharpArray> {
1097+
let len = self.storage.len();
1098+
let mut count = 0;
1099+
1100+
while count < len {
1101+
let item = self.get_item_at_index(count as isize, py)?;
1102+
if !predicate.call1((item,))?.is_truthy()? {
1103+
break;
1104+
}
1105+
count += 1;
1106+
}
1107+
1108+
if count == len {
1109+
let fs_cons = FSharpCons::extract(cons, self.storage.get_type());
1110+
return fs_cons.allocate(py, 0);
1111+
}
1112+
1113+
self.skip(py, count as isize, cons)
1114+
}
1115+
10901116
pub fn chunk_by_size(&self, py: Python<'_>, chunk_size: usize) -> PyResult<Self> {
10911117
if chunk_size < 1 {
10921118
return Err(PyErr::new::<exceptions::PyValueError, _>(
@@ -2908,17 +2934,26 @@ impl FSharpArray {
29082934
}
29092935
}
29102936

2911-
#[pyo3(signature = (projection, comparer))]
2937+
#[pyo3(signature = (projection, comparer=None))]
29122938
pub fn sort_by(
29132939
&self,
29142940
py: Python<'_>,
29152941
projection: &Bound<'_, PyAny>,
2916-
comparer: &Bound<'_, PyAny>,
2942+
comparer: Option<&Bound<'_, PyAny>>,
29172943
) -> PyResult<FSharpArray> {
29182944
let mut result = self.clone();
2945+
// Use provided comparer or create a default one
2946+
let default_comparer;
2947+
let comparer_ref = match comparer {
2948+
Some(c) => c,
2949+
None => {
2950+
default_comparer = DefaultComparer::new()?.into_pyobject(py)?;
2951+
&default_comparer
2952+
}
2953+
};
29192954
result.storage = result
29202955
.storage
2921-
.sort_by_with_projection(py, projection, comparer)?;
2956+
.sort_by_with_projection(py, projection, comparer_ref)?;
29222957
Ok(result)
29232958
}
29242959

@@ -3188,6 +3223,17 @@ pub fn skip(
31883223
array.skip(py, count, cons)
31893224
}
31903225

3226+
#[pyfunction]
3227+
#[pyo3(signature = (predicate, array, cons=None))]
3228+
pub fn skip_while(
3229+
py: Python<'_>,
3230+
predicate: &Bound<'_, PyAny>,
3231+
array: &FSharpArray,
3232+
cons: Option<&Bound<'_, PyAny>>,
3233+
) -> PyResult<FSharpArray> {
3234+
array.skip_while(py, predicate, cons)
3235+
}
3236+
31913237
#[pyfunction]
31923238
pub fn chunk_by_size(
31933239
py: Python<'_>,
@@ -4042,11 +4088,12 @@ pub fn sort(
40424088
}
40434089

40444090
#[pyfunction]
4091+
#[pyo3(signature = (projection, array, comparer=None))]
40454092
pub fn sort_by(
40464093
py: Python<'_>,
40474094
projection: &Bound<'_, PyAny>,
40484095
array: &FSharpArray,
4049-
comparer: &Bound<'_, PyAny>,
4096+
comparer: Option<&Bound<'_, PyAny>>,
40504097
) -> PyResult<FSharpArray> {
40514098
array.sort_by(py, projection, comparer)
40524099
}
@@ -4492,6 +4539,7 @@ pub fn register_array_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()
44924539
m.add_function(wrap_pyfunction!(set_slice, &m)?)?;
44934540
m.add_function(wrap_pyfunction!(singleton, &m)?)?;
44944541
m.add_function(wrap_pyfunction!(skip, &m)?)?;
4542+
m.add_function(wrap_pyfunction!(skip_while, &m)?)?;
44954543
m.add_function(wrap_pyfunction!(sort, &m)?)?;
44964544
m.add_function(wrap_pyfunction!(sort_by, &m)?)?;
44974545
m.add_function(wrap_pyfunction!(sort_in_place, &m)?)?;

tests/Python/TestArray.fs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,14 @@ let ``test Array.truncate works`` () =
727727
try xs |> Array.truncate 20 |> Array.length with _ -> -1
728728
|> equal 5
729729

730+
[<Fact>]
731+
let ``test Array.skipWhile works`` () =
732+
let xs = [|1; 2; 3; 4; 5|]
733+
xs |> Array.skipWhile (fun x -> x < 3) |> equal [|3; 4; 5|]
734+
xs |> Array.skipWhile (fun x -> x < 1) |> equal [|1; 2; 3; 4; 5|]
735+
xs |> Array.skipWhile (fun x -> x < 6) |> equal [||]
736+
[||] |> Array.skipWhile (fun x -> x < 3) |> equal [||]
737+
730738
// [<Fact>]
731739
// let ``test Array.sortDescending works`` () =
732740
// let xs = [|3; 4; 1; -3; 2; 10|]

0 commit comments

Comments
 (0)