Repository: aeye-lab/pymovements (python)

Issue:
deprecate utils modules that are not intended for end-users
## Description of the problem
Most of our utils modules are not really intended to be used by end-users , but should be used via high-level interfaces that call these methods.

This follows an anti-pattern where there is high coupling between the `utils` module and other modules and reduced cohesion inside modules like `dataset`.

Also this crowds our online documentation with irrelevant functions no end-user will need.

## Description of a solution

Instead, we should increase the cohesion inside each module and reduce coupling between modules.

Candidates are:
- [x] `utils.aois` #1080 
- [x] `utils.archives` -> `dataset._utils.archives` #1028 
- [x] `utils.checks` -> `_utils._checks` #1094 
- [x] remove `utils.decorators` #1008 
- [x] `utils.downloads` -> `dataset._utils.downloads` #1030 
- [x] `utils.filters` -> `events._utils._filters` #1032
- [x] `utils.parsing` -> `gaze._utils.parsing` #976 
- [x] `utils.paths` -> `_utils._paths` #1029 
- [ ] `utils.plotting` -> `plotting._utils.matlab` #1081 
- [x] `utils.strings` -> `_utils._strings` #1031 


Pull request description:
depr: deprecate `utils.strings`
## :warning: Deprecation

`utils.strings` is deprecated and will be removed in pymovements v0.26.0

## Description

Deprecate `utils.strings` as none of the included are intended for the end-user.

The following warning is printed:

```
DeprecationWarning: Call to deprecated function (or staticmethod) curly_to_regex. (This module will be removed in v0.26.0.) -- Deprecated since version v0.21.0.
```



partly resolves #999

## Implemented changes

- [x] moved `utils/strings.py` to `_utils/_strings.py` and adjusted to new location
- [x] use `_utils.strings` in remaining source
- [x] created deprecated function in `utils/strings.py`
- [x] moved `tests/unit/utils/strings_test.py` to `tests/unit/_utils/_strings_test.py`
- [x] add test that functions are correctly deprecated
- [x] add test that functions are removed in planned version


Patch:
diff --git a/src/pymovements/_utils/_strings.py b/src/pymovements/_utils/_strings.py
new file mode 100644
index 0000000..6cf3e58
--- /dev/null
+++ b/src/pymovements/_utils/_strings.py
@@ -0,0 +1,64 @@
+# Copyright (c) 2022-2025 The pymovements Project Authors
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+"""Provides string specific funtions."""
+from __future__ import annotations
+
+import re
+
+CURLY_TO_REGEX = re.compile(
+    r'(?:^|(?<=[^{])|(?<={{)){(?P<name>[^{][0-9a-zA-z_]*?)(?:\:(?P<quantity>\d*)(?P<type>[sd])?)?}',
+)
+
+
+def curly_to_regex(s: str) -> re.Pattern:
+    """Return regex pattern converted from provided python formatting style pattern.
+
+    By default all parameters are strings, if you want to specify number you can do: {num:d}
+    If you want to specify parameter's length you can do: {two_symbols:2} or {four_digits:4d}
+    Characters { and } can be escaped the same way as in python: {{ and }}
+    For example:
+                r'{subject_id:d}_{session_name}.csv'
+    converts to r'(?P<subject_id>[0-9]+)_(?P<session_name>.+).csv'
+
+    Parameters
+    ----------
+    s: str
+        Pattern in python formatting style.
+
+    Returns
+    -------
+    re.Pattern
+        Converted regex patterns.
+    """
+
+    def replace_aux(match: re.Match) -> str:     # Auxiliary replacement function
+        pattern = r'.'
+        if match.group('type') == 'd':
+            pattern = r'[0-9]'
+        elif match.group('type') == 's':
+            pattern = r'.'
+        quantity = r'+'
+        if match.group('quantity'):
+            quantity = f'{{{match.group("quantity")}}}'
+        return fr'(?P<{match.group("name")}>{pattern}{quantity})'
+
+    result = CURLY_TO_REGEX.sub(replace_aux, s)
+    result = result.replace('{{', '{').replace('}}', '}')
+    return re.compile(result)
diff --git a/src/pymovements/dataset/dataset_files.py b/src/pymovements/dataset/dataset_files.py
index 05c1d31..9578691 100644
--- a/src/pymovements/dataset/dataset_files.py
+++ b/src/pymovements/dataset/dataset_files.py
@@ -28,6 +28,7 @@ import polars as pl
 from tqdm.auto import tqdm
 
 from pymovements._utils._paths import match_filepaths
+from pymovements._utils._strings import curly_to_regex
 from pymovements.dataset.dataset_definition import DatasetDefinition
 from pymovements.dataset.dataset_paths import DatasetPaths
 from pymovements.events import EventDataFrame
