calkit-python 0.47.7__py3-none-any.whl → 0.47.8__py3-none-any.whl

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 (24) hide show
  1. calkit/cli/latex.py +158 -45
  2. calkit/models/pipeline.py +8 -1
  3. calkit/pipeline.py +27 -2
  4. calkit/tests/cli/test_latex.py +56 -4
  5. calkit/tests/models/test_pipeline.py +2 -1
  6. calkit/tests/test_pipeline.py +27 -0
  7. {calkit_python-0.47.7.dist-info → calkit_python-0.47.8.dist-info}/METADATA +1 -1
  8. {calkit_python-0.47.7.dist-info → calkit_python-0.47.8.dist-info}/RECORD +24 -24
  9. {calkit_python-0.47.7.data → calkit_python-0.47.8.data}/data/etc/jupyter/jupyter_server_config.d/calkit.json +0 -0
  10. {calkit_python-0.47.7.data → calkit_python-0.47.8.data}/data/share/jupyter/labextensions/calkit/package.json +0 -0
  11. {calkit_python-0.47.7.data → calkit_python-0.47.8.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig +0 -0
  12. {calkit_python-0.47.7.data → calkit_python-0.47.8.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json +0 -0
  13. {calkit_python-0.47.7.data → calkit_python-0.47.8.data}/data/share/jupyter/labextensions/calkit/static/506.c34b070098184e33.js +0 -0
  14. {calkit_python-0.47.7.data → calkit_python-0.47.8.data}/data/share/jupyter/labextensions/calkit/static/57d13a7399cec3c5.png +0 -0
  15. {calkit_python-0.47.7.data → calkit_python-0.47.8.data}/data/share/jupyter/labextensions/calkit/static/616.528427eb54a4a0c0.js +0 -0
  16. {calkit_python-0.47.7.data → calkit_python-0.47.8.data}/data/share/jupyter/labextensions/calkit/static/740.6bf87276e9e5788a.js +0 -0
  17. {calkit_python-0.47.7.data → calkit_python-0.47.8.data}/data/share/jupyter/labextensions/calkit/static/899.f9b9f9bf705a6493.js +0 -0
  18. {calkit_python-0.47.7.data → calkit_python-0.47.8.data}/data/share/jupyter/labextensions/calkit/static/935.46ecc6bf99aa593a.js +0 -0
  19. {calkit_python-0.47.7.data → calkit_python-0.47.8.data}/data/share/jupyter/labextensions/calkit/static/remoteEntry.109a3a8379365e59.js +0 -0
  20. {calkit_python-0.47.7.data → calkit_python-0.47.8.data}/data/share/jupyter/labextensions/calkit/static/style.js +0 -0
  21. {calkit_python-0.47.7.data → calkit_python-0.47.8.data}/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json +0 -0
  22. {calkit_python-0.47.7.dist-info → calkit_python-0.47.8.dist-info}/WHEEL +0 -0
  23. {calkit_python-0.47.7.dist-info → calkit_python-0.47.8.dist-info}/entry_points.txt +0 -0
  24. {calkit_python-0.47.7.dist-info → calkit_python-0.47.8.dist-info}/licenses/LICENSE +0 -0
calkit/cli/latex.py CHANGED
@@ -345,6 +345,9 @@ _VERBATIM_PARAM_RE = re.compile(
345
345
  r"(\\(?:verbatiminput\*?|lstinputlisting))"
346
346
  r"(?=\s*(?:\[[^\]\n]*\])?\s*\{[^}\n]*#[0-9])"
347
347
  )
348
+ _INPUT_WRAPPER_RE = re.compile(
349
+ r"\\([A-Za-z@]+)\*?\s*(?:\[[^\]]*\]\s*)*\{\s*\\(?:input|include)\s*\{"
350
+ )
348
351
  get_diff_path = calkit.latex.get_diff_path
349
352
  _default_base_ref = calkit.latex.default_base_ref
350
353
 
@@ -409,7 +412,9 @@ def diff(
409
412
  typer.Option(
410
413
  "--latexmk-rc",
411
414
  "-r",
412
- help="Path to a latexmkrc file to build the marked-up document with.",
415
+ help=(
416
+ "Path to a latexmkrc file to build the marked-up document with."
417
+ ),
413
418
  ),
414
419
  ] = None,
415
420
  latexmk_args: Annotated[
@@ -473,7 +478,11 @@ def diff(
473
478
  bool,
474
479
  typer.Option(
475
480
  "--keep-tex",
476
- help="Keep the generated diff .tex file for inspection.",
481
+ help=(
482
+ "Keep the old, new, and diff .tex files beside the diff "
483
+ "PDF for inspection, e.g., "
484
+ ".calkit/latex-diffs/v1/paper/main-old.tex."
485
+ ),
477
486
  ),
478
487
  ] = False,
479
488
  no_check: Annotated[
@@ -508,6 +517,53 @@ def diff(
508
517
  the comparison shows its own.
509
518
  """
510
519
 
520
+ def stage_path(stage: dict, path: str) -> str:
521
+ """A stage's path in the project's frame rather than its wdir's."""
522
+ return Path(
523
+ os.path.normpath(os.path.join(stage.get("wdir") or "", path))
524
+ ).as_posix()
525
+
526
+ def find_latex_stage() -> tuple[str | None, dict | None]:
527
+ """The pipeline stage that builds the document, if any."""
528
+ ck_info = calkit.load_calkit_info()
529
+ stages = (ck_info.get("pipeline") or {}).get("stages") or {}
530
+ target = Path(os.path.normpath(tex_file)).as_posix()
531
+ for name, stage in stages.items():
532
+ if (
533
+ isinstance(stage, dict)
534
+ and stage.get("kind") == "latex"
535
+ and stage_path(stage, stage.get("target_path") or "") == target
536
+ ):
537
+ return name, stage
538
+ return None, None
539
+
540
+ def stage_inputs(name: str, stage: dict) -> list[str]:
541
+ """What a stage's diffs fetch at each revision.
542
+
543
+ Its dependencies in the compiled pipeline, which include other
544
+ stages' outputs it reads, found there since calkit.yaml only names
545
+ the stages they come from.
546
+ """
547
+ from calkit.models.pipeline import LatexStage
548
+
549
+ try:
550
+ dvc_stage = calkit.ryaml.load(Path("dvc.yaml").read_text())[
551
+ "stages"
552
+ ][name]
553
+ deps = [
554
+ dep if isinstance(dep, str) else next(iter(dep))
555
+ for dep in dvc_stage.get("deps") or []
556
+ ]
557
+ except Exception:
558
+ deps = LatexStage.model_validate(stage | {"name": name}).dvc_deps
559
+ skip = {tex_file, latexmk_rc_path}
560
+ return [
561
+ path
562
+ for dep in deps
563
+ if (path := stage_path(stage, dep)) not in skip
564
+ and not path.startswith(".calkit/")
565
+ ]
566
+
511
567
  def fetch_dvc_inputs(root: str, rev: str, paths: list[str]) -> list[str]:
512
568
  """Fetch the DVC-tracked content of ``paths`` at ``rev`` into ``root``.
513
569
 
@@ -629,7 +685,9 @@ def diff(
629
685
  Detection finds a file by its own name or pointer, but a file in a
630
686
  directory DVC tracks as a whole has only the directory's pointer,
631
687
  which can't say what's inside, so those directories beside the
632
- document are included whole.
688
+ document are included whole. Neither finds another stage's output
689
+ DVC doesn't cache, so the pipeline's inputs for the document are
690
+ added too.
633
691
  """
634
692
  doc_dir = Path(root, os.path.dirname(tex_file))
635
693
  pointed = [
@@ -637,7 +695,8 @@ def diff(
637
695
  for p in sorted(doc_dir.rglob("*.dvc"))
638
696
  if p.is_file()
639
697
  ]
640
- return calkit.latex.detect_inputs(tex_file, wdir=root) + pointed
698
+ detected = calkit.latex.detect_inputs(tex_file, wdir=root)
699
+ return detected + pointed + pipeline_inputs
641
700
 
642
701
  def point_changed_figures_at_base(base_root: str, head_root: str) -> None:
643
702
  """Make the older side's changed figures refer to its own copies.
@@ -698,6 +757,20 @@ def diff(
698
757
  from_ref = _default_base_ref(repo)
699
758
  if to_ref is None and not os.path.isfile(tex_file):
700
759
  raise_error(f"{tex_file} does not exist")
760
+ # A document the pipeline builds is diffed the way it's built, with its
761
+ # stage's environment, settings, and inputs, unless told otherwise
762
+ stage_name, stage = find_latex_stage()
763
+ pipeline_inputs: list[str] = []
764
+ if stage is not None:
765
+ if environment is None:
766
+ environment = stage.get("environment")
767
+ if latexmk_rc_path is None and stage.get("latexmkrc_path"):
768
+ latexmk_rc_path = stage_path(stage, stage["latexmkrc_path"])
769
+ latexmk_args = latexmk_args or list(stage.get("latexmk_args") or [])
770
+ latexdiff_args = latexdiff_args or list(
771
+ stage.get("latexdiff_args") or []
772
+ )
773
+ pipeline_inputs = stage_inputs(str(stage_name), stage)
701
774
  if output is None:
702
775
  output = get_diff_path(
703
776
  tex_file,
@@ -766,9 +839,9 @@ def diff(
766
839
  break_verbatim_params(root)
767
840
  point_changed_figures_at_base(checkouts["base"], head_root)
768
841
  _build_diff(
769
- base_tex=sides["base"],
770
- head_tex=sides["head"],
771
- tex_file=tex_file,
842
+ base_tex_fpath=sides["base"],
843
+ head_tex_fpath=sides["head"],
844
+ tex_file_fpath=tex_file,
772
845
  head_root=head_root,
773
846
  output=output,
774
847
  environment=environment,
@@ -825,9 +898,9 @@ def _remove_worktree(path: str) -> None:
825
898
 
826
899
 
827
900
  def _build_diff(
828
- base_tex: str,
829
- head_tex: str,
830
- tex_file: str,
901
+ base_tex_fpath: str,
902
+ head_tex_fpath: str,
903
+ tex_file_fpath: str,
831
904
  head_root: str,
832
905
  output: str,
833
906
  environment: str | None,
@@ -840,39 +913,63 @@ def _build_diff(
840
913
  force: bool,
841
914
  verbose: bool,
842
915
  ) -> None:
843
- """Mark up one document against another and build the result."""
916
+ """Mark up one document against another and build the result.
917
+
918
+ base_tex_fpath is the older revision's document, head_tex_fpath the
919
+ newer revision's, and tex_file_fpath the document as named on the
920
+ command line. head_root is where the newer side lives: a checkout,
921
+ or the working tree, beside which the marked-up document is built
922
+ so its relative inputs resolve.
923
+ """
844
924
  # Built beside the newer side, so \graphicspath, \bibliography, and
845
925
  # relative \includegraphics resolve the way they do for the real thing,
846
926
  # against that revision's own files
847
- tex_dir = os.path.dirname(tex_file) or "."
927
+ tex_dir = os.path.dirname(tex_file_fpath) or "."
848
928
  build_dir = os.path.normpath(os.path.join(head_root, tex_dir))
849
- stem = Path(tex_file).stem
850
- diff_tex = os.path.join(build_dir, f"{stem}-diff.tex")
851
- # Where --keep-tex leaves it: beside the document, since a checkout is
852
- # removed afterwards
853
- kept_tex = os.path.normpath(os.path.join(tex_dir, f"{stem}-diff.tex"))
929
+ stem = Path(tex_file_fpath).stem
930
+ diff_tex_fpath = os.path.join(build_dir, f"{stem}-diff.tex")
931
+ # Where --keep-tex leaves its copies: beside the diff PDF, since a
932
+ # checkout is removed afterwards. The old and new files are what
933
+ # latexdiff saw, after verbatim fixes and figure repointing, so a
934
+ # --flatten or macro expansion failure can be inspected.
935
+ kept_stem = os.path.splitext(output)[0]
936
+ kept_diff_fpath = f"{kept_stem}-diff.tex"
937
+ kept_old_fpath = f"{kept_stem}-old.tex"
938
+ kept_new_fpath = f"{kept_stem}-new.tex"
939
+ marked_up: bytes | None = None
854
940
  aux_dir = os.path.join(build_dir, calkit.latex.DIFF_AUX_DIRNAME)
855
941
  try:
856
942
  # --flatten pulls \input and \include files into one document on
857
943
  # each side, so a multi-file paper compares as a whole
858
944
  latexdiff_cmd = ["latexdiff", "--flatten", "--encoding=utf8"]
859
- # A checkout's verbatim inputs named by macro parameters are already
860
- # broken, so this only finds the working tree's, which can't be
861
- # edited. A filter breaks those instead, though latexdiff's
862
- # --filter-script mangles anything outside Latin-1.
863
- sources = [base_tex, head_tex] + [
945
+ sources = [base_tex_fpath, head_tex_fpath] + [
864
946
  path
865
- for side in (base_tex, head_tex)
947
+ for side in (base_tex_fpath, head_tex_fpath)
866
948
  for path in calkit.latex.detect_inputs(side)
867
949
  if Path(path).suffix in calkit.latex._SOURCE_EXTS
868
950
  ]
869
- if any(
870
- os.path.isfile(path)
871
- and _VERBATIM_PARAM_RE.search(
872
- Path(path).read_text(encoding="utf-8", errors="replace")
873
- )
951
+ texts = [
952
+ Path(path).read_text(encoding="utf-8", errors="replace")
874
953
  for path in sources
875
- ):
954
+ if os.path.isfile(path)
955
+ ]
956
+ # A macro wrapping an \input or \include, e.g., one making an
957
+ # appendix single column, gets the whole flattened file as its
958
+ # argument, which latexdiff would otherwise mark up as one token
959
+ wrappers = sorted(
960
+ {
961
+ name
962
+ for text in texts
963
+ for name in _INPUT_WRAPPER_RE.findall(text)
964
+ }
965
+ )
966
+ if wrappers:
967
+ latexdiff_cmd.append("--append-textcmd=" + ";".join(wrappers))
968
+ # A checkout's verbatim inputs named by macro parameters are already
969
+ # broken, so this only finds the working tree's, which can't be
970
+ # edited. A filter breaks those instead, though latexdiff's
971
+ # --filter-script mangles anything outside Latin-1.
972
+ if any(_VERBATIM_PARAM_RE.search(text) for text in texts):
876
973
  filter_path = Path(DIFF_TMP_DIR, "verbatim-param-filter.pl")
877
974
  os.makedirs(filter_path.parent, exist_ok=True)
878
975
  # latexdiff appends a newline to what it sends, so drop it
@@ -896,7 +993,7 @@ def _build_diff(
896
993
  # defaults
897
994
  latexdiff_cmd += latexdiff_args
898
995
  cmd = _tex_cmd(
899
- latexdiff_cmd + [base_tex, head_tex],
996
+ latexdiff_cmd + [base_tex_fpath, head_tex_fpath],
900
997
  environment=environment,
901
998
  no_check=no_check,
902
999
  verbose=verbose,
@@ -904,7 +1001,9 @@ def _build_diff(
904
1001
  )
905
1002
  typer.echo("Marking up the document with latexdiff")
906
1003
  try:
907
- marked_up = subprocess.check_output(cmd)
1004
+ # No stdin, so an environment's container isn't given a TTY,
1005
+ # which would merge latexdiff's warnings into the document
1006
+ marked_up = subprocess.check_output(cmd, stdin=subprocess.DEVNULL)
908
1007
  except FileNotFoundError:
909
1008
  raise_error(
910
1009
  "latexdiff was not found; it ships with TeX Live, so a "
@@ -944,8 +1043,7 @@ def _build_diff(
944
1043
  ):
945
1044
  typer.echo(f"{output} is up to date")
946
1045
  return
947
- with open(diff_tex, "wb") as f:
948
- f.write(marked_up)
1046
+ Path(diff_tex_fpath).write_bytes(marked_up)
949
1047
  os.makedirs(aux_dir, exist_ok=True)
950
1048
  rel_aux = calkit.latex.DIFF_AUX_DIRNAME
951
1049
  latexmk_cmd = ["latexmk"]
@@ -965,7 +1063,7 @@ def _build_diff(
965
1063
  # User pass-through args come last so they can override Calkit's
966
1064
  # defaults
967
1065
  latexmk_cmd += latexmk_args
968
- latexmk_cmd.append(diff_tex)
1066
+ latexmk_cmd.append(diff_tex_fpath)
969
1067
  cmd = _tex_cmd(
970
1068
  latexmk_cmd,
971
1069
  environment=environment,
@@ -993,10 +1091,13 @@ def _build_diff(
993
1091
  if excerpt:
994
1092
  typer.echo(f"From {log_path.as_posix()}:", err=True)
995
1093
  typer.echo("\n".join(excerpt), err=True)
996
- raise_error(
997
- "latexmk failed on the marked-up document with exit status "
998
- f"{e.returncode}; rerun with --keep-tex to inspect it"
1094
+ msg = (
1095
+ "latexmk failed on the diff document with exit code "
1096
+ f"{e.returncode}"
999
1097
  )
1098
+ if not keep_tex:
1099
+ msg += "; rerun with --keep-tex to inspect"
1100
+ raise_error(msg)
1000
1101
  built = os.path.join(aux_dir, f"{stem}-diff.pdf")
1001
1102
  if not os.path.isfile(built):
1002
1103
  raise_error("latexmk did not produce a PDF")
@@ -1007,16 +1108,28 @@ def _build_diff(
1007
1108
  if os.path.abspath(head_root) == os.path.abspath("."):
1008
1109
  shutil.rmtree(aux_dir, ignore_errors=True)
1009
1110
  os.makedirs(os.path.dirname(state_path), exist_ok=True)
1010
- with open(state_path, "w") as f:
1011
- f.write(digest)
1111
+ Path(state_path).write_text(digest)
1012
1112
  typer.echo(f"Wrote {output}")
1013
1113
  finally:
1014
- if os.path.isfile(diff_tex):
1015
- in_place = os.path.abspath(diff_tex) == os.path.abspath(kept_tex)
1016
- if keep_tex and not in_place:
1017
- shutil.copy(diff_tex, kept_tex)
1018
- if not keep_tex or not in_place:
1019
- os.remove(diff_tex)
1114
+ if keep_tex:
1115
+ os.makedirs(os.path.dirname(output) or ".", exist_ok=True)
1116
+ if marked_up is not None:
1117
+ Path(kept_diff_fpath).write_bytes(marked_up)
1118
+ for src, kept_fpath in (
1119
+ (base_tex_fpath, kept_old_fpath),
1120
+ (head_tex_fpath, kept_new_fpath),
1121
+ ):
1122
+ if os.path.isfile(src):
1123
+ shutil.copy(src, kept_fpath)
1124
+ for kept_fpath in (
1125
+ kept_old_fpath,
1126
+ kept_new_fpath,
1127
+ kept_diff_fpath,
1128
+ ):
1129
+ if os.path.isfile(kept_fpath):
1130
+ typer.echo(f"Kept {kept_fpath} for inspection")
1131
+ if os.path.isfile(diff_tex_fpath):
1132
+ os.remove(diff_tex_fpath)
1020
1133
 
1021
1134
 
1022
1135
  @latex_app.command(name="to-docx")
calkit/models/pipeline.py CHANGED
@@ -989,11 +989,16 @@ class LatexStage(Stage):
989
989
  default=[],
990
990
  description="Comparisons to keep for this document, each a pair of "
991
991
  "revisions. A bare string is shorthand for comparing that revision "
992
- "against the working tree.",
992
+ "against HEAD.",
993
993
  )
994
994
  diff_pdf_storage: Literal["git", "dvc"] | None = Field(
995
995
  default="dvc", description="Where to store the resulting diff PDFs."
996
996
  )
997
+ keep_diff_tex: bool = Field(
998
+ default=False,
999
+ description="Keep the old, new, and marked-up .tex files beside "
1000
+ "each diff PDF for inspection.",
1001
+ )
997
1002
  verbose: bool = Field(
998
1003
  default=False, description="Show full latexmk output."
999
1004
  )
@@ -1081,6 +1086,8 @@ class LatexStage(Stage):
1081
1086
  cmd += f" --latexmk-arg {shlex.quote(arg)}"
1082
1087
  for arg in self.latexdiff_args:
1083
1088
  cmd += f" --latexdiff-arg {shlex.quote(arg)}"
1089
+ if self.keep_diff_tex:
1090
+ cmd += " --keep-tex"
1084
1091
  # Each revision gets its own copies of any of these that are
1085
1092
  # tracked with DVC, since a checkout only has their pointers
1086
1093
  for input_path in inputs:
calkit/pipeline.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import itertools
4
4
  import os
5
+ import posixpath
5
6
  import re
6
7
  import warnings
7
8
  from collections.abc import Callable
@@ -1407,11 +1408,16 @@ def _ensure_latex_aux_gitignore(
1407
1408
  can be wiped by a clean build (``latexmk -C``). The globs are unanchored,
1408
1409
  so aux files are caught wherever latexmk writes them relative to the
1409
1410
  source -- next to it, or in an ``aux``/output subdirectory via
1410
- ``$aux_dir``/``$out_dir``/``output_dir``. ``*.pdf`` is never ignored so the
1411
- compiled output stays tracked.
1411
+ ``$aux_dir``/``$out_dir``/``output_dir``. A stage's own ``aux_dir`` is
1412
+ also ignored whole. ``*.pdf`` is never ignored so the compiled output
1413
+ stays tracked.
1412
1414
 
1413
1415
  Returns True if ``.gitignore`` was modified.
1414
1416
  """
1417
+
1418
+ def norm(path: str) -> str:
1419
+ return posixpath.normpath(path.replace("\\", "/"))
1420
+
1415
1421
  aux_globs = [
1416
1422
  "*.aux",
1417
1423
  "*.bbl",
@@ -1434,6 +1440,25 @@ def _ensure_latex_aux_gitignore(
1434
1440
  base = os.path.join(wdir or ".", stage.wdir) if stage.wdir else wdir or "."
1435
1441
  source_dir = os.path.dirname(stage.target_path)
1436
1442
  gitignore_path = os.path.join(base, source_dir, ".gitignore")
1443
+ # An aux dir is ignored whole, since packages like glossaries write
1444
+ # files with extensions no list could anticipate. One outside the
1445
+ # source directory can't be named from its .gitignore. Pure path
1446
+ # arithmetic, since os.path.relpath resolves 'aux' to a device on
1447
+ # Windows.
1448
+ if stage.aux_dir is not None:
1449
+ aux = norm(stage.aux_dir)
1450
+ src = norm(source_dir or ".")
1451
+ if src == ".":
1452
+ inside = aux != "." and not aux.startswith(("../", "/"))
1453
+ rel = aux
1454
+ else:
1455
+ inside = aux.startswith(src + "/")
1456
+ rel = aux[len(src) + 1 :]
1457
+ if inside:
1458
+ aux_globs.append(f"/{rel}/*")
1459
+ # Unless the PDF goes there too
1460
+ if stage.output_dir is not None and norm(stage.output_dir) == aux:
1461
+ aux_globs.append(f"!/{rel}/*.pdf")
1437
1462
  return _write_managed_gitignore_block(
1438
1463
  gitignore_path, marker="calkit latex aux files", lines=aux_globs
1439
1464
  )
@@ -390,7 +390,8 @@ def test_latex_diff_dvc_inputs(tmp_dir, tmp_path_factory):
390
390
  f.write("\\documentclass{article}\n")
391
391
  f.write("\\newcommand{\\wc}[1]{\\verbatiminput{#1.wcsum}}\n")
392
392
  f.write("\\begin{document}\nGreen\u2019s function\n")
393
- f.write("\\includegraphics{figs/plot}\n\\end{document}\n")
393
+ f.write("\\includegraphics{figs/plot}\n")
394
+ f.write("\\singlecol{\\input{setup}}\n\\end{document}\n")
394
395
  with open("paper/.latexmkrc", "w") as f:
395
396
  f.write("$aux_dir = 'aux';\n")
396
397
  with open("paper/figs/plot.png", "w") as f:
@@ -451,7 +452,11 @@ def test_latex_diff_dvc_inputs(tmp_dir, tmp_path_factory):
451
452
  output = get_diff_path("paper/main.tex", "v1", "HEAD")
452
453
  with open(output) as f:
453
454
  assert f.read() == "old\nnew\n"
454
- with open("paper/main-diff.tex", encoding="utf-8") as f:
455
+ # --keep-tex keeps what latexdiff saw beside the diff PDF, so a
456
+ # --flatten or macro expansion failure can be inspected
457
+ kept_stem = output.removesuffix(".pdf")
458
+ assert not os.path.exists("paper/main-diff.tex")
459
+ with open(f"{kept_stem}-diff.tex", encoding="utf-8") as f:
455
460
  marked_up = f.read()
456
461
  # A verbatim input named by a macro parameter is broken onto its own
457
462
  # line in each checkout rather than by latexdiff's --filter-script,
@@ -459,10 +464,21 @@ def test_latex_diff_dvc_inputs(tmp_dir, tmp_path_factory):
459
464
  assert "\\verbatiminput%\n{#1.wcsum}" in marked_up
460
465
  assert "Green\u2019s function" in marked_up
461
466
  with open(stubs / "latexdiff-args.txt") as f:
462
- assert "--filter-script" not in f.read()
467
+ latexdiff_args = f.read()
468
+ assert "--filter-script" not in latexdiff_args
469
+ # A macro wrapping an input is marked up as text, not one token
470
+ assert "--append-textcmd=singlecol" in latexdiff_args
463
471
  assert "\\includegraphics{../../base/paper/figs/plot.png}" in marked_up
464
472
  assert "\\includegraphics{figs/plot}" in marked_up
465
473
  assert not os.path.exists("paper/figs")
474
+ with open(f"{kept_stem}-old.tex", encoding="utf-8") as f:
475
+ old_tex = f.read()
476
+ with open(f"{kept_stem}-new.tex", encoding="utf-8") as f:
477
+ new_tex = f.read()
478
+ assert "\\verbatiminput%\n{#1.wcsum}" in old_tex
479
+ assert "\\verbatiminput%\n{#1.wcsum}" in new_tex
480
+ assert "../../base/paper/figs/plot.png" in old_tex
481
+ assert "../../base/paper/figs/plot.png" not in new_tex
466
482
  # The diff is built with the document's rc file, read before the
467
483
  # directories Calkit sets so those win, and latexdiff gets its options
468
484
  with open(stubs / "latexmk-args.txt") as f:
@@ -548,7 +564,43 @@ def test_latex_diff_dvc_inputs(tmp_dir, tmp_path_factory):
548
564
  assert result.returncode != 0
549
565
  assert "! Undefined control sequence." in result.stderr
550
566
  assert "l.3 \\oops" in result.stderr
551
- assert "exit status 12" in result.stderr
567
+ assert "exit code 12" in result.stderr
568
+ # Detection can't find another stage's uncached output at a revision
569
+ os.remove(stubs / "setup.txt")
570
+ bare = ["calkit", "latex", "diff", "paper/main.tex", "--from", "v1"]
571
+ bare += ["--to", "HEAD", "--force"]
572
+ result = subprocess.run(bare, capture_output=True, text=True, env=env)
573
+ assert result.returncode == 0, result.stderr
574
+ assert not os.path.exists(stubs / "setup.txt")
575
+ # But a document the pipeline builds is diffed the way its stage builds
576
+ # it, fetching what the compiled stage depends on
577
+ with open("calkit.yaml", "w") as f:
578
+ f.write(
579
+ "pipeline:\n"
580
+ " stages:\n"
581
+ " paper:\n"
582
+ " kind: latex\n"
583
+ " target_path: paper/main.tex\n"
584
+ " latexmkrc_path: paper/.latexmkrc\n"
585
+ " latexdiff_args: [--type=CFONT]\n"
586
+ )
587
+ with open("dvc.yaml", "a") as f:
588
+ f.write(
589
+ " paper:\n"
590
+ " cmd: calkit latex build paper/main.tex\n"
591
+ " deps: [paper/main.tex, paper/.latexmkrc, paper/setup.tex]\n"
592
+ )
593
+ result = subprocess.run(bare, capture_output=True, text=True, env=env)
594
+ assert result.returncode == 0, result.stderr
595
+ with open(stubs / "setup.txt") as f:
596
+ assert f.read() == "present\n"
597
+ with open(stubs / "latexdiff-args.txt") as f:
598
+ assert "--type=CFONT" in f.read().split()
599
+ with open(stubs / "latexmk-args.txt") as f:
600
+ latexmk_args = f.read().split()
601
+ assert latexmk_args[latexmk_args.index("-r") + 1].endswith(
602
+ "paper/.latexmkrc"
603
+ )
552
604
 
553
605
 
554
606
  def test_marked_up_digest_ignores_the_header():
@@ -630,11 +630,12 @@ def test_latex_stage_diffs():
630
630
  latexdiff_args=["--graphics-markup=both"],
631
631
  inputs=["pubs/paper-1/figs/"],
632
632
  diffs=[["v1", "v2"]],
633
+ keep_diff_tex=True,
633
634
  )
634
635
  assert configured.extra_dvc_stages()["paper-1-diff-v1-v2"]["cmd"] == (
635
636
  "calkit latex diff -e tex --no-check --from v1 --to v2 "
636
637
  "-r pubs/paper-1/.latexmkrc --latexmk-arg -shell-escape "
637
- "--latexdiff-arg --graphics-markup=both "
638
+ "--latexdiff-arg --graphics-markup=both --keep-tex "
638
639
  "--input pubs/paper-1/figs/ "
639
640
  "--output-dir .calkit/latex-diffs/v1..v2 pubs/paper-1/main.tex"
640
641
  )
@@ -2647,6 +2647,33 @@ def test_ensure_latex_aux_gitignore(tmp_dir):
2647
2647
  # The block is not duplicated and the unrelated entry survives
2648
2648
  assert contents.count("# >>> calkit latex aux files") == 1
2649
2649
  assert "figs/*.png" in contents
2650
+ # An aux dir is ignored whole, since packages write files with any
2651
+ # extension, but not one outside the source directory
2652
+ stage.output_dir = None
2653
+ stage.aux_dir = "paper/aux"
2654
+ assert _ensure_latex_aux_gitignore(stage)
2655
+ with open(gitignore_path) as f:
2656
+ contents = f.read()
2657
+ assert "/aux/*\n" in contents
2658
+ assert "!/aux/*.pdf" not in contents
2659
+ # Unless the PDF goes there too
2660
+ stage.output_dir = "paper/aux"
2661
+ assert _ensure_latex_aux_gitignore(stage)
2662
+ with open(gitignore_path) as f:
2663
+ assert "!/aux/*.pdf" in f.read()
2664
+ stage.output_dir = None
2665
+ stage.aux_dir = "build/aux"
2666
+ assert _ensure_latex_aux_gitignore(stage)
2667
+ with open(gitignore_path) as f:
2668
+ assert "aux/*" not in f.read()
2669
+ # Nor the source directory itself
2670
+ stage.aux_dir = "paper"
2671
+ assert _ensure_latex_aux_gitignore(stage) is False
2672
+ # Windows separators name the same directory
2673
+ stage.aux_dir = "paper\\aux"
2674
+ assert _ensure_latex_aux_gitignore(stage)
2675
+ with open(gitignore_path) as f:
2676
+ assert "/aux/*\n" in f.read()
2650
2677
  # When the stage has a wdir, paths are relative to it, so the .gitignore
2651
2678
  # lands under <wdir>/<source dir>, not the project root
2652
2679
  os.makedirs("sub/doc")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: calkit-python
3
- Version: 0.47.7
3
+ Version: 0.47.8
4
4
  Summary: Reproducibility simplified.
5
5
  Project-URL: Homepage, https://calkit.org
6
6
  Project-URL: Issues, https://github.com/calkit/calkit/issues
@@ -28,7 +28,7 @@ calkit/notebooks.py,sha256=guMNqW8lYoweNXsxqzbnM5kbHFCBcLTKpYroBh8bRp8,10393
28
28
  calkit/office.py,sha256=mZqCxkIsmWthBAeJZ5SiGe21fn5zD2zXsy1E6nQIpWE,1291
29
29
  calkit/ops.py,sha256=lLlZAFeplU8uLeMavr99d__Iof-YYwKijus74WfLipk,854
30
30
  calkit/overleaf.py,sha256=ycCX6G28CTd8IprCAh4o9ihtgECi4dR1hwwgfUnBZmw,53210
31
- calkit/pipeline.py,sha256=bOooce1KzXKoepFetfVO_DipkEqaBtGxjTG9ksGDtr0,105167
31
+ calkit/pipeline.py,sha256=3D_nyZJlOVE4rEnAIwXuguvgbwIbemFHTYngi1omuUI,106180
32
32
  calkit/procedures.py,sha256=ZZtoWWmq_H0dyuUb6M-g_y77GRsJzxLMovT_X0RtYrM,2860
33
33
  calkit/provenance.py,sha256=tnj_2yWkVk929GEKr76lacO2jCzGCzpz8_Ygr6Mds_Q,33863
34
34
  calkit/questions.py,sha256=DZIeIJODan_z0SSaxiI2gzpgb6MbMoxwhdVNUiJHH9c,55432
@@ -46,7 +46,7 @@ calkit/cli/describe.py,sha256=LMODpla_g7hyTI7Oi7V5dAb9WSp5nZ_4-XhSYmZP1eo,3608
46
46
  calkit/cli/dev.py,sha256=cu4QsfDiA_KurGx4WJcgA2qgfghMoZutE8kl4IZbUcM,1070
47
47
  calkit/cli/hub.py,sha256=ZBOlL7-mV57q_ynr4hPuhV7NlkKEkINSRksXD-jjiOg,2972
48
48
  calkit/cli/import_.py,sha256=QOZEfKUmqMntle47PRZhN8cs7gfWCBte_smPZqksl54,25563
49
- calkit/cli/latex.py,sha256=bYz_bkWqzRPbuWyedVAH8fFQPfK3BxfUTRD_jIuPyTU,53129
49
+ calkit/cli/latex.py,sha256=1JPyAV3kJ-36ZskuSaJsLZEv-T2YNoUNZ1rkACHAl18,58052
50
50
  calkit/cli/list.py,sha256=VsYyx1bSYFkanJdd1L0vGH6t6Tm8SA7IyIqcfR85qaI,21064
51
51
  calkit/cli/new.py,sha256=VUgPE5iOpflnuJxUNcy-GzAR-IIs9KRDwwkQtblWfoQ,158690
52
52
  calkit/cli/notebooks.py,sha256=aR4I0qatOtkrAxEG5PVLej20SBLKo8qr-lxDSzgljUQ,38836
@@ -68,7 +68,7 @@ calkit/models/core.py,sha256=mL9ImXwt1iTtDKzdmt96F9ONevLSTWhIKb_xAEJFb5I,80570
68
68
  calkit/models/docx.py,sha256=rgR1Y5s_yEomOeC6YlhHvsMDC9TmZp3rPN1wEt_93ds,2539
69
69
  calkit/models/io.py,sha256=0nup-gDz3FbiD8AT5pTpxji6XN_rW5AsijErsIFdpbg,2330
70
70
  calkit/models/iteration.py,sha256=_QajYhWVXV9ge9naX8ZkT1PBNzfc75cpl2BR2f8VPOo,2945
71
- calkit/models/pipeline.py,sha256=zwHDeEx5UiSy8NlEOd6QSHPXpz6JgrgeE5CwVWgS-RI,96540
71
+ calkit/models/pipeline.py,sha256=quC15Sd6Zf77cNJrTK1vTGqx9eUxT_GLZhuH4B0pfwg,96777
72
72
  calkit/resources/README.md,sha256=sqqGXkhVLGeo50cXHx6XDm_yYB3klyDhudisZlLpNuE,1463
73
73
  calkit/resources/__init__.py,sha256=QhDLrihdVQGyW2xEGzj2fIHGheq6Pn8W8jwlrjmmRv0,5122
74
74
  calkit/resources/devcontainer/Dockerfile,sha256=U56BhC7sB9qlwpSMMb8CNHgNyuF-ZCV7X7E88rx6KQQ,3091
@@ -121,7 +121,7 @@ calkit/tests/test_matlab.py,sha256=7yFKOYDCtxYIkRyFOwTIxlvzx03Pac0FKGIUT4Zx7zA,7
121
121
  calkit/tests/test_models.py,sha256=SKzEGPF-M5BuWDMIJiJfkEnlgMdCkDKGkI8eGk1FNGE,16745
122
122
  calkit/tests/test_notebooks.py,sha256=mqeMC_VaTdfMDxewuXlXEJDrZ_sdo4ZjbbOmQaBllF8,3822
123
123
  calkit/tests/test_overleaf.py,sha256=Itc4K3mYInHi36ZWhqH7drCHLCDbH1SXk3z_fB0-W8o,5886
124
- calkit/tests/test_pipeline.py,sha256=27BfaE9oCwj9BfDdLQWr-Yco_mpJ1mvULTgajbfLOEI,135627
124
+ calkit/tests/test_pipeline.py,sha256=Vy_wn150QwFUcVtCGgRU514v65An7C7Xa0_2VphlPdc,136699
125
125
  calkit/tests/test_procedures.py,sha256=NJUIDy-gkou1BBABeWjazm__-8ZxHwvzzjkLm6kbviI,11359
126
126
  calkit/tests/test_provenance.py,sha256=bkL6a7E2Q-JINS8oLS5hs5x-2IzRQkzKJ8HWxSeiemo,23818
127
127
  calkit/tests/test_questions.py,sha256=fOAzeOX66vy6bVOmixQ6ZZ5hMrQXQAd_qpY6sFFNJuY,36903
@@ -139,7 +139,7 @@ calkit/tests/cli/test_delete.py,sha256=qIbgwKc8nBjmkynzyVuVAjrQB96XSmGRet_jfxs19
139
139
  calkit/tests/cli/test_describe.py,sha256=uJ6r11Wkh1oVkPB-U9Kt5ktCSDFLTSL0OCIHy43sWIU,2657
140
140
  calkit/tests/cli/test_hub.py,sha256=z3ne8Bnjv-V85OEh15FbAV4STrFwZKMgl_DgWwQ2wzY,7385
141
141
  calkit/tests/cli/test_import.py,sha256=snQgCw6SM9V3g5NS8oBK-2B3bYFVyJcuXtJuFtDtfA8,13441
142
- calkit/tests/cli/test_latex.py,sha256=b-M_Gp2i3GNxD-RdaV5G891TInXI7-aBhobnwxofKQA,25333
142
+ calkit/tests/cli/test_latex.py,sha256=w_Nl3_2UhleAlZOXTel_mMCj4RnW2aRDpRfMm3EntS4,27746
143
143
  calkit/tests/cli/test_list.py,sha256=rQYPloLuFCmDbTjq7Dcq93tjo_RrmKccVYCyl0TBlqY,10688
144
144
  calkit/tests/cli/test_new.py,sha256=L6MPRBu66G_AJCxYW_s1u7GznEWihwV2N2gH3t5Djww,76919
145
145
  calkit/tests/cli/test_notebooks.py,sha256=RcFnZAnyEnWpgPj1Hsnhryn0gCncgtHRxDSEROanOlE,11904
@@ -161,28 +161,28 @@ calkit/tests/jupyterlab/test_routes.py,sha256=TEEyoHiKnzzKOkQsJcMPKdqpKFQ25yBCvV
161
161
  calkit/tests/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
162
162
  calkit/tests/models/test_core.py,sha256=iDi0mvgZDBj2n7bo-cyKd8dvRZHl5ywWbcab4BPsqcE,8421
163
163
  calkit/tests/models/test_iteration.py,sha256=vjuDKwyYQAV4w1qF9VJWQEHksq3KRd6gtT39_a-RhnI,644
164
- calkit/tests/models/test_pipeline.py,sha256=6oNoJV7ookVKpeQcgs0sOSVaO99PvX_xbb-cToTc3ZQ,33446
164
+ calkit/tests/models/test_pipeline.py,sha256=CU8WhqKsxtnHxW5eqDfCOx2fEr47bCBbl4IyUMcvEdw,33485
165
165
  calkit/agent_skills/add-pipeline-stage/SKILL.md,sha256=kpYYb-rJsS4B9p1Ub3gL21irCG5ZZ4DvWZpdJeEwPZk,3772
166
166
  calkit/agent_skills/build-paper-pipeline/SKILL.md,sha256=aV9t86FxQNkeXyswM03fb5LW3ns4pcsWQyYNZ33niLI,6829
167
167
  calkit/agent_skills/check-questions/SKILL.md,sha256=yn6geOQUNQnJbM_Bn9grULerzreGvZob8vNtawWgvCs,9653
168
168
  calkit/agent_skills/check-reproducibility/SKILL.md,sha256=5SGmtnbq8bSJW0Zwav_aVzfuhilHwBkkdxGuZvPHxGU,4699
169
169
  calkit/agent_skills/conventions/SKILL.md,sha256=kK0Sa54uIORAslE3U53sXJUG-TMdM-Ie72hCMIzzbxQ,8909
170
170
  calkit/agent_skills/create-pipeline/SKILL.md,sha256=2FGw1iDQVRk5FUq79GP3lPdgvgof2Wi1O9O-abGGtgs,5422
171
- calkit_python-0.47.7.data/data/etc/jupyter/jupyter_server_config.d/calkit.json,sha256=CWrZP--JDGz8fsvbAlKr_duTL1vDriGT48SL1YCtkY0,81
172
- calkit_python-0.47.7.data/data/share/jupyter/labextensions/calkit/package.json,sha256=0hnHqhCly59Wg7urFGahc2mSupCWwiPfwKWCYfnbB2E,6202
173
- calkit_python-0.47.7.data/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig,sha256=egkbkHmJYzDARH3_4qpoF75_Svpj_dfPaIwRU-PNjW4,6064
174
- calkit_python-0.47.7.data/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json,sha256=YSpIrwpwB-lQBk9Mwv-npedWTOOZreO6nlWZhqlWyGI,840
175
- calkit_python-0.47.7.data/data/share/jupyter/labextensions/calkit/static/506.c34b070098184e33.js,sha256=0PxdmgdZvLeh6RBcfgN1pcriNO8oCeTK5WJvUIEA3MY,112384
176
- calkit_python-0.47.7.data/data/share/jupyter/labextensions/calkit/static/57d13a7399cec3c5.png,sha256=svHD7-cMtTnRITFwugwsVaB9nZ-h8A61ose8z32HiRE,24850
177
- calkit_python-0.47.7.data/data/share/jupyter/labextensions/calkit/static/616.528427eb54a4a0c0.js,sha256=d0rHq7cqAjj1GNAMLOpl8dyY8yyKUQcLPCOUr-wJGdQ,8129
178
- calkit_python-0.47.7.data/data/share/jupyter/labextensions/calkit/static/740.6bf87276e9e5788a.js,sha256=4E0sK8x-ldVkL-1sM07wgJGS1vtVD9Uly7VtoFTi23c,221
179
- calkit_python-0.47.7.data/data/share/jupyter/labextensions/calkit/static/899.f9b9f9bf705a6493.js,sha256=RNTIItG9k-A_Ip54fRR3QMCjiffkY0bo8zQ2Xh7TyOY,57096
180
- calkit_python-0.47.7.data/data/share/jupyter/labextensions/calkit/static/935.46ecc6bf99aa593a.js,sha256=Cmp0KWM5YDZ-zcsJWWgqCMIF6adeHQH80rxz1sas45E,51183
181
- calkit_python-0.47.7.data/data/share/jupyter/labextensions/calkit/static/remoteEntry.109a3a8379365e59.js,sha256=4JavPLcn703qVSC2E1_MD1thrJNL4N-eITjnfNhSDSM,15066
182
- calkit_python-0.47.7.data/data/share/jupyter/labextensions/calkit/static/style.js,sha256=LTlA3M58hwjqmhomdcrNRO9GM58zZcf0V76WX9HI6EQ,146
183
- calkit_python-0.47.7.data/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json,sha256=2BGdItLJwO3fAopLl4eJvp26TEwuscYC0-yFaF-LaLU,13683
184
- calkit_python-0.47.7.dist-info/METADATA,sha256=E3tFj286O1vbHqfshaUyQcskf_lsVXACrn1KiXPiwfM,16997
185
- calkit_python-0.47.7.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
186
- calkit_python-0.47.7.dist-info/entry_points.txt,sha256=TtpHpcv2vFVAVYxFVp0XrwCV_z7FWIOguEiypXRDoOw,131
187
- calkit_python-0.47.7.dist-info/licenses/LICENSE,sha256=9ZamCaSUTZk9rcrnf-sWFKLOHr3ws-S_dgKMegW4nw8,1056
188
- calkit_python-0.47.7.dist-info/RECORD,,
171
+ calkit_python-0.47.8.data/data/etc/jupyter/jupyter_server_config.d/calkit.json,sha256=CWrZP--JDGz8fsvbAlKr_duTL1vDriGT48SL1YCtkY0,81
172
+ calkit_python-0.47.8.data/data/share/jupyter/labextensions/calkit/package.json,sha256=0hnHqhCly59Wg7urFGahc2mSupCWwiPfwKWCYfnbB2E,6202
173
+ calkit_python-0.47.8.data/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig,sha256=egkbkHmJYzDARH3_4qpoF75_Svpj_dfPaIwRU-PNjW4,6064
174
+ calkit_python-0.47.8.data/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json,sha256=YSpIrwpwB-lQBk9Mwv-npedWTOOZreO6nlWZhqlWyGI,840
175
+ calkit_python-0.47.8.data/data/share/jupyter/labextensions/calkit/static/506.c34b070098184e33.js,sha256=0PxdmgdZvLeh6RBcfgN1pcriNO8oCeTK5WJvUIEA3MY,112384
176
+ calkit_python-0.47.8.data/data/share/jupyter/labextensions/calkit/static/57d13a7399cec3c5.png,sha256=svHD7-cMtTnRITFwugwsVaB9nZ-h8A61ose8z32HiRE,24850
177
+ calkit_python-0.47.8.data/data/share/jupyter/labextensions/calkit/static/616.528427eb54a4a0c0.js,sha256=d0rHq7cqAjj1GNAMLOpl8dyY8yyKUQcLPCOUr-wJGdQ,8129
178
+ calkit_python-0.47.8.data/data/share/jupyter/labextensions/calkit/static/740.6bf87276e9e5788a.js,sha256=4E0sK8x-ldVkL-1sM07wgJGS1vtVD9Uly7VtoFTi23c,221
179
+ calkit_python-0.47.8.data/data/share/jupyter/labextensions/calkit/static/899.f9b9f9bf705a6493.js,sha256=RNTIItG9k-A_Ip54fRR3QMCjiffkY0bo8zQ2Xh7TyOY,57096
180
+ calkit_python-0.47.8.data/data/share/jupyter/labextensions/calkit/static/935.46ecc6bf99aa593a.js,sha256=Cmp0KWM5YDZ-zcsJWWgqCMIF6adeHQH80rxz1sas45E,51183
181
+ calkit_python-0.47.8.data/data/share/jupyter/labextensions/calkit/static/remoteEntry.109a3a8379365e59.js,sha256=4JavPLcn703qVSC2E1_MD1thrJNL4N-eITjnfNhSDSM,15066
182
+ calkit_python-0.47.8.data/data/share/jupyter/labextensions/calkit/static/style.js,sha256=LTlA3M58hwjqmhomdcrNRO9GM58zZcf0V76WX9HI6EQ,146
183
+ calkit_python-0.47.8.data/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json,sha256=2BGdItLJwO3fAopLl4eJvp26TEwuscYC0-yFaF-LaLU,13683
184
+ calkit_python-0.47.8.dist-info/METADATA,sha256=7QaNb1OukL89ocLWGDqC7hgG9v1hxZkGYNSbJ3grN0o,16997
185
+ calkit_python-0.47.8.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
186
+ calkit_python-0.47.8.dist-info/entry_points.txt,sha256=TtpHpcv2vFVAVYxFVp0XrwCV_z7FWIOguEiypXRDoOw,131
187
+ calkit_python-0.47.8.dist-info/licenses/LICENSE,sha256=9ZamCaSUTZk9rcrnf-sWFKLOHr3ws-S_dgKMegW4nw8,1056
188
+ calkit_python-0.47.8.dist-info/RECORD,,