diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9c33f98..44442f5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,7 +26,7 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up Python 3.10 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -39,6 +39,10 @@ jobs: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Check types with mypy + run: | + python3 -m pip install '.[types]' + python3 -m mypy cats - name: Test with pytest run: | - python3 -m pytest + python3 -m pytest --cov diff --git a/cats/__init__.py b/cats/__init__.py index 3318da4..5aaf9a0 100644 --- a/cats/__init__.py +++ b/cats/__init__.py @@ -196,7 +196,7 @@ def to_json(self, dateformat: str = "", **kwargs) -> str: def schedule_at(output: CATSOutput, args: list[str]) -> None: "Schedule job with optimal start time using at(1)" proc = subprocess.Popen(args, stdout=subprocess.PIPE) - output = subprocess.check_output( + proc_output = subprocess.check_output( ( "at", "-t", @@ -206,7 +206,7 @@ def schedule_at(output: CATSOutput, args: list[str]) -> None: ) -def main(arguments=None) -> Optional[int]: +def main(arguments=None) -> int: parser = parse_arguments() args = parser.parse_args(arguments) if args.command and not args.scheduler: @@ -215,7 +215,7 @@ def main(arguments=None) -> Optional[int]: " specify the scheduler with the -s or --scheduler option" ) return 1 - config, CI_API_interface, location, duration = get_runtime_config(args) + config, CI_API_interface, location, duration, _jobinfo, _PUE = get_runtime_config(args) ######################## ## Obtain CI forecast ## @@ -272,6 +272,7 @@ def main(arguments=None) -> Optional[int]: print(output) if args.command and args.scheduler == "at": schedule_at(output, args.command.split()) + return 0 if __name__ == "__main__": diff --git a/cats/check_clean_arguments.py b/cats/check_clean_arguments.py index a301f06..3af20eb 100644 --- a/cats/check_clean_arguments.py +++ b/cats/check_clean_arguments.py @@ -1,6 +1,6 @@ import re import sys -from typing import Iterable, Optional, TypedDict +from typing import Iterable, Optional, TypedDict, cast class JobInfo(TypedDict): @@ -28,7 +28,11 @@ def validate_jobinfo( "cpus", "gpus", ) - info = dict([match.groups() for match in re.finditer(r"(\w+)=([\w.]+)", jobinfo)]) + + info_list = [match.groups() for match in re.finditer(r"(\w+)=([\w.]+)", jobinfo)] + info = {} + for item in info_list: + info[item[0]] = item[1] # Check if some information is missing if missing_keys := set(expected_info_keys) - set(info.keys()): @@ -46,11 +50,11 @@ def validate_jobinfo( for key in [k for k in info if k != "partition"]: try: info[key] = int(info[key]) - assert info[key] >= 0 + assert int(info[key]) >= 0 except (ValueError, AssertionError): sys.stderr.write( f"ERROR: job info key {key} should be a positive integer\n" ) return None - return JobInfo(info) + return cast(JobInfo, info) diff --git a/cats/configure.py b/cats/configure.py index f7b73f7..305a288 100644 --- a/cats/configure.py +++ b/cats/configure.py @@ -13,7 +13,7 @@ import logging import sys from collections.abc import Mapping -from typing import Any +from typing import Any, Union import requests import yaml @@ -24,7 +24,8 @@ __all__ = ["get_runtime_config"] -def get_runtime_config(args) -> tuple[dict, APIInterface, str, int]: +def get_runtime_config(args) -> tuple[Mapping[str, Any], APIInterface, str, int, + Union[list[tuple[int, float]],None],Any]: """Return the runtime cats configuration from list of command line arguments and content of configuration file. @@ -47,7 +48,7 @@ def get_runtime_config(args) -> tuple[dict, APIInterface, str, int]: try: duration = int(args.duration) except ValueError: - logging.eror(msg) + logging.error(msg) raise ValueError if duration <= 0: logging.error(msg) @@ -91,13 +92,14 @@ def CI_API_from_config_or_args(args, config) -> APIInterface: api = "carbonintensity.org.uk" # default value logging.warning(f"Unspecified carbon intensity forecast service, using {api}") try: - return API_interfaces[api] + interface = API_interfaces[api] except KeyError: logging.error( f"Error: {api} is not a valid API choice. It must be one of " "\n".join( API_interfaces.keys() ) ) + return interface def get_location_from_config_or_args(args, config) -> str: diff --git a/pyproject.toml b/pyproject.toml index 133461d..f7b763e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,8 @@ dependencies = ["requests-cache>=1.0", "PyYAML>=6.0"] [project.optional-dependencies] - test = ["pytest", "numpy>=1.5.0", "pytest-subprocess==1.5.0"] + test = ["pytest", "numpy>=1.5.0", "pytest-subprocess==1.5.0", "pytest-cov"] + types = ["mypy", "types-PyYAML", "types-redis", "types-requests", "types-ujson"] [project.urls] Home = "https://github.com/GreenScheduler/cats" diff --git a/tests/test_output.py b/tests/test_output.py index 2f16e19..74bae78 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -31,10 +31,11 @@ @pytest.mark.parametrize( "output,expected", [ - (OUTPUT, "Best job start time: 2024-03-16 02:00:00"), + (OUTPUT, "Best job start time: 2024-03-16 02:00:00\n"), ( OUTPUT_WITH_EMISSION_ESTIMATE, """Best job start time: 2024-03-16 02:00:00 + Estimated emmissions for running job now: 19 Estimated emmissions for running delayed job: 9 (- 10)""", ),