diff --git a/news/wavelength-config.rst b/news/wavelength-config.rst new file mode 100644 index 0000000..1bf4566 --- /dev/null +++ b/news/wavelength-config.rst @@ -0,0 +1,23 @@ +**Added:** + +* Functionality to read wavelength and anode type directly from a diffpy configuration file. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/labpdfproc/tools.py b/src/diffpy/labpdfproc/tools.py index 8511984..ea5299d 100644 --- a/src/diffpy/labpdfproc/tools.py +++ b/src/diffpy/labpdfproc/tools.py @@ -7,6 +7,7 @@ XQUANTITIES, ) from diffpy.utils.tools import ( + _load_config, check_and_build_global_config, compute_mud, get_package_info, @@ -159,6 +160,52 @@ def set_input_lists(args): return args +def load_wavelength_from_config_file(args): + """Load wavelength and anode type from config files. + + It prioritizes values in the following order: + 1. cli inputs, 2. local config file, 3. global config file. + + Parameters + ---------- + args : argparse.Namespace + The arguments from the parser. + + Returns + ------- + args : argparse.Namespace + The updated arguments with the updated wavelength and anode type. + """ + global_config = _load_config(Path().home() / "diffpyconfig.json") + local_config = _load_config(Path().cwd() / "diffpyconfig.json") + local_has_data = local_config and ( + "wavelength" in local_config or "anode_type" in local_config + ) + global_has_data = global_config and ( + "wavelength" in global_config or "anode_type" in global_config + ) + if not local_has_data and not global_has_data: + print( + "No configuration file was found containing information " + "about the wavelength or anode type. \n" + "You can add the wavelength or anode type " + "to a configuration file on the current computer " + "and it will be automatically associated with " + "subsequent diffpy data by default. \n" + "You will only have to do that once. \n" + "For more information, please refer to www.diffpy.org/" + "diffpy.labpdfproc/examples/toolsexample.html" + ) + + if args.wavelength or args.anode_type: + return args + config = local_config if local_has_data else global_config + if config: + args.wavelength = args.wavelength or config.get("wavelength") + args.anode_type = args.anode_type or config.get("anode_type") + return args + + def set_wavelength(args): """Set the wavelength based on the given anode_type or wavelength. @@ -174,7 +221,7 @@ def set_wavelength(args): ------ ValueError Raised if: - (1) neither wavelength or anode type is provided, + (1) neither wavelength or anode type is provided and xtype is not the two-theta grid, (2) both are provided, (3) anode_type is not one of the known sources, @@ -185,7 +232,7 @@ def set_wavelength(args): args : argparse.Namespace The updated arguments with the wavelength. """ - # first load values from config file + args = load_wavelength_from_config_file(args) if args.wavelength is None and args.anode_type is None: if args.xtype not in ANGLEQUANTITIES: raise ValueError( @@ -209,7 +256,7 @@ def set_wavelength(args): ) if matched_anode_type is None: raise ValueError( - f"Anode type not recognized. " + f"Anode type '{args.anode_type}' not recognized. " f"Please rerun specifying an anode_type " f"from {*known_sources, }." ) @@ -217,7 +264,7 @@ def set_wavelength(args): args.wavelength = WAVELENGTHS[args.anode_type] elif args.wavelength is not None and args.wavelength <= 0: raise ValueError( - "No valid wavelength. " + f"Wavelength = {args.wavelength} is not valid. " "Please rerun specifying a known anode_type " "or a positive wavelength." ) diff --git a/tests/conftest.py b/tests/conftest.py index 2dcb4a5..63d4646 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -60,6 +60,7 @@ def user_filesystem(tmp_path): f.write(f"{str(input_dir.resolve() / 'good_data.txt')}\n") home_config_data = { + "wavelength": 0.3, "owner_name": "home_username", "owner_email": "home@email.com", "owner_orcid": "home_orcid", diff --git a/tests/test_tools.py b/tests/test_tools.py index 6319a42..9ccc4b3 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -1,3 +1,4 @@ +import json import os import re from pathlib import Path @@ -11,6 +12,7 @@ load_package_info, load_user_info, load_user_metadata, + load_wavelength_from_config_file, preprocessing_args, set_input_lists, set_mud, @@ -201,6 +203,131 @@ def test_set_output_directory_bad(user_filesystem): assert not Path(actual_args.output_directory).is_dir() +@pytest.mark.parametrize( + "inputs, expected", + [ + # Test with only a home config file (no local config), + # expect to return values directly from args + # if either wavelength or anode type is specified, + # otherwise update args with values from the home config file + # (wavelength=0.3, no anode type). + # This test only checks loading behavior, + # not value validation (which is handled by `set_wavelength`). + # C1: no args, expect to update arg values from home config + ([], {"wavelength": 0.3, "anode_type": None}), + # C2: wavelength provided, expect to return args unchanged + (["--wavelength", "0.25"], {"wavelength": 0.25, "anode_type": None}), + # C3: anode type provided, expect to return args unchanged + (["--anode-type", "Mo"], {"wavelength": None, "anode_type": "Mo"}), + # C4: both wavelength and anode type provided, + # expect to return args unchanged + ( + ["--wavelength", "0.7", "--anode-type", "Mo"], + {"wavelength": 0.7, "anode_type": "Mo"}, + ), + ], +) +def test_load_wavelength_from_config_file_with_home_conf_file( + mocker, user_filesystem, inputs, expected +): + cwd = Path(user_filesystem) + home_dir = cwd / "home_dir" + mocker.patch("pathlib.Path.home", lambda _: home_dir) + os.chdir(cwd) + + cli_inputs = ["data.xy", "--mud", "2.5"] + inputs + actual_args = get_args(cli_inputs) + actual_args = load_wavelength_from_config_file(actual_args) + assert actual_args.wavelength == expected["wavelength"] + assert actual_args.anode_type == expected["anode_type"] + + +@pytest.mark.parametrize( + "inputs, expected", + [ + # Test when a local config file exists, + # expect to return values directly from args + # if either wavelength or anode type is specified, + # otherwise update args with values from the local config file + # (wavelength=0.6, no anode type). + # Results should be the same whether if the home config exists. + # This test only checks loading behavior, + # not value validation (which is handled by `set_wavelength`). + # C1: no args, expect to update arg values from local config + ([], {"wavelength": 0.6, "anode_type": None}), + # C2: wavelength provided, expect to return args unchanged + (["--wavelength", "0.25"], {"wavelength": 0.25, "anode_type": None}), + # C3: anode type provided, expect to return args unchanged + (["--anode-type", "Mo"], {"wavelength": None, "anode_type": "Mo"}), + # C4: both wavelength and anode type provided, + # expect to return args unchanged + ( + ["--wavelength", "0.7", "--anode-type", "Mo"], + {"wavelength": 0.7, "anode_type": "Mo"}, + ), + ], +) +def test_load_wavelength_from_config_file_with_local_conf_file( + mocker, user_filesystem, inputs, expected +): + cwd = Path(user_filesystem) + home_dir = cwd / "home_dir" + mocker.patch("pathlib.Path.home", lambda _: home_dir) + os.chdir(cwd) + local_config_data = {"wavelength": 0.6} + with open(cwd / "diffpyconfig.json", "w") as f: + json.dump(local_config_data, f) + + cli_inputs = ["data.xy", "--mud", "2.5"] + inputs + actual_args = get_args(cli_inputs) + actual_args = load_wavelength_from_config_file(actual_args) + assert actual_args.wavelength == expected["wavelength"] + assert actual_args.anode_type == expected["anode_type"] + + # remove home config file, expect the same results + confile = home_dir / "diffpyconfig.json" + os.remove(confile) + assert actual_args.wavelength == expected["wavelength"] + assert actual_args.anode_type == expected["anode_type"] + + +@pytest.mark.parametrize( + "inputs, expected", + [ + # Test when no config files exist, + # expect to return args without modification. + # This test only checks loading behavior, + # not value validation (which is handled by `set_wavelength`). + # C1: no args + ([], {"wavelength": None, "anode_type": None}), + # C1: wavelength provided + (["--wavelength", "0.25"], {"wavelength": 0.25, "anode_type": None}), + # C2: anode type provided + (["--anode-type", "Mo"], {"wavelength": None, "anode_type": "Mo"}), + # C4: both wavelength and anode type provided + ( + ["--wavelength", "0.7", "--anode-type", "Mo"], + {"wavelength": 0.7, "anode_type": "Mo"}, + ), + ], +) +def test_load_wavelength_from_config_file_without_conf_files( + mocker, user_filesystem, inputs, expected +): + cwd = Path(user_filesystem) + home_dir = cwd / "home_dir" + mocker.patch("pathlib.Path.home", lambda _: home_dir) + os.chdir(cwd) + confile = home_dir / "diffpyconfig.json" + os.remove(confile) + + cli_inputs = ["data.xy", "--mud", "2.5"] + inputs + actual_args = get_args(cli_inputs) + actual_args = load_wavelength_from_config_file(actual_args) + assert actual_args.wavelength == expected["wavelength"] + assert actual_args.anode_type == expected["anode_type"] + + @pytest.mark.parametrize( "inputs, expected", [ @@ -279,13 +406,13 @@ def test_set_wavelength(inputs, expected): ( # C3: invalid anode type # expect error asking to specify a valid anode type ["--anode-type", "invalid"], - f"Anode type not recognized. " + f"Anode type 'invalid' not recognized. " f"Please rerun specifying an anode_type from {*known_sources, }.", ), ( # C4: invalid wavelength # expect error asking to specify a valid wavelength or anode type - ["--wavelength", "0"], - "No valid wavelength. " + ["--wavelength", "-0.2"], + "Wavelength = -0.2 is not valid. " "Please rerun specifying a known anode_type " "or a positive wavelength.", ),