looper 1.6.0a2__tar.gz → 1.7.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. {looper-1.6.0a2/looper.egg-info → looper-1.7.0}/PKG-INFO +3 -3
  2. looper-1.7.0/looper/_version.py +1 -0
  3. {looper-1.6.0a2 → looper-1.7.0}/looper/cli_looper.py +24 -4
  4. {looper-1.6.0a2 → looper-1.7.0}/looper/conductor.py +14 -0
  5. {looper-1.6.0a2 → looper-1.7.0}/looper/looper.py +11 -4
  6. {looper-1.6.0a2 → looper-1.7.0}/looper/project.py +3 -5
  7. {looper-1.6.0a2 → looper-1.7.0/looper.egg-info}/PKG-INFO +3 -3
  8. {looper-1.6.0a2 → looper-1.7.0}/looper.egg-info/requires.txt +2 -2
  9. {looper-1.6.0a2 → looper-1.7.0}/requirements/requirements-all.txt +2 -2
  10. {looper-1.6.0a2 → looper-1.7.0}/requirements/requirements-test.txt +1 -1
  11. looper-1.7.0/tests/test_natural_range.py +206 -0
  12. looper-1.6.0a2/looper/_version.py +0 -1
  13. looper-1.6.0a2/tests/test_natural_range.py +0 -196
  14. {looper-1.6.0a2 → looper-1.7.0}/LICENSE.txt +0 -0
  15. {looper-1.6.0a2 → looper-1.7.0}/MANIFEST.in +0 -0
  16. {looper-1.6.0a2 → looper-1.7.0}/README.md +0 -0
  17. {looper-1.6.0a2 → looper-1.7.0}/logo_looper.svg +0 -0
  18. {looper-1.6.0a2 → looper-1.7.0}/looper/__init__.py +0 -0
  19. {looper-1.6.0a2 → looper-1.7.0}/looper/__main__.py +0 -0
  20. {looper-1.6.0a2 → looper-1.7.0}/looper/cli_divvy.py +0 -0
  21. {looper-1.6.0a2 → looper-1.7.0}/looper/const.py +0 -0
  22. {looper-1.6.0a2 → looper-1.7.0}/looper/default_config/divvy_config.yaml +0 -0
  23. {looper-1.6.0a2 → looper-1.7.0}/looper/default_config/divvy_templates/localhost_bulker_template.sub +0 -0
  24. {looper-1.6.0a2 → looper-1.7.0}/looper/default_config/divvy_templates/localhost_docker_template.sub +0 -0
  25. {looper-1.6.0a2 → looper-1.7.0}/looper/default_config/divvy_templates/localhost_singularity_template.sub +0 -0
  26. {looper-1.6.0a2 → looper-1.7.0}/looper/default_config/divvy_templates/localhost_template.sub +0 -0
  27. {looper-1.6.0a2 → looper-1.7.0}/looper/default_config/divvy_templates/lsf_template.sub +0 -0
  28. {looper-1.6.0a2 → looper-1.7.0}/looper/default_config/divvy_templates/sge_template.sub +0 -0
  29. {looper-1.6.0a2 → looper-1.7.0}/looper/default_config/divvy_templates/slurm_singularity_template.sub +0 -0
  30. {looper-1.6.0a2 → looper-1.7.0}/looper/default_config/divvy_templates/slurm_template.sub +0 -0
  31. {looper-1.6.0a2 → looper-1.7.0}/looper/divvy.py +0 -0
  32. {looper-1.6.0a2 → looper-1.7.0}/looper/exceptions.py +0 -0
  33. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates/footer.html +0 -0
  34. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates/footer_index.html +0 -0
  35. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates/head.html +0 -0
  36. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates/index.html +0 -0
  37. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates/logo.html +0 -0
  38. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates/navbar.html +0 -0
  39. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates/navbar_links.html +0 -0
  40. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates/navbar_list_parent.html +0 -0
  41. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates/object.html +0 -0
  42. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates/project_object.html +0 -0
  43. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates/sample.html +0 -0
  44. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates/status.html +0 -0
  45. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates/status_table.html +0 -0
  46. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates/status_table_no_links.html +0 -0
  47. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates_old/footer.html +0 -0
  48. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates_old/footer_index.html +0 -0
  49. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates_old/head.html +0 -0
  50. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates_old/index.html +0 -0
  51. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates_old/logo.html +0 -0
  52. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates_old/navbar.html +0 -0
  53. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates_old/navbar_links.html +0 -0
  54. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates_old/navbar_list_parent.html +0 -0
  55. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates_old/object.html +0 -0
  56. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates_old/project_object.html +0 -0
  57. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates_old/sample.html +0 -0
  58. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates_old/status.html +0 -0
  59. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates_old/status_table.html +0 -0
  60. {looper-1.6.0a2 → looper-1.7.0}/looper/jinja_templates_old/status_table_no_links.html +0 -0
  61. {looper-1.6.0a2 → looper-1.7.0}/looper/parser_types.py +0 -0
  62. {looper-1.6.0a2 → looper-1.7.0}/looper/pipeline_interface.py +0 -0
  63. {looper-1.6.0a2 → looper-1.7.0}/looper/plugins.py +0 -0
  64. {looper-1.6.0a2 → looper-1.7.0}/looper/processed_project.py +0 -0
  65. {looper-1.6.0a2 → looper-1.7.0}/looper/schemas/divvy_config_schema.yaml +0 -0
  66. {looper-1.6.0a2 → looper-1.7.0}/looper/schemas/pipeline_interface_schema_generic.yaml +0 -0
  67. {looper-1.6.0a2 → looper-1.7.0}/looper/schemas/pipeline_interface_schema_project.yaml +0 -0
  68. {looper-1.6.0a2 → looper-1.7.0}/looper/schemas/pipeline_interface_schema_sample.yaml +0 -0
  69. {looper-1.6.0a2 → looper-1.7.0}/looper/utils.py +0 -0
  70. {looper-1.6.0a2 → looper-1.7.0}/looper.egg-info/SOURCES.txt +0 -0
  71. {looper-1.6.0a2 → looper-1.7.0}/looper.egg-info/dependency_links.txt +0 -0
  72. {looper-1.6.0a2 → looper-1.7.0}/looper.egg-info/entry_points.txt +0 -0
  73. {looper-1.6.0a2 → looper-1.7.0}/looper.egg-info/top_level.txt +0 -0
  74. {looper-1.6.0a2 → looper-1.7.0}/requirements/requirements-doc.txt +0 -0
  75. {looper-1.6.0a2 → looper-1.7.0}/setup.cfg +0 -0
  76. {looper-1.6.0a2 → looper-1.7.0}/setup.py +0 -0
  77. {looper-1.6.0a2 → looper-1.7.0}/tests/test_clean.py +0 -0
  78. {looper-1.6.0a2 → looper-1.7.0}/tests/test_desired_sample_range.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: looper
