Repository: Galileo-Galilei/kedro-mlflow
Pull request #602 (2024-10-17 21:11:45)

⬆️ Update ruff requirement from <0.6.0,>=0.5.0 to >=0.5.0,<0.8.0
Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/astral-sh/ruff/releases">ruff's releases</a>.</em></p>
<blockquote>
<h2>0.7.0</h2>
<h2>Release Notes</h2>
<p>Check out the <a href="https://astral.sh/blog/ruff-v0.7.0">blog post</a> for a migration guide and overview of the changes!</p>
<h3>Breaking changes</h3>
<ul>
<li>The pytest rules <code>PT001</code> and <code>PT023</code> now default to omitting the decorator parentheses when there are no arguments
(<a href="https://redirect.github.com/astral-sh/ruff/pull/12838">#12838</a>, <a href="https://redirect.github.com/astral-sh/ruff/pull/13292">#13292</a>).
This was a change that we attempted to make in Ruff v0.6.0, but only partially made due to an error on our part.
See the <a href="https://astral.sh/blog/ruff-v0.7.0">blog post</a> for more details.</li>
<li>The <code>useless-try-except</code> rule (in our <code>tryceratops</code> category) has been recoded from <code>TRY302</code> to
<code>TRY203</code> (<a href="https://redirect.github.com/astral-sh/ruff/pull/13502">#13502</a>). This ensures Ruff's code is consistent with
the same rule in the <a href="https://github.com/guilatrova/tryceratops"><code>tryceratops</code></a> linter.</li>
<li>The <code>lint.allow-unused-imports</code> setting has been removed (<a href="https://redirect.github.com/astral-sh/ruff/pull/13677">#13677</a>). Use
<a href="https://docs.astral.sh/ruff/settings/#lint_pyflakes_allowed-unused-imports"><code>lint.pyflakes.allow-unused-imports</code></a>
instead.</li>
</ul>
<h3>Formatter preview style</h3>
<ul>
<li>Normalize implicit concatenated f-string quotes per part (<a href="https://redirect.github.com/astral-sh/ruff/pull/13539">#13539</a>)</li>
</ul>
<h3>Preview linter features</h3>
<ul>
<li>[<code>refurb</code>] implement <code>hardcoded-string-charset</code> (FURB156) (<a href="https://redirect.github.com/astral-sh/ruff/pull/13530">#13530</a>)</li>
<li>[<code>refurb</code>] Count codepoints not bytes for <code>slice-to-remove-prefix-or-suffix (FURB188)</code> (<a href="https://redirect.github.com/astral-sh/ruff/pull/13631">#13631</a>)</li>
</ul>
<h3>Rule changes</h3>
<ul>
<li>[<code>pylint</code>] Mark <code>PLE1141</code> fix as unsafe (<a href="https://redirect.github.com/astral-sh/ruff/pull/13629">#13629</a>)</li>
<li>[<code>flake8-async</code>] Consider async generators to be &quot;checkpoints&quot; for <code>cancel-scope-no-checkpoint</code> (<code>ASYNC100</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/13639">#13639</a>)</li>
<li>[<code>flake8-bugbear</code>] Do not suggest setting parameter <code>strict=</code> to <code>False</code> in <code>B905</code> diagnostic message (<a href="https://redirect.github.com/astral-sh/ruff/pull/13656">#13656</a>)</li>
<li>[<code>flake8-todos</code>] Only flag the word &quot;TODO&quot;, not words starting with &quot;todo&quot; (<code>TD006</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/13640">#13640</a>)</li>
<li>[<code>pycodestyle</code>] Fix whitespace-related false positives and false negatives inside type-parameter lists (<code>E231</code>, <code>E251</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/13704">#13704</a>)</li>
<li>[<code>flake8-simplify</code>] Stabilize preview behavior for <code>SIM115</code> so that the rule can detect files
being opened from a wider range of standard-library functions (<a href="https://redirect.github.com/astral-sh/ruff/pull/12959">#12959</a>).</li>
</ul>
<h3>CLI</h3>
<ul>
<li>Add explanation of fixable in <code>--statistics</code> command (<a href="https://redirect.github.com/astral-sh/ruff/pull/13774">#13774</a>)</li>
</ul>
<h3>Bug fixes</h3>
<ul>
<li>[<code>pyflakes</code>] Allow <code>ipytest</code> cell magic (<code>F401</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/13745">#13745</a>)</li>
<li>[<code>flake8-use-pathlib</code>] Fix <code>PTH123</code> false positive when <code>open</code> is passed a file descriptor (<a href="https://redirect.github.com/astral-sh/ruff/pull/13616">#13616</a>)</li>
<li>[<code>flake8-bandit</code>] Detect patterns from multi line SQL statements (<code>S608</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/13574">#13574</a>)</li>
<li>[<code>flake8-pyi</code>] - Fix dropped expressions in <code>PYI030</code> autofix (<a href="https://redirect.github.com/astral-sh/ruff/pull/13727">#13727</a>)</li>
</ul>
<h2>Contributors</h2>
<ul>
<li><a href="https://github.com/AlexWaygood"><code>@​AlexWaygood</code></a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md">ruff's changelog</a>.</em></p>
<blockquote>
<h2>0.7.0</h2>
<p>Check out the <a href="https://astral.sh/blog/ruff-v0.7.0">blog post</a> for a migration guide and overview of the changes!</p>
<h3>Breaking changes</h3>
<ul>
<li>The pytest rules <code>PT001</code> and <code>PT023</code> now default to omitting the decorator parentheses when there are no arguments
(<a href="https://redirect.github.com/astral-sh/ruff/pull/12838">#12838</a>, <a href="https://redirect.github.com/astral-sh/ruff/pull/13292">#13292</a>).
This was a change that we attempted to make in Ruff v0.6.0, but only partially made due to an error on our part.
See the <a href="https://astral.sh/blog/ruff-v0.7.0">blog post</a> for more details.</li>
<li>The <code>useless-try-except</code> rule (in our <code>tryceratops</code> category) has been recoded from <code>TRY302</code> to
<code>TRY203</code> (<a href="https://redirect.github.com/astral-sh/ruff/pull/13502">#13502</a>). This ensures Ruff's code is consistent with
the same rule in the <a href="https://github.com/guilatrova/tryceratops"><code>tryceratops</code></a> linter.</li>
<li>The <code>lint.allow-unused-imports</code> setting has been removed (<a href="https://redirect.github.com/astral-sh/ruff/pull/13677">#13677</a>). Use
<a href="https://docs.astral.sh/ruff/settings/#lint_pyflakes_allowed-unused-imports"><code>lint.pyflakes.allow-unused-imports</code></a>
instead.</li>
</ul>
<h3>Formatter preview style</h3>
<ul>
<li>Normalize implicit concatenated f-string quotes per part (<a href="https://redirect.github.com/astral-sh/ruff/pull/13539">#13539</a>)</li>
</ul>
<h3>Preview linter features</h3>
<ul>
<li>[<code>refurb</code>] implement <code>hardcoded-string-charset</code> (FURB156) (<a href="https://redirect.github.com/astral-sh/ruff/pull/13530">#13530</a>)</li>
<li>[<code>refurb</code>] Count codepoints not bytes for <code>slice-to-remove-prefix-or-suffix (FURB188)</code> (<a href="https://redirect.github.com/astral-sh/ruff/pull/13631">#13631</a>)</li>
</ul>
<h3>Rule changes</h3>
<ul>
<li>[<code>pylint</code>] Mark <code>PLE1141</code> fix as unsafe (<a href="https://redirect.github.com/astral-sh/ruff/pull/13629">#13629</a>)</li>
<li>[<code>flake8-async</code>] Consider async generators to be &quot;checkpoints&quot; for <code>cancel-scope-no-checkpoint</code> (<code>ASYNC100</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/13639">#13639</a>)</li>
<li>[<code>flake8-bugbear</code>] Do not suggest setting parameter <code>strict=</code> to <code>False</code> in <code>B905</code> diagnostic message (<a href="https://redirect.github.com/astral-sh/ruff/pull/13656">#13656</a>)</li>
<li>[<code>flake8-todos</code>] Only flag the word &quot;TODO&quot;, not words starting with &quot;todo&quot; (<code>TD006</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/13640">#13640</a>)</li>
<li>[<code>pycodestyle</code>] Fix whitespace-related false positives and false negatives inside type-parameter lists (<code>E231</code>, <code>E251</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/13704">#13704</a>)</li>
<li>[<code>flake8-simplify</code>] Stabilize preview behavior for <code>SIM115</code> so that the rule can detect files
being opened from a wider range of standard-library functions (<a href="https://redirect.github.com/astral-sh/ruff/pull/12959">#12959</a>).</li>
</ul>
<h3>CLI</h3>
<ul>
<li>Add explanation of fixable in <code>--statistics</code> command (<a href="https://redirect.github.com/astral-sh/ruff/pull/13774">#13774</a>)</li>
</ul>
<h3>Bug fixes</h3>
<ul>
<li>[<code>pyflakes</code>] Allow <code>ipytest</code> cell magic (<code>F401</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/13745">#13745</a>)</li>
<li>[<code>flake8-use-pathlib</code>] Fix <code>PTH123</code> false positive when <code>open</code> is passed a file descriptor (<a href="https://redirect.github.com/astral-sh/ruff/pull/13616">#13616</a>)</li>
<li>[<code>flake8-bandit</code>] Detect patterns from multi line SQL statements (<code>S608</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/13574">#13574</a>)</li>
<li>[<code>flake8-pyi</code>] - Fix dropped expressions in <code>PYI030</code> autofix (<a href="https://redirect.github.com/astral-sh/ruff/pull/13727">#13727</a>)</li>
</ul>
<h2>0.6.9</h2>
<h3>Preview features</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/astral-sh/ruff/commit/5e6de4e0c69660e8ca8608d1ac965216197756ce"><code>5e6de4e</code></a> Changelog for Ruff v0.7 (<a href="https://redirect.github.com/astral-sh/ruff/issues/13794">#13794</a>)</li>
<li><a href="https://github.com/astral-sh/ruff/commit/70e5c4a8ba679e80554c02bce7435dde085eb5bf"><code>70e5c4a</code></a> Recode <code>TRY302</code> to <code>TRY203</code> (<a href="https://redirect.github.com/astral-sh/ruff/issues/13502">#13502</a>)</li>
<li><a href="https://github.com/astral-sh/ruff/commit/9218d6bedc1063c110a8205db144fedc4366a3ec"><code>9218d6b</code></a> Remove <code>allow-unused-imports</code> setting from the common lint options (<a href="https://redirect.github.com/astral-sh/ruff/issues/13677">#13677</a>)</li>
<li><a href="https://github.com/astral-sh/ruff/commit/1b79ae9817d2c5da6ac81b069c5dff71e1e771c8"><code>1b79ae9</code></a> [ruff-0.7] Stabilise the expansion of <code>open-file-with-context-handler</code> to wor...</li>
<li><a href="https://github.com/astral-sh/ruff/commit/2b87587ac2b407c9dfb0d020339218e12321a898"><code>2b87587</code></a> [<code>flake8-pytest-style</code>] Fix defaults when <code>lint.flake8-pytest-style</code> config s...</li>
<li><a href="https://github.com/astral-sh/ruff/commit/d1e15f6246265dab43bbfbc0f9c10c8a8e720347"><code>d1e15f6</code></a> Remove tab-size setting (<a href="https://redirect.github.com/astral-sh/ruff/issues/12835">#12835</a>)</li>
<li><a href="https://github.com/astral-sh/ruff/commit/89a82158a12fe903c0bcbbbeafc9e00b9f888539"><code>89a8215</code></a> Remove error messages for removed CLI aliases (<a href="https://redirect.github.com/astral-sh/ruff/issues/12833">#12833</a>)</li>
<li><a href="https://github.com/astral-sh/ruff/commit/202c6a6d753ba07aee27220eaece04b7f52b9d6a"><code>202c6a6</code></a> Remove <code>output-format=text</code> setting (<a href="https://redirect.github.com/astral-sh/ruff/issues/12836">#12836</a>)</li>
<li><a href="https://github.com/astral-sh/ruff/commit/5c3c0c4705dfba0ba777df384482464e29e7d86c"><code>5c3c0c4</code></a> [red-knot] Inference for comparison of union types (<a href="https://redirect.github.com/astral-sh/ruff/issues/13781">#13781</a>)</li>
<li><a href="https://github.com/astral-sh/ruff/commit/6b7a738825a5268590fbf4e9724930734d326f32"><code>6b7a738</code></a> Add explanation of fixable in <code>--statistics</code> command (<a href="https://redirect.github.com/astral-sh/ruff/issues/13774">#13774</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/astral-sh/ruff/compare/0.5.0...0.7.0">compare view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3028a6c..911eb47 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
 
 ## [Unreleased]
 
+### Added
+
+-   :sparkles: Implement missing PipelineML filtering functionalities to let kedro display resume hints and avoid breaking kedro-viz ([#377, Calychas](https://github.com/Galileo-Galilei/kedro-mlflow/pull/377), [#601, Calychas](https://github.com/Galileo-Galilei/kedro-mlflow/pull/601))
+-   :sparkles: Sanitize parameters name with unsupported characters to avoid mlflow errors when logging ([#595, pascalwhoop](https://github.com/Galileo-Galilei/kedro-mlflow/pull/595))
+
 ## [0.13.2] - 2024-10-15
 
 ### Fixed
diff --git a/kedro_mlflow/framework/hooks/mlflow_hook.py b/kedro_mlflow/framework/hooks/mlflow_hook.py
index 41b4501..7aed8cc 100644
--- a/kedro_mlflow/framework/hooks/mlflow_hook.py
+++ b/kedro_mlflow/framework/hooks/mlflow_hook.py
@@ -1,3 +1,5 @@
+import os
+import re
 from logging import Logger, getLogger
 from pathlib import Path
 from tempfile import TemporaryDirectory
@@ -303,6 +305,11 @@ class MlflowHook:
                     d=params_inputs, recursive=self.recursive, sep=self.sep
                 )
 
+            # sanitize params inputs to avoid mlflow errors
+            params_inputs = {
+                self.sanitize_param_name(k): v for k, v in params_inputs.items()
+            }
+
             # logging parameters based on defined strategy
             for k, v in params_inputs.items():
                 self._log_param(k, v)
@@ -446,5 +453,26 @@ class MlflowHook:
             # hence it should not be modified. this is only a safeguard
             switch_catalog_logging(catalog, True)
 
+    def sanitize_param_name(self, name: str) -> str:
+        # regex taken from MLFlow codebase: https://github.com/mlflow/mlflow/blob/e40e782b6fcab473159e6d4fee85bc0fc10f78fd/mlflow/utils/validation.py#L140C1-L148C44
+
+        # for windows colon ':' are not accepted
+        matching_pattern = r"^[/\w.\- ]*$" if is_windows() else r"^[/\w.\- :]*$"
+
+        if re.match(matching_pattern, name):
+            return name
+        else:
+            replacement_pattern = r"[^/\w.\- ]" if is_windows() else r"[^/\w.\- :]"
+            # Replace invalid characters with underscore
+            sanitized_name = re.sub(replacement_pattern, "_", name)
+            self._logger.warning(
+                f"'{name}' is not a valid name for a mlflow paramter. It is renamed as '{sanitized_name}'"
+            )
+            return sanitized_name
+
+
+def is_windows():
+    return os.name == "nt"
+
 
 mlflow_hook = MlflowHook()
diff --git a/kedro_mlflow/pipeline/pipeline_ml.py b/kedro_mlflow/pipeline/pipeline_ml.py
index bdd6f80..5283190 100644
--- a/kedro_mlflow/pipeline/pipeline_ml.py
+++ b/kedro_mlflow/pipeline/pipeline_ml.py
@@ -4,17 +4,10 @@ from typing import Dict, Iterable, Optional, Union
 from kedro.pipeline import Pipeline
 from kedro.pipeline.node import Node
 
-MSG_NOT_IMPLEMENTED = (
-    "This method is not implemented because it does "
-    "not make sense for 'PipelineML'. "
-    "Manipulate directly the training pipeline and "
-    "recreate the 'PipelineML' with 'pipeline_ml_factory' factory."
-)
-
-MSG_WARNING_KEDRO_VIZ = (
+MSG_WARNING_PIPELINEML_DEMOTED = (
     "BEWARE - This 'Pipeline' is no longer a 'PipelineML' object. "
-    "This method is only implemented for compatibility with kedro-viz "
-    "but should never be used directly.\nSee "
+    "This method is only implemented for compatibility with kedro-viz and pipeline resume hints on failure."
+    "It should never be used directly.\nSee "
     "https://github.com/Galileo-Galilei/kedro-mlflow/issues/569 "
     " for more context. "
 )
@@ -173,16 +166,18 @@ class PipelineML(Pipeline):
         )
 
     def only_nodes(self, *node_names: str) -> "Pipeline":  # pragma: no cover
-        raise NotImplementedError(MSG_NOT_IMPLEMENTED)
+        self._logger.warning(MSG_WARNING_PIPELINEML_DEMOTED)
+        return self.training.only_nodes(*node_names)
 
     def only_nodes_with_namespace(
         self, node_namespace: str
     ) -> "Pipeline":  # pragma: no cover
-        self._logger.warning(MSG_WARNING_KEDRO_VIZ)
+        self._logger.warning(MSG_WARNING_PIPELINEML_DEMOTED)
         return self.training.only_nodes_with_namespace(node_namespace)
 
-    def only_nodes_with_inputs(self, *inputs: str) -> "PipelineML":  # pragma: no cover
-        raise NotImplementedError(MSG_NOT_IMPLEMENTED)
+    def only_nodes_with_inputs(self, *inputs: str) -> "Pipeline":  # pragma: no cover
+        self._logger.warning(MSG_WARNING_PIPELINEML_DEMOTED)
+        return self.training.only_nodes_with_inputs(*inputs)
 
     def from_inputs(self, *inputs: str) -> "PipelineML":  # pragma: no cover
         # exceptionnally, we don't call super() because it raises
@@ -194,10 +189,9 @@ class PipelineML(Pipeline):
         pipeline = self.training.from_inputs(*inputs)
         return self._turn_pipeline_to_ml(pipeline)
 
-    def only_nodes_with_outputs(
-        self, *outputs: str
-    ) -> "PipelineML":  # pragma: no cover
-        raise NotImplementedError(MSG_NOT_IMPLEMENTED)
+    def only_nodes_with_outputs(self, *outputs: str) -> "Pipeline":  # pragma: no cover
+        self._logger.warning(MSG_WARNING_PIPELINEML_DEMOTED)
+        return self.training.only_nodes_with_outputs(*outputs)
 
     def to_outputs(self, *outputs: str) -> "PipelineML":  # pragma: no cover
         # see from_inputs for an explanation of why we don't call super()
@@ -225,13 +219,13 @@ class PipelineML(Pipeline):
 
     def filter(
         self,
-        tags: Iterable[str] = None,
-        from_nodes: Iterable[str] = None,
-        to_nodes: Iterable[str] = None,
-        node_names: Iterable[str] = None,
-        from_inputs: Iterable[str] = None,
-        to_outputs: Iterable[str] = None,
-        node_namespace: str = None,
+        tags: Optional[Iterable[str]] = None,
+        from_nodes: Optional[Iterable[str]] = None,
+        to_nodes: Optional[Iterable[str]] = None,
+        node_names: Optional[Iterable[str]] = None,
+        from_inputs: Optional[Iterable[str]] = None,
+        to_outputs: Optional[Iterable[str]] = None,
+        node_namespace: Optional[str] = None,
     ) -> "Pipeline":
         # see from_inputs for an explanation of why we don't call super()
         pipeline = self.training.filter(
@@ -246,10 +240,11 @@ class PipelineML(Pipeline):
         return self._turn_pipeline_to_ml(pipeline)
 
     def __add__(self, other):  # pragma: no cover
-        raise NotImplementedError(MSG_NOT_IMPLEMENTED)
+        self._logger.warning(MSG_WARNING_PIPELINEML_DEMOTED)
+        return self.training + other
 
     def __sub__(self, other):  # pragma: no cover
-        self._logger.warning(MSG_WARNING_KEDRO_VIZ)
+        self._logger.warning(MSG_WARNING_PIPELINEML_DEMOTED)
         return self.training - other
 
     def __and__(self, other):  # pragma: no cover
@@ -258,7 +253,8 @@ class PipelineML(Pipeline):
         return self._turn_pipeline_to_ml(new_pipeline)
 
     def __or__(self, other):  # pragma: no cover
-        raise NotImplementedError(MSG_NOT_IMPLEMENTED)
+        self._logger.warning(MSG_WARNING_PIPELINEML_DEMOTED)
+        return self.training | other
 
 
 class KedroMlflowPipelineMLError(Exception):
diff --git a/setup.py b/setup.py
index 94be3ec..4c727a9 100644
--- a/setup.py
+++ b/setup.py
@@ -50,12 +50,12 @@ setup(
             "pytest-cov>=2.8.0, <6.0.0",
             "pytest-lazy-fixture>=0.6.0, <1.0.0",
             "pytest-mock>=3.1.0, <4.0.0",
-            "ruff>=0.5.0,<0.6.0",  # ensure consistency with pre-commit
+            "ruff>=0.5.0,<0.8.0",  # ensure consistency with pre-commit
             "scikit-learn>=0.23.0, <1.6.0",
             "kedro-datasets[pandas.CSVDataSet]",
         ],
         "dev": [
-            "pre-commit>=2.0.0,<4.0.0",
+            "pre-commit>=2.0.0,<5.0.0",
             "jupyter>=1.0.0,<2.0.0",
         ],
     },


Test patch:
diff --git a/tests/framework/hooks/test_hook_log_parameters.py b/tests/framework/hooks/test_hook_log_parameters.py
index 4bbd5d9..286858b 100644
--- a/tests/framework/hooks/test_hook_log_parameters.py
+++ b/tests/framework/hooks/test_hook_log_parameters.py
@@ -1,3 +1,4 @@
+import os
 from pathlib import Path
 from typing import Dict
 
@@ -74,6 +75,136 @@ def dummy_catalog():
     return catalog
 
 
+@pytest.mark.parametrize(
+    "param_name,expected_name",
+    [
+        ("valid_param", "valid_param"),
+        ("valid-param", "valid-param"),
+        ("invalid/param", "invalid/param"),
+        ("invalid.param", "invalid.param"),
+        ("[invalid]$param", "_invalid__param"),
+    ],
+)
+def test_parameter_name_sanitization(
+    kedro_project, dummy_run_params, param_name, expected_name
+):
+    mlflow_tracking_uri = (kedro_project / "mlruns").as_uri()
+    mlflow.set_tracking_uri(mlflow_tracking_uri)
+
+    node_inputs = {f"params:{param_name}": "test_value"}
+
+    bootstrap_project(kedro_project)
+    with KedroSession.create(
+        project_path=kedro_project,
+    ) as session:
+        context = session.load_context()
+        mlflow_node_hook = MlflowHook()
+        mlflow_node_hook.after_context_created(context)
+
+        with mlflow.start_run():
+            mlflow_node_hook.before_pipeline_run(
+                run_params=dummy_run_params,
+                pipeline=Pipeline([]),
+                catalog=DataCatalog(),
+            )
+            mlflow_node_hook.before_node_run(
+                node=node(func=lambda x: x, inputs=dict(x="a"), outputs=None),
+                catalog=DataCatalog(),
+                inputs=node_inputs,
+                is_async=False,
+            )
+            run_id = mlflow.active_run().info.run_id
+
+        mlflow_client = MlflowClient(mlflow_tracking_uri)
+        current_run = mlflow_client.get_run(run_id)
+        assert expected_name in current_run.data.params
+        assert current_run.data.params[expected_name] == "test_value"
+
+
+@pytest.mark.skipif(
+    os.name != "nt", reason="Windows does not log params with colon symbol"
+)
+def test_parameter_name_with_colon_sanitization_on_windows(
+    kedro_project, dummy_run_params
+):
+    param_name = "valid:param"
+    expected_name = "valid_param"
+
+    mlflow_tracking_uri = (kedro_project / "mlruns").as_uri()
+    mlflow.set_tracking_uri(mlflow_tracking_uri)
+
+    node_inputs = {f"params:{param_name}": "test_value"}
+
+    bootstrap_project(kedro_project)
+    with KedroSession.create(
+        project_path=kedro_project,
+    ) as session:
+        context = session.load_context()
+        mlflow_node_hook = MlflowHook()
+        mlflow_node_hook.after_context_created(context)
+
+        with mlflow.start_run():
+            mlflow_node_hook.before_pipeline_run(
+                run_params=dummy_run_params,
+                pipeline=Pipeline([]),
+                catalog=DataCatalog(),
+            )
+            mlflow_node_hook.before_node_run(
+                node=node(func=lambda x: x, inputs=dict(x="a"), outputs=None),
+                catalog=DataCatalog(),
+                inputs=node_inputs,
+                is_async=False,
+            )
+            run_id = mlflow.active_run().info.run_id
+
+        mlflow_client = MlflowClient(mlflow_tracking_uri)
+        current_run = mlflow_client.get_run(run_id)
+        assert expected_name in current_run.data.params
+        assert current_run.data.params[expected_name] == "test_value"
+
+
+@pytest.mark.skipif(
+    os.name == "nt", reason="Linux and Mac do log params with colon symbol"
+)
+def test_parameter_name_with_colon_sanitization_on_mac_linux(
+    kedro_project, dummy_run_params
+):
+    param_name = "valid:param"
+    expected_name = "valid:param"
+
+    mlflow_tracking_uri = (kedro_project / "mlruns").as_uri()
+    mlflow.set_tracking_uri(mlflow_tracking_uri)
+
+    node_inputs = {f"params:{param_name}": "test_value"}
+
+    bootstrap_project(kedro_project)
+    with KedroSession.create(
+        project_path=kedro_project,
+    ) as session:
+        context = session.load_context()
+        mlflow_node_hook = MlflowHook()
+        mlflow_node_hook.after_context_created(context)
+
+        with mlflow.start_run():
+            mlflow_node_hook.before_pipeline_run(
+                run_params=dummy_run_params,
+                pipeline=Pipeline([]),
+                catalog=DataCatalog(),
+            )
+            mlflow_node_hook.before_node_run(
+                node=node(func=lambda x: x, inputs=dict(x="a"), outputs=None),
+                catalog=DataCatalog(),
+                inputs=node_inputs,
+                is_async=False,
+            )
+            run_id = mlflow.active_run().info.run_id
+
+        mlflow_client = MlflowClient(mlflow_tracking_uri)
+        current_run = mlflow_client.get_run(run_id)
+        assert expected_name in current_run.data.params
+        assert current_run.data.params[expected_name] == "test_value"
+
+
 def test_pipeline_run_hook_getting_configs(
     kedro_project,
     dummy_run_params,
diff --git a/tests/pipeline/test_pipeline_ml.py b/tests/pipeline/test_pipeline_ml.py
index a56f8ff..63d3763 100644
--- a/tests/pipeline/test_pipeline_ml.py
+++ b/tests/pipeline/test_pipeline_ml.py
@@ -90,6 +90,7 @@ def pipeline_ml_with_intermediary_artifacts():
                 inputs="data",
                 outputs="encoder",
                 tags=["training"],
+                name="node_fit_encoder_fun_data",
             ),
             node(
                 func=apply_encoder_fun,
@@ -258,11 +259,69 @@ def catalog_with_parameters():
     return catalog_with_parameters
 
 
+def test_pipeline_ml_only_nodes(
+    caplog,
+    pipeline_ml_with_intermediary_artifacts,
+):
+    """When the pipeline is filtered with only_nodes, we return only the training pipeline. This is for kedro-viz and resume hints compatibility"""
+
+    # pipeline_ml_with_namespace are fixture in conftest
+
+    # remember : the arguments are iterable, so do not pass string directly (e.g ["training"] rather than training)
+
+    filtered_pipeline_ml = pipeline_ml_with_intermediary_artifacts.only_nodes(
+        "node_fit_encoder_fun_data"
+    )
+
+    # PipelineML class must be preserved when filtering
+    # inference should be unmodified
+    # training pipeline nodes must be identical to kedro filtering.
+    assert isinstance(filtered_pipeline_ml, Pipeline)
+    assert not isinstance(filtered_pipeline_ml, PipelineML)
+    assert str(filtered_pipeline_ml) == str(
+        pipeline_ml_with_intermediary_artifacts.training.only_nodes(
+            "node_fit_encoder_fun_data"
+        )
+    )
+    assert (
+        "for compatibility with kedro-viz and pipeline resume hints on failure"
+        in caplog.text
+    )
+
+
+def test_pipeline_ml_only_nodes_with_outputs(
+    caplog,
+    pipeline_ml_with_intermediary_artifacts,
+):
+    """When the pipeline is filtered with only_nodes, we return only the training pipeline. This is for kedro-viz and resume hints compatibility"""
+
+    # pipeline_ml_with_intermediary_artifacts are fixture in conftest
+
+    # remember : the arguments are iterable, so do not pass string directly (e.g ["training"] rather than training)
+
+    filtered_pipeline_ml = (
+        pipeline_ml_with_intermediary_artifacts.only_nodes_with_outputs("data")
+    )
+
+    # PipelineML class must be preserved when filtering
+    # inference should be unmodified
+    # training pipeline nodes must be identical to kedro filtering.
+    assert isinstance(filtered_pipeline_ml, Pipeline)
+    assert not isinstance(filtered_pipeline_ml, PipelineML)
+    assert str(filtered_pipeline_ml) == str(
+        pipeline_ml_with_intermediary_artifacts.training.only_nodes_with_outputs("data")
+    )
+    assert (
+        "for compatibility with kedro-viz and pipeline resume hints on failure"
+        in caplog.text
+    )
+
+
 def test_pipeline_ml_only_nodes_with_namespace(
     caplog,
     pipeline_ml_with_namespace,
 ):
-    """When the pipeline is filtered with only_nodes_with_namespace, we return only the training pipeline. This is for kedro viz compatibility"""
+    """When the pipeline is filtered with only_nodes, we return only the training pipeline. This is for kedro-viz and resume hints compatibility"""
 
     # pipeline_ml_with_namespace are fixture in conftest
 
@@ -278,7 +337,10 @@ def test_pipeline_ml_only_nodes_with_namespace(
     assert isinstance(filtered_pipeline_ml, Pipeline)
     assert not isinstance(filtered_pipeline_ml, PipelineML)
     assert str(filtered_pipeline_ml) == str(pipeline_ml_with_namespace.training)
-    assert "kedro-viz but should never be" in caplog.text
+    assert (
+        "for compatibility with kedro-viz and pipeline resume hints on failure"
+        in caplog.text
+    )
 
 
 def test_pipeline_ml_substraction(
@@ -301,7 +363,58 @@ def test_pipeline_ml_substraction(
     # training pipeline nodes must be identical to kedro filtering.
     assert isinstance(filtered_pipeline_ml, Pipeline)
     assert not isinstance(filtered_pipeline_ml, PipelineML)
-    assert "kedro-viz but should never be" in caplog.text
+    assert (
+        "for compatibility with kedro-viz and pipeline resume hints on failure"
+        in caplog.text
+    )
+
+
+def test_pipeline_ml_addition(
+    caplog,
+    pipeline_ml_with_namespace,
+    pipeline_ml_with_tag,
+):
+    """When the pipeline is filtered with only_nodes_with_namespace, we return only the training pipeline. This is for kedro viz compatibility"""
+
+    # pipeline_ml_with_namespace are fixture in conftest
+
+    # remember : the arguments are iterable, so do not pass string directly (e.g ["training"] rather than training)
+
+    sum_of_pipeline_ml = pipeline_ml_with_namespace + pipeline_ml_with_tag
+
+    # PipelineML class must be preserved when filtering
+    # inference should be unmodified
+    # training pipeline nodes must be identical to kedro filtering.
+    assert isinstance(sum_of_pipeline_ml, Pipeline)
+    assert not isinstance(sum_of_pipeline_ml, PipelineML)
+    assert (
+        "for compatibility with kedro-viz and pipeline resume hints on failure"
+        in caplog.text
+    )
+
+
+def test_pipeline_ml_or(
+    caplog,
+    pipeline_ml_with_namespace,
+    pipeline_ml_with_tag,
+):
+    """When the pipeline is filtered with only_nodes_with_namespace, we return only the training pipeline. This is for kedro viz compatibility"""
+
+    # pipeline_ml_with_namespace are fixture in conftest
+
+    # remember : the arguments are iterable, so do not pass string directly (e.g ["training"] rather than training)
+
+    or_of_pipeline_ml = pipeline_ml_with_namespace | pipeline_ml_with_tag
+
+    # PipelineML class must be preserved when filtering
+    # inference should be unmodified
+    # training pipeline nodes must be identical to kedro filtering.
+    assert isinstance(or_of_pipeline_ml, Pipeline)
+    assert not isinstance(or_of_pipeline_ml, PipelineML)
+    assert (
+        "for compatibility with kedro-viz and pipeline resume hints on failure"
+        in caplog.text
+    )
 
 
 @pytest.mark.parametrize(
@@ -316,7 +429,7 @@ def test_pipeline_ml_substraction(
         (None, None, None, None, ["data"]),
     ],
 )
-def test_filtering_pipeline_ml(
+def test_pipeline_ml_filtering(
     mocker,
     pipeline_with_tag,
     pipeline_ml_with_tag,
@@ -374,7 +487,7 @@ def test_filtering_pipeline_ml(
         (None, None, None, ["preprocess_fun([raw_data]) -> [data]"], None),
     ],
 )
-def test_filtering_generate_invalid_pipeline_ml(
+def test_pipeline_ml__filtering_generate_invalid_pipeline_ml(
     mocker,
     pipeline_ml_obj,
     tags,
@@ -405,7 +518,7 @@ def test_filtering_generate_invalid_pipeline_ml(
 #     pass
 
 
-def test_too_many_free_inputs():
+def test_pipeline_ml_too_many_free_inputs():
     with pytest.raises(KedroMlflowPipelineMLError, match="No free input is allowed"):
         pipeline_ml_factory(
             training=Pipeline(
@@ -430,7 +543,7 @@ def test_too_many_free_inputs():
         )
 
 
-def test_tagging(pipeline_ml_with_tag):
+def test_pipeline_ml_tagging(pipeline_ml_with_tag):
     new_pl = pipeline_ml_with_tag.tag(["hello"])
     assert all(["hello" in node.tags for node in new_pl.nodes])