From 1352035741b999f4e64bb7f78bb1ba1a902e7952 Mon Sep 17 00:00:00 2001 From: sanmaxdev <46221775+sanmaxdev@users.noreply.github.com> Date: Tue, 23 Jun 2026 07:33:56 +0000 Subject: [PATCH 1/6] feat: add ICS output suffix option --- docs/examples.md | 8 ++++++- holidays/generate_ics.py | 31 +++++++++++++++++++++--- tests/test_generate_ics.py | 49 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/docs/examples.md b/docs/examples.md index 74becf5892..cfb32485ea 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: @@ -661,6 +661,12 @@ Generate a holiday calendar for multiple years and save it to a custom file: holidays-ics US --years 2025-2027 --output us_holidays.ics ``` +Append a custom suffix to the generated file name: + +```shell +holidays-ics CH --subdiv ZH --years 2025 --output-suffix -personal.ics +``` + Generate a calendar containing only bank holidays: ```shell diff --git a/holidays/generate_ics.py b/holidays/generate_ics.py index 04201269d8..25896d8a83 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 = self.args.output_suffix if self.args.output_suffix is not None else ".ics" + 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..38017373d7 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,31 @@ 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_and_suffix_are_mutually_exclusive(self): + with ( + self.argv("US", "--output", "calendar.ics", "--output-suffix", "-personal.ics"), + self.assertRaises(SystemExit), + ): + IcsGenerator() + def test_generate_calendar_error(self): with patch( "holidays.ical.ICalExporter.save_ics", From 700d4fef95cdd42cb256f1189cb270549cbad710 Mon Sep 17 00:00:00 2001 From: sanmaxdev Date: Wed, 1 Jul 2026 17:05:35 +0000 Subject: [PATCH 2/6] test: cover output suffix option boundaries --- tests/test_generate_ics.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_generate_ics.py b/tests/test_generate_ics.py index 38017373d7..c6a3c4de4c 100644 --- a/tests/test_generate_ics.py +++ b/tests/test_generate_ics.py @@ -474,6 +474,21 @@ def test_filename_output_and_suffix_are_mutually_exclusive(self): ): IcsGenerator() + def test_output_suffix_does_not_consume_long_options(self): + with self.argv("US", "--output-suffix", "--list-languages"): + with self.assertRaises(SystemExit): + IcsGenerator() + + def test_output_suffix_missing_value_uses_argparse_error(self): + with self.argv("US", "--output-suffix"): + with self.assertRaises(SystemExit): + IcsGenerator() + + def test_output_suffix_equals_form_is_unchanged(self): + args = IcsGenerator.normalize_output_suffix_args(["US", "--output-suffix=-personal.ics"]) + + self.assertEqual(args, ["US", "--output-suffix=-personal.ics"]) + def test_generate_calendar_error(self): with patch( "holidays.ical.ICalExporter.save_ics", From 69df81a8a1063b14f6071b8ce5408d110559b229 Mon Sep 17 00:00:00 2001 From: sanmaxdev Date: Thu, 2 Jul 2026 23:08:23 +0000 Subject: [PATCH 3/6] fix: address output suffix review --- holidays/generate_ics.py | 2 +- tests/test_generate_ics.py | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/holidays/generate_ics.py b/holidays/generate_ics.py index 25896d8a83..bef8e8387e 100644 --- a/holidays/generate_ics.py +++ b/holidays/generate_ics.py @@ -218,7 +218,7 @@ 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_suffix = self.args.output_suffix if self.args.output_suffix is not None else ".ics" + output_suffix = self.args.output_suffix or ".ics" output_path = ( self.args.output or f"{self.args.code}{subdiv_part}_{years_part}{output_suffix}" ) diff --git a/tests/test_generate_ics.py b/tests/test_generate_ics.py index c6a3c4de4c..a00577fa3d 100644 --- a/tests/test_generate_ics.py +++ b/tests/test_generate_ics.py @@ -474,16 +474,6 @@ def test_filename_output_and_suffix_are_mutually_exclusive(self): ): IcsGenerator() - def test_output_suffix_does_not_consume_long_options(self): - with self.argv("US", "--output-suffix", "--list-languages"): - with self.assertRaises(SystemExit): - IcsGenerator() - - def test_output_suffix_missing_value_uses_argparse_error(self): - with self.argv("US", "--output-suffix"): - with self.assertRaises(SystemExit): - IcsGenerator() - def test_output_suffix_equals_form_is_unchanged(self): args = IcsGenerator.normalize_output_suffix_args(["US", "--output-suffix=-personal.ics"]) From 05a16748485b4486c7b05a4cb208317c01c16c8e Mon Sep 17 00:00:00 2001 From: sanmaxdev Date: Fri, 3 Jul 2026 23:05:20 +0000 Subject: [PATCH 4/6] test: remove duplicate output suffix case --- tests/test_generate_ics.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/test_generate_ics.py b/tests/test_generate_ics.py index a00577fa3d..38017373d7 100644 --- a/tests/test_generate_ics.py +++ b/tests/test_generate_ics.py @@ -474,11 +474,6 @@ def test_filename_output_and_suffix_are_mutually_exclusive(self): ): IcsGenerator() - def test_output_suffix_equals_form_is_unchanged(self): - args = IcsGenerator.normalize_output_suffix_args(["US", "--output-suffix=-personal.ics"]) - - self.assertEqual(args, ["US", "--output-suffix=-personal.ics"]) - def test_generate_calendar_error(self): with patch( "holidays.ical.ICalExporter.save_ics", From 50862186af4d50ae2d6ccdae4635de84cb1132aa Mon Sep 17 00:00:00 2001 From: ~Jhellico Date: Sat, 4 Jul 2026 15:43:10 +0300 Subject: [PATCH 5/6] Update test --- tests/test_generate_ics.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/test_generate_ics.py b/tests/test_generate_ics.py index 38017373d7..f870e81884 100644 --- a/tests/test_generate_ics.py +++ b/tests/test_generate_ics.py @@ -467,13 +467,6 @@ def test_filename_output_suffix_does_not_append_extension(self): self.assertTrue((temp_dir / "US_2025-personal").exists()) self.assertFalse((temp_dir / "US_2025-personal.ics").exists()) - def test_filename_output_and_suffix_are_mutually_exclusive(self): - with ( - self.argv("US", "--output", "calendar.ics", "--output-suffix", "-personal.ics"), - self.assertRaises(SystemExit), - ): - IcsGenerator() - def test_generate_calendar_error(self): with patch( "holidays.ical.ICalExporter.save_ics", From 9c7f3d40152828c0f2992c43640c0644dbd61067 Mon Sep 17 00:00:00 2001 From: sanmaxdev Date: Mon, 6 Jul 2026 05:08:40 +0000 Subject: [PATCH 6/6] fix: preserve empty ICS output suffix --- holidays/generate_ics.py | 2 +- tests/test_generate_ics.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/holidays/generate_ics.py b/holidays/generate_ics.py index bef8e8387e..8fbe8cc4c6 100644 --- a/holidays/generate_ics.py +++ b/holidays/generate_ics.py @@ -218,7 +218,7 @@ 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_suffix = self.args.output_suffix or ".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}" ) diff --git a/tests/test_generate_ics.py b/tests/test_generate_ics.py index f870e81884..db14accb4c 100644 --- a/tests/test_generate_ics.py +++ b/tests/test_generate_ics.py @@ -467,6 +467,14 @@ def test_filename_output_suffix_does_not_append_extension(self): 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",