3
- Version: 1.6.0a2
3
+ Version: 1.7.0
4
4
  Summary: A pipeline submission engine that parses sample inputs and submits pipelines for each sample.
5
5
  Home-page: https://github.com/pepkit/looper
6
6
  Author: Nathan Sheffield, Vince Reuter, Michal Stolarczyk, Johanna Klughammer, Andre Rendeiro
@@ -22,8 +22,8 @@ Requires-Dist: jinja2
22
22
  Requires-Dist: logmuse>=0.2.0
23
23
  Requires-Dist: pandas>=2.0.2
24
24
  Requires-Dist: pephubclient>=0.1.2
25
- Requires-Dist: peppy>=0.40.0.a4
26
- Requires-Dist: pipestat>=0.6.0a9
25
+ Requires-Dist: peppy>=0.40.0
26
+ Requires-Dist: pipestat>=0.8.0
27
27
  Requires-Dist: pyyaml>=3.12
28
28
  Requires-Dist: rich>=9.10.0
29
29
  Requires-Dist: ubiquerg>=0.5.2
@@ -0,0 +1 @@
1
+ __version__ = "1.7.0"
@@ -219,19 +219,27 @@ def build_parser():
219
219
  for subparser in [run_subparser, rerun_subparser]:
220
220
  subparser.add_argument(
221
221
  "-u",
222
- "--lump",
222
+ "--lump-s",
223
223
  default=None,
224
224
  metavar="X",
225
225
  type=html_range(min_val=0, max_val=100, step=0.1, value=0),
226
- help="Total input file size (GB) to batch into one job",
226
+ help="Lump by size: total input file size (GB) to batch into one job",
227
227
  )
228
228
  subparser.add_argument(
229
229
  "-n",
230
- "--lumpn",
230
+ "--lump-n",
231
231
  default=None,
232
232
  metavar="N",
233
233
  type=html_range(min_val=1, max_val="num_samples", value=1),
234
- help="Number of commands to batch into one job",
234
+ help="Lump by number: number of samples to batch into one job",
235
+ )
236
+ subparser.add_argument(
237
+ "-j",
238
+ "--lump-j",
239
+ default=None,
240
+ metavar="J",
241
+ type=int,
242
+ help="Lump samples into number of jobs.",
235
243
  )
