runem 0.0.32__tar.gz → 0.1.1__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.
- {runem-0.0.32 → runem-0.1.1}/HISTORY.md +88 -0
- {runem-0.0.32 → runem-0.1.1}/PKG-INFO +8 -7
- {runem-0.0.32 → runem-0.1.1}/requirements-test.txt +5 -5
- runem-0.1.1/requirements.txt +9 -0
- runem-0.1.1/runem/VERSION +1 -0
- {runem-0.0.32 → runem-0.1.1}/runem/command_line.py +42 -1
- {runem-0.0.32 → runem-0.1.1}/runem/hook_manager.py +5 -1
- {runem-0.0.32 → runem-0.1.1}/runem/job_execute.py +12 -6
- {runem-0.0.32 → runem-0.1.1}/runem/job_wrapper.py +1 -1
- {runem-0.0.32 → runem-0.1.1}/runem/runem.py +63 -48
- {runem-0.0.32 → runem-0.1.1}/runem/types.py +112 -7
- {runem-0.0.32 → runem-0.1.1}/runem.egg-info/PKG-INFO +8 -7
- {runem-0.0.32 → runem-0.1.1}/runem.egg-info/requires.txt +7 -6
- {runem-0.0.32 → runem-0.1.1}/tests/test_job_execute.py +1 -1
- {runem-0.0.32 → runem-0.1.1}/tests/test_runem.py +11 -5
- runem-0.0.32/requirements.txt +0 -3
- runem-0.0.32/runem/VERSION +0 -1
- {runem-0.0.32 → runem-0.1.1}/Containerfile +0 -0
- {runem-0.0.32 → runem-0.1.1}/LICENSE +0 -0
- {runem-0.0.32 → runem-0.1.1}/MANIFEST.in +0 -0
- {runem-0.0.32 → runem-0.1.1}/README.md +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/__init__.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/__main__.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/base.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/blocking_print.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/cli/initialise_options.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/cli.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/config.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/config_metadata.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/config_parse.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/files.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/informative_dict.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/job.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/job_filter.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/job_runner_simple_command.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/job_wrapper_python.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/log.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/py.typed +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/report.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/run_command.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/runem_version.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem/utils.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem.egg-info/SOURCES.txt +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem.egg-info/dependency_links.txt +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem.egg-info/entry_points.txt +0 -0
- {runem-0.0.32 → runem-0.1.1}/runem.egg-info/top_level.txt +0 -0
- {runem-0.0.32 → runem-0.1.1}/setup.cfg +0 -0
- {runem-0.0.32 → runem-0.1.1}/setup.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/__init__.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/cli/test_initialise_options.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/conftest.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/data/help_output.3.10.txt +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/data/help_output.3.11.txt +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/intentional_test_error.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/sanitise_reports_footer.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/test_base.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/test_blocking_print.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/test_cli.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/test_config.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/test_config_parse.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/test_files.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/test_hook_manager.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/test_informative_dict.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/test_job.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/test_job_filter.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/test_job_runner_simple_command.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/test_job_wrapper.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/test_job_wrapper_python.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/test_report.py +0 -0
- {runem-0.0.32 → runem-0.1.1}/tests/test_run_command.py +0 -0
@@ -4,6 +4,94 @@ Changelog
|
|
4
4
|
|
5
5
|
(unreleased)
|
6
6
|
------------
|
7
|
+
- Merge pull request #58 from lursight/fix/completed_job_counts. [Frank
|
8
|
+
Harrison]
|
9
|
+
|
10
|
+
fix(complete-job-count): fixes and simplifies the in-progress report code
|
11
|
+
- Fix(complete-job-count): fixes and simplifies the in-progress report
|
12
|
+
code. [Frank Harrison]
|
13
|
+
|
14
|
+
The core problem was that the remaining-job-count didn't match the
|
15
|
+
number of job-labels that were being shown.
|
16
|
+
|
17
|
+
The root-cause was due to some of the job-tasks completing before the
|
18
|
+
`_update_progress()` thread had started monitoring the jobs.
|
19
|
+
|
20
|
+
We fix this by adding a new parameter to the job-executer that tracks
|
21
|
+
when job completed. It's a very simple fix and reduces the overall
|
22
|
+
complexity for about the same cost of threading-primitives.
|
23
|
+
|
24
|
+
|
25
|
+
0.1.0 (2024-11-17)
|
26
|
+
------------------
|
27
|
+
- Release: version 0.1.0 🚀 [Frank Harrison]
|
28
|
+
- Merge pull request #57 from lursight/feat/replace_halo_with_rich.
|
29
|
+
[Frank Harrison]
|
30
|
+
|
31
|
+
feat(rich): moves to using rich instead of halo for spinners
|
32
|
+
- Feat(rich): moves to using rich instead of halo for spinners. [Frank
|
33
|
+
Harrison]
|
34
|
+
- Merge pull request #56 from lursight/feat/better_job_function_typing.
|
35
|
+
[Frank Harrison]
|
36
|
+
|
37
|
+
feat(better-job-function-typing): adds stronger typing for job and hook tasks
|
38
|
+
- Feat(better-job-function-typing): adds stronger typing for job and
|
39
|
+
hook tasks. [Frank Harrison]
|
40
|
+
|
41
|
+
We do this by typing the kwargs convenience variable, for which we need
|
42
|
+
to use `Unpack` (for back compatibility), and `type_extensions` for
|
43
|
+
cross-python-version compatibility (i.e. something that work for all
|
44
|
+
targeted version of python).
|
45
|
+
|
46
|
+
As a side-effect of this we get the benefit of seeing when and where we
|
47
|
+
add extra data into the call-stack for `job_execute()` and it emerges
|
48
|
+
that we only extend the key-word args when we are calling hooks in a
|
49
|
+
non-threaded way, which makes sense.
|
50
|
+
|
51
|
+
We do several things to achieve this:
|
52
|
+
1. We have common parameters passed to both hooks and job-tasks
|
53
|
+
- These common parameters make the hooks and job-task feel similar
|
54
|
+
to develop.
|
55
|
+
2. We put all hook-specific kwargs in one place, and mark each as
|
56
|
+
optional.
|
57
|
+
- this, for now, is mainly because we only have one hook, so this
|
58
|
+
will likely change.
|
59
|
+
3. We share and combine kwargs in a range of inheritance types, mainly
|
60
|
+
to work with(/around?) python-typing, which isn't great in this
|
61
|
+
type of situation.
|
62
|
+
- Chore(ignores): adds the tox/ dir to the ignores. [Frank Harrison]
|
63
|
+
- Chore(ignores): update git ignore for coverage files and docs gen.
|
64
|
+
[Frank Harrison]
|
65
|
+
|
66
|
+
The docs are recent additions.
|
67
|
+
- Merge pull request #55 from lursight/chore/test_improvements. [Frank
|
68
|
+
Harrison]
|
69
|
+
|
70
|
+
Chore/test improvements
|
71
|
+
- Chore(coverage): reduce false-positives by deleteing old
|
72
|
+
coverage_report files. [Frank Harrison]
|
73
|
+
|
74
|
+
Sometimes we would have stale .coverage_report.* files left behind where
|
75
|
+
from the multi-distributed pytest runs. These would lead more lines
|
76
|
+
being reported as coverage than actual - aka false-positive test passes
|
77
|
+
for coverage.
|
78
|
+
- Chore(help-tests): fixes the help output width in tests. [Frank
|
79
|
+
Harrison]
|
80
|
+
|
81
|
+
This reduces false-negative test results where we get word-splits in
|
82
|
+
directories or between process counters or other dynamic content.
|
83
|
+
|
84
|
+
This was due to the width of the terminal when the test was being run
|
85
|
+
being variant with developer-machine. This uses a fixed-width output,
|
86
|
+
reducing, if not stopping issues.
|
87
|
+
- Chore(deps): updates black 24.3.0 -> 24.10.0. [Frank Harrison]
|
88
|
+
- Chore(deps): updates to latest pytest 8.1.1 -> 8.3.3 + plugins. [Frank
|
89
|
+
Harrison]
|
90
|
+
|
91
|
+
|
92
|
+
0.0.32 (2024-11-17)
|
93
|
+
-------------------
|
94
|
+
- Release: version 0.0.32 🚀 [Frank Harrison]
|
7
95
|
- Merge pull request #54 from lursight/chore/use_tox_on_release. [Frank
|
8
96
|
Harrison]
|
9
97
|
|
@@ -1,17 +1,18 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: runem
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.1.1
|
4
4
|
Summary: Awesome runem created by lursight
|
5
5
|
Home-page: https://github.com/lursight/runem/
|
6
6
|
Author: lursight
|
7
7
|
Description-Content-Type: text/markdown
|
8
8
|
License-File: LICENSE
|
9
|
-
Requires-Dist: halo
|
10
9
|
Requires-Dist: packaging
|
11
10
|
Requires-Dist: PyYAML
|
11
|
+
Requires-Dist: rich
|
12
|
+
Requires-Dist: typing_extensions
|
12
13
|
Provides-Extra: tests
|
13
|
-
Requires-Dist: black==24.
|
14
|
-
Requires-Dist: coverage==7.
|
14
|
+
Requires-Dist: black==24.10.0; extra == "tests"
|
15
|
+
Requires-Dist: coverage==7.5; extra == "tests"
|
15
16
|
Requires-Dist: docformatter==1.7.5; extra == "tests"
|
16
17
|
Requires-Dist: flake8-bugbear==24.2.6; extra == "tests"
|
17
18
|
Requires-Dist: flake8==7.0.0; extra == "tests"
|
@@ -22,10 +23,10 @@ Requires-Dist: mypy==1.9.0; extra == "tests"
|
|
22
23
|
Requires-Dist: pydocstyle==6.3.0; extra == "tests"
|
23
24
|
Requires-Dist: pylint==3.1.0; extra == "tests"
|
24
25
|
Requires-Dist: pylama==8.4.1; extra == "tests"
|
25
|
-
Requires-Dist: pytest-cov==
|
26
|
+
Requires-Dist: pytest-cov==6.0.0; extra == "tests"
|
26
27
|
Requires-Dist: pytest-profiling==1.7.0; extra == "tests"
|
27
|
-
Requires-Dist: pytest-xdist==3.
|
28
|
-
Requires-Dist: pytest==8.
|
28
|
+
Requires-Dist: pytest-xdist==3.6.1; extra == "tests"
|
29
|
+
Requires-Dist: pytest==8.3.3; extra == "tests"
|
29
30
|
Requires-Dist: setuptools; extra == "tests"
|
30
31
|
Requires-Dist: termplotlib==0.3.9; extra == "tests"
|
31
32
|
Requires-Dist: tox; extra == "tests"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# This requirements are for development and testing only, not for production.
|
2
|
-
black==24.
|
3
|
-
coverage==7.
|
2
|
+
black==24.10.0
|
3
|
+
coverage==7.5
|
4
4
|
docformatter==1.7.5
|
5
5
|
flake8-bugbear==24.2.6
|
6
6
|
flake8==7.0.0
|
@@ -11,10 +11,10 @@ mypy==1.9.0
|
|
11
11
|
pydocstyle==6.3.0
|
12
12
|
pylint==3.1.0
|
13
13
|
pylama==8.4.1
|
14
|
-
pytest-cov==
|
14
|
+
pytest-cov==6.0.0
|
15
15
|
pytest-profiling==1.7.0
|
16
|
-
pytest-xdist==3.
|
17
|
-
pytest==8.
|
16
|
+
pytest-xdist==3.6.1
|
17
|
+
pytest==8.3.3
|
18
18
|
setuptools
|
19
19
|
termplotlib==0.3.9
|
20
20
|
tox
|
@@ -0,0 +1 @@
|
|
1
|
+
0.1.1
|
@@ -12,6 +12,45 @@ from runem.types import JobNames, OptionConfig, OptionsWritable
|
|
12
12
|
from runem.utils import printable_set
|
13
13
|
|
14
14
|
|
15
|
+
class HelpFormatterFixedWidth(argparse.HelpFormatter):
|
16
|
+
"""This works around test issues via constant width helo output.
|
17
|
+
|
18
|
+
This ensures that we get more constant for help-text by fixing the width to
|
19
|
+
something reasonable.
|
20
|
+
"""
|
21
|
+
|
22
|
+
def __init__(self, prog: typing.Any) -> None:
|
23
|
+
# Override the default width with a fixed width, for tests.
|
24
|
+
super().__init__(
|
25
|
+
prog,
|
26
|
+
# Pretty wide so we do not get wrapping on directories
|
27
|
+
# or process-count output.
|
28
|
+
width=1000,
|
29
|
+
)
|
30
|
+
|
31
|
+
|
32
|
+
def _get_argparse_help_formatter() -> typing.Any:
|
33
|
+
"""Returns a help-formatter for argparse.
|
34
|
+
|
35
|
+
This is for tests only to fake terminals of a constant with when rendering help
|
36
|
+
output.
|
37
|
+
"""
|
38
|
+
# Check environment variable to see if we're in tests and need a fixed width
|
39
|
+
# help output.
|
40
|
+
use_fixed_width = os.getenv("RUNEM_FIXED_HELP_WIDTH", None)
|
41
|
+
|
42
|
+
if use_fixed_width:
|
43
|
+
# Use custom formatter with the width specified in the environment variable
|
44
|
+
return (
|
45
|
+
lambda prog: HelpFormatterFixedWidth( # pylint: disable=unnecessary-lambda
|
46
|
+
prog
|
47
|
+
)
|
48
|
+
)
|
49
|
+
|
50
|
+
# Use default formatter
|
51
|
+
return argparse.HelpFormatter
|
52
|
+
|
53
|
+
|
15
54
|
def parse_args(
|
16
55
|
config_metadata: ConfigMetadata, argv: typing.List[str]
|
17
56
|
) -> ConfigMetadata:
|
@@ -23,7 +62,9 @@ def parse_args(
|
|
23
62
|
Returns the parsed args, the jobs_names_to_run, job_phases_to_run, job_tags_to_run
|
24
63
|
"""
|
25
64
|
parser = argparse.ArgumentParser(
|
26
|
-
add_help=False,
|
65
|
+
add_help=False,
|
66
|
+
description="Runs the Lursight Lang test-suite",
|
67
|
+
formatter_class=_get_argparse_help_formatter(),
|
27
68
|
)
|
28
69
|
parser.add_argument(
|
29
70
|
"-H", "--help", action="help", help="show this help message and exit"
|
@@ -1,6 +1,8 @@
|
|
1
1
|
import typing
|
2
2
|
from collections import defaultdict
|
3
3
|
|
4
|
+
from typing_extensions import Unpack
|
5
|
+
|
4
6
|
from runem.config_metadata import ConfigMetadata
|
5
7
|
from runem.job import Job
|
6
8
|
from runem.job_execute import job_execute
|
@@ -10,6 +12,7 @@ from runem.types import (
|
|
10
12
|
HookConfig,
|
11
13
|
HookName,
|
12
14
|
Hooks,
|
15
|
+
HookSpecificKwargs,
|
13
16
|
HooksStore,
|
14
17
|
JobConfig,
|
15
18
|
)
|
@@ -69,7 +72,7 @@ class HookManager:
|
|
69
72
|
self,
|
70
73
|
hook_name: HookName,
|
71
74
|
config_metadata: ConfigMetadata,
|
72
|
-
**kwargs:
|
75
|
+
**kwargs: Unpack[HookSpecificKwargs],
|
73
76
|
) -> None:
|
74
77
|
"""Invokes all functions registered to a specific hook."""
|
75
78
|
hooks: typing.List[HookConfig] = self.hooks_store.get(hook_name, [])
|
@@ -92,6 +95,7 @@ class HookManager:
|
|
92
95
|
job_execute(
|
93
96
|
job_config,
|
94
97
|
running_jobs={},
|
98
|
+
completed_jobs={},
|
95
99
|
config_metadata=config_metadata,
|
96
100
|
file_lists=file_lists,
|
97
101
|
**kwargs,
|
@@ -5,13 +5,17 @@ import uuid
|
|
5
5
|
from datetime import timedelta
|
6
6
|
from timeit import default_timer as timer
|
7
7
|
|
8
|
+
from typing_extensions import Unpack
|
9
|
+
|
8
10
|
from runem.config_metadata import ConfigMetadata
|
9
11
|
from runem.informative_dict import ReadOnlyInformativeDict
|
10
12
|
from runem.job import Job
|
11
13
|
from runem.job_wrapper import get_job_wrapper
|
12
14
|
from runem.log import error, log
|
13
15
|
from runem.types import (
|
16
|
+
FilePathList,
|
14
17
|
FilePathListLookup,
|
18
|
+
HookSpecificKwargs,
|
15
19
|
JobConfig,
|
16
20
|
JobFunction,
|
17
21
|
JobReturn,
|
@@ -26,7 +30,7 @@ def job_execute_inner(
|
|
26
30
|
job_config: JobConfig,
|
27
31
|
config_metadata: ConfigMetadata,
|
28
32
|
file_lists: FilePathListLookup,
|
29
|
-
**kwargs:
|
33
|
+
**kwargs: Unpack[HookSpecificKwargs],
|
30
34
|
) -> typing.Tuple[JobTiming, JobReturn]:
|
31
35
|
"""Wrapper for running a job inside a sub-process.
|
32
36
|
|
@@ -36,13 +40,12 @@ def job_execute_inner(
|
|
36
40
|
if config_metadata.args.verbose:
|
37
41
|
log(f"START: '{label}'")
|
38
42
|
root_path: pathlib.Path = config_metadata.cfg_filepath.parent
|
39
|
-
function: JobFunction
|
40
43
|
job_tags: typing.Optional[JobTags] = Job.get_job_tags(job_config)
|
41
44
|
os.chdir(root_path)
|
42
|
-
function = get_job_wrapper(job_config, config_metadata.cfg_filepath)
|
45
|
+
function: JobFunction = get_job_wrapper(job_config, config_metadata.cfg_filepath)
|
43
46
|
|
44
47
|
# get the files for all files found for this job's tags
|
45
|
-
file_list = Job.get_job_files(file_lists, job_tags)
|
48
|
+
file_list: FilePathList = Job.get_job_files(file_lists, job_tags)
|
46
49
|
|
47
50
|
if not file_list:
|
48
51
|
# no files to work on
|
@@ -77,8 +80,9 @@ def job_execute_inner(
|
|
77
80
|
log(f"job: running: '{Job.get_job_name(job_config)}'")
|
78
81
|
reports: JobReturn
|
79
82
|
try:
|
83
|
+
assert isinstance(function, JobFunction)
|
80
84
|
reports = function(
|
81
|
-
options=ReadOnlyInformativeDict(config_metadata.options),
|
85
|
+
options=ReadOnlyInformativeDict(config_metadata.options),
|
82
86
|
file_list=file_list,
|
83
87
|
procs=config_metadata.args.procs,
|
84
88
|
root_path=root_path,
|
@@ -107,9 +111,10 @@ def job_execute_inner(
|
|
107
111
|
def job_execute(
|
108
112
|
job_config: JobConfig,
|
109
113
|
running_jobs: typing.Dict[str, str],
|
114
|
+
completed_jobs: typing.Dict[str, str],
|
110
115
|
config_metadata: ConfigMetadata,
|
111
116
|
file_lists: FilePathListLookup,
|
112
|
-
**kwargs:
|
117
|
+
**kwargs: Unpack[HookSpecificKwargs],
|
113
118
|
) -> typing.Tuple[JobTiming, JobReturn]:
|
114
119
|
"""Thin-wrapper around job_execute_inner needed for mocking in tests.
|
115
120
|
|
@@ -123,5 +128,6 @@ def job_execute(
|
|
123
128
|
file_lists,
|
124
129
|
**kwargs,
|
125
130
|
)
|
131
|
+
completed_jobs[this_id] = running_jobs[this_id]
|
126
132
|
del running_jobs[this_id]
|
127
133
|
return results
|
@@ -18,7 +18,7 @@ def get_job_wrapper(job_wrapper: JobWrapper, cfg_filepath: pathlib.Path) -> JobF
|
|
18
18
|
# validate that the command is "understandable" and usable.
|
19
19
|
command_string: str = job_wrapper["command"]
|
20
20
|
validate_simple_command(command_string)
|
21
|
-
return job_runner_simple_command
|
21
|
+
return job_runner_simple_command
|
22
22
|
|
23
23
|
# if we do not have a simple command address assume we have just an addressed
|
24
24
|
# function
|
@@ -28,17 +28,19 @@ import typing
|
|
28
28
|
from collections import defaultdict
|
29
29
|
from datetime import timedelta
|
30
30
|
from itertools import repeat
|
31
|
-
from multiprocessing.managers import DictProxy,
|
31
|
+
from multiprocessing.managers import DictProxy, ValueProxy
|
32
32
|
from timeit import default_timer as timer
|
33
|
+
from types import TracebackType
|
33
34
|
|
34
|
-
from
|
35
|
+
from rich.console import Console, ConsoleOptions, ConsoleRenderable, RenderResult
|
36
|
+
from rich.spinner import Spinner
|
37
|
+
from rich.text import Text
|
35
38
|
|
36
39
|
from runem.command_line import parse_args
|
37
40
|
from runem.config import load_project_config, load_user_configs
|
38
41
|
from runem.config_metadata import ConfigMetadata
|
39
42
|
from runem.config_parse import load_config_metadata
|
40
43
|
from runem.files import find_files
|
41
|
-
from runem.job import Job
|
42
44
|
from runem.job_execute import job_execute
|
43
45
|
from runem.job_filter import filter_jobs
|
44
46
|
from runem.log import error, log, warn
|
@@ -58,6 +60,8 @@ from runem.types import (
|
|
58
60
|
)
|
59
61
|
from runem.utils import printable_set
|
60
62
|
|
63
|
+
rich_console = Console()
|
64
|
+
|
61
65
|
|
62
66
|
def _determine_run_parameters(argv: typing.List[str]) -> ConfigMetadata:
|
63
67
|
"""Loads config, parsing cli input and produces the run config.
|
@@ -85,10 +89,40 @@ def _determine_run_parameters(argv: typing.List[str]) -> ConfigMetadata:
|
|
85
89
|
return config_metadata
|
86
90
|
|
87
91
|
|
92
|
+
class DummySpinner(ConsoleRenderable): # pragma: no cover
|
93
|
+
"""A dummy spinner for when spinners are disabled."""
|
94
|
+
|
95
|
+
def __init__(self) -> None:
|
96
|
+
self.text = ""
|
97
|
+
|
98
|
+
def __rich__(self) -> Text:
|
99
|
+
"""Return a rich Text object for rendering."""
|
100
|
+
return Text(self.text)
|
101
|
+
|
102
|
+
def __rich_console__(
|
103
|
+
self, console: Console, options: ConsoleOptions
|
104
|
+
) -> RenderResult:
|
105
|
+
"""Yield an empty string or placeholder text."""
|
106
|
+
yield Text(self.text)
|
107
|
+
|
108
|
+
def __enter__(self) -> None:
|
109
|
+
"""Support for context manager."""
|
110
|
+
pass
|
111
|
+
|
112
|
+
def __exit__(
|
113
|
+
self,
|
114
|
+
exc_type: typing.Optional[typing.Type[BaseException]],
|
115
|
+
exc_value: typing.Optional[BaseException],
|
116
|
+
traceback: typing.Optional[TracebackType],
|
117
|
+
) -> None:
|
118
|
+
"""Support for context manager."""
|
119
|
+
pass
|
120
|
+
|
121
|
+
|
88
122
|
def _update_progress(
|
89
123
|
label: str,
|
90
124
|
running_jobs: typing.Dict[str, str],
|
91
|
-
|
125
|
+
completed_jobs: typing.Dict[str, str],
|
92
126
|
all_jobs: Jobs,
|
93
127
|
is_running: ValueProxy[bool],
|
94
128
|
num_workers: int,
|
@@ -99,57 +133,37 @@ def _update_progress(
|
|
99
133
|
Args:
|
100
134
|
label (str): The identifier.
|
101
135
|
running_jobs (Dict[str, str]): The currently running jobs.
|
102
|
-
seen_jobs (List[str]): Jobs that the function has previously tracked.
|
103
136
|
all_jobs (Jobs): All jobs, encompassing both completed and running jobs.
|
104
137
|
is_running (ValueProxy[bool]): Flag indicating if jobs are still running.
|
105
138
|
num_workers (int): Indicates the number of workers performing the jobs.
|
106
139
|
"""
|
107
|
-
# Using
|
140
|
+
# Using the `rich` module to show a loading spinner on console
|
141
|
+
spinner: typing.Union[Spinner, DummySpinner]
|
108
142
|
if show_spinner:
|
109
|
-
spinner =
|
110
|
-
|
111
|
-
|
112
|
-
# The set of all job labels, and the set of completed jobs
|
113
|
-
all_job_names: typing.Set[str] = {Job.get_job_name(job) for job in all_jobs}
|
114
|
-
completed_jobs: typing.Set[str] = set()
|
143
|
+
spinner = Spinner("dots", text="Starting tasks...")
|
144
|
+
else:
|
145
|
+
spinner = DummySpinner()
|
115
146
|
|
116
|
-
# This dataset is used to track changes between iterations
|
117
147
|
last_running_jobs_set: typing.Set[str] = set()
|
118
148
|
|
119
|
-
|
120
|
-
|
121
|
-
|
149
|
+
with rich_console.status(spinner):
|
150
|
+
while is_running.value:
|
151
|
+
running_jobs_set: typing.Set[str] = set(running_jobs.values())
|
122
152
|
|
123
|
-
|
124
|
-
|
153
|
+
# Progress report
|
154
|
+
progress: str = f"{len(completed_jobs)}/{len(all_jobs)}"
|
155
|
+
running_jobs_list = printable_set(
|
156
|
+
running_jobs_set
|
157
|
+
) # Reflect current running jobs accurately
|
158
|
+
report: str = f"{label}: {progress}({num_workers}): {running_jobs_list}"
|
159
|
+
if show_spinner:
|
160
|
+
spinner.text = report
|
161
|
+
else:
|
162
|
+
if last_running_jobs_set != running_jobs_set:
|
163
|
+
rich_console.log(report)
|
125
164
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
# Check if we're closing to completion
|
130
|
-
workers_retiring: bool = len(remaining_jobs) <= num_workers
|
131
|
-
|
132
|
-
if workers_retiring:
|
133
|
-
# Handle edge case: a task may have disappeared whilst process was sleeping
|
134
|
-
all_completed_jobs: typing.Set[str] = all_job_names - remaining_jobs
|
135
|
-
disappeared_jobs.update(all_completed_jobs - running_jobs_set)
|
136
|
-
|
137
|
-
completed_jobs.update(disappeared_jobs)
|
138
|
-
|
139
|
-
# Prepare progress report
|
140
|
-
progress: str = f"{len(completed_jobs)}/{len(all_jobs)}"
|
141
|
-
running_jobs_list = printable_set(running_jobs_set)
|
142
|
-
if show_spinner:
|
143
|
-
spinner.text = f"{label}: {progress}({num_workers}): {running_jobs_list}"
|
144
|
-
|
145
|
-
# Update the tracked dataset for the next iteration
|
146
|
-
last_running_jobs_set = running_jobs_set
|
147
|
-
|
148
|
-
# Sleep to decrease frequency of updates and reduce CPU usage
|
149
|
-
time.sleep(0.1)
|
150
|
-
|
151
|
-
if show_spinner:
|
152
|
-
spinner.stop()
|
165
|
+
# Sleep for reduced CPU usage
|
166
|
+
time.sleep(0.1)
|
153
167
|
|
154
168
|
|
155
169
|
def _process_jobs(
|
@@ -187,8 +201,8 @@ def _process_jobs(
|
|
187
201
|
subprocess_error: typing.Optional[BaseException] = None
|
188
202
|
|
189
203
|
with multiprocessing.Manager() as manager:
|
190
|
-
seen_jobs: ListProxy[str] = manager.list()
|
191
204
|
running_jobs: DictProxy[typing.Any, typing.Any] = manager.dict()
|
205
|
+
completed_jobs: DictProxy[typing.Any, typing.Any] = manager.dict()
|
192
206
|
is_running: ValueProxy[bool] = manager.Value("b", True)
|
193
207
|
|
194
208
|
terminal_writer_process = multiprocessing.Process(
|
@@ -196,7 +210,7 @@ def _process_jobs(
|
|
196
210
|
args=(
|
197
211
|
phase,
|
198
212
|
running_jobs,
|
199
|
-
|
213
|
+
completed_jobs,
|
200
214
|
jobs,
|
201
215
|
is_running,
|
202
216
|
num_concurrent_procs,
|
@@ -209,10 +223,11 @@ def _process_jobs(
|
|
209
223
|
with multiprocessing.Pool(processes=num_concurrent_procs) as pool:
|
210
224
|
# use starmap so we can pass down the job-configs and the args and the files
|
211
225
|
in_out_job_run_metadatas[phase] = pool.starmap(
|
212
|
-
job_execute,
|
226
|
+
job_execute, # no kwargs passed for jobs here
|
213
227
|
zip(
|
214
228
|
jobs,
|
215
229
|
repeat(running_jobs),
|
230
|
+
repeat(completed_jobs),
|
216
231
|
repeat(config_metadata),
|
217
232
|
repeat(file_lists),
|
218
233
|
),
|
@@ -1,9 +1,37 @@
|
|
1
|
-
|
1
|
+
"""
|
2
|
+
|
3
|
+
Some note on Unpack and kwargs:
|
4
|
+
We *try* to strongly type `**kwargs` for clarity.
|
5
|
+
We have tried several ways to define a Generic type that encapsulates
|
6
|
+
`**kwargs: SingleType`
|
7
|
+
... but none of the solutions worked with python 3.9 -> 3.12 and mypy 1.9.0,
|
8
|
+
so we have to recommend instead using:
|
9
|
+
`**kwargs: Unpack[KwArgsType]`
|
10
|
+
|
11
|
+
For this to work across versions of python where support for Unpack changes;
|
12
|
+
for example `Unpack` is a python 3.12 feature, but available in the
|
13
|
+
`typing_extensions` module.
|
14
|
+
|
15
|
+
So, for now, it looks like we get away with importing `Unpack` from the
|
16
|
+
`typing_extensions` module, even in python 3.12, so we will use, and
|
17
|
+
recommend using, the `typing_extensions` of `Unpack`, until it becomes
|
18
|
+
obsolete.
|
19
|
+
|
20
|
+
Alternatively, we can use the following, but it's unnecessarily verbose.
|
21
|
+
|
22
|
+
if sys.version_info >= (3, 12): # pragma: no coverage
|
23
|
+
from typing import Unpack
|
24
|
+
else: # pragma: no coverage
|
25
|
+
from typing_extensions import Unpack
|
26
|
+
"""
|
27
|
+
|
2
28
|
import pathlib
|
3
29
|
import typing
|
4
30
|
from datetime import timedelta
|
5
31
|
from enum import Enum
|
6
32
|
|
33
|
+
from typing_extensions import Unpack
|
34
|
+
|
7
35
|
from runem.informative_dict import InformativeDict, ReadOnlyInformativeDict
|
8
36
|
|
9
37
|
|
@@ -96,12 +124,6 @@ FilePathSerialise = str
|
|
96
124
|
FilePathList = typing.List[FilePathSerialise]
|
97
125
|
FilePathListLookup = typing.DefaultDict[JobTag, FilePathList]
|
98
126
|
|
99
|
-
# FIXME: this type is no-longer the actual spec of the test-functions
|
100
|
-
JobFunction = typing.Union[
|
101
|
-
typing.Callable[[argparse.Namespace, OptionsWritable, FilePathList], None],
|
102
|
-
typing.Callable[[typing.Any], None],
|
103
|
-
]
|
104
|
-
|
105
127
|
|
106
128
|
class JobParamConfig(typing.TypedDict):
|
107
129
|
"""Configures what parameters are passed to the test-callable.
|
@@ -250,3 +272,86 @@ ConfigNodes = typing.Union[
|
|
250
272
|
Config = typing.List[ConfigNodes]
|
251
273
|
|
252
274
|
UserConfigMetadata = typing.List[typing.Tuple[Config, pathlib.Path]]
|
275
|
+
|
276
|
+
|
277
|
+
class CommonKwargs(
|
278
|
+
typing.TypedDict,
|
279
|
+
total=True, # each of these are guaranteed to exist in jobs and hooks
|
280
|
+
):
|
281
|
+
"""Defines the base args that are passed to all jobs.
|
282
|
+
|
283
|
+
As we call hooks and job-task in the same manner, this defines the variables that we
|
284
|
+
can access from both hooks and job-tasks.
|
285
|
+
"""
|
286
|
+
|
287
|
+
root_path: pathlib.Path # the path where the .runem.yml file is
|
288
|
+
job: JobConfig # the job or hook task spec ¢ TODO: rename this
|
289
|
+
label: str # the name of the hook or the job-label
|
290
|
+
options: Options # options passed in on the command line
|
291
|
+
procs: int # the max number of concurrent procs to run
|
292
|
+
verbose: bool # control log verbosity
|
293
|
+
|
294
|
+
|
295
|
+
class HookSpecificKwargs(typing.TypedDict, total=False):
|
296
|
+
"""Defines the args that are passed down to the hooks.
|
297
|
+
|
298
|
+
NOTE: that although these however
|
299
|
+
outside of the *hook* context, the data will not be present. Such is the
|
300
|
+
difficulty in dynamic programming.
|
301
|
+
"""
|
302
|
+
|
303
|
+
wall_clock_time_saved: timedelta # only on `HookName.ON_EXIT`
|
304
|
+
|
305
|
+
|
306
|
+
class JobTaskKwargs(
|
307
|
+
typing.TypedDict,
|
308
|
+
total=False, # for now, we don't enforce these types for job-context, but we should.
|
309
|
+
):
|
310
|
+
"""Defines the task-specific args for job-task functions."""
|
311
|
+
|
312
|
+
file_list: FilePathList
|
313
|
+
record_sub_job_time: typing.Optional[typing.Callable[[str, timedelta], None]]
|
314
|
+
|
315
|
+
|
316
|
+
class HookKwargs(CommonKwargs, HookSpecificKwargs):
|
317
|
+
"""A merged set of kwargs for runem-hooks."""
|
318
|
+
|
319
|
+
pass
|
320
|
+
|
321
|
+
|
322
|
+
class JobKwargs(CommonKwargs, JobTaskKwargs):
|
323
|
+
"""A merged set of kwargs for job-tasks."""
|
324
|
+
|
325
|
+
pass
|
326
|
+
|
327
|
+
|
328
|
+
class AllKwargs(CommonKwargs, JobTaskKwargs, HookSpecificKwargs):
|
329
|
+
"""A merged set of kwargs for al job-functions."""
|
330
|
+
|
331
|
+
pass
|
332
|
+
|
333
|
+
|
334
|
+
@typing.runtime_checkable
|
335
|
+
class JobFunction(typing.Protocol):
|
336
|
+
def __call__(self, **kwargs: Unpack[AllKwargs]) -> JobReturn: # pragma: no cover
|
337
|
+
"""Defines the call() protocol's abstract pattern for job-tasks."""
|
338
|
+
|
339
|
+
@property
|
340
|
+
def __name__(self) -> str: # pragma: no cover
|
341
|
+
"""Defines the name protocol for job-task functions.
|
342
|
+
|
343
|
+
This is primarily used for internal tests but can be useful for introspection.
|
344
|
+
"""
|
345
|
+
|
346
|
+
|
347
|
+
def _hook_example(
|
348
|
+
wall_clock_time_saved: timedelta,
|
349
|
+
**kwargs: typing.Any,
|
350
|
+
) -> None:
|
351
|
+
"""An example hook."""
|
352
|
+
|
353
|
+
|
354
|
+
def _job_task_example(
|
355
|
+
**kwargs: Unpack[JobKwargs],
|
356
|
+
) -> None:
|
357
|
+
"""An example job-task function."""
|
@@ -1,17 +1,18 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: runem
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.1.1
|
4
4
|
Summary: Awesome runem created by lursight
|
5
5
|
Home-page: https://github.com/lursight/runem/
|
6
6
|
Author: lursight
|
7
7
|
Description-Content-Type: text/markdown
|
8
8
|
License-File: LICENSE
|
9
|
-
Requires-Dist: halo
|
10
9
|
Requires-Dist: packaging
|
11
10
|
Requires-Dist: PyYAML
|
11
|
+
Requires-Dist: rich
|
12
|
+
Requires-Dist: typing_extensions
|
12
13
|
Provides-Extra: tests
|
13
|
-
Requires-Dist: black==24.
|
14
|
-
Requires-Dist: coverage==7.
|
14
|
+
Requires-Dist: black==24.10.0; extra == "tests"
|
15
|
+
Requires-Dist: coverage==7.5; extra == "tests"
|
15
16
|
Requires-Dist: docformatter==1.7.5; extra == "tests"
|
16
17
|
Requires-Dist: flake8-bugbear==24.2.6; extra == "tests"
|
17
18
|
Requires-Dist: flake8==7.0.0; extra == "tests"
|
@@ -22,10 +23,10 @@ Requires-Dist: mypy==1.9.0; extra == "tests"
|
|
22
23
|
Requires-Dist: pydocstyle==6.3.0; extra == "tests"
|
23
24
|
Requires-Dist: pylint==3.1.0; extra == "tests"
|
24
25
|
Requires-Dist: pylama==8.4.1; extra == "tests"
|
25
|
-
Requires-Dist: pytest-cov==
|
26
|
+
Requires-Dist: pytest-cov==6.0.0; extra == "tests"
|
26
27
|
Requires-Dist: pytest-profiling==1.7.0; extra == "tests"
|
27
|
-
Requires-Dist: pytest-xdist==3.
|
28
|
-
Requires-Dist: pytest==8.
|
28
|
+
Requires-Dist: pytest-xdist==3.6.1; extra == "tests"
|
29
|
+
Requires-Dist: pytest==8.3.3; extra == "tests"
|
29
30
|
Requires-Dist: setuptools; extra == "tests"
|
30
31
|
Requires-Dist: termplotlib==0.3.9; extra == "tests"
|
31
32
|
Requires-Dist: tox; extra == "tests"
|
@@ -1,10 +1,11 @@
|
|
1
|
-
halo
|
2
1
|
packaging
|
3
2
|
PyYAML
|
3
|
+
rich
|
4
|
+
typing_extensions
|
4
5
|
|
5
6
|
[tests]
|
6
|
-
black==24.
|
7
|
-
coverage==7.
|
7
|
+
black==24.10.0
|
8
|
+
coverage==7.5
|
8
9
|
docformatter==1.7.5
|
9
10
|
flake8-bugbear==24.2.6
|
10
11
|
flake8==7.0.0
|
@@ -15,10 +16,10 @@ mypy==1.9.0
|
|
15
16
|
pydocstyle==6.3.0
|
16
17
|
pylint==3.1.0
|
17
18
|
pylama==8.4.1
|
18
|
-
pytest-cov==
|
19
|
+
pytest-cov==6.0.0
|
19
20
|
pytest-profiling==1.7.0
|
20
|
-
pytest-xdist==3.
|
21
|
-
pytest==8.
|
21
|
+
pytest-xdist==3.6.1
|
22
|
+
pytest==8.3.3
|
22
23
|
setuptools
|
23
24
|
termplotlib==0.3.9
|
24
25
|
tox
|
@@ -45,7 +45,7 @@ def _job_execute_and_capture_stdout(
|
|
45
45
|
ret_err: typing.Optional[BaseException] = None
|
46
46
|
with io.StringIO() as buf, redirect_stdout(buf):
|
47
47
|
try:
|
48
|
-
job_execute(job_config, running_jobs, config_metadata, file_lists)
|
48
|
+
job_execute(job_config, running_jobs, {}, config_metadata, file_lists)
|
49
49
|
except BaseException as err: # pylint: disable=broad-exception-caught
|
50
50
|
# capture the error and return it
|
51
51
|
ret_err = err
|
@@ -669,6 +669,8 @@ def test_runem_help(
|
|
669
669
|
As we build features we want to ensure that the help output stays consistent as we
|
670
670
|
leverage the argparse system to generate the help for a specific .runem.yml config
|
671
671
|
"""
|
672
|
+
# Ensure we get fixed-width output.
|
673
|
+
os.environ["RUNEM_FIXED_HELP_WIDTH"] = "1"
|
672
674
|
runem_cli_switches: typing.List[str] = ["--help"]
|
673
675
|
runem_stdout: typing.List[str]
|
674
676
|
error_raised: typing.Optional[BaseException]
|
@@ -1673,11 +1675,12 @@ def test_progress_updater_with_running_jobs(
|
|
1673
1675
|
show_spinner: bool,
|
1674
1676
|
) -> None:
|
1675
1677
|
running_jobs: typing.Dict[str, str] = {"job1": "running", "job2": "pending"}
|
1678
|
+
completed_jobs: typing.Dict[str, str] = {}
|
1676
1679
|
with pytest.raises(SleepCalledError), multiprocessing.Manager() as manager:
|
1677
1680
|
_update_progress(
|
1678
1681
|
"dummy label",
|
1679
1682
|
running_jobs,
|
1680
|
-
|
1683
|
+
completed_jobs,
|
1681
1684
|
all_jobs=[],
|
1682
1685
|
is_running=manager.Value("b", True),
|
1683
1686
|
num_workers=1,
|
@@ -1688,6 +1691,7 @@ def test_progress_updater_with_running_jobs(
|
|
1688
1691
|
|
1689
1692
|
def test_progress_updater_with_running_jobs_and_10_jobs(mock_sleep: Mock) -> None:
|
1690
1693
|
running_jobs: typing.Dict[str, str] = {"job1": "running", "job2": "pending"}
|
1694
|
+
completed_jobs: typing.Dict[str, str] = {}
|
1691
1695
|
job_config: JobConfig = {
|
1692
1696
|
"addr": {
|
1693
1697
|
"file": __file__,
|
@@ -1713,7 +1717,7 @@ def test_progress_updater_with_running_jobs_and_10_jobs(mock_sleep: Mock) -> Non
|
|
1713
1717
|
_update_progress(
|
1714
1718
|
"dummy label",
|
1715
1719
|
running_jobs,
|
1716
|
-
|
1720
|
+
completed_jobs,
|
1717
1721
|
all_jobs=all_jobs,
|
1718
1722
|
is_running=manager.Value("b", True),
|
1719
1723
|
num_workers=1,
|
@@ -1724,11 +1728,12 @@ def test_progress_updater_with_running_jobs_and_10_jobs(mock_sleep: Mock) -> Non
|
|
1724
1728
|
|
1725
1729
|
def test_progress_updater_without_running_jobs(mock_sleep: Mock) -> None:
|
1726
1730
|
running_jobs: typing.Dict[str, str] = {}
|
1731
|
+
completed_jobs: typing.Dict[str, str] = {}
|
1727
1732
|
with pytest.raises(SleepCalledError), multiprocessing.Manager() as manager:
|
1728
1733
|
_update_progress(
|
1729
1734
|
"dummy label",
|
1730
1735
|
running_jobs,
|
1731
|
-
|
1736
|
+
completed_jobs,
|
1732
1737
|
all_jobs=[],
|
1733
1738
|
is_running=manager.Value("b", True),
|
1734
1739
|
num_workers=1,
|
@@ -1739,11 +1744,12 @@ def test_progress_updater_without_running_jobs(mock_sleep: Mock) -> None:
|
|
1739
1744
|
|
1740
1745
|
def test_progress_updater_with_empty_running_jobs(mock_sleep: Mock) -> None:
|
1741
1746
|
running_jobs: typing.Dict[str, str] = {"job1": ""}
|
1747
|
+
completed_jobs: typing.Dict[str, str] = {}
|
1742
1748
|
with pytest.raises(SleepCalledError), multiprocessing.Manager() as manager:
|
1743
1749
|
_update_progress(
|
1744
1750
|
"dummy label",
|
1745
1751
|
running_jobs,
|
1746
|
-
|
1752
|
+
completed_jobs,
|
1747
1753
|
all_jobs=[],
|
1748
1754
|
is_running=manager.Value("b", True),
|
1749
1755
|
num_workers=1,
|
@@ -1767,7 +1773,7 @@ def test_progress_updater_with_false(show_spinner: bool) -> None:
|
|
1767
1773
|
_update_progress(
|
1768
1774
|
"dummy label",
|
1769
1775
|
running_jobs,
|
1770
|
-
|
1776
|
+
{},
|
1771
1777
|
[],
|
1772
1778
|
manager.Value("b", False),
|
1773
1779
|
1,
|
runem-0.0.32/requirements.txt
DELETED
runem-0.0.32/runem/VERSION
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
0.0.32
|
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
|
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
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|