@@ -377,3 +377,240 @@ For comparison the :class:`janus_core.processing.observables.Velocity` built-in'
377377
378378 def __call__(self, atoms: Atoms) -> list[float]:
379379 return atoms.get_velocities()[self.atoms_slice, :][:, self._indices].flatten()
380+
381+
382+ Deprecating a parameter
383+ =======================
384+
385+ When deprecating a parameter, we aim to include a warning for at least one release cycle
386+ before removing the original parameter, to minimise the impact on users.
387+
388+ This deprecation period will be extended following the release of v1.
389+
390+ Deprecation should be handled for both the CLI and Python interfaces,
391+ as described below for the requirements in renaming ``model_path`` to ``model``,
392+ as well as renaming ``filter_func`` to ``filter_class`` when these requirements differ.
393+
394+
395+ Python interface
396+ ----------------
397+
398+ 1. Update the parameters.
399+
400+ In addition to adding the new parameter, the old parameter default should be changed to ``None``:
401+
402+ .. code-block:: python
403+
404+ def __init__(
405+ self,
406+ ...
407+ model: PathLike | None = None,
408+ model_path: PathLike | None = None,
409+ ...
410+ filter_func: Callable | str | None = None,
411+ filter_kwargs: dict[str, Any] | None = None,
412+ )
413+
414+ All references to the old paramter must be updated, which may include cases where separate calculations interact:
415+
416+ .. code-block:: python
417+
418+ self.minimize_kwargs.setdefault("filter_class", None)
419+
420+
421+ 2. Handle the duplicated parameters.
422+
423+ If both are specifed, we should raise an error if possible:
424+
425+ .. code-block:: python
426+
427+ if model_path:
428+ # `model` is a new parameter, so there is no reason to be using both
429+ if model:
430+ raise ValueError(
431+ "`model` has replaced `model_path`. Please only use `model`"
432+ )
433+ self.model = model_path
434+
435+
436+ If the new parameter' s default is not ` ` None` ` , it' s usually acceptable to allow both,
437+ prefering the old option, as this is only not ``None`` if it has been set explicitly:
438+
439+ .. code-block:: python
440+
441+ if filter_func:
442+ filter_class = filter_func
443+
444+
445+ 3. Raise a ``FutureWarning`` if the old parameter is set.
446+
447+ This usually needs to be later than the duplicate parameters are handled,
448+ to ensure logging has been set up:
449+
450+ .. code-block:: python
451+
452+ if model_path:
453+ warn(
454+ "`model_path` has been deprecated. Please use `model`.",
455+ FutureWarning,
456+ stacklevel=2,
457+ )
458+
459+
460+ 4. Update the parameter docstrings:
461+
462+ .. code-block:: python
463+
464+ """"
465+ Parameters
466+ ----------
467+ ...
468+ model
469+ MLIP model label, path to model, or loaded model. Default is `None`.
470+ model_path
471+ Deprecated. Please use `model`.
472+ ...
473+ """"
474+
475+
476+ 5. Test that the old option still correctly sets the new parameter.
477+
478+ This should also check that a ``FutureWarning`` is raised:
479+
480+ .. code-block:: python
481+
482+ def test_deprecation_model_path():
483+ """Test FutureWarning raised for model_path."""
484+ skip_extras("mace")
485+
486+ with pytest.warns(FutureWarning, match="`model_path` has been deprecated"):
487+ sp = SinglePoint(
488+ arch="mace_mp",
489+ model_path=MACE_PATH,
490+ struct=DATA_PATH / "NaCl.cif",
491+ )
492+
493+ assert sp.struct.calc.parameters["model"] == str(MACE_PATH.as_posix())
494+
495+
496+ 6. Replace the old option in tests and documentation, including:
497+
498+ - README
499+ - Python user guide
500+ - Python tutorial notebooks
501+ - Python tests
502+
503+
504+ Command line
505+ ------------
506+
507+ 1. Add the new parameter:
508+
509+ .. code-block:: python
510+
511+ Model = Annotated[
512+ str | None,
513+ Option(
514+ help="MLIP model name, or path to model.", rich_help_panel="MLIP calculator"
515+ ),
516+ ]
517+
518+ 2. Add the ``deprecated_option`` callback to the old parameter,
519+ which prints a warning if it is used, and hide the option:
520+
521+ .. code-block:: python
522+
523+ from janus_core.cli.utils import deprecated_option
524+
525+ ModelPath = Annotated[
526+ str | None,
527+ Option(
528+ help="Deprecated. Please use --model",
529+ rich_help_panel="MLIP calculator",
530+ callback=deprecated_option,
531+ hidden=True,
532+ ),
533+ ]
534+
535+ 3. Update the docstrings, with a deprecation note for consistency:
536+
537+ .. code-block:: python
538+
539+ """
540+ Parameters
541+ ----------
542+ ...
543+ model
544+ Path to MLIP model or name of model. Default is `None`.
545+ model_path
546+ Deprecated. Please use `model`.
547+ ...
548+ """"
549+
550+
551+ 4. Handle the duplicate values.
552+
553+ If the parameter is passed directly to the Python interface, and no other parameters depend on it,
554+ it may be sufficient to pass both through and handle it within the Python interface, as is the case for ``model_path``:
555+
556+ .. code-block:: python
557+
558+ singlepoint_kwargs = {
559+ ...
560+ "model": model,
561+ "model_path": model_path,
562+ ...
563+ }
564+
565+
566+ However, it may be necessary to ensure only one value has been specified before the Python interface is reached,
567+ as is the case for ``filter_func`` in ``geomopt``:
568+
569+ .. code-block:: python
570+
571+ if filter_func and filter_class:
572+ raise ValueError("--filter-func is deprecated, please only use --filter")
573+
574+
575+ 5. Test that the old option still correctly sets the new parameter.
576+
577+ Ideally, this would also check that a ``FutureWarning`` is raised,
578+ but capture of this is inconsistent during tests, so we do not currently test against this:
579+
580+ .. code-block:: python
581+
582+ def test_model_path_deprecated(tmp_path):
583+ """Test model_path sets model."""
584+ file_prefix = tmp_path / "NaCl"
585+ results_path = tmp_path / "NaCl-results.extxyz"
586+ log_path = tmp_path / "test.log"
587+
588+ result = runner.invoke(
589+ app,
590+ [
591+ "singlepoint",
592+ "--struct",
593+ DATA_PATH / "NaCl.cif",
594+ "--arch",
595+ "mace_mp",
596+ "--model-path",
597+ MACE_PATH,
598+ "--log",
599+ log_path,
600+ "--file-prefix",
601+ file_prefix,
602+ "--no-tracker",
603+ ],
604+ )
605+ assert result.exit_code == 0
606+
607+ atoms = read(results_path)
608+ assert "model" in atoms.info
609+ assert atoms.info["model"] == str(MACE_PATH.as_posix())
610+
611+ 6. Replace the old CLI option in tests and documentation, including:
612+
613+ - README
614+ - CLI user guide
615+ - CLI tutorial notebooks
616+ - CLI tests
0 commit comments