236
244
 
237
245
  check_subparser.add_argument(
@@ -495,6 +503,18 @@ def build_parser():
495
503
  help="Number of attributes to display",
496
504
  type=int,
497
505
  )
506
+ parser.add_argument(
507
+ "--commands",
508
+ action="version",
509
+ version="{}".format(" ".join(subparsers.choices.keys())),
510
+ )
511
+
512
+ report_subparser.add_argument(
513
+ "--portable",
514
+ help="Makes html report portable.",
515
+ action="store_true",
516
+ )
517
+
498
518
  result.append(parser)
499
519
  return result
500
520
 
@@ -6,6 +6,7 @@ import os
6
6
  import subprocess
7
7
  import time
8
8
  import yaml
9
+ from math import ceil
9
10
  from copy import copy, deepcopy
10
11
  from json import loads
11
12
  from subprocess import check_output
@@ -132,6 +133,7 @@ class SubmissionConductor(object):
132
133
  compute_variables=None,
133
134
  max_cmds=None,
134
135
  max_size=None,
136
+ max_jobs=None,
135
137
  automatic=True,
136
138
  collate=False,
137
139
  ):
@@ -166,6 +168,8 @@ class SubmissionConductor(object):
166
168
  include in a single job script.
167
169
  :param int | float | NoneType max_size: Upper bound on total file
168
170
  size of inputs used by the commands lumped into single job script.
171
+ :param int | float | NoneType max_jobs: Upper bound on total number of jobs to
172
+ group samples for submission.
169
173
  :param bool automatic: Whether the submission should be automatic once
170
174
  the pool reaches capacity.
171
175
  :param bool collate: Whether a collate job is to be submitted (runs on
@@ -200,6 +204,16 @@ class SubmissionConductor(object):
200
204
  "{}".format(self.extra_pipe_args)
201
205
  )
202
206
 
207
+ if max_jobs:
208
+ if max_jobs == 0 or max_jobs < 0:
209
+ raise ValueError(
210
+ "If specified, max job command count must be a positive integer, greater than zero."
211
+ )
212
+
213
+ num_samples = len(self.prj.samples)
214
+ samples_per_job = num_samples / max_jobs
215
+ max_cmds = ceil(samples_per_job)
216
+
203
217
  if not self.collate:
204
218
  self.automatic = automatic
205
219
  if max_cmds is None and max_size is None:
@@ -404,8 +404,9 @@ class Runner(Executor):
404
404
  extra_args=args.command_extra,
405
405
  extra_args_override=args.command_extra_override,
406
406
  ignore_flags=args.ignore_flags,
407
- max_cmds=args.lumpn,
408
- max_size=args.lump,
407
+ max_cmds=args.lump_n,
408
+ max_size=args.lump_s,
409
+ max_jobs=args.lump_j,
409
410
  )
410
411
  submission_conductors[piface.pipe_iface_file] = conductor
411
412
 
@@ -547,12 +548,16 @@ class Reporter(Executor):
547
548
  p = self.prj
548
549
  project_level = args.project
549
550
 
551
+ portable = args.portable
552
+
550
553
  if project_level:
551
554
  psms = self.prj.get_pipestat_managers(project_level=True)
552
555
  print(psms)
553
556
  for name, psm in psms.items():
554
557
  # Summarize will generate the static HTML Report Function
555
- report_directory = psm.summarize(looper_samples=self.prj.samples)
558
+ report_directory = psm.summarize(
559
+ looper_samples=self.prj.samples, portable=portable
560
+ )
556
561
  print(f"Report directory: {report_directory}")
557
562
  else:
