diff --git a/.gitignore b/.gitignore index 48b85e1..d996915 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,7 @@ cython_debug/ # linting .ruff_cache + +# SimLogger data directory (test artifacts) +data/ + diff --git a/README.md b/README.md index c317085..1efcbe0 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,63 @@ print(sampleArrayLoaded) # [1, 2, 3, 4] ``` +## Autosave Command Line Arguments +Never forget to save your argparse CLI arguments again! SimLogger provides **automatic** saving of command-line arguments. + +### Fully Automatic (Recommended) +Use `SimLogger.ArgumentParser` instead of `argparse.ArgumentParser` for completely automatic saving: + +```python +from SimLogger import SimLogger + +# Set your simulation tag once +SimLogger.setSimTag('myExperiment') + +# Use SimLogger.ArgumentParser instead of argparse.ArgumentParser +parser = SimLogger.ArgumentParser(description='My Experiment') +parser.add_argument('--learning_rate', type=float, default=0.001) +parser.add_argument('--epochs', type=int, default=100) +parser.add_argument('--model_name', type=str, default='resnet50') + +# Arguments are AUTOMATICALLY saved when you call parse_args()! +args = parser.parse_args() + +# Outputs +# 2024-06-25 10:44:44,063 [INFO ] Logger Loaded +# 2024-06-25 10:44:44,064 [INFO ] ARGS,myExperiment,saved,data/obj/myExperiment_args_2024-06-25_10-44-44.pkl +# 2024-06-25 10:44:44,064 [INFO ] ARG,myExperiment,learning_rate,0.001 +# 2024-06-25 10:44:44,064 [INFO ] ARG,myExperiment,epochs,100 +# 2024-06-25 10:44:44,064 [INFO ] ARG,myExperiment,model_name,resnet50 +``` + +You can also pass the `simTag` directly to the parser: +```python +parser = SimLogger.ArgumentParser(simTag='myExperiment') +``` + +### Manual Saving +If you prefer to manually control when arguments are saved, you can use the `saveArgs()` function: + +```python +import argparse +from SimLogger import SimLogger + +parser = argparse.ArgumentParser() +parser.add_argument('--learning_rate', type=float, default=0.001) +args = parser.parse_args() + +# Manually save all CLI arguments +SimLogger.saveArgs('myExperiment', args) +``` + +### Loading Saved Arguments +Later, you can load the saved arguments: +```python +from SimLogger import SimLogger +loaded_args = SimLogger.getObj('myExperiment', 'args') +print(loaded_args['learning_rate']) # 0.001 +``` + ## Installation Install the python package ```pip install SimLogger``` diff --git a/src/SimLogger/SimLogger.py b/src/SimLogger/SimLogger.py index 657daf2..32e15a1 100644 --- a/src/SimLogger/SimLogger.py +++ b/src/SimLogger/SimLogger.py @@ -5,8 +5,10 @@ import pandas as pd import os from git import Repo +import argparse isLoaded = False +_simTag = None # Global variable to store the simulation tag for auto-saving def setupLogger(fileName="example.log", githubLink=None): @@ -64,6 +66,45 @@ def logNotes(notes): logging.info(notes) +def saveArgs(simTag, args, objFolder=os.path.join("data", "obj"), makeNote=True): + """Save argparse command line arguments to a pickle file and optionally log them. + This is a convenience function to automatically save CLI arguments from argparse. + + Args: + simTag (str): Unique tag for the simulation + args (argparse.Namespace): Parsed arguments from argparse + ArgumentParser.parse_args() + objFolder (str): Folder where the pickled file will be saved + makeNote (bool): Sets whether an additional note is made about the + arguments + + Returns: + filePath (str): the unique file path where the pickled arguments are saved + + Example: + >>> import argparse + >>> from SimLogger import SimLogger + >>> parser = argparse.ArgumentParser() + >>> parser.add_argument('--param1', type=int, default=10) + >>> parser.add_argument('--param2', type=str, default='test') + >>> args = parser.parse_args() + >>> SimLogger.saveArgs('mySimulation', args) + """ + # Convert argparse.Namespace to dictionary for better readability + args_dict = vars(args) + + # Save the arguments as a pickled object + filePath = saveObj(simTag, "args", args_dict, objFolder=objFolder, makeNote=False) + + # Log each argument individually for easy reference + if makeNote: + logNotes("ARGS," + simTag + ",saved," + filePath) + for key, value in args_dict.items(): + logNotes("ARG," + simTag + "," + key + "," + str(value)) + + return filePath + + def saveObj(simTag, objTag, obj, objFolder=os.path.join("data", "obj"), makeNote=False): """Save an object (obj) to pickle file in the object folder. The file name will be the {simTag}_{objTag}_{dateTimeString}.pkl @@ -228,3 +269,89 @@ def isSimTagUsed(simTag, objFolder=os.path.join("data", "obj")): if len(fileList) > 0: return False return True + + +class ArgumentParser(argparse.ArgumentParser): + """Custom ArgumentParser that automatically saves arguments when + parse_args() is called. + + This class extends argparse.ArgumentParser to provide automatic saving of + command-line arguments. When parse_args() is called, the arguments are + automatically saved using saveArgs() if a simulation tag has been set. + + To use this feature: + 1. Set the simulation tag using setSimTag() before parsing arguments + 2. Use SimLogger.ArgumentParser instead of argparse.ArgumentParser + 3. Call parse_args() as usual - arguments will be saved automatically + + Example: + >>> from SimLogger import SimLogger + >>> SimLogger.setSimTag('myExperiment') + >>> parser = SimLogger.ArgumentParser() + >>> parser.add_argument('--learning_rate', type=float, default=0.001) + >>> args = parser.parse_args() # Arguments are automatically saved! + """ + + def __init__(self, *args, simTag=None, autoSave=True, **kwargs): + """Initialize the ArgumentParser. + + Args: + simTag (str): Optional simulation tag for auto-saving arguments. + If not provided, uses the global simTag set by setSimTag(). + autoSave (bool): Whether to automatically save arguments when + parse_args() is called. Default is True. + *args, **kwargs: All other arguments are passed to + argparse.ArgumentParser + """ + super().__init__(*args, **kwargs) + self._simTag = simTag + self._autoSave = autoSave + + def parse_args(self, args=None, namespace=None): + """Parse arguments and automatically save them if autoSave is enabled. + + Args: + args: List of strings to parse. If None, uses sys.argv. + namespace: Object to populate with parsed arguments. + + Returns: + Namespace object with parsed arguments + """ + parsed_args = super().parse_args(args, namespace) + + # Auto-save if enabled and we have a simTag + if self._autoSave: + simTag = self._simTag if self._simTag is not None else _simTag + if simTag is not None: + saveArgs(simTag, parsed_args) + else: + # Log a warning if auto-save is enabled but no simTag is set + global isLoaded + if not isLoaded: + setupLogger() + logging.warning( + "ArgumentParser auto-save is enabled but no simTag is set. " + "Use setSimTag() or pass simTag to ArgumentParser constructor." + ) + + return parsed_args + + +def setSimTag(simTag): + """Set the global simulation tag for automatic argument saving. + + This sets the simulation tag that will be used by SimLogger.ArgumentParser + for automatic saving of parsed arguments. + + Args: + simTag (str): Unique tag for the simulation + + Example: + >>> from SimLogger import SimLogger + >>> SimLogger.setSimTag('myExperiment') + >>> parser = SimLogger.ArgumentParser() + >>> parser.add_argument('--param1', type=int) + >>> args = parser.parse_args() # Automatically saved to 'myExperiment' + """ + global _simTag + _simTag = simTag diff --git a/tests/test_SimLogger.py b/tests/test_SimLogger.py index 90bc4a5..05b44e7 100644 --- a/tests/test_SimLogger.py +++ b/tests/test_SimLogger.py @@ -1,4 +1,5 @@ from SimLogger import SimLogger +import argparse def test_save_and_get(): @@ -10,3 +11,107 @@ def test_save_and_get(): aLoadedObjTag = SimLogger.getObj(simTag, objTag) assert a == aLoadedUniqueId assert a == aLoadedObjTag + + +def test_save_args(): + # Create a simple argparse parser and parse some test arguments + parser = argparse.ArgumentParser() + parser.add_argument("--param1", type=int, default=42) + parser.add_argument("--param2", type=str, default="test_value") + parser.add_argument("--param3", type=float, default=3.14) + + # Parse with default values + args = parser.parse_args([]) + + simTag = "testing-args-simTag" + + # Save the args + filePath = SimLogger.saveArgs(simTag, args) + + # Verify the file was created + assert filePath is not None + assert "args" in filePath + assert simTag in filePath + + # Load the args back + loaded_args = SimLogger.getObj(simTag, "args") + + # Verify the loaded args match the original + assert loaded_args["param1"] == 42 + assert loaded_args["param2"] == "test_value" + assert loaded_args["param3"] == 3.14 + + +def test_save_args_with_custom_values(): + # Create a parser and parse with custom values + parser = argparse.ArgumentParser() + parser.add_argument("--learning_rate", type=float, default=0.001) + parser.add_argument("--epochs", type=int, default=100) + parser.add_argument("--model_name", type=str, default="default") + + # Parse with custom values + args = parser.parse_args( + ["--learning_rate", "0.01", "--epochs", "50", "--model_name", "custom_model"] + ) + + simTag = "testing-custom-args" + + # Save the args + SimLogger.saveArgs(simTag, args, makeNote=True) + + # Load the args back + loaded_args = SimLogger.getObj(simTag, "args") + + # Verify the loaded args match the custom values + assert loaded_args["learning_rate"] == 0.01 + assert loaded_args["epochs"] == 50 + assert loaded_args["model_name"] == "custom_model" + + +def test_argument_parser_auto_save_with_simTag_param(): + # Test automatic saving using ArgumentParser with simTag parameter + parser = SimLogger.ArgumentParser(simTag="auto-save-test-1") + parser.add_argument("--param1", type=int, default=100) + parser.add_argument("--param2", type=str, default="auto_test") + + # Parse args - should automatically save + parser.parse_args([]) + + # Verify the args were automatically saved + loaded_args = SimLogger.getObj("auto-save-test-1", "args") + assert loaded_args["param1"] == 100 + assert loaded_args["param2"] == "auto_test" + + +def test_argument_parser_auto_save_with_setSimTag(): + # Test automatic saving using ArgumentParser with global setSimTag + SimLogger.setSimTag("auto-save-test-2") + parser = SimLogger.ArgumentParser() + parser.add_argument("--learning_rate", type=float, default=0.002) + parser.add_argument("--batch_size", type=int, default=64) + + # Parse args - should automatically save using global simTag + parser.parse_args(["--learning_rate", "0.005", "--batch_size", "128"]) + + # Verify the args were automatically saved + loaded_args = SimLogger.getObj("auto-save-test-2", "args") + assert loaded_args["learning_rate"] == 0.005 + assert loaded_args["batch_size"] == 128 + + +def test_argument_parser_auto_save_disabled(): + # Test that auto-save can be disabled + parser = SimLogger.ArgumentParser(simTag="should-not-save", autoSave=False) + parser.add_argument("--param1", type=int, default=999) + + # Parse args - should NOT automatically save + parser.parse_args([]) + + # Verify the args were NOT saved + import os + + objFolder = os.path.join("data", "obj") + fileList = os.listdir(objFolder) + matching_files = [f for f in fileList if "should-not-save" in f] + # Should have no files since auto-save was disabled + assert len(matching_files) == 0