@@ -37,7 +38,6 @@ from pymovements.gaze.io import from_asc
 from pymovements.gaze.io import from_csv
 from pymovements.gaze.io import from_ipc
 from pymovements.reading_measures import ReadingMeasures
-from pymovements.utils.strings import curly_to_regex
 
 
 def scan_dataset(definition: DatasetDefinition, paths: DatasetPaths) -> pl.DataFrame:
diff --git a/src/pymovements/stimulus/image.py b/src/pymovements/stimulus/image.py
index b6bd721..9c2cbed 100644
--- a/src/pymovements/stimulus/image.py
+++ b/src/pymovements/stimulus/image.py
@@ -23,8 +23,8 @@ from __future__ import annotations
 from pathlib import Path
 
 from pymovements._utils._paths import get_filepaths
+from pymovements._utils._strings import curly_to_regex
 from pymovements.utils.plotting import draw_image_stimulus
-from pymovements.utils.strings import curly_to_regex
 
 
 class ImageStimulus:
diff --git a/src/pymovements/utils/strings.py b/src/pymovements/utils/strings.py
index 6cf3e58..adf4aa0 100644
--- a/src/pymovements/utils/strings.py
+++ b/src/pymovements/utils/strings.py
@@ -17,16 +17,24 @@
 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
-"""Provides string specific funtions."""
+"""Provides string specific funtions.
+
+.. deprecated:: v0.21.1
+   This module will be removed in v0.26.0.
+"""
 from __future__ import annotations
 
 import re
 
-CURLY_TO_REGEX = re.compile(
-    r'(?:^|(?<=[^{])|(?<={{)){(?P<name>[^{][0-9a-zA-z_]*?)(?:\:(?P<quantity>\d*)(?P<type>[sd])?)?}',
-)
+from deprecated.sphinx import deprecated
 
+from pymovements._utils._strings import curly_to_regex as _curly_to_regex
 
+
+@deprecated(
+    reason='This module will be removed in v0.26.0.',
+    version='v0.21.1',
+)
 def curly_to_regex(s: str) -> re.Pattern:
     """Return regex pattern converted from provided python formatting style pattern.
 
@@ -37,6 +45,9 @@ def curly_to_regex(s: str) -> re.Pattern:
                 r'{subject_id:d}_{session_name}.csv'
     converts to r'(?P<subject_id>[0-9]+)_(?P<session_name>.+).csv'
 
+    .. deprecated:: v0.21.1
+       This module will be removed in v0.26.0.
+
     Parameters
     ----------
     s: str
@@ -47,18 +58,4 @@ def curly_to_regex(s: str) -> re.Pattern:
     re.Pattern
         Converted regex patterns.
     """
-
-    def replace_aux(match: re.Match) -> str:     # Auxiliary replacement function
-        pattern = r'.'
-        if match.group('type') == 'd':
-            pattern = r'[0-9]'
-        elif match.group('type') == 's':
-            pattern = r'.'
-        quantity = r'+'
-        if match.group('quantity'):
-            quantity = f'{{{match.group("quantity")}}}'
-        return fr'(?P<{match.group("name")}>{pattern}{quantity})'
-
-    result = CURLY_TO_REGEX.sub(replace_aux, s)
-    result = result.replace('{{', '{').replace('}}', '}')
-    return re.compile(result)
+    return _curly_to_regex(s=s)


Test patch:
diff --git a/tests/unit/_utils/_strings_test.py b/tests/unit/_utils/_strings_test.py
new file mode 100644
index 0000000..261867d
--- /dev/null
+++ b/tests/unit/_utils/_strings_test.py
@@ -0,0 +1,96 @@
+# Copyright (c) 2023-2025 The pymovements Project Authors
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+"""Test pymovements string utilities."""
+from __future__ import annotations
+
+import re
+
+import pytest
+
+from pymovements._utils._strings import curly_to_regex
+
+
+@pytest.mark.parametrize(
+    ('pattern', 'expected_regex'),
+    [
+        pytest.param(
+            r'{subject_id:d}_{session_name}.csv',
+            re.compile(r'(?P<subject_id>[0-9]+)_(?P<session_name>.+).csv'),
+            id='test_basic_pattern',
+        ),
+        pytest.param(
+            r'',
+            re.compile(r''),
+            id='test_empty_string',
+        ),
+        pytest.param(
+            r'test',
+            re.compile(r'test'),
+            id='test_no_curly_braces',
+        ),
+        pytest.param(
+            r'{test}',
+            re.compile(r'(?P<test>.+)'),
+            id='test_one_curly_brace',
+        ),
+        pytest.param(
+            r'{t3ST}',
+            re.compile(r'(?P<t3ST>.+)'),
+            id='test_various_characters_as_name',
+        ),
+        pytest.param(
+            r'{test1}_{test2}',
+            re.compile(r'(?P<test1>.+)_(?P<test2>.+)'),
+            id='test_two_curly_braces',
+        ),
+        pytest.param(
+            r'{{{test1}}}',
+            re.compile(r'{(?P<test1>.+)}'),
+            id='test_nested_curly_braces',
+        ),
+        pytest.param(
+            r'{{test1}}',
+            re.compile(r'{test1}'),
+            id='test_escaped_curly_braces',
+        ),
+        pytest.param(
+            r'{num:s}',
+            re.compile(r'(?P<num>.+)'),
+            id='test_explicit_string',
+        ),
+        pytest.param(
+            r'{num:d}',
+            re.compile(r'(?P<num>[0-9]+)'),
+            id='test_numbers',
+        ),
+        pytest.param(
+            r'{num:42}',
+            re.compile(r'(?P<num>.{42})'),
+            id='test_quantities',
+        ),
+        pytest.param(
+            r'{num:5d}',
+            re.compile(r'(?P<num>[0-9]{5})'),
+            id='test_quantities_2',
+        ),
+    ],
+)
+def test_curly_to_regex(pattern, expected_regex):
+    assert curly_to_regex(pattern) == expected_regex
diff --git a/tests/unit/utils/strings_test.py b/tests/unit/utils/strings_test.py
index 03ab80d..a1da6bf 100644
--- a/tests/unit/utils/strings_test.py
+++ b/tests/unit/utils/strings_test.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2023-2025 The pymovements Project Authors
+# Copyright (c) 2025 The pymovements Project Authors
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documentation files (the "Software"), to deal
@@ -17,80 +17,35 @@
 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
-"""Test pymovements string utilities."""
-from __future__ import annotations
-
+"""Tests deprecated utils.strings."""
 import re
 
 import pytest
 
+from pymovements import __version__
 from pymovements.utils.strings import curly_to_regex
 
 
-@pytest.mark.parametrize(
-    ('pattern', 'expected_regex'),
-    [
-        pytest.param(
-            r'{subject_id:d}_{session_name}.csv',
-            re.compile(r'(?P<subject_id>[0-9]+)_(?P<session_name>.+).csv'),
-            id='test_basic_pattern',
-        ),
-        pytest.param(
-            r'',
-            re.compile(r''),
-            id='test_empty_string',
-        ),
-        pytest.param(
-            r'test',
-            re.compile(r'test'),
-            id='test_no_curly_braces',
-        ),
-        pytest.param(
-            r'{test}',
-            re.compile(r'(?P<test>.+)'),
-            id='test_one_curly_brace',
-        ),
-        pytest.param(
-            r'{t3ST}',
-            re.compile(r'(?P<t3ST>.+)'),
-            id='test_various_characters_as_name',
-        ),
-        pytest.param(
-            r'{test1}_{test2}',
-            re.compile(r'(?P<test1>.+)_(?P<test2>.+)'),
-            id='test_two_curly_braces',
-        ),
-        pytest.param(
-            r'{{{test1}}}',
-            re.compile(r'{(?P<test1>.+)}'),
-            id='test_nested_curly_braces',
-        ),
-        pytest.param(
-            r'{{test1}}',
-            re.compile(r'{test1}'),
-            id='test_escaped_curly_braces',
-        ),
-        pytest.param(
-            r'{num:s}',
-            re.compile(r'(?P<num>.+)'),
-            id='test_explicit_string',
-        ),
-        pytest.param(
-            r'{num:d}',
-            re.compile(r'(?P<num>[0-9]+)'),
-            id='test_numbers',
-        ),
-        pytest.param(
-            r'{num:42}',
-            re.compile(r'(?P<num>.{42})'),
-            id='test_quantities',
-        ),
-        pytest.param(
-            r'{num:5d}',
-            re.compile(r'(?P<num>[0-9]{5})'),
-            id='test_quantities_2',
-        ),
-    ],
-)
-def test_curly_to_regex(pattern, expected_regex):
-    assert curly_to_regex(pattern) == expected_regex
+@pytest.mark.filterwarnings('ignore::DeprecationWarning')
+def test_curly_to_regex():
+    curly_to_regex('foo')
+
+
+def test_curly_to_regex_deprecated():
+    with pytest.raises(DeprecationWarning):
+        curly_to_regex('foo')
+
+
+def test_curly_to_regex_removed():
+    with pytest.raises(DeprecationWarning) as info:
+        curly_to_regex('foo')
+
+    regex = re.compile(r'.*will be removed in v(?P<version>[0-9]*[.][0-9]*[.][0-9]*)[.)].*')
+
+    msg = info.value.args[0]
+    remove_version = regex.match(msg).groupdict()['version']
+    current_version = __version__.split('+')[0]
+    assert current_version < remove_version, (
+        f'utils/strings.py was planned to be removed in v{remove_version}. '
+        f'Current version is v{current_version}.'
+    )