558
563
  for piface_source_samples in self.prj._samples_by_piface(
@@ -567,7 +572,9 @@ class Reporter(Executor):
567
572
  print(psms)
568
573
  for name, psm in psms.items():
569
574
  # Summarize will generate the static HTML Report Function
570
- report_directory = psm.summarize(looper_samples=self.prj.samples)
575
+ report_directory = psm.summarize(
576
+ looper_samples=self.prj.samples, portable=portable
577
+ )
571
578
  print(f"Report directory: {report_directory}")
572
579
 
573
580
 
@@ -111,9 +111,7 @@ class Project(peppyProject):
111
111
  compute settings.
112
112
  """
113
113
 
114
- def __init__(
115
- self, cfg=None, amendments=None, divcfg_path=None, runp=False, **kwargs
116
- ):
114
+ def __init__(self, cfg=None, amendments=None, divcfg_path=None, **kwargs):
117
115
  super(Project, self).__init__(cfg=cfg, amendments=amendments)
118
116
  prj_dict = kwargs.get("project_dict")
119
117
  pep_config = kwargs.get("pep_config", None)
@@ -122,7 +120,7 @@ class Project(peppyProject):
122
120
 
123
121
  # init project from pephub pep_config:
124
122
  if prj_dict is not None and cfg is None:
125
- self.from_dict(prj_dict)
123
+ self._from_dict(prj_dict)
126
124
  self["_config_file"] = os.getcwd() # for finding pipeline interface
127
125
  self["pep_config"] = pep_config
128
126
 
@@ -916,7 +914,7 @@ def make_set(items):
916
914
  try:
917
915
  # Check if user input single integer value for inclusion/exclusion criteria
918
916
  if len(items) == 1:
919
- items = list(map(int, items)) # list(int(items[0]))
917
+ items = list(map(str, items)) # list(int(items[0]))
920
918
  except:
921
919
  if isinstance(items, str):
922
920
  items = [items]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: looper
3
- Version: 1.6.0a2
3
+ Version: 1.7.0
4
4
  Summary: A pipeline submission engine that parses sample inputs and submits pipelines for each sample.
5
5
  Home-page: https://github.com/pepkit/looper
6
6
  Author: Nathan Sheffield, Vince Reuter, Michal Stolarczyk, Johanna Klughammer, Andre Rendeiro
@@ -22,8 +22,8 @@ Requires-Dist: jinja2
22
22
  Requires-Dist: logmuse>=0.2.0
23
23
  Requires-Dist: pandas>=2.0.2
24
24
  Requires-Dist: pephubclient>=0.1.2
25
- Requires-Dist: peppy>=0.40.0.a4
26
- Requires-Dist: pipestat>=0.6.0a9
25
+ Requires-Dist: peppy>=0.40.0
26
+ Requires-Dist: pipestat>=0.8.0
27
27
  Requires-Dist: pyyaml>=3.12
28
28
  Requires-Dist: rich>=9.10.0
29
29
  Requires-Dist: ubiquerg>=0.5.2
@@ -5,8 +5,8 @@ jinja2
5
5
  logmuse>=0.2.0
6
6
  pandas>=2.0.2
7
7
  pephubclient>=0.1.2
8
- peppy>=0.40.0.a4
9
- pipestat>=0.6.0a9
8
+ peppy>=0.40.0
9
+ pipestat>=0.8.0
10
10
  pyyaml>=3.12
11
11
  rich>=9.10.0
12
12
  ubiquerg>=0.5.2
@@ -5,8 +5,8 @@ jinja2
5
5
  logmuse>=0.2.0
6
6
  pandas>=2.0.2
7
7
  pephubclient>=0.1.2
8
- peppy>=0.40.0.a4
9
- pipestat>=0.6.0a9
8
+ peppy>=0.40.0
9
+ pipestat>=0.8.0
10
10
  pyyaml>=3.12
11
11
  rich>=9.10.0
12
12
  ubiquerg>=0.5.2
@@ -1,4 +1,4 @@
1
- hypothesis
1
+ hypothesis >= 6.84.3
2
2
  mock
3
3
  pytest
4
4
  pytest-cov
@@ -0,0 +1,206 @@
1
+ """Tests for the natural numbers range data type"""
2
+
3
+ from typing import *
4
+ import pytest
5
+ from hypothesis import given, strategies as st
6
+ from looper.utils import NatIntervalException, NatIntervalInclusive
7
+
8
+
9
+ gen_pos_int = st.integers(min_value=1)
10
+ gen_opt_int = st.one_of(st.integers(), st.none())
11
+
12
+
13
+ def is_non_pos(opt_int: Optional[int]) -> bool:
14
+ """Determine whether the given value is non-positive (and non-null)."""
15
+ return opt_int is not None and opt_int < 1
16
+
17
+
18
+ def pytest_generate_tests(metafunc):
19
+ if "legit_delim" in metafunc.fixturenames:
20
+ metafunc.parametrize("legit_delim", [":", "-"])
21
+
22
+
23
+ def nondecreasing_pair_strategy(**kwargs):
24
+ """Generate a pair of values in which first respects given upper bound and second is no more than first."""
25
+ return st.tuples(st.integers(**kwargs), st.integers(**kwargs)).filter(
26
+ lambda p: p[0] <= p[1]
27
+ )
28
+
29
+
30
+ class NaturalRangePureConstructorTests:
31
+ """Tests for direct use of natural range primary constructor"""
32
+
33
+ @given(upper_bound=gen_pos_int)
34
+ def test_zero_is_prohibited(self, upper_bound):
35
+ """Separate this case since it's an edge case."""
36
+ with pytest.raises(NatIntervalException):
37
+ NatIntervalInclusive(0, upper_bound)
38
+
39
+ @given(bounds=nondecreasing_pair_strategy(max_value=0))
40
+ def test_non_positive_is_prohibited(self, bounds):
41
+ lo, hi = bounds
42
+ with pytest.raises(NatIntervalException):
43
+ NatIntervalInclusive(lo, hi)
44
+
45
+ @given(bounds=st.tuples(st.integers(), st.integers()).filter(lambda p: p[0] > p[1]))
46
+ def test_upper_less_than_lower__fails_as_expected(self, bounds):
47
+ lo, hi = bounds
48
+ with pytest.raises(NatIntervalException):
49
+ NatIntervalInclusive(lo, hi)
50
+
51
+
52
+ class NaturalRangeFromStringTests:
53
+ """Tests for parsing of natural number range from text, like CLI arg"""
54
+
55
+
56
+ @pytest.mark.parametrize(
57
+ "arg_template", ["0{sep}0", "{sep}0", "0{sep}", "0{sep}0", "{sep}0", "0{sep}"]
58
+ )
59
+ @given(upper_bound=gen_pos_int)
60
+ def test_from_string__zero__does_not_parse(arg_template, legit_delim, upper_bound):
61
+ arg = arg_template.format(sep=legit_delim)
62
+ with pytest.raises(NatIntervalException):
63
+ NatIntervalInclusive.from_string(arg, upper_bound=upper_bound)
64
+
65
+
66
+ @given(upper_bound=st.integers())
67
+ def test_from_string__just_delimiter__does_not_parse(legit_delim, upper_bound):
68
+ with pytest.raises(NatIntervalException):
69
+ NatIntervalInclusive.from_string(legit_delim, upper_bound=upper_bound)
70
+
71
+
72
+ @given(
73
+ lo_hi_upper=st.tuples(gen_opt_int, gen_opt_int, st.integers()).filter(
74
+ lambda t: (t[0] is not None or t[1] is not None)
75
+ and any(is_non_pos(n) for n in t)
76
+ )
77
+ )
78
+ def test_from_string__nonpositive_values__fail_with_expected_error(
79
+ lo_hi_upper, legit_delim
80
+ ):
81
+ lo, hi, upper_bound = lo_hi_upper
82
+ if lo is None and hi is None:
83
+ raise ValueError("Both lower and upper bound generated are null.")
84
+ if lo is None:
85
+ arg = legit_delim + str(hi)
86
+ elif hi is None:
87
+ arg = str(lo) + legit_delim
88
+ else:
89
+ arg = str(lo) + legit_delim + str(hi)
90
+ with pytest.raises(NatIntervalException):
91
+ NatIntervalInclusive.from_string(arg, upper_bound=upper_bound)
92
+
93
+
94
+ @pytest.mark.parametrize("arg", ["1,2", "1;2", "1_2", "1/2", "1.2", "1~2"])
95
+ @given(upper_bound=st.integers(min_value=3))
96
+ def test_from_string__illegal_delimiter__fail_with_expected_error(arg, upper_bound):
97
+ with pytest.raises(NatIntervalException):
98
+ NatIntervalInclusive.from_string(arg, upper_bound=upper_bound)
99
+
100
+
101
+ @given(
102
+ lower_and_limit=st.tuples(st.integers(), st.integers()).filter(
103
+ lambda p: p[1] < p[0]
104
+ )
105
+ )
106
+ def test_from_string__one_sided_lower_with_samples_lt_bound__fails(
107
+ lower_and_limit, legit_delim
108
+ ):
109
+ lower, limit = lower_and_limit
110
+ arg = str(lower) + legit_delim
111
+ with pytest.raises(NatIntervalException):
112
+ NatIntervalInclusive.from_string(arg, upper_bound=limit)
113
+
114
+
115
+ @given(lower_and_upper=nondecreasing_pair_strategy(min_value=1))
116
+ def test_from_string__one_sided_lower_with_samples_gteq_bound__succeeds(
117
+ lower_and_upper, legit_delim
118
+ ):
119
+ lo, upper_bound = lower_and_upper
120
+ exp = NatIntervalInclusive(lo, upper_bound)
121
+ arg = str(lo) + legit_delim
122
+ obs = NatIntervalInclusive.from_string(arg, upper_bound=upper_bound)
123
+ assert obs == exp
124
+
125
+
126
+ @given(upper_and_limit=nondecreasing_pair_strategy(min_value=1))
127
+ def test_from_string__one_sided_upper_with_samples_gteq_bound__succeeds(
128
+ upper_and_limit, legit_delim
129
+ ):
130
+ upper, limit = upper_and_limit
131
+ exp = NatIntervalInclusive(1, upper)
132
+ arg = legit_delim + str(upper)
133
+ obs = NatIntervalInclusive.from_string(arg, upper_bound=limit)
134
+ assert obs == exp
135
+
136
+
137
+ @given(
138
+ upper_and_limit=st.tuples(
139
+ st.integers(min_value=1), st.integers(min_value=1)
140
+ ).filter(lambda p: p[1] < p[0])
141
+ )
142
+ def test_from_string__one_sided_upper_with_samples_lt_bound__uses_bound(
143
+ upper_and_limit, legit_delim
144
+ ):
145
+ upper, limit = upper_and_limit
146
+ exp = NatIntervalInclusive(1, limit)
147
+ arg = legit_delim + str(upper)
148
+ obs = NatIntervalInclusive.from_string(arg, upper_bound=limit)
149
+ assert obs == exp
150
+
151
+
152
+ @given(
153
+ lower_upper_limit=st.tuples(gen_pos_int, gen_pos_int, gen_pos_int).filter(
154
+ lambda t: t[1] < t[0] or t[2] < t[0]
155
+ )
156
+ )
157
+ def test_from_string__two_sided_parse_upper_lt_lower(lower_upper_limit, legit_delim):
158
+ lo, hi, lim = lower_upper_limit
159
+ arg = str(lo) + legit_delim + str(hi)
160
+ with pytest.raises(NatIntervalException):
161
+ NatIntervalInclusive.from_string(arg, upper_bound=lim)
162
+
163
+
164
+ @given(
165
+ lo_hi_limit=st.tuples(st.integers(min_value=2), gen_pos_int, gen_pos_int).filter(
166
+ lambda t: t[2] < t[0] <= t[1]
167
+ )
168
+ )
169
+ def test_from_string__two_sided_parse_upper_gteq_lower_with_upper_limit_lt_lower(
170
+ lo_hi_limit, legit_delim
171
+ ):
172
+ lo, hi, limit = lo_hi_limit
173
+ arg = str(lo) + legit_delim + str(hi)
174
+ with pytest.raises(NatIntervalException):
175
+ NatIntervalInclusive.from_string(arg, upper_bound=limit)
176
+
177
+
178
+ @given(
179
+ lo_hi_limit=st.tuples(gen_pos_int, gen_pos_int, gen_pos_int).filter(
180
+ lambda t: t[0] < t[2] < t[1]
181
+ )
182
+ )
183
+ def test_from_string__two_sided_parse_upper_gteq_lower_with_upper_limit_between_lower_and_upper(
184
+ lo_hi_limit,
185
+ legit_delim,
186
+ ):
187
+ lo, hi, limit = lo_hi_limit
188
+ exp = NatIntervalInclusive(lo, limit)
189
+ arg = str(lo) + legit_delim + str(hi)
190
+ obs = NatIntervalInclusive.from_string(arg, upper_bound=limit)
191
+ assert obs == exp
192
+
193
+
194
+ @given(
195
+ lo_hi_upper=st.tuples(gen_pos_int, gen_pos_int, gen_pos_int).filter(
196
+ lambda t: t[0] <= t[1] <= t[2]
197
+ )
198
+ )
199
+ def test_from_string__two_sided_parse_upper_gteq_lower_with_upper_limit_gteq_upper(
200
+ lo_hi_upper, legit_delim
201
+ ):
202
+ lo, hi, upper_bound = lo_hi_upper
203
+ exp = NatIntervalInclusive(lo, hi)
204
+ arg = f"{str(lo)}{legit_delim}{str(hi)}"
205
+ obs = NatIntervalInclusive.from_string(arg, upper_bound=upper_bound)
206
+ assert obs == exp
@@ -1 +0,0 @@
1
- __version__ = "1.6.0a2"
@@ -1,196 +0,0 @@
1
- """Tests for the natural numbers range data type"""
2
-
3
- from typing import *
4
- import pytest
5
- from hypothesis import Phase, given, settings, strategies as st
6
- from looper.utils import NatIntervalException, NatIntervalInclusive
7
-
8
-
9
- gen_pos_int = st.integers(min_value=1)
10
- gen_opt_int = st.one_of(st.integers(), st.none())
11
-
12
-
13
- def is_non_pos(opt_int: Optional[int]) -> bool:
14
- """Determine whether the given value is non-positive (and non-null)."""
15
- return opt_int is not None and opt_int < 1
16
-
17
-
18
- def pytest_generate_tests(metafunc):
19
- if "legit_delim" in metafunc.fixturenames:
20
- metafunc.parametrize("legit_delim", [":", "-"])
21
-
22
-
23
- def nondecreasing_pair_strategy(**kwargs):
24
- """Generate a pair of values in which first respects given upper bound and second is no more than first."""
25
- return st.tuples(st.integers(**kwargs), st.integers(**kwargs)).filter(
26
- lambda p: p[0] <= p[1]
27
- )
28
-
29
-
30
- class NaturalRangePureConstructorTests:
31
- """Tests for direct use of natural range primary constructor"""
32
-
33
- @given(upper_bound=gen_pos_int)
34
- def test_zero_is_prohibited(self, upper_bound):
35
- """Separate this case since it's an edge case."""
36
- with pytest.raises(NatIntervalException):
37
- NatIntervalInclusive(0, upper_bound)
38
-
39
- @given(bounds=nondecreasing_pair_strategy(max_value=0))
40
- def test_non_positive_is_prohibited(self, bounds):
41
- lo, hi = bounds
42
- with pytest.raises(NatIntervalException):
43
- NatIntervalInclusive(lo, hi)
44
-
45
- @given(bounds=st.tuples(st.integers(), st.integers()).filter(lambda p: p[0] > p[1]))
46
- def test_upper_less_than_lower__fails_as_expected(self, bounds):
47
- lo, hi = bounds
48
- with pytest.raises(NatIntervalException):
49
- NatIntervalInclusive(lo, hi)
50
-
51
-
52
- @pytest.mark.skip(reason="Unable to reproduce test failing locally.")
53
- class NaturalRangeFromStringTests:
54
- """Tests for parsing of natural number range from text, like CLI arg"""
55
-
56
- @pytest.mark.parametrize(
57
- "arg_template", ["0{sep}0", "{sep}0", "0{sep}", "0{sep}0", "{sep}0", "0{sep}"]
58
- )
59
- @given(upper_bound=gen_pos_int)
60
- def test_zero__does_not_parse(self, arg_template, legit_delim, upper_bound):
61
- arg = arg_template.format(sep=legit_delim)
62
- with pytest.raises(NatIntervalException):
63
- NatIntervalInclusive.from_string(arg, upper_bound=upper_bound)
64
-
65
- @given(upper_bound=st.integers())
66
- def test_just_delimiter__does_not_parse(self, legit_delim, upper_bound):
67
- with pytest.raises(NatIntervalException):
68
- NatIntervalInclusive.from_string(legit_delim, upper_bound=upper_bound)
69
-
70
- @given(
71
- lo_hi_upper=st.tuples(gen_opt_int, gen_opt_int, st.integers()).filter(
72
- lambda t: (t[0] is not None or t[1] is not None)
73
- and any(is_non_pos(n) for n in t)
74
- )
75
- )
76
- def test_nonpositive_values__fail_with_expected_error(
77
- self, lo_hi_upper, legit_delim
78
- ):
79
- lo, hi, upper_bound = lo_hi_upper
80
- if lo is None and hi is None:
81
- raise ValueError("Both lower and upper bound generated are null.")
82
- if lo is None:
83
- arg = legit_delim + str(hi)
84
- elif hi is None:
85
- arg = str(lo) + legit_delim
86
- else:
87
- arg = str(lo) + legit_delim + str(hi)
88
- with pytest.raises(NatIntervalException):
89
- NatIntervalInclusive.from_string(arg, upper_bound=upper_bound)
90
-
91
- @pytest.mark.parametrize("arg", ["1,2", "1;2", "1_2", "1/2", "1.2", "1~2"])
92
- @given(upper_bound=st.integers(min_value=3))
93
- def test_illegal_delimiter__fail_with_expected_error(self, arg, upper_bound):
94
- with pytest.raises(NatIntervalException):
95
- NatIntervalInclusive.from_string(arg, upper_bound=upper_bound)
96
-
97
- @given(
98
- lower_and_limit=st.tuples(st.integers(), st.integers()).filter(
99
- lambda p: p[1] < p[0]
100
- )
101
- )
102
- def test_one_sided_lower_with_samples_lt_bound__fails(
103
- self, lower_and_limit, legit_delim
104
- ):
105
- lower, limit = lower_and_limit
106
- arg = str(lower) + legit_delim
107
- with pytest.raises(NatIntervalException):
108
- NatIntervalInclusive.from_string(arg, upper_bound=limit)
109
-
110
- @given(lower_and_upper=nondecreasing_pair_strategy(min_value=1))
111
- def test_one_sided_lower_with_samples_gteq_bound__succeeds(
112
- self, lower_and_upper, legit_delim
113
- ):
114
- lo, upper_bound = lower_and_upper
115
- exp = NatIntervalInclusive(lo, upper_bound)
116
- arg = str(lo) + legit_delim
117
- obs = NatIntervalInclusive.from_string(arg, upper_bound=upper_bound)
118
- assert obs == exp
119
-
120
- @given(upper_and_limit=nondecreasing_pair_strategy(min_value=1))
121
- def test_one_sided_upper_with_samples_gteq_bound__succeeds(
122
- self, upper_and_limit, legit_delim
123
- ):
124
- upper, limit = upper_and_limit
125
- exp = NatIntervalInclusive(1, upper)
126
- arg = legit_delim + str(upper)
127
- obs = NatIntervalInclusive.from_string(arg, upper_bound=limit)
128
- assert obs == exp
129
-
130
- @given(
131
- upper_and_limit=st.tuples(
132
- st.integers(min_value=1), st.integers(min_value=1)
133
- ).filter(lambda p: p[1] < p[0])
134
- )
135
- def test_one_sided_upper_with_samples_lt_bound__uses_bound(
136
- self, upper_and_limit, legit_delim
137
- ):
138
- upper, limit = upper_and_limit
139
- exp = NatIntervalInclusive(1, limit)
140
- arg = legit_delim + str(upper)
141
- obs = NatIntervalInclusive.from_string(arg, upper_bound=limit)
142
- assert obs == exp
143
-
144
- @given(
145
- lower_upper_limit=st.tuples(gen_pos_int, gen_pos_int, gen_pos_int).filter(
146
- lambda t: t[1] < t[0] or t[2] < t[0]
147
- )
148
- )
149
- def test_two_sided_parse_upper_lt_lower(self, lower_upper_limit, legit_delim):
150
- lo, hi, lim = lower_upper_limit
151
- arg = str(lo) + legit_delim + str(hi)
152
- with pytest.raises(NatIntervalException):
153
- NatIntervalInclusive.from_string(arg, upper_bound=lim)
154
-
155
- @given(
156
- lo_hi_limit=st.tuples(
157
- st.integers(min_value=2), gen_pos_int, gen_pos_int
158
- ).filter(lambda t: t[2] < t[0] <= t[1])
159
- )
160
- def test_two_sided_parse_upper_gteq_lower_with_upper_limit_lt_lower(
161
- self, lo_hi_limit, legit_delim
162
- ):
163
- lo, hi, limit = lo_hi_limit
164
- arg = str(lo) + legit_delim + str(hi)
165
- with pytest.raises(NatIntervalException):
166
- NatIntervalInclusive.from_string(arg, upper_bound=limit)
167
-
168
- @given(
169
- lo_hi_limit=st.tuples(gen_pos_int, gen_pos_int, gen_pos_int).filter(
170
- lambda t: t[0] < t[2] < t[1]
171
- )
172
- )
173
- def test_two_sided_parse_upper_gteq_lower_with_upper_limit_between_lower_and_upper(
174
- self,
175
- lo_hi_limit,
176
- legit_delim,
177
- ):
178
- lo, hi, limit = lo_hi_limit
179
- exp = NatIntervalInclusive(lo, limit)
180
- arg = str(lo) + legit_delim + str(hi)
181
- obs = NatIntervalInclusive.from_string(arg, upper_bound=limit)
182
- assert obs == exp
183
-
184
- @given(
185
- lo_hi_upper=st.tuples(gen_pos_int, gen_pos_int, gen_pos_int).filter(
186
- lambda t: t[0] <= t[1] <= t[2]
187
- )
188
- )
189
- def test_two_sided_parse_upper_gteq_lower_with_upper_limit_gteq_upper(
190
- self, lo_hi_upper, legit_delim
191
- ):
192
- lo, hi, upper_bound = lo_hi_upper
193
- exp = NatIntervalInclusive(lo, hi)
194
- arg = f"{str(lo)}{legit_delim}{str(hi)}"
195
- obs = NatIntervalInclusive.from_string(arg, upper_bound=upper_bound)
196
- assert obs == exp
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes