calkit-python 0.8.4__tar.gz → 0.9.0__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 (59) hide show
  1. {calkit_python-0.8.4 → calkit_python-0.9.0}/PKG-INFO +3 -2
  2. {calkit_python-0.8.4 → calkit_python-0.9.0}/README.md +2 -1
  3. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/__init__.py +1 -1
  4. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/cli/main.py +123 -11
  5. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/conda.py +37 -7
  6. calkit_python-0.9.0/calkit/tests/test_conda.py +126 -0
  7. {calkit_python-0.8.4 → calkit_python-0.9.0}/.github/FUNDING.yml +0 -0
  8. {calkit_python-0.8.4 → calkit_python-0.9.0}/.github/workflows/publish-test.yml +0 -0
  9. {calkit_python-0.8.4 → calkit_python-0.9.0}/.github/workflows/publish.yml +0 -0
  10. {calkit_python-0.8.4 → calkit_python-0.9.0}/.gitignore +0 -0
  11. {calkit_python-0.8.4 → calkit_python-0.9.0}/LICENSE +0 -0
  12. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/cli/__init__.py +0 -0
  13. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/cli/config.py +0 -0
  14. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/cli/core.py +0 -0
  15. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/cli/import_.py +0 -0
  16. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/cli/list.py +0 -0
  17. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/cli/new.py +0 -0
  18. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/cli/notebooks.py +0 -0
  19. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/cli/office.py +0 -0
  20. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/cloud.py +0 -0
  21. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/config.py +0 -0
  22. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/core.py +0 -0
  23. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/data.py +0 -0
  24. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/docker.py +0 -0
  25. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/dvc.py +0 -0
  26. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/git.py +0 -0
  27. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/gui.py +0 -0
  28. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/jupyter.py +0 -0
  29. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/magics.py +0 -0
  30. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/models.py +0 -0
  31. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/office.py +0 -0
  32. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/server.py +0 -0
  33. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/templates/__init__.py +0 -0
  34. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/templates/core.py +0 -0
  35. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/templates/latex/__init__.py +0 -0
  36. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/templates/latex/article/paper.tex +0 -0
  37. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/templates/latex/core.py +0 -0
  38. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/templates/latex/jfm/jfm.bst +0 -0
  39. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/templates/latex/jfm/jfm.cls +0 -0
  40. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
  41. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/templates/latex/jfm/paper.tex +0 -0
  42. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/templates/latex/jfm/upmath.sty +0 -0
  43. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/tests/__init__.py +0 -0
  44. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/tests/cli/__init__.py +0 -0
  45. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/tests/cli/test_list.py +0 -0
  46. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/tests/cli/test_main.py +0 -0
  47. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/tests/cli/test_new.py +0 -0
  48. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/tests/test_core.py +0 -0
  49. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/tests/test_dvc.py +0 -0
  50. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/tests/test_jupyter.py +0 -0
  51. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/tests/test_magics.py +0 -0
  52. {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/tests/test_templates.py +0 -0
  53. {calkit_python-0.8.4 → calkit_python-0.9.0}/docs/tutorials/adding-latex-pub-docker.md +0 -0
  54. {calkit_python-0.8.4 → calkit_python-0.9.0}/docs/tutorials/conda-envs.md +0 -0
  55. {calkit_python-0.8.4 → calkit_python-0.9.0}/docs/tutorials/img/run-proc.png +0 -0
  56. {calkit_python-0.8.4 → calkit_python-0.9.0}/docs/tutorials/notebook-pipeline.md +0 -0
  57. {calkit_python-0.8.4 → calkit_python-0.9.0}/docs/tutorials/procedures.md +0 -0
  58. {calkit_python-0.8.4 → calkit_python-0.9.0}/pyproject.toml +0 -0
  59. {calkit_python-0.8.4 → calkit_python-0.9.0}/test/pipeline.ipynb +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: calkit-python
3
- Version: 0.8.4
3
+ Version: 0.9.0
4
4
  Summary: Reproducibility simplified.
5
5
  Project-URL: Homepage, https://github.com/calkit/calkit
6
6
  Project-URL: Issues, https://github.com/calkit/calkit/issues
@@ -46,7 +46,8 @@ Our goal is to make reproducibility easier so it becomes more common.
46
46
  To do this, we try to make it easy for users to follow two simple rules:
47
47
 
48
48
  1. **Keep everything in version control.** This includes large files like
49
- datasets, enabled by DVC. The [Calkit cloud](https://calkit.io)
49
+ datasets, enabled by DVC.
50
+ The [Calkit cloud](https://github.com/calkit/calkit-cloud)
50
51
  serves as a simple default DVC remote storage location for those who do not
51
52
  want to manage their own infrastructure.
52
53
  2. **Generate all important artifacts with a single pipeline.** There should be
@@ -15,7 +15,8 @@ Our goal is to make reproducibility easier so it becomes more common.
15
15
  To do this, we try to make it easy for users to follow two simple rules:
16
16
 
17
17
  1. **Keep everything in version control.** This includes large files like
18
- datasets, enabled by DVC. The [Calkit cloud](https://calkit.io)
18
+ datasets, enabled by DVC.
19
+ The [Calkit cloud](https://github.com/calkit/calkit-cloud)
19
20
  serves as a simple default DVC remote storage location for those who do not
20
21
  want to manage their own infrastructure.
21
22
  2. **Generate all important artifacts with a single pipeline.** There should be
@@ -1,4 +1,4 @@
1
- __version__ = "0.8.4"
1
+ __version__ = "0.9.0"
2
2
 
3
3
  from .core import *
4
4
  from . import git
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import csv
6
+ import functools
6
7
  import hashlib
7
8
  import json
8
9
  import os
@@ -387,7 +388,10 @@ def run_dvc_repro(
387
388
  args += ["--pipeline", pipeline]
388
389
  if downstream is not None:
389
390
  args += downstream
390
- subprocess.check_call(["dvc", "repro"] + args)
391
+ try:
392
+ subprocess.check_call(["dvc", "repro"] + args)
393
+ except subprocess.CalledProcessError:
394
+ raise_error("DVC pipeline failed")
391
395
  # Now parse stage metadata for calkit objects
392
396
  if not os.path.isfile("dvc.yaml"):
393
397
  raise_error("No dvc.yaml file found")
@@ -504,6 +508,20 @@ def run_in_env(
504
508
  ),
505
509
  ),
506
510
  ] = None,
511
+ no_check: Annotated[
512
+ bool,
513
+ typer.Option(
514
+ "--no-check",
515
+ help="Don't check the environment is valid before running in it.",
516
+ ),
517
+ ] = False,
518
+ relaxed_check: Annotated[
519
+ bool,
520
+ typer.Option(
521
+ "--relaxed",
522
+ help="Check the environment in a relaxed way, if applicable.",
523
+ ),
524
+ ] = False,
507
525
  verbose: Annotated[
508
526
  bool, typer.Option("--verbose", "-v", help="Print verbose output.")
509
527
  ] = False,
@@ -529,6 +547,8 @@ def run_in_env(
529
547
  env_name = default_env_name
530
548
  if env_name is None:
531
549
  raise_error("Environment must be specified if there are multiple")
550
+ if env_name not in envs:
551
+ raise_error(f"Environment '{env_name}' does not exist")
532
552
  env = envs[env_name]
533
553
  if wdir is not None:
534
554
  cwd = os.path.abspath(wdir)
@@ -539,6 +559,15 @@ def run_in_env(
539
559
  shell = env.get("shell", "sh")
540
560
  platform = env.get("platform")
541
561
  if env["kind"] == "docker":
562
+ if "image" not in env:
563
+ raise_error("Image must be defined for Docker environments")
564
+ if "path" in env and not no_check:
565
+ check_docker_env(
566
+ tag=env["image"],
567
+ fpath=env["path"],
568
+ platform=env.get("platform"),
569
+ quiet=True,
570
+ )
542
571
  shell_cmd = " ".join(cmd)
543
572
  docker_cmd = [
544
573
  "docker",
@@ -560,14 +589,75 @@ def run_in_env(
560
589
  ]
561
590
  if verbose:
562
591
  typer.echo(f"Running command: {docker_cmd}")
563
- subprocess.check_call(docker_cmd, cwd=wdir)
592
+ try:
593
+ subprocess.check_call(docker_cmd, cwd=wdir)
594
+ except subprocess.CalledProcessError:
595
+ raise_error("Failed to run in Docker environment")
564
596
  elif env["kind"] == "conda":
565
597
  with open(env["path"]) as f:
566
598
  conda_env = calkit.ryaml.load(f)
599
+ if not no_check:
600
+ check_conda_env(
601
+ env_fpath=env["path"], relaxed=relaxed_check, quiet=True
602
+ )
567
603
  cmd = ["conda", "run", "-n", conda_env["name"]] + cmd
568
604
  if verbose:
569
605
  typer.echo(f"Running command: {cmd}")
570
- subprocess.check_call(cmd, cwd=wdir)
606
+ try:
607
+ subprocess.check_call(cmd, cwd=wdir)
608
+ except subprocess.CalledProcessError:
609
+ raise_error("Failed to run in Conda environment")
610
+ elif env["kind"] in ["pixi", "uv"]:
611
+ env_cmd = []
612
+ if "name" in env:
613
+ env_cmd = ["--environment", env["name"]]
614
+ cmd = [env["kind"], "run"] + env_cmd + cmd
615
+ if verbose:
616
+ typer.echo(f"Running command: {cmd}")
617
+ try:
618
+ subprocess.check_call(cmd, cwd=wdir)
619
+ except subprocess.CalledProcessError:
620
+ raise_error(f"Failed to run in {env['kind']} environment")
621
+ elif env["kind"] == "uv-venv":
622
+ # TODO: This doesn't work on Windows
623
+ if "prefix" not in env:
624
+ raise_error("uv-venv environments require a prefix")
625
+ if "path" not in env:
626
+ raise_error("uv-venv environments require a path")
627
+ prefix = env["prefix"]
628
+ path = env["path"]
629
+ shell_cmd = " ".join(cmd)
630
+ # Check environment
631
+ if not no_check:
632
+ if not os.path.isdir(prefix):
633
+ if verbose:
634
+ typer.echo(f"Creating uv-venv at {prefix}")
635
+ try:
636
+ subprocess.check_call(["uv", "venv", prefix], cwd=wdir)
637
+ except subprocess.CalledProcessError:
638
+ raise_error(f"Failed to create uv-venv at {prefix}")
639
+ fname, ext = os.path.splitext(path)
640
+ lock_fpath = fname + "-lock" + ext
641
+ check_cmd = (
642
+ f". {prefix}/bin/activate "
643
+ f"&& uv pip install -q -r {path} "
644
+ f"&& uv pip freeze > {lock_fpath} "
645
+ "&& deactivate"
646
+ )
647
+ try:
648
+ if verbose:
649
+ typer.echo(f"Running command: {check_cmd}")
650
+ subprocess.check_output(check_cmd, shell=True, cwd=wdir)
651
+ except subprocess.CalledProcessError:
652
+ raise_error("Failed to check uv-venv")
653
+ # Now run the command
654
+ cmd = f". {prefix}/bin/activate && {shell_cmd} && deactivate"
655
+ if verbose:
656
+ typer.echo(f"Running command: {cmd}")
657
+ try:
658
+ subprocess.check_call(cmd, shell=True, cwd=wdir)
659
+ except subprocess.CalledProcessError:
660
+ raise_error("Failed to run in uv-venv")
571
661
  else:
572
662
  raise_error("Environment kind not supported")
573
663
 
@@ -605,7 +695,7 @@ def check_call(
605
695
  name="build-docker",
606
696
  help="Build Docker image if missing or different from lock file.",
607
697
  )
608
- def build_docker(
698
+ def check_docker_env(
609
699
  tag: Annotated[str, typer.Argument(help="Image tag.")],
610
700
  fpath: Annotated[
611
701
  str, typer.Option("-i", "--input", help="Path to input Dockerfile.")
@@ -613,6 +703,9 @@ def build_docker(
613
703
  platform: Annotated[
614
704
  str, typer.Option("--platform", help="Which platform(s) to build for.")
615
705
  ] = None,
706
+ quiet: Annotated[
707
+ bool, typer.Option("--quiet", "-q", help="Be quiet.")
708
+ ] = False,
616
709
  ):
617
710
  def get_docker_inspect():
618
711
  out = json.loads(
@@ -626,28 +719,31 @@ def build_docker(
626
719
  _ = out[0].pop("DockerVersion")
627
720
  return out
628
721
 
629
- typer.echo(f"Checking for existing image with tag {tag}")
722
+ outfile = open(os.devnull, "w") if quiet else None
723
+ typer.echo(f"Checking for existing image with tag {tag}", file=outfile)
630
724
  # First call Docker inspect
631
725
  try:
632
726
  inspect = get_docker_inspect()
633
727
  except subprocess.CalledProcessError:
634
- typer.echo(f"No image with tag {tag} found locally")
728
+ typer.echo(f"No image with tag {tag} found locally", file=outfile)
635
729
  inspect = []
636
- typer.echo(f"Reading Dockerfile from {fpath}")
730
+ typer.echo(f"Reading Dockerfile from {fpath}", file=outfile)
637
731
  with open(fpath) as f:
638
732
  dockerfile = f.read()
639
733
  dockerfile_md5 = hashlib.md5(dockerfile.encode()).hexdigest()
640
734
  lock_fpath = fpath + "-lock.json"
641
735
  rebuild = True
642
736
  if os.path.isfile(lock_fpath):
643
- typer.echo(f"Reading lock file: {lock_fpath}")
737
+ typer.echo(f"Reading lock file: {lock_fpath}", file=outfile)
644
738
  with open(lock_fpath) as f:
645
739
  lock = json.load(f)
646
740
  else:
647
- typer.echo(f"Lock file ({lock_fpath}) does not exist")
741
+ typer.echo(f"Lock file ({lock_fpath}) does not exist", file=outfile)
648
742
  lock = None
649
743
  if inspect and lock:
650
- typer.echo("Checking image and Dockerfile against lock file")
744
+ typer.echo(
745
+ "Checking image and Dockerfile against lock file", file=outfile
746
+ )
651
747
  rebuild = inspect[0]["RootFS"]["Layers"] != lock[0]["RootFS"][
652
748
  "Layers"
653
749
  ] or dockerfile_md5 != lock[0].get("DockerfileMD5")
@@ -827,7 +923,23 @@ def check_conda_env(
827
923
  ),
828
924
  ),
829
925
  ] = None,
926
+ relaxed: Annotated[
927
+ bool,
928
+ typer.Option(
929
+ "--relaxed", help="Treat conda and pip dependencies as equivalent."
930
+ ),
931
+ ] = False,
932
+ quiet: Annotated[
933
+ bool, typer.Option("--quiet", "-q", help="Be quiet.")
934
+ ] = False,
830
935
  ):
936
+ if quiet:
937
+ log_func = functools.partial(typer.echo, file=open(os.devnull, "w"))
938
+ else:
939
+ log_func = typer.echo
831
940
  calkit.conda.check_env(
832
- env_fpath=env_fpath, output_fpath=output_fpath, log_func=typer.echo
941
+ env_fpath=env_fpath,
942
+ output_fpath=output_fpath,
943
+ log_func=log_func,
944
+ relaxed=relaxed,
833
945
  )
@@ -4,27 +4,40 @@ import json
4
4
  import os
5
5
  import subprocess
6
6
 
7
+ from pydantic import BaseModel
8
+
7
9
  import calkit
8
10
  from calkit import ryaml
9
11
 
10
12
 
13
+ class EnvCheckResult(BaseModel):
14
+ env_exists: bool | None = None
15
+ env_needs_export: bool | None = None
16
+ env_needs_rebuild: bool | None = None
17
+
18
+
11
19
  def check_env(
12
20
  env_fpath: str = "environment.yml",
13
21
  use_mamba=True,
14
22
  log_func=None,
15
23
  output_fpath: str = None,
16
- ):
24
+ relaxed: bool = False,
25
+ ) -> EnvCheckResult:
17
26
  """Check that a conda environment matches its spec.
18
27
 
19
28
  If it doesn't match, recreate it.
20
29
 
21
30
  Note that this only works with exact or no version specification.
22
31
  Using greater than and less than operators is not supported.
32
+
33
+ If ``relaxed`` is enabled, dependencies can exist in either the conda or
34
+ pip category.
23
35
  """
24
36
  conda = "mamba" if use_mamba else "conda"
25
37
  if log_func is None:
26
38
  log_func = calkit.logger.info
27
39
  log_func(f"Checking conda env defined in {env_fpath}")
40
+ res = EnvCheckResult()
28
41
  envs = json.loads(
29
42
  subprocess.check_output([conda, "env", "list", "--json"]).decode()
30
43
  )["envs"]
@@ -44,11 +57,13 @@ def check_env(
44
57
  # Check if env even exists
45
58
  if env_name not in existing_env_names:
46
59
  log_func(f"Environment {env_name} doesn't exist; creating")
60
+ res.env_exists = False
47
61
  # Environment doesn't exist, so create it
48
62
  subprocess.check_call([conda, "env", "create", "-y", "-f", env_fpath])
49
63
  env_needs_rebuild = False
50
64
  env_needs_export = True
51
65
  else:
66
+ res.env_exists = True
52
67
  env_needs_export = False
53
68
  # Environment does exist, so check it
54
69
  if os.path.isfile(env_check_fpath):
@@ -67,6 +82,7 @@ def check_env(
67
82
  else:
68
83
  env_needs_export = True
69
84
  if env_needs_export:
85
+ res.env_needs_export = True
70
86
  log_func(f"Exporting existing env to {env_check_fpath}")
71
87
  env_check = json.loads(
72
88
  subprocess.check_output(
@@ -100,6 +116,12 @@ def check_env(
100
116
  else:
101
117
  required_conda_deps = env_spec["dependencies"]
102
118
  required_pip_deps = []
119
+ if relaxed:
120
+ log_func("Running in relaxed mode; combining pip and conda deps")
121
+ for dep in existing_pip_deps:
122
+ existing_conda_deps.append(dep.replace("==", "="))
123
+ for dep in required_pip_deps:
124
+ required_conda_deps.append(dep.replace("==", "="))
103
125
  log_func("Checking conda dependencies")
104
126
  for dep in required_conda_deps:
105
127
  dep_split = dep.split("=")
@@ -121,7 +143,7 @@ def check_env(
121
143
  log_func(f"Found missing dependency: {dep}")
122
144
  env_needs_rebuild = True
123
145
  break
124
- if not env_needs_rebuild:
146
+ if not env_needs_rebuild and not relaxed:
125
147
  log_func("Checking pip dependencies")
126
148
  for dep in required_pip_deps:
127
149
  dep_split = dep.split("==")
@@ -142,11 +164,13 @@ def check_env(
142
164
  env_needs_rebuild = True
143
165
  break
144
166
  if env_needs_rebuild:
167
+ res.env_needs_rebuild = True
145
168
  log_func(f"Rebuilding {env_name} since it does not match spec")
146
169
  subprocess.check_call([conda, "env", "create", "-y", "-f", env_fpath])
147
170
  env_needs_export = True
148
171
  else:
149
172
  log_func(f"Environment {env_name} matches spec")
173
+ res.env_needs_rebuild = False
150
174
  # If the env was rebuilt, export the env check
151
175
  if env_needs_export:
152
176
  log_func(f"Exporting existing env to {env_check_fpath}")
@@ -171,8 +195,14 @@ def check_env(
171
195
  if output_fpath is None:
172
196
  fname, ext = os.path.splitext(env_fpath)
173
197
  output_fpath = fname + "-lock" + ext
174
- log_func(f"Exporting lock file to {output_fpath}")
175
- with open(output_fpath, "w") as f:
176
- _ = env_check.pop("mtime")
177
- _ = env_check.pop("prefix")
178
- ryaml.dump(env_check, f)
198
+ if (
199
+ not res.env_exists
200
+ or res.env_needs_rebuild
201
+ or not os.path.isfile(output_fpath)
202
+ ):
203
+ log_func(f"Exporting lock file to {output_fpath}")
204
+ with open(output_fpath, "w") as f:
205
+ _ = env_check.pop("mtime")
206
+ _ = env_check.pop("prefix")
207
+ ryaml.dump(env_check, f)
208
+ return res
@@ -0,0 +1,126 @@
1
+ """Tests for the ``conda`` module."""
2
+
3
+ import subprocess
4
+ import uuid
5
+
6
+ import pytest
7
+
8
+ from calkit.conda import check_env
9
+
10
+
11
+ def delete_env(name: str):
12
+ subprocess.check_call(["mamba", "env", "remove", "-y", "-n", name])
13
+
14
+
15
+ @pytest.fixture
16
+ def env_name():
17
+ # Setup code
18
+ name = "tmp_" + str(uuid.uuid4())[:12]
19
+ yield name
20
+ # Teardown code
21
+ delete_env(name)
22
+
23
+
24
+ def test_check_env(tmp_dir, env_name):
25
+ subprocess.check_call(["git", "init"])
26
+ subprocess.check_call(["dvc", "init"])
27
+ subprocess.check_call(
28
+ [
29
+ "calkit",
30
+ "new",
31
+ "conda-env",
32
+ "-n",
33
+ env_name,
34
+ "python",
35
+ "pip",
36
+ "--pip",
37
+ "pxl",
38
+ ]
39
+ )
40
+ res = check_env()
41
+ assert not res.env_exists
42
+ res = check_env()
43
+ assert res.env_exists
44
+ assert not res.env_needs_export
45
+ assert not res.env_needs_rebuild
46
+ # Now let's update the env spec so it needs a rebuild
47
+ subprocess.check_call(
48
+ [
49
+ "calkit",
50
+ "new",
51
+ "conda-env",
52
+ "--overwrite",
53
+ "-n",
54
+ env_name,
55
+ "python=3.11.0",
56
+ "pip",
57
+ "--pip",
58
+ "pxl",
59
+ ]
60
+ )
61
+ res = check_env()
62
+ assert res.env_exists
63
+ assert not res.env_needs_export
64
+ assert res.env_needs_rebuild
65
+ res = check_env()
66
+ assert not res.env_needs_rebuild
67
+ # Check relaxed mode, where we allow dependencies to be in either the pip
68
+ # or conda section
69
+ subprocess.check_call(
70
+ [
71
+ "calkit",
72
+ "new",
73
+ "conda-env",
74
+ "--overwrite",
75
+ "-n",
76
+ env_name,
77
+ "python=3.11.0",
78
+ "pip",
79
+ "sqlalchemy",
80
+ ]
81
+ )
82
+ subprocess.check_call(
83
+ [
84
+ "conda",
85
+ "run",
86
+ "-n",
87
+ env_name,
88
+ "pip",
89
+ "install",
90
+ "--upgrade",
91
+ "sqlalchemy",
92
+ ]
93
+ )
94
+ res = check_env()
95
+ assert res.env_needs_rebuild
96
+ subprocess.check_call(
97
+ [
98
+ "calkit",
99
+ "new",
100
+ "conda-env",
101
+ "--overwrite",
102
+ "-n",
103
+ env_name,
104
+ "python=3.11.0",
105
+ "pip",
106
+ "sqlalchemy",
107
+ ]
108
+ )
109
+ res = check_env(relaxed=True)
110
+ assert not res.env_needs_rebuild
111
+ subprocess.check_call(
112
+ [
113
+ "calkit",
114
+ "new",
115
+ "conda-env",
116
+ "--overwrite",
117
+ "-n",
118
+ env_name,
119
+ "python=3.11.0",
120
+ "pip",
121
+ "--pip",
122
+ "sqlalchemy",
123
+ ]
124
+ )
125
+ res = check_env(relaxed=True)
126
+ assert not res.env_needs_rebuild
File without changes
File without changes