diff --git a/docs/examples.md b/docs/examples.md index 5a40e60ac7..0b9b3b350f 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -599,7 +599,7 @@ the [icalendar](https://github.com/collective/icalendar) package. For user convenience, the library includes `holidays-ics` tool for generating iCalendar (.ics) files from holiday calendars provided by the library. It supports country, subdivision, and financial market calendars, holiday category filtering, localized holiday names, -year selection, and custom output file names. +year selection, custom output file names, and suffixes for generated output file names. The tool can be run either as an installed command: @@ -667,6 +667,12 @@ Calendar containing unofficial holidays only: holidays-ics US --categories unofficial ``` +Append a custom suffix to the generated file name: + +```shell +holidays-ics CH --subdiv ZH --years 2025 --output-suffix personal.ics +``` + Calendar containing public and optional holidays: ```shell diff --git a/holidays/generate_ics.py b/holidays/generate_ics.py index 04201269d8..8fbe8cc4c6 100644 --- a/holidays/generate_ics.py +++ b/holidays/generate_ics.py @@ -61,7 +61,13 @@ def __init__(self): parser.add_argument( "-l", "--language", help="Language code for holiday names (e.g., en_US, es)" ) - parser.add_argument("-o", "--output", help="Output file path (e.g., holidays.ics)") + + output_group = parser.add_mutually_exclusive_group() + output_group.add_argument("-o", "--output", help="Output file path (e.g., holidays.ics)") + output_group.add_argument( + "--output-suffix", + help="Suffix to append to the generated output filename base (e.g., -MYNOTE.ics)", + ) list_group = parser.add_mutually_exclusive_group() list_group.add_argument( @@ -73,7 +79,23 @@ def __init__(self): list_group.add_argument( "--list-languages", action="store_true", help="List supported languages" ) - self.args = parser.parse_args() + self.args = parser.parse_args(self.normalize_output_suffix_args(sys.argv[1:])) + + @staticmethod + def normalize_output_suffix_args(args: list[str]) -> list[str]: + """Allow suffix values that begin with '-' without requiring '=' syntax.""" + normalized_args = list(args) + try: + option_index = normalized_args.index("--output-suffix") + except ValueError: + return normalized_args + + value_index = option_index + 1 + if value_index < len(normalized_args): + suffix = normalized_args[value_index] + if suffix.startswith("-") and not suffix.startswith("--"): + normalized_args[option_index : value_index + 1] = [f"--output-suffix={suffix}"] + return normalized_args @staticmethod def parse_years(years: str) -> tuple[int, int]: @@ -196,7 +218,10 @@ def run(self) -> None: start_year, end_year = self.args.years years_part = f"{start_year}_{end_year}" if start_year != end_year else f"{start_year}" subdiv_part = f"_{self.args.subdiv.upper().replace(' ', '_')}" if self.args.subdiv else "" - output_path = self.args.output or f"{self.args.code}{subdiv_part}_{years_part}.ics" + output_suffix = ".ics" if self.args.output_suffix is None else self.args.output_suffix + output_path = ( + self.args.output or f"{self.args.code}{subdiv_part}_{years_part}{output_suffix}" + ) try: holiday_obj = self.entity_loader( diff --git a/tests/test_generate_ics.py b/tests/test_generate_ics.py index 9a60227dfe..db14accb4c 100644 --- a/tests/test_generate_ics.py +++ b/tests/test_generate_ics.py @@ -127,6 +127,30 @@ def test_parse_categories(self): IcsGenerator.parse_categories("BANK,Public,optional"), ["bank", "public", "optional"] ) + def test_normalize_output_suffix_args(self): + self.assertEqual( + IcsGenerator.normalize_output_suffix_args(["US", "--output-suffix", "-personal.ics"]), + ["US", "--output-suffix=-personal.ics"], + ) + self.assertEqual( + IcsGenerator.normalize_output_suffix_args(["US", "--output-suffix", "personal.ics"]), + ["US", "--output-suffix", "personal.ics"], + ) + self.assertEqual( + IcsGenerator.normalize_output_suffix_args(["US", "--output-suffix=-personal.ics"]), + ["US", "--output-suffix=-personal.ics"], + ) + self.assertEqual( + IcsGenerator.normalize_output_suffix_args( + ["US", "--output-suffix", "--list-categories"] + ), + ["US", "--output-suffix", "--list-categories"], + ) + self.assertEqual( + IcsGenerator.normalize_output_suffix_args(["US", "--output-suffix"]), + ["US", "--output-suffix"], + ) + def test_validate_country_code(self): with self.argv("US"): generator = IcsGenerator() @@ -425,6 +449,32 @@ def test_filename_subdivision(self): self.assertTrue((temp_dir / "US_CA_2025.ics").exists()) + def test_filename_output_suffix(self): + with self.temp_cwd() as temp_dir: + with self.argv( + "CH", "--subdiv", "ZH", "--years", "2020", "--output-suffix", "-MYNOTE.ics" + ): + IcsGenerator().run() + + self.assertTrue((temp_dir / "CH_ZH_2020-MYNOTE.ics").exists()) + self.assertFalse((temp_dir / "CH_ZH_2020.ics").exists()) + + def test_filename_output_suffix_does_not_append_extension(self): + with self.temp_cwd() as temp_dir: + with self.argv("US", "--years", "2025", "--output-suffix", "-personal"): + IcsGenerator().run() + + self.assertTrue((temp_dir / "US_2025-personal").exists()) + self.assertFalse((temp_dir / "US_2025-personal.ics").exists()) + + def test_filename_output_suffix_allows_empty_suffix(self): + with self.temp_cwd() as temp_dir: + with self.argv("US", "--years", "2025", "--output-suffix", ""): + IcsGenerator().run() + + self.assertTrue((temp_dir / "US_2025").exists()) + self.assertFalse((temp_dir / "US_2025.ics").exists()) + def test_generate_calendar_error(self): with patch( "holidays.ical.ICalExporter.save_ics",