heatmap_cli 0.25.3__tar.gz → 0.25.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. {heatmap_cli-0.25.3/src/heatmap_cli.egg-info → heatmap_cli-0.25.4}/PKG-INFO +1 -1
  2. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/pyproject.toml +1 -1
  3. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/src/heatmap_cli/__init__.py +1 -2
  4. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/src/heatmap_cli/cli.py +70 -10
  5. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/src/heatmap_cli/heatmap.py +15 -8
  6. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4/src/heatmap_cli.egg-info}/PKG-INFO +1 -1
  7. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_demo_option.py +22 -0
  8. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_start_date_option.py +22 -0
  9. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_title_option.py +14 -0
  10. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/LICENSE.md +0 -0
  11. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/README.md +0 -0
  12. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/setup.cfg +0 -0
  13. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/src/heatmap_cli/__main__.py +0 -0
  14. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/src/heatmap_cli.egg-info/SOURCES.txt +0 -0
  15. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/src/heatmap_cli.egg-info/dependency_links.txt +0 -0
  16. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/src/heatmap_cli.egg-info/entry_points.txt +0 -0
  17. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/src/heatmap_cli.egg-info/requires.txt +0 -0
  18. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/src/heatmap_cli.egg-info/top_level.txt +0 -0
  19. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_animate_by_week.py +0 -0
  20. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_animate_no_frames.py +0 -0
  21. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_animate_open.py +0 -0
  22. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_annotate_flag.py +0 -0
  23. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_annotation_truncation.py +0 -0
  24. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_cbar_flag.py +0 -0
  25. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_cmap_option.py +0 -0
  26. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_cmax_option.py +0 -0
  27. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_cmin_option.py +0 -0
  28. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_configure_cbar.py +0 -0
  29. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_debug_flag.py +0 -0
  30. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_end_date_option.py +0 -0
  31. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_env_flag.py +0 -0
  32. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_heatmap_direct.py +0 -0
  33. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_heatmap_errors.py +0 -0
  34. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_heatmap_open.py +0 -0
  35. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_heatmap_utils.py +0 -0
  36. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_heatmap_wrapper.py +0 -0
  37. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_help_flag.py +0 -0
  38. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_iso_53_week_year.py +0 -0
  39. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_no_annotate.py +0 -0
  40. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_open_heatmap.py +0 -0
  41. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_output_dir_option.py +0 -0
  42. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_purge_flag.py +0 -0
  43. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_quiet_flag.py +0 -0
  44. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_refresh_output_dir.py +0 -0
  45. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_setup_logging.py +0 -0
  46. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_verbose_flag.py +0 -0
  47. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_version_flag.py +0 -0
  48. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_week_option.py +0 -0
  49. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_year_option.py +0 -0
  50. {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_yes_flag.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: heatmap_cli
3
- Version: 0.25.3
3
+ Version: 0.25.4
4
4
  Summary: A console program that generates a yearly calendar heatmap.
5
5
  Author-email: Kian-Meng Ang <kianmeng@cpan.org>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -25,7 +25,7 @@ classifiers = [
25
25
  "Programming Language :: Python",
26
26
  "Topic :: Scientific/Engineering :: Visualization",
27
27
  ]
28
- version = "0.25.3"
28
+ version = "0.25.4"
29
29
  keywords = ["heatmap", "cli", "visualization", "calendar", "data-viz"]
30
30
  dependencies = [
31
31
  "matplotlib",
@@ -31,7 +31,6 @@ import matplotlib as mpl
31
31
  import matplotlib.pyplot as plt
32
32
  import pandas as pd
33
33
  import seaborn as sns
34
- from PIL import Image
35
34
 
36
35
  from heatmap_cli.heatmap import _get_output_dir
37
36
  from heatmap_cli.heatmap import run as generate_heatmaps
@@ -194,7 +193,7 @@ class EnvironmentAction(argparse.Action):
194
193
  for name, version in (
195
194
  ("matplotlib", mpl.__version__),
196
195
  ("pandas", pd.__version__),
197
- ("pillow", Image.__version__),
196
+ ("pillow", metadata.version("pillow")),
198
197
  ("seaborn", sns.__version__),
199
198
  )
200
199
  )
@@ -43,6 +43,7 @@ from heatmap_cli import (
43
43
  setup_logging,
44
44
  )
45
45
  from heatmap_cli.heatmap import (
46
+ _sanitize_string_for_filename,
46
47
  _weeks_in_iso_year,
47
48
  )
48
49
  from heatmap_cli.heatmap import (
@@ -208,8 +209,8 @@ def build_parser() -> argparse.ArgumentParser:
208
209
  type=_valid_date,
209
210
  default=None,
210
211
  help=(
211
- "filter from the date of the year from the CSV file "
212
- "(default: %(default)s)"
212
+ "filter from the date of the year from the CSV file and "
213
+ "this will overwrite -y and -w option (default: %(default)s)"
213
214
  ),
214
215
  metavar="START_DATE",
215
216
  )
@@ -424,6 +425,49 @@ def _generate_cmap_help() -> str:
424
425
  return cmap_help + cmap_default + "\n" + cmap_choices.rstrip()
425
426
 
426
427
 
428
+ def _resolve_date_filter_overrides(
429
+ parsed_args: argparse.Namespace,
430
+ ) -> None:
431
+ """Apply --start-date/--end-date overrides to the year and week filters.
432
+
433
+ A --start-date/--end-date pair defines a self-contained calendar date
434
+ range that overrides the -y/--year and -w/--week cutoff, avoiding a
435
+ double filter between the ISO-week year column and the raw date column.
436
+ When only one endpoint is given it derives a partial-year view; when
437
+ both are given --end-date wins for the year/week since it caps the range.
438
+
439
+ Args:
440
+ parsed_args (argparse.Namespace): The newly parsed arguments.
441
+
442
+ Returns:
443
+ None
444
+ """
445
+ # Capture the user-supplied filters before --end-date/--start-date clobber
446
+ # them, so the overriding of -y/-w is reported instead of happening
447
+ # silently.
448
+ explicit_year = parsed_args.year
449
+ explicit_week = parsed_args.week
450
+
451
+ if parsed_args.end_date:
452
+ (year, week, _day) = parsed_args.end_date.isocalendar()
453
+ parsed_args.year = [year]
454
+ parsed_args.week = week
455
+ elif parsed_args.start_date:
456
+ if explicit_year is not None:
457
+ logger.warning(
458
+ "argument --start-date: --year is ignored when "
459
+ "--start-date is used.",
460
+ )
461
+ if explicit_week is not None:
462
+ logger.warning(
463
+ "argument --start-date: --week is ignored when "
464
+ "--start-date is used.",
465
+ )
466
+ (year, _week, _day) = parsed_args.start_date.isocalendar()
467
+ parsed_args.year = [year]
468
+ parsed_args.week = _weeks_in_iso_year(year)
469
+
470
+
427
471
  def _process_args(
428
472
  parser: argparse.ArgumentParser,
429
473
  parsed_args: argparse.Namespace,
@@ -445,14 +489,7 @@ def _process_args(
445
489
  # double filter between the ISO-week year column and the raw date column.
446
490
  # When only one endpoint is given it derives a partial-year view; when
447
491
  # both are given --end-date wins for the year/week since it caps the range.
448
- if parsed_args.end_date:
449
- (year, week, _day) = parsed_args.end_date.isocalendar()
450
- parsed_args.year = [year]
451
- parsed_args.week = week
452
- elif parsed_args.start_date:
453
- (year, _week, _day) = parsed_args.start_date.isocalendar()
454
- parsed_args.year = [year]
455
- parsed_args.week = _weeks_in_iso_year(year)
492
+ _resolve_date_filter_overrides(parsed_args)
456
493
 
457
494
  if (
458
495
  parsed_args.start_date
@@ -479,6 +516,10 @@ def _process_args(
479
516
  if parsed_args.cmap is None:
480
517
  parsed_args.cmap = [DEFAULT_CMAP]
481
518
 
519
+ # Reject titles that sanitize to an empty string, which would otherwise
520
+ # yield a degenerate filename like 001__<cmap>.png.
521
+ _validate_title(parser, parsed_args.title)
522
+
482
523
  # A GIF output is only ever produced by the animation path, so asking for
483
524
  # gif implies --animate-by-week; otherwise fig.savefig(format="gif") would
484
525
  # emit a single-frame still instead of an animation.
@@ -488,6 +529,23 @@ def _process_args(
488
529
  return parsed_args
489
530
 
490
531
 
532
+ def _validate_title(
533
+ parser: argparse.ArgumentParser,
534
+ title: str | None,
535
+ ) -> None:
536
+ """Reject titles that would produce a degenerate filename.
537
+
538
+ Args:
539
+ parser (argparse.ArgumentParser): The argument parser instance.
540
+ title (str | None): The title value from the command line.
541
+
542
+ Returns:
543
+ None
544
+ """
545
+ if title is not None and not _sanitize_string_for_filename(title):
546
+ parser.error("argument --title: cannot be empty or symbolic only.")
547
+
548
+
491
549
  def main(args: Sequence[str] | None = None) -> None:
492
550
  """Run the main program flow.
493
551
 
@@ -510,6 +568,8 @@ def main(args: Sequence[str] | None = None) -> None:
510
568
 
511
569
  processed_args = _process_args(parser, parsed_args)
512
570
  setup_logging(processed_args)
571
+ if processed_args.seed is not None and not processed_args.demo:
572
+ logger.warning("--seed is ignored unless --demo is used")
513
573
  if processed_args.demo:
514
574
  generate_demo(processed_args)
515
575
  return
@@ -300,14 +300,21 @@ def _generate_heatmap_wrapper(
300
300
  cmap: str,
301
301
  config: argparse.Namespace,
302
302
  dataframe: pd.DataFrame,
303
- ) -> Image.Image | Path | None:
304
- """Wrapper for _generate_heatmap to handle args in Pool."""
305
- return _generate_heatmap(
306
- seq,
307
- cmap,
308
- config,
309
- dataframe,
310
- return_image=False,
303
+ ) -> Path | None:
304
+ """Wrapper for _generate_heatmap to handle args in Pool.
305
+
306
+ Workers always save to disk (``return_image=False``), so the result is a
307
+ file Path, never an in-memory image.
308
+ """
309
+ return cast(
310
+ "Path | None",
311
+ _generate_heatmap(
312
+ seq,
313
+ cmap,
314
+ config,
315
+ dataframe,
316
+ return_image=False,
317
+ ),
311
318
  )
312
319
 
313
320
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: heatmap_cli
3
- Version: 0.25.3
3
+ Version: 0.25.4
4
4
  Summary: A console program that generates a yearly calendar heatmap.
5
5
  Author-email: Kian-Meng Ang <kianmeng@cpan.org>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -109,6 +109,28 @@ def test_demo_with_purge_survives(cli_runner):
109
109
  assert "Input file not found" not in ret.stderr
110
110
 
111
111
 
112
+ def test_seed_without_demo_warns(cli_runner, csv_file):
113
+ csv = csv_file("sample.csv")
114
+ ret = cli_runner(
115
+ csv,
116
+ "-y",
117
+ "2024",
118
+ "-w",
119
+ "52",
120
+ "--seed",
121
+ "42",
122
+ expect_error=False,
123
+ )
124
+ assert ret.returncode == 0
125
+ assert "--seed is ignored unless --demo is used" in ret.stderr
126
+
127
+
128
+ def test_seed_with_demo_no_warning(cli_runner):
129
+ ret = cli_runner("--demo", 1, "--seed", "42", expect_error=False)
130
+ assert ret.returncode == 0
131
+ assert "--seed is ignored unless --demo is used" not in ret.stderr
132
+
133
+
112
134
  def test_demo_with_tilde_output_dir(cli_runner, monkeypatch, tmpdir):
113
135
  """--demo must expand ~ in --output-dir like the rest of the pipeline."""
114
136
  home = Path(tmpdir, "home")
@@ -76,3 +76,25 @@ def test_start_date_overrides_year_cutoff(cli_runner, csv_file):
76
76
  csv = csv_file("sample.csv")
77
77
  ret = cli_runner(csv, "-d", "-s", "2023-06-01", "--year", "2024")
78
78
  assert "year=[2023]" in ret.stderr
79
+
80
+
81
+ def test_start_date_warns_when_year_overridden(cli_runner, csv_file):
82
+ """Passing -y with --start-date must warn that -y is ignored."""
83
+ csv = csv_file("sample.csv")
84
+ ret = cli_runner(csv, "-s", "2023-06-01", "--year", "2024")
85
+ assert "argument --start-date: --year is ignored when" in ret.stderr
86
+
87
+
88
+ @pytest.mark.parametrize("option", ["-w", "--week"])
89
+ def test_start_date_warns_when_week_overridden(cli_runner, csv_file, option):
90
+ """Passing -w with --start-date must warn that -w is ignored."""
91
+ csv = csv_file("sample.csv")
92
+ ret = cli_runner(csv, "-s", "2023-06-01", option, "20")
93
+ assert "argument --start-date: --week is ignored when" in ret.stderr
94
+
95
+
96
+ def test_start_date_alone_no_override_warning(cli_runner, csv_file):
97
+ """--start-date used alone must not emit an override warning."""
98
+ csv = csv_file("sample.csv")
99
+ ret = cli_runner(csv, "-s", "2023-06-01")
100
+ assert "is ignored when --start-date" not in ret.stderr
@@ -25,6 +25,20 @@ def test_debug_logs(cli_runner, csv_file, option):
25
25
  assert "my title is foobar" in ret.stderr
26
26
 
27
27
 
28
+ @pytest.mark.parametrize("option", ["-t", "--title"])
29
+ def test_empty_title_rejected(cli_runner, csv_file, option):
30
+ csv = csv_file("sample.csv")
31
+ ret = cli_runner(csv, "-y", "2024", option, "")
32
+ assert "argument --title: cannot be empty or symbolic only." in ret.stderr
33
+
34
+
35
+ @pytest.mark.parametrize("option", ["-t", "--title"])
36
+ def test_symbols_only_title_rejected(cli_runner, csv_file, option):
37
+ csv = csv_file("sample.csv")
38
+ ret = cli_runner(csv, "-y", "2024", option, "!!!")
39
+ assert "argument --title: cannot be empty or symbolic only." in ret.stderr
40
+
41
+
28
42
  def test_default_title(cli_runner, csv_file):
29
43
  csv = csv_file("sample.csv")
30
44
  ret = cli_runner(csv, "-y", "2024", "-d", "-w", "42")
File without changes
File without changes
File without changes