Repository: dask/dask
Pull request #11710 (2025-01-31 11:29:21)

`arange`: support kwargs
Change `arange` to accept `stop` and `step` as kwargs, in accordance to both numpy and the [Array API standard](https://data-apis.org/array-api/latest/API_specification/generated/array_api.arange.html#arange).

github-actions[bot]: ## Unit Test Results
_See [test report](https://dask.github.io/dask/test_report.html) for an extended history of previous test failures. This is useful for diagnosing flaky tests._

     15 files  ±0       15 suites  ±0   4h 27m 33s ⏱️ +57s
 17 142 tests ±0   15 962 ✅ ±0   1 180 💤 ±0  0 ❌ ±0 
211 226 runs  ±0  194 193 ✅ +4  17 033 💤  - 4  0 ❌ ±0 

Results for commit c1e4fa59. ± Comparison against base commit 7f490b91.

[test-results]:data:application/gzip;base64,H4sIAKe8nGcC/03MSw7DIAxF0a1EjDvA5he6mcqhIKHmUxEYVd17SZSSDO+x/D4sxNGv7N6BunVsLTGf9SyJclzmrTVXolK95v1uQOK/H2txbn+y+oKv+N4Qet4sUByrneBTWtIhqczbNAIg6qPbtJVgRcNj2nDR7DK993XZLdMUcw3mwMtAyjo+SEIvNUGAoIjQGoM2iIE7GnrFvj/qpAMHGgEAAA==

crusaderky: CI failures are unrelated; ready for review and merge

Patch:
diff --git a/dask/array/creation.py b/dask/array/creation.py
index 786629a56..ec7e96f8e 100644
--- a/dask/array/creation.py
+++ b/dask/array/creation.py
@@ -382,7 +382,9 @@ def linspace(
 
 
 @array_creation_dispatch.register_inplace("numpy")
-def arange(*args, chunks="auto", like=None, dtype=None, **kwargs):
+def arange(
+    start=None, /, stop=None, step=1, *, chunks="auto", like=None, dtype=None, **kwargs
+):
     """
     Return evenly spaced values from `start` to `stop` with step size `step`.
 
@@ -395,13 +397,15 @@ def arange(*args, chunks="auto", like=None, dtype=None, **kwargs):
 
     Parameters
     ----------
-    start : int, optional
-        The starting value of the sequence. The default is 0.
-    stop : int
-        The end of the interval, this value is excluded from the interval.
-    step : int, optional
-        The spacing between the values. The default is 1 when not specified.
-        The last value of the sequence.
+    start : int | float, optional
+        If ``stop`` is specified, the start of interval (inclusive); otherwise, the end
+        of the interval (exclusive). Default: 0 when ``stop`` is specified.
+    stop : int | float
+        The end of the interval (exclusive).
+    step : int | float, optional
+        The distance between two adjacent elements ``(out[i+1] - out[i])``.
+        Must not be 0; may be negative, this results in an empty array if stop >= start.
+        Default: 1.
     chunks :  int
         The number of samples on each block. Note that the last block will have
         fewer samples if ``len(array) % chunks != 0``.
@@ -420,22 +424,12 @@ def arange(*args, chunks="auto", like=None, dtype=None, **kwargs):
     --------
     dask.array.linspace
     """
-    if len(args) == 1:
+    if start is None and stop is None:
+        raise TypeError("arange() requires stop to be specified.")
+    elif start is None:
         start = 0
-        stop = args[0]
-        step = 1
-    elif len(args) == 2:
-        start = args[0]
-        stop = args[1]
-        step = 1
-    elif len(args) == 3:
-        start, stop, step = args
-    else:
-        raise TypeError(
-            """
-        arange takes 3 positional arguments: arange([start], stop, [step])
-        """
-        )
+    elif stop is None:
+        start, stop = 0, start
 
     num = int(max(np.ceil((stop - start) / step), 0))
 
diff --git a/dask/tokenize.py b/dask/tokenize.py
index d6601778c..b0f0a5a77 100644
--- a/dask/tokenize.py
+++ b/dask/tokenize.py
@@ -114,15 +114,16 @@ normalize_token.register(
 
 @normalize_token.register((types.MappingProxyType, dict))
 def normalize_dict(d):
-    if id(d) in _SEEN:
-        return "__seen", _SEEN[id(d)][0]
-    _SEEN[id(d)] = len(_SEEN), d
-    try:
-        return "dict", _normalize_seq_func(
-            sorted(d.items(), key=lambda kv: hash(kv[0]))
-        )
-    finally:
-        _SEEN.pop(id(d), None)
+    with tokenize_lock:
+        if id(d) in _SEEN:
+            return "__seen", _SEEN[id(d)][0]
+        _SEEN[id(d)] = len(_SEEN), d
+        try:
+            return "dict", _normalize_seq_func(
+                sorted(d.items(), key=lambda kv: hash(kv[0]))
+            )
+        finally:
+            _SEEN.pop(id(d), None)
 
 
 @normalize_token.register(OrderedDict)
@@ -145,13 +146,14 @@ def _normalize_seq_func(seq: Iterable[object]) -> tuple[object, ...]:
             return item
         return normalize_token(item)
 
-    if id(seq) in _SEEN:
-        return "__seen", _SEEN[id(seq)][0]
-    _SEEN[id(seq)] = len(_SEEN), seq
-    try:
-        return tuple(map(_inner_normalize_token, seq))
-    finally:
-        del _SEEN[id(seq)]
+    with tokenize_lock:
+        if id(seq) in _SEEN:
+            return "__seen", _SEEN[id(seq)][0]
+        _SEEN[id(seq)] = len(_SEEN), seq
+        try:
+            return tuple(map(_inner_normalize_token, seq))
+        finally:
+            del _SEEN[id(seq)]
 
 
 @normalize_token.register((tuple, list))


Test patch:
diff --git a/dask/array/tests/test_creation.py b/dask/array/tests/test_creation.py
index d7687e43f..e8f51479f 100644
--- a/dask/array/tests/test_creation.py
+++ b/dask/array/tests/test_creation.py
@@ -227,6 +227,26 @@ def test_arange():
     nparr = np.arange(0, -1, 0.5)
     assert_eq(darr, nparr)
 
+    # stop and/or step as kwargs
+    darr = da.arange(stop=10)
+    nparr = np.arange(stop=10)
+    assert_eq(darr, nparr)
+
+    darr = da.arange(10, step=2)
+    nparr = np.arange(10, step=2)
+    assert_eq(darr, nparr)
+
+    darr = da.arange(stop=10, step=2)
+    nparr = np.arange(stop=10, step=2)
+    assert_eq(darr, nparr)
+
+    darr = da.arange(3, stop=10, step=2)
+    nparr = np.arange(3, stop=10, step=2)
+    assert_eq(darr, nparr)
+
+    with pytest.raises(TypeError, match="requires stop"):
+        da.arange()
+
     # Unexpected or missing kwargs
     with pytest.raises(TypeError, match="whatsthis"):
         da.arange(10, chunks=-1, whatsthis=1)
diff --git a/dask/tests/test_tokenize.py b/dask/tests/test_tokenize.py
index a2a5f625d..4bf3ca224 100644
--- a/dask/tests/test_tokenize.py
+++ b/dask/tests/test_tokenize.py
@@ -10,6 +10,7 @@ import random
 import subprocess
 import sys
 import textwrap
+from concurrent.futures import ThreadPoolExecutor
 from enum import Enum, Flag, IntEnum, IntFlag
 from typing import Union
 
@@ -1446,3 +1447,20 @@ def test_tokenize_recursive_respects_ensure_deterministic():
 
     with pytest.raises(RuntimeError):
         tokenize(Foo(), ensure_deterministic=True)
+
+
+def test_tokenize_nested_sequence_thread_safe():
+    nested_list = []
+    for ix in range(100):
+        nested_list = [ix, nested_list]
+
+    nested_dict = {}
+    for ix in range(50):
+        nested_dict = {ix: nested_dict}
+
+    normalize_token(nested_list)
+    with ThreadPoolExecutor() as pool:
+        futures = [pool.submit(normalize_token, nested_list) for _ in range(1000)]
+        assert len({f.result() for f in futures}) == 1
+        futures = [pool.submit(normalize_token, nested_dict) for _ in range(1000)]
+        assert len({f.result() for f in futures}) == 1