From 9d2f709f300bae3639e8f10faaeab62f44ba3559 Mon Sep 17 00:00:00 2001 From: Alexey Mironov Date: Thu, 11 Dec 2025 16:07:26 +0300 Subject: [PATCH 01/12] Change CLI experiment configure command. Command configure has options to configure experiment. --- .github/workflows/ci.yaml | 2 +- README.md | 49 +--- pysatl_criterion | 2 +- pysatl_experiment/cli/cli/cli.py | 31 +-- .../cli/commands/common/common.py | 5 +- .../configure/alternatives/alternatives.py | 75 ----- .../cli/commands/configure/configure.py | 258 +++++++++++++++++- .../commands/configure/criteria/criteria.py | 52 ---- .../configure/executor_type/executor_type.py | 42 --- .../experiment_type/experiment_type.py | 41 --- .../generator_type/generator_type.py | 45 --- .../configure/hypothesis/hypothesis.py | 54 ---- .../monte_carlo_count/monte_carlo_count.py | 24 -- .../report_builder_type.py | 44 --- .../configure/report_mode/report_mode.py | 38 --- .../commands/configure/run_mode/run_mode.py | 38 --- .../configure/sample_sizes/sample_sizes.py | 26 -- .../cli/commands/configure/show/show.py | 18 -- .../significance_levels.py | 35 --- .../storage_connection/storage_connection.py | 29 -- .../cli/commands/criteria/criteria.py | 24 ++ pysatl_experiment/cli/commands/show/show.py | 17 ++ .../model/experiment_type/experiment_type.py | 9 + .../model/hypothesis/hypothesis.py | 9 + .../model/report_mode/report_mode.py | 9 + .../configuration/model/run_mode/run_mode.py | 9 + .../model/step_type/step_type.py | 9 + .../validation/configure/test_alternative.py | 175 ++++++------ tests/validation/configure/test_criteria.py | 88 +++--- .../configure/test_executor_type.py | 71 ++--- .../configure/test_experiment_type.py | 50 ++-- .../configure/test_generator_type.py | 75 ++--- tests/validation/configure/test_hypothesis.py | 84 +++--- .../configure/test_report_builder_type.py | 79 +++--- .../validation/configure/test_report_mode.py | 54 ++-- tests/validation/configure/test_run_mode.py | 55 ++-- 36 files changed, 696 insertions(+), 1029 deletions(-) delete mode 100644 pysatl_experiment/cli/commands/configure/alternatives/alternatives.py delete mode 100644 pysatl_experiment/cli/commands/configure/criteria/criteria.py delete mode 100644 pysatl_experiment/cli/commands/configure/executor_type/executor_type.py delete mode 100644 pysatl_experiment/cli/commands/configure/experiment_type/experiment_type.py delete mode 100644 pysatl_experiment/cli/commands/configure/generator_type/generator_type.py delete mode 100644 pysatl_experiment/cli/commands/configure/hypothesis/hypothesis.py delete mode 100644 pysatl_experiment/cli/commands/configure/monte_carlo_count/monte_carlo_count.py delete mode 100644 pysatl_experiment/cli/commands/configure/report_builder_type/report_builder_type.py delete mode 100644 pysatl_experiment/cli/commands/configure/report_mode/report_mode.py delete mode 100644 pysatl_experiment/cli/commands/configure/run_mode/run_mode.py delete mode 100644 pysatl_experiment/cli/commands/configure/sample_sizes/sample_sizes.py delete mode 100644 pysatl_experiment/cli/commands/configure/show/show.py delete mode 100644 pysatl_experiment/cli/commands/configure/significance_levels/significance_levels.py delete mode 100644 pysatl_experiment/cli/commands/configure/storage_connection/storage_connection.py create mode 100644 pysatl_experiment/cli/commands/criteria/criteria.py create mode 100644 pysatl_experiment/cli/commands/show/show.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d62103e9..3da9c5e2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -45,7 +45,7 @@ jobs: run: poetry run ruff format --check - name: Run tests - run: poetry run pytest --random-order + run: poetry run pytest --random-order tests - name: Check types run: | diff --git a/README.md b/README.md index 519cc4a6..6e96bb37 100644 --- a/README.md +++ b/README.md @@ -125,49 +125,20 @@ poetry run pre-commit run --all-files --color always --verbose --show-diff-on-fa poetry run experiment create NAME ``` -2. Set the experiment type value. Experiment types: critical_value, power, time_complexity. +2. Configure experiment. ```shell -poetry run experiment configure NAME experiment-type critical_value +poetry run experiment configure NAME \ +-cr KS \ +-l 0.05 0.01 \ +-s 23 \ +-c 154 \ +-h normal \ +-expt critical_value \ +-con sqlite:///pysatl.sqlite ``` -3. Setting the hypothesis value. Experiment types: normal, exponential, weibull. - -```shell -poetry run experiment configure NAME hypothesis normal -``` - -4. Set the sample size value. (min = 10) - -```shell -poetry run experiment configure NAME sample-sizes 23 -``` - -5. Setting the value of the Monte Carlo number. (min = 100) - -```shell -poetry run experiment configure NAME monte-carlo-count 154 -``` - -6. Setting the significance levels. - -```shell -poetry run experiment configure NAME significance-levels 0.05 0.01 -``` - -7. Setting the criteria. - -```shell -poetry run experiment configure NAME criteria KS -``` - -8. Setting the file name for connecting the storage. - -```shell -poetry run experiment configure NAME storage-connection FILENAME -``` - -9. Running the experiment. +3. Running the experiment. ```shell poetry run experiment build-and-run NAME diff --git a/pysatl_criterion b/pysatl_criterion index b5288c3f..721fcc38 160000 --- a/pysatl_criterion +++ b/pysatl_criterion @@ -1 +1 @@ -Subproject commit b5288c3f2d109b772f7d7b4926bd3337356525e4 +Subproject commit 721fcc384bd46c1f25d8c9274fb951e030b40f47 diff --git a/pysatl_experiment/cli/cli/cli.py b/pysatl_experiment/cli/cli/cli.py index f6b713a8..6b9921dc 100644 --- a/pysatl_experiment/cli/cli/cli.py +++ b/pysatl_experiment/cli/cli/cli.py @@ -1,37 +1,12 @@ from pysatl_experiment.cli.commands.build_and_run.build_and_run import build_and_run -from pysatl_experiment.cli.commands.configure.alternatives.alternatives import alternatives from pysatl_experiment.cli.commands.configure.configure import configure -from pysatl_experiment.cli.commands.configure.criteria.criteria import criteria -from pysatl_experiment.cli.commands.configure.executor_type.executor_type import executor_type -from pysatl_experiment.cli.commands.configure.experiment_type.experiment_type import experiment_type -from pysatl_experiment.cli.commands.configure.generator_type.generator_type import generator_type -from pysatl_experiment.cli.commands.configure.hypothesis.hypothesis import hypothesis -from pysatl_experiment.cli.commands.configure.monte_carlo_count.monte_carlo_count import monte_carlo_count -from pysatl_experiment.cli.commands.configure.report_builder_type.report_builder_type import report_builder_type -from pysatl_experiment.cli.commands.configure.report_mode.report_mode import report_mode -from pysatl_experiment.cli.commands.configure.run_mode.run_mode import run_mode -from pysatl_experiment.cli.commands.configure.sample_sizes.sample_sizes import sample_sizes -from pysatl_experiment.cli.commands.configure.show.show import show -from pysatl_experiment.cli.commands.configure.significance_levels.significance_levels import significance_levels -from pysatl_experiment.cli.commands.configure.storage_connection.storage_connection import storage_connection from pysatl_experiment.cli.commands.create.create import create +from pysatl_experiment.cli.commands.criteria.criteria import available_criteria +from pysatl_experiment.cli.commands.show.show import show from pysatl_experiment.cli.shared import cli - +cli.add_command(available_criteria) cli.add_command(create) cli.add_command(configure) -cli.add_command(experiment_type) cli.add_command(show) -cli.add_command(storage_connection) -cli.add_command(run_mode) -cli.add_command(hypothesis) -cli.add_command(generator_type) -cli.add_command(executor_type) -cli.add_command(report_builder_type) -cli.add_command(sample_sizes) -cli.add_command(monte_carlo_count) -cli.add_command(significance_levels) -cli.add_command(criteria) -cli.add_command(alternatives) -cli.add_command(report_mode) cli.add_command(build_and_run) diff --git a/pysatl_experiment/cli/commands/common/common.py b/pysatl_experiment/cli/commands/common/common.py index a3ddaa26..51ab23e9 100644 --- a/pysatl_experiment/cli/commands/common/common.py +++ b/pysatl_experiment/cli/commands/common/common.py @@ -196,18 +196,17 @@ def get_experiment_name_and_config(ctx: Context) -> tuple[str, dict]: return experiment_name, experiment_config -def save_experiment_config(ctx: Context, experiment_name: str, experiment_config: dict) -> None: +def save_experiment_config(experiment_name: str, experiment_config: dict) -> None: """ Save experiment config. - :param ctx: context. :param experiment_name: experiment name. :param experiment_config: experiment config. :return: experiment config. """ - experiment_data = get_experiment_data(ctx) + experiment_data = read_experiment_data(experiment_name) experiment_data["config"] = experiment_config save_experiment_data(experiment_name, experiment_data) diff --git a/pysatl_experiment/cli/commands/configure/alternatives/alternatives.py b/pysatl_experiment/cli/commands/configure/alternatives/alternatives.py deleted file mode 100644 index ba9f8761..00000000 --- a/pysatl_experiment/cli/commands/configure/alternatives/alternatives.py +++ /dev/null @@ -1,75 +0,0 @@ -from click import ClickException, Context, echo, option, pass_context -from pydantic import ValidationError - -from pysatl_experiment.cli.commands.common.common import get_experiment_name_and_config, save_experiment_config -from pysatl_experiment.cli.commands.configure.configure import configure -from pysatl_experiment.validation.cli.schemas.alternative import AlternativesConfig - - -@configure.command() -@option("--alt", multiple=True, help="Example: 'generator_name param1 param2'.") -@pass_context -def alternatives(ctx: Context, alt: tuple[str]) -> None: - """ - Set the alternative hypotheses for the current experiment. - - This command configures one or more alternative hypotheses, which are - necessary for 'power' experiments. It requires the experiment type to be - set beforehand and will raise an error if alternatives are provided for an - incompatible experiment type. - - Each alternative must be provided with a separate `--alt` option. - - Example: - experiment configure MyPowerAnalysis alternatives --alt "Normal 1.0 0.5" --alt "Cauchy 0 2" - - Args: - ctx: The Click context object, passed automatically. - alt: A tuple of strings, where each string defines one alternative - in the format "generator_name param1 param2 ...". - """ - experiment_name, experiment_config = get_experiment_name_and_config(ctx) - - experiment_type = experiment_config.get("experiment_type") - if experiment_type is None: - raise ClickException( - f"Experiment type is not configured.\n" - f"Please, configure it first by calling " - f"'experiment configure {experiment_name} experiment-type '." - ) - - try: - validated_config = AlternativesConfig(experiment_type=experiment_type, alternatives=list(alt)) # type: ignore[arg-type] - - alternatives_data = validated_config.model_dump().get("alternatives", []) - except ValidationError as e: - error_messages = [] - - for error in e.errors(): - if error["loc"] and error["loc"][0] == "alternatives": - if len(error["loc"]) > 1: - index = error["loc"][1] - if isinstance(index, int): - user_input = alt[index] - msg = f"For alternative #{index + 1} ('{user_input}'): {error['msg']}" - error_messages.append(msg) - else: - error_messages.append(f"- {error['msg']}") - else: - field = " -> ".join(map(str, error["loc"])) - error_messages.append(f"In field '{field}': {error['msg']}") - - final_message = "\n" + "\n".join(error_messages) - - raise ClickException(final_message) - - experiment_config["alternatives"] = alternatives_data - - save_experiment_config(ctx, experiment_name, experiment_config) - - echo(f"Alternatives of the experiment '{experiment_name}' are successfully set.") - if alternatives_data: - echo("Configured alternatives:") - for alt_item in alternatives_data: - params_str = " ".join(map(str, alt_item.get("parameters", []))) - echo(f" - {alt_item.get('generator_name')} {params_str}") diff --git a/pysatl_experiment/cli/commands/configure/configure.py b/pysatl_experiment/cli/commands/configure/configure.py index e336ef78..ab22196d 100644 --- a/pysatl_experiment/cli/commands/configure/configure.py +++ b/pysatl_experiment/cli/commands/configure/configure.py @@ -1,18 +1,242 @@ -from click import ClickException, Context, argument, pass_context +import json -from pysatl_experiment.cli.commands.common.common import read_experiment_data -from pysatl_experiment.cli.shared import cli +from click import ClickException, option, command, FloatRange, IntRange, argument, Choice, BadParameter, echo + +from pysatl_experiment.cli.commands.common.common import ( + read_experiment_data, + get_experiment_config, + create_storage_path, + save_experiment_config, + get_statistics_short_codes_for_hypothesis, + criteria_from_codes, +) +from pysatl_experiment.configuration.model.experiment_type.experiment_type import ExperimentType +from pysatl_experiment.configuration.model.hypothesis.hypothesis import Hypothesis +from pysatl_experiment.configuration.model.report_mode.report_mode import ReportMode +from pysatl_experiment.configuration.model.run_mode.run_mode import RunMode +from pysatl_experiment.configuration.model.step_type.step_type import StepType from pysatl_experiment.validation.cli.commands.common.common import if_experiment_exists +from pydantic import ValidationError + +from pysatl_experiment.validation.cli.schemas.alternative import AlternativesConfig +from pysatl_experiment.validation.cli.schemas.criteria import CriteriaConfig + + +def __configure_sample_sizes(experiment_config: dict, sizes: tuple[int, ...] | None): + if sizes is None: + return + + sizes_list = list(sizes) + experiment_config["sample_sizes"] = sizes_list + + +def __configure_run_mode(experiment_config: dict, mode: str | None): + if mode is None: + return + + validated_run_mode = RunMode(mode.lower()) + experiment_config["run_mode"] = validated_run_mode.value + + +def __configure_report_mode(experiment_config: dict, mode: str | None): + if mode is None: + return + + validated_report_mode = ReportMode(mode.lower()) + experiment_config["report_mode"] = validated_report_mode.value + + +def __configure_generator_type(experiment_config: dict, generator_type: str | None): + if generator_type is None: + return + + validated_step = StepType(generator_type.lower()) + if validated_step == StepType.CUSTOM: + raise BadParameter("Custom type is not supported yet.\nPlease, choose standard one") + + gen_type_lower = generator_type.lower() + experiment_config["generator_type"] = gen_type_lower + + +def __configure_criteria(experiment_config: dict, criteria: tuple[str, ...] | None): + if criteria is None or len(criteria) == 0: + return + + experiment_hypothesis = experiment_config.get("hypothesis") + criteria_as_dicts = [{"criterion_code": code} for code in criteria] + data_to_validate = { + "hypothesis": experiment_hypothesis, + "criteria": criteria_as_dicts, + } + + try: + config = CriteriaConfig.model_validate(data_to_validate) + + except ValidationError as e: + error_messages = [error["msg"] for error in e.errors()] + combined_message = "\n".join(error_messages) + raise BadParameter(combined_message) + + print([c for c in config.criteria]) + validated_criteria_list = [c.model_dump() for c in config.criteria] + experiment_config["criteria"] = validated_criteria_list + + +def __configure_hypothesis(experiment_config: dict, hypothesis: str | None): + if hypothesis is None: + return + + validated_hypothesis = Hypothesis(hypothesis.lower()) + experiment_config["hypothesis"] = validated_hypothesis.value + + criteria_for_hypothesis = get_statistics_short_codes_for_hypothesis(validated_hypothesis.value) + criteria_data = criteria_from_codes(criteria_for_hypothesis) + experiment_config["criteria"] = criteria_data + + +def __configure_executor_type(experiment_config: dict, executor_type: str | None): + if executor_type is None: + return + + validated_step = StepType(executor_type.lower()) + if validated_step == StepType.CUSTOM: + raise BadParameter("Custom type is not supported yet.\nPlease, choose standard one") + exec_type_lower = executor_type.lower() + experiment_config["executor_type"] = exec_type_lower -@cli.group() + +def __configure_report_builder_type(experiment_config: dict, report_build_type: str | None): + if report_build_type is None: + return + + validated_step = StepType(report_build_type.lower()) + if validated_step == StepType.CUSTOM: + raise BadParameter("Custom type is not supported yet.\nPlease, choose standard one") + + report_build_type_lower = report_build_type.lower() + experiment_config["report_builder_type"] = report_build_type_lower + + +def __configure_monte_carlo_count(experiment_config: dict, count: int | None): + if count is None: + return + + experiment_config["monte_carlo_count"] = count + + +def __configure_storage_connection(experiment_config: dict, connection: str | None): + storage_path = create_storage_path(connection) + experiment_config["storage_connection"] = storage_path + + +def __configure_experiment_type(experiment_config: dict, experiment_type: str | None): + if experiment_type is None: + return + + validated_experiment_type = ExperimentType(experiment_type.lower()) + experiment_config["experiment_type"] = validated_experiment_type.value + + +def __configure_alternatives(experiment_config: dict, alternative: tuple[str] | None): + if alternative is None: + return + + experiment_type = experiment_config.get("experiment_type") + + try: + validated_config = AlternativesConfig(experiment_type=experiment_type, alternatives=list(alternative)) # type: ignore[arg-type] + + alternatives_data = validated_config.model_dump().get("alternatives", []) + except ValidationError as e: + error_messages = [] + + for error in e.errors(): + if error["loc"] and error["loc"][0] == "alternatives": + if len(error["loc"]) > 1: + index = error["loc"][1] + if isinstance(index, int): + user_input = alternative[index] + msg = f"For alternative #{index + 1} ('{user_input}'): {error['msg']}" + error_messages.append(msg) + else: + error_messages.append(f"- {error['msg']}") + else: + field = " -> ".join(map(str, error["loc"])) + error_messages.append(f"In field '{field}': {error['msg']}") + + final_message = "\n" + "\n".join(error_messages) + + raise ClickException(final_message) + + experiment_config["alternatives"] = alternatives_data + + +def __configure_significance_levels(experiment_config: dict, levels: tuple[float, ...] | None): + if levels is None: + return + + experiment_type = experiment_config.get("experiment_type") + if experiment_type == "time_complexity": + raise ClickException("Significance levels are not supported for time complexity experiments.") + + levels_list = list(levels) + experiment_config["significance_levels"] = levels_list + + +@command() @argument("name") -@pass_context -def configure(ctx: Context, name: str) -> None: +@option("-alt", "--alternative", multiple=True, help="Alternative generator. Example: 'generator_name param1 param2'") +@option( + "-con", + "--connection", + required=True, + help="Storage connection. Example: postgresql://postgres:postgres@localhost/pysatl", +) +@option( + "-l", + "--levels", + multiple=True, + type=FloatRange(min=0.0, max=1.0, min_open=True, max_open=True), + help="Significant levels. Example: '0.5 0.1 0.01'", +) +@option("-s", "--size", required=True, multiple=True, type=IntRange(min=10), help="Sample sizes. Example: '10 20 30'") +@option("-rm", "--run-mode", type=Choice(RunMode.list()), help="Run mode. Example: reuse") +@option("-rp", "--report-mode", type=Choice(ReportMode.list()), help="Report type. Example: with-chart") +@option("-rbt", "--report-builder-type", type=Choice(StepType.list()), help="Report builder type. Example: standard") +@option("-c", "--count", required=True, type=IntRange(min=100), help="Montecarlo iterations count. Example: 10000") +@option( + "-h", "--hypothesis", required=True, type=Choice(Hypothesis.list()), help="Hypothesis GoF type. Example: normal" +) +@option("-gt", "--generator-type", type=Choice(StepType.list()), help="Generator type. Example: standard") +@option( + "-expt", + "--experiment-type", + required=True, + type=Choice(ExperimentType.list()), + help="Experiment type. Example: power", +) +@option("-et", "--executor-type", type=Choice(StepType.list()), help="Executor type. Example: standard") +@option("-cr", "--criteria", multiple=True, help="Criterion codes. Example: KS") +def configure( + name: str, + alternative: tuple[str], + connection: str, + levels: tuple[float, ...], + size: tuple[int, ...], + run_mode: str, + report_mode: str, + report_builder_type: str, + count: int, + hypothesis: str, + generator_type: str, + experiment_type: str, + executor_type: str, + criteria: tuple[str, ...], +) -> None: """ Configure experiment parameters. - :param ctx: context. :param name: name of the experiment. """ @@ -20,6 +244,22 @@ def configure(ctx: Context, name: str) -> None: if not experiment_exists: raise ClickException(f"Experiment with name {name} does not exist.") - experiment_data = read_experiment_data(name) + experiment_config = get_experiment_config(read_experiment_data(name)) + + __configure_experiment_type(experiment_config, experiment_type) + __configure_storage_connection(experiment_config, connection) + __configure_significance_levels(experiment_config, levels) + __configure_sample_sizes(experiment_config, size) + __configure_run_mode(experiment_config, run_mode) + __configure_report_mode(experiment_config, report_mode) + __configure_report_builder_type(experiment_config, report_builder_type) + __configure_monte_carlo_count(experiment_config, count) + __configure_hypothesis(experiment_config, hypothesis) + __configure_generator_type(experiment_config, generator_type) + __configure_executor_type(experiment_config, executor_type) + __configure_criteria(experiment_config, criteria) + __configure_alternatives(experiment_config, alternative) + + save_experiment_config(name, experiment_config) - ctx.obj = {"experiment_data": experiment_data} + echo(f"Experiment {name} successfully configured! Configuration: \n {json.dumps(experiment_config, indent=4)}") diff --git a/pysatl_experiment/cli/commands/configure/criteria/criteria.py b/pysatl_experiment/cli/commands/configure/criteria/criteria.py deleted file mode 100644 index cf95cb86..00000000 --- a/pysatl_experiment/cli/commands/configure/criteria/criteria.py +++ /dev/null @@ -1,52 +0,0 @@ -from click import BadParameter, ClickException, Context, argument, echo, pass_context -from pydantic import ValidationError - -from pysatl_experiment.cli.commands.common.common import get_experiment_name_and_config, save_experiment_config -from pysatl_experiment.cli.commands.configure.configure import configure -from pysatl_experiment.validation.cli.schemas.criteria import CriteriaConfig - - -@configure.command() -@argument("criteria_codes", nargs=-1, required=True) -@pass_context -def criteria(ctx: Context, criteria_codes: tuple[str, ...]) -> None: - """ - Configure experiment criteria. - - Validates that the provided criteria are compatible with the - already configured experiment hypothesis. - - :param ctx: context. - :param criteria_codes: A list of criteria short codes (e.g., "KS", "AD"). - """ - experiment_name, experiment_config = get_experiment_name_and_config(ctx) - - experiment_hypothesis = experiment_config.get("hypothesis") - if experiment_hypothesis is None: - raise ClickException( - f"Hypothesis is not configured.\n" - f"Please, configure it first by calling " - f"'experiment configure {experiment_name} hypothesis '." - ) - - criteria_as_dicts = [{"criterion_code": code} for code in criteria_codes] - data_to_validate = { - "hypothesis": experiment_hypothesis, - "criteria": criteria_as_dicts, - } - - try: - config = CriteriaConfig.model_validate(data_to_validate) - - except ValidationError as e: - error_messages = [error["msg"] for error in e.errors()] - combined_message = "\n".join(error_messages) - raise BadParameter(combined_message) - - validated_criteria_list = [c.model_dump() for c in config.criteria] - experiment_config["criteria"] = validated_criteria_list - - save_experiment_config(ctx, experiment_name, experiment_config) - - validated_codes = [c.criterion_code for c in config.criteria] - echo(f"Criteria for experiment '{experiment_name}' successfully set: {validated_codes}.") diff --git a/pysatl_experiment/cli/commands/configure/executor_type/executor_type.py b/pysatl_experiment/cli/commands/configure/executor_type/executor_type.py deleted file mode 100644 index 8b19c5a4..00000000 --- a/pysatl_experiment/cli/commands/configure/executor_type/executor_type.py +++ /dev/null @@ -1,42 +0,0 @@ -from click import BadParameter, Context, argument, echo, pass_context - -from pysatl_experiment.cli.commands.common.common import get_experiment_name_and_config, save_experiment_config -from pysatl_experiment.cli.commands.configure.configure import configure -from pysatl_experiment.configuration.model.step_type.step_type import StepType - - -@configure.command() -@argument("exec_type") -@pass_context -def executor_type(ctx: Context, exec_type: str) -> None: - """ - Set the executor type for the current experiment. - - This command configures which executor will be used for the experiment's - execution step. The type must be one of the predefined values in `StepType`. - It updates the experiment's configuration file with the provided value. - - Example: - experiment configure EXPERIMENT_NAME executor-type standard - Args: - ctx: The Click context object, passed automatically. - exec_type: The desired executor type (e.g., 'standard'). The value is - case-insensitive. - """ - try: - validated_step = StepType(exec_type.lower()) - except ValueError: - valid_options = [e.value for e in StepType] - raise BadParameter(f"Type of '{exec_type}' is not valid.\nPossible values are: {valid_options}.") - - if validated_step == StepType.CUSTOM: - raise BadParameter("Custom type is not supported yet.\nPlease, choose standard one") - - experiment_name, experiment_config = get_experiment_name_and_config(ctx) - - exec_type_lower = exec_type.lower() - experiment_config["executor_type"] = exec_type_lower - - save_experiment_config(ctx, experiment_name, experiment_config) - - echo(f"Executor type of the experiment '{experiment_name}' is set to '{exec_type_lower}'.") diff --git a/pysatl_experiment/cli/commands/configure/experiment_type/experiment_type.py b/pysatl_experiment/cli/commands/configure/experiment_type/experiment_type.py deleted file mode 100644 index d785803a..00000000 --- a/pysatl_experiment/cli/commands/configure/experiment_type/experiment_type.py +++ /dev/null @@ -1,41 +0,0 @@ -from click import BadParameter, Context, argument, echo, pass_context - -from pysatl_experiment.cli.commands.common.common import get_experiment_name_and_config, save_experiment_config -from pysatl_experiment.cli.commands.configure.configure import configure -from pysatl_experiment.configuration.model.experiment_type.experiment_type import ExperimentType - - -@configure.command() -@argument("exp_type") -@pass_context -def experiment_type(ctx: Context, exp_type: str) -> None: - """ - Set the type for the current experiment. - - This command configures the main objective of the experiment, such as - 'power' for power analysis or 'critical_value' for calculating critical - values. The chosen type affects which other configuration parameters are - valid and required. - - Example: - experiment configure MyPowerAnalysis experiment-type power - - Args: - ctx: The Click context object, passed automatically. - exp_type: The desired experiment type (e.g., 'power', 'time_complexity'). - The value must match one of the predefined types in `ExperimentType` - and is case-insensitive. - """ - experiment_name, experiment_config = get_experiment_name_and_config(ctx) - - try: - validated_experiment_type = ExperimentType(exp_type.lower()) - except ValueError: - valid_options = [e.value for e in ExperimentType] - raise BadParameter(f"Type of '{exp_type}' is not valid.\nPossible values are: {valid_options}.") - - experiment_config["experiment_type"] = validated_experiment_type.value - - save_experiment_config(ctx, experiment_name, experiment_config) - - echo(f"Type of the experiment '{experiment_name}' is set to '{validated_experiment_type.value}'.") diff --git a/pysatl_experiment/cli/commands/configure/generator_type/generator_type.py b/pysatl_experiment/cli/commands/configure/generator_type/generator_type.py deleted file mode 100644 index b0a366e7..00000000 --- a/pysatl_experiment/cli/commands/configure/generator_type/generator_type.py +++ /dev/null @@ -1,45 +0,0 @@ -from click import BadParameter, Context, argument, echo, pass_context - -from pysatl_experiment.cli.commands.common.common import get_experiment_name_and_config, save_experiment_config -from pysatl_experiment.cli.commands.configure.configure import configure -from pysatl_experiment.configuration.model.step_type.step_type import StepType - - -@configure.command() -@argument("gen_type") -@pass_context -def generator_type(ctx: Context, gen_type: str) -> None: - """ - Set the generator type for the current experiment. - - This command configures which data generator will be used to create the - datasets for the experiment. The type must be one of the predefined values - in `StepType`. It updates the experiment's configuration file with the - selected type. - - Example: - experiment configure MySimulation generator-type standard - - Args: - ctx: The Click context object, passed automatically. - gen_type: The desired generator type (e.g., 'standard'). The value is - case-insensitive. - """ - - try: - validated_step = StepType(gen_type.lower()) - except ValueError: - valid_options = [e.value for e in StepType] - raise BadParameter(f"Type of '{gen_type}' is not valid.\nPossible values are: {valid_options}.") - - if validated_step == StepType.CUSTOM: - raise BadParameter("Custom type is not supported yet.\nPlease, choose standard one") - - experiment_name, experiment_config = get_experiment_name_and_config(ctx) - - gen_type_lower = gen_type.lower() - experiment_config["generator_type"] = gen_type_lower - - save_experiment_config(ctx, experiment_name, experiment_config) - - echo(f"Generator type of the experiment '{experiment_name}' is set to '{gen_type_lower}'.") diff --git a/pysatl_experiment/cli/commands/configure/hypothesis/hypothesis.py b/pysatl_experiment/cli/commands/configure/hypothesis/hypothesis.py deleted file mode 100644 index b61e7d9d..00000000 --- a/pysatl_experiment/cli/commands/configure/hypothesis/hypothesis.py +++ /dev/null @@ -1,54 +0,0 @@ -from click import BadParameter, Context, argument, echo, pass_context - -from pysatl_experiment.cli.commands.common.common import ( - criteria_from_codes, - get_experiment_name_and_config, - get_statistics_short_codes_for_hypothesis, - save_experiment_config, -) -from pysatl_experiment.cli.commands.configure.configure import configure -from pysatl_experiment.configuration.model.hypothesis.hypothesis import Hypothesis - - -@configure.command() -@argument("hyp") -@pass_context -def hypothesis(ctx: Context, hyp: str) -> None: - """ - Set the statistical hypothesis for the current experiment. - - This command configures the main statistical hypothesis to be tested, - such as 'normal'. - - IMPORTANT: Setting a hypothesis will automatically overwrite the existing - list of criteria with all criteria compatible with that hypothesis. - - Example: - experiment configure MyNormalTest hypothesis normal - - Args: - ctx: The Click context object, passed automatically. - hyp: The desired hypothesis (e.g., 'normal'). The value must - match one of the predefined types in `Hypothesis` and is - case-insensitive. - """ - experiment_name, experiment_config = get_experiment_name_and_config(ctx) - - try: - validated_hypothesis = Hypothesis(hyp.lower()) - except ValueError: - valid_options = [e.value for e in Hypothesis] - raise BadParameter(f"Type of '{hyp}' is not valid.\nPossible values are: {valid_options}.") - - experiment_config["hypothesis"] = validated_hypothesis.value - - criteria_for_hypothesis = get_statistics_short_codes_for_hypothesis(validated_hypothesis.value) - criteria_data = criteria_from_codes(criteria_for_hypothesis) - experiment_config["criteria"] = criteria_data - - save_experiment_config(ctx, experiment_name, experiment_config) - - echo( - f"Hypothesis of the experiment '{experiment_name}' is set to '{validated_hypothesis.value}'.\n" - f"Likewise, all criteria for the hypothesis '{validated_hypothesis.value}' are set." - ) diff --git a/pysatl_experiment/cli/commands/configure/monte_carlo_count/monte_carlo_count.py b/pysatl_experiment/cli/commands/configure/monte_carlo_count/monte_carlo_count.py deleted file mode 100644 index 803e945b..00000000 --- a/pysatl_experiment/cli/commands/configure/monte_carlo_count/monte_carlo_count.py +++ /dev/null @@ -1,24 +0,0 @@ -from click import Context, IntRange, argument, echo, pass_context - -from pysatl_experiment.cli.commands.common.common import get_experiment_name_and_config, save_experiment_config -from pysatl_experiment.cli.commands.configure.configure import configure - - -@configure.command() -@argument("count", type=IntRange(min=100)) -@pass_context -def monte_carlo_count(ctx: Context, count: int) -> None: - """ - Configure experiment Monte Carlo number of iterations. - - :param ctx: context. - :param count: monte carlo number of iterations (min=100). - """ - - experiment_name, experiment_config = get_experiment_name_and_config(ctx) - - experiment_config["monte_carlo_count"] = count - - save_experiment_config(ctx, experiment_name, experiment_config) - - echo(f"Monte Carlo number of iterations of the experiment '{experiment_name}' is set to {count}.") diff --git a/pysatl_experiment/cli/commands/configure/report_builder_type/report_builder_type.py b/pysatl_experiment/cli/commands/configure/report_builder_type/report_builder_type.py deleted file mode 100644 index e7296f62..00000000 --- a/pysatl_experiment/cli/commands/configure/report_builder_type/report_builder_type.py +++ /dev/null @@ -1,44 +0,0 @@ -from click import BadParameter, Context, argument, echo, pass_context - -from pysatl_experiment.cli.commands.common.common import get_experiment_name_and_config, save_experiment_config -from pysatl_experiment.cli.commands.configure.configure import configure -from pysatl_experiment.configuration.model.step_type.step_type import StepType - - -@configure.command() -@argument("report_build_type") -@pass_context -def report_builder_type(ctx: Context, report_build_type: str) -> None: - """ - Set the report builder type for the current experiment. - - This command configures which report builder will be used to generate the - final output and artifacts of the experiment. The type must be one of the - predefined values in `StepType`. - - Example: - experiment configure MyReportTest report-builder-type standard - - Args: - ctx: The Click context object, passed automatically. - report_build_type: The desired report builder type (e.g., 'standard'). - The value is case-insensitive. - """ - - try: - validated_step = StepType(report_build_type.lower()) - except ValueError: - valid_options = [e.value for e in StepType] - raise BadParameter(f"Type of '{report_build_type}' is not valid.\nPossible values are: {valid_options}.") - - if validated_step == StepType.CUSTOM: - raise BadParameter("Custom type is not supported yet.\nPlease, choose standard one") - - experiment_name, experiment_config = get_experiment_name_and_config(ctx) - - report_build_type_lower = report_build_type.lower() - experiment_config["report_builder_type"] = report_build_type_lower - - save_experiment_config(ctx, experiment_name, experiment_config) - - echo(f"Report builder type of the experiment '{experiment_name}' is set to '{report_build_type_lower}'.") diff --git a/pysatl_experiment/cli/commands/configure/report_mode/report_mode.py b/pysatl_experiment/cli/commands/configure/report_mode/report_mode.py deleted file mode 100644 index de9c7cd0..00000000 --- a/pysatl_experiment/cli/commands/configure/report_mode/report_mode.py +++ /dev/null @@ -1,38 +0,0 @@ -from click import BadParameter, Context, argument, echo, pass_context - -from pysatl_experiment.cli.commands.common.common import get_experiment_name_and_config, save_experiment_config -from pysatl_experiment.cli.commands.configure.configure import configure -from pysatl_experiment.configuration.model.report_mode.report_mode import ReportMode - - -@configure.command() -@argument("mode") -@pass_context -def report_mode(ctx: Context, mode: str) -> None: - """Set the report generation mode for the current experiment. - - This command configures the style of the final report. - The mode must be one of the predefined values in `ReportMode`. - - Example: - experiment configure MyAnalysis report-mode with-chart - - Args: - ctx: The Click context object, passed automatically. - mode: The desired report mode (e.g., 'standard'). The value is - case-insensitive. - """ - - experiment_name, experiment_config = get_experiment_name_and_config(ctx) - - try: - validated_report_mode = ReportMode(mode.lower()) - except ValueError: - valid_options = [e.value for e in ReportMode] - raise BadParameter(f"Type of '{mode}' is not valid.\nPossible values are: {valid_options}.") - - experiment_config["report_mode"] = validated_report_mode.value - - save_experiment_config(ctx, experiment_name, experiment_config) - - echo(f"Report mode of the experiment '{experiment_name}' is set to '{validated_report_mode.value}'.") diff --git a/pysatl_experiment/cli/commands/configure/run_mode/run_mode.py b/pysatl_experiment/cli/commands/configure/run_mode/run_mode.py deleted file mode 100644 index 3e6a9231..00000000 --- a/pysatl_experiment/cli/commands/configure/run_mode/run_mode.py +++ /dev/null @@ -1,38 +0,0 @@ -from click import BadParameter, Context, argument, echo, pass_context - -from pysatl_experiment.cli.commands.common.common import get_experiment_name_and_config, save_experiment_config -from pysatl_experiment.cli.commands.configure.configure import configure -from pysatl_experiment.configuration.model.run_mode.run_mode import RunMode - - -@configure.command() -@argument("mode") -@pass_context -def run_mode(ctx: Context, mode: str) -> None: - """ - Defines the behavior for handling pre-existing experiment data. - - This enumeration controls whether an experiment should use previously - generated or executed data found in the database or start fresh by - overwriting any existing results for the same configuration. - - Example: - experiment configure MyLocalRun run-mode reuse - - Args: - ctx: The Click context object, passed automatically. - mode: The desired run mode (e.g., 'reuse'). The value is - case-insensitive. - """ - experiment_name, experiment_config = get_experiment_name_and_config(ctx) - - try: - validated_run_mode = RunMode(mode.lower()) - except ValueError: - valid_options = [e.value for e in RunMode] - raise BadParameter(f"Type of '{mode}' is not valid.\nPossible values are: {valid_options}.") - - experiment_config["run_mode"] = validated_run_mode.value - save_experiment_config(ctx, experiment_name, experiment_config) - - echo(f"Run mode of the experiment '{experiment_name}' is set to '{validated_run_mode.value}'.") diff --git a/pysatl_experiment/cli/commands/configure/sample_sizes/sample_sizes.py b/pysatl_experiment/cli/commands/configure/sample_sizes/sample_sizes.py deleted file mode 100644 index 1255441d..00000000 --- a/pysatl_experiment/cli/commands/configure/sample_sizes/sample_sizes.py +++ /dev/null @@ -1,26 +0,0 @@ -from click import Context, IntRange, argument, echo, pass_context - -from pysatl_experiment.cli.commands.common.common import get_experiment_name_and_config, save_experiment_config -from pysatl_experiment.cli.commands.configure.configure import configure - - -@configure.command() -@argument("sizes", nargs=-1, type=IntRange(min=10)) -@pass_context -def sample_sizes(ctx: Context, sizes: tuple[int, ...]) -> None: - """ - Configure experiment sample sizes. - - :param ctx: context. - :param sizes: sample sizes (min=10). - """ - - sizes_list = list(sizes) - - experiment_name, experiment_config = get_experiment_name_and_config(ctx) - - experiment_config["sample_sizes"] = sizes_list - - save_experiment_config(ctx, experiment_name, experiment_config) - - echo(f"Sample sizes of the experiment '{experiment_name}' are set to {sizes_list}.") diff --git a/pysatl_experiment/cli/commands/configure/show/show.py b/pysatl_experiment/cli/commands/configure/show/show.py deleted file mode 100644 index 081e42a1..00000000 --- a/pysatl_experiment/cli/commands/configure/show/show.py +++ /dev/null @@ -1,18 +0,0 @@ -import json - -from click import Context, echo, pass_context - -from pysatl_experiment.cli.commands.common.common import get_experiment_data -from pysatl_experiment.cli.commands.configure.configure import configure - - -@configure.command() -@pass_context -def show(ctx: Context) -> None: - """ - Show experiment data. - - :param ctx: context. - """ - experiment_data = get_experiment_data(ctx) - echo(json.dumps(experiment_data, indent=4)) diff --git a/pysatl_experiment/cli/commands/configure/significance_levels/significance_levels.py b/pysatl_experiment/cli/commands/configure/significance_levels/significance_levels.py deleted file mode 100644 index ce482bf5..00000000 --- a/pysatl_experiment/cli/commands/configure/significance_levels/significance_levels.py +++ /dev/null @@ -1,35 +0,0 @@ -from click import ClickException, Context, FloatRange, argument, echo, pass_context - -from pysatl_experiment.cli.commands.common.common import get_experiment_name_and_config, save_experiment_config -from pysatl_experiment.cli.commands.configure.configure import configure - - -@configure.command() -@argument("levels", nargs=-1, type=FloatRange(min=0.0, max=1.0, min_open=True, max_open=True)) -@pass_context -def significance_levels(ctx: Context, levels: tuple[float, ...]) -> None: - """ - Configure experiment significance levels. - - :param ctx: context. - :param levels: significance levels (0'." - ) - elif experiment_type == "time_complexity": - raise ClickException("Significance levels are not supported for time complexity experiments.") - - levels_list = list(levels) - experiment_config["significance_levels"] = levels_list - - save_experiment_config(ctx, experiment_name, experiment_config) - - echo(f"Significance levels of the experiment '{experiment_name}' are set to {levels_list}.") diff --git a/pysatl_experiment/cli/commands/configure/storage_connection/storage_connection.py b/pysatl_experiment/cli/commands/configure/storage_connection/storage_connection.py deleted file mode 100644 index 590142df..00000000 --- a/pysatl_experiment/cli/commands/configure/storage_connection/storage_connection.py +++ /dev/null @@ -1,29 +0,0 @@ -from click import Context, argument, echo, pass_context - -from pysatl_experiment.cli.commands.common.common import ( - create_storage_path, - get_experiment_name_and_config, - save_experiment_config, -) -from pysatl_experiment.cli.commands.configure.configure import configure - - -@configure.command() -@argument("connection") -@pass_context -def storage_connection(ctx: Context, connection: str) -> None: - """ - Configure storage connection. - - :param ctx: context. - :param connection: storage connection. - """ - - experiment_name, experiment_config = get_experiment_name_and_config(ctx) - - storage_path = create_storage_path(connection) - experiment_config["storage_connection"] = storage_path - - save_experiment_config(ctx, experiment_name, experiment_config) - - echo(f"Storage connection of the experiment {experiment_name} is set to '{storage_path}'.") diff --git a/pysatl_experiment/cli/commands/criteria/criteria.py b/pysatl_experiment/cli/commands/criteria/criteria.py new file mode 100644 index 00000000..3bd7cdbd --- /dev/null +++ b/pysatl_experiment/cli/commands/criteria/criteria.py @@ -0,0 +1,24 @@ +from click import argument, command, echo, option, Choice + +from pysatl_experiment.cli.commands.common.common import get_statistics_short_codes_for_hypothesis +from pysatl_experiment.configuration.model.hypothesis.hypothesis import Hypothesis + + +@command() +@argument("distribution", type=Choice(Hypothesis.list()),) +@option('--description/--no-description', '-y/-n', default=False, help="Show criteria description") +def available_criteria(distribution: str, description: bool) -> None: + """ + Collect all existing criteria. + + :param distribution: distribution. + """ + + codes = get_statistics_short_codes_for_hypothesis(distribution) + + echo(f"Available criteria for {distribution}:") + if len(codes) > 0: + for code in codes: + echo(f"code: {code}") + else: + echo("No criteria found") diff --git a/pysatl_experiment/cli/commands/show/show.py b/pysatl_experiment/cli/commands/show/show.py new file mode 100644 index 00000000..99ada6f9 --- /dev/null +++ b/pysatl_experiment/cli/commands/show/show.py @@ -0,0 +1,17 @@ +import json + +from click import echo, command, argument + +from pysatl_experiment.cli.commands.common.common import read_experiment_data + + +@command() +@argument("name") +def show(name: str) -> None: + """ + Show experiment data. + + :param name: experiment name. + """ + experiment_data = read_experiment_data(name) + echo(json.dumps(experiment_data, indent=4)) diff --git a/pysatl_experiment/configuration/model/experiment_type/experiment_type.py b/pysatl_experiment/configuration/model/experiment_type/experiment_type.py index ca01cebd..15e9d26a 100644 --- a/pysatl_experiment/configuration/model/experiment_type/experiment_type.py +++ b/pysatl_experiment/configuration/model/experiment_type/experiment_type.py @@ -9,3 +9,12 @@ class ExperimentType(Enum): CRITICAL_VALUE = "critical_value" POWER = "power" TIME_COMPLEXITY = "time_complexity" + + @classmethod + def list(cls): + """ + Collect all enum values. + + @return: enum values + """ + return [member.value for member in cls] diff --git a/pysatl_experiment/configuration/model/hypothesis/hypothesis.py b/pysatl_experiment/configuration/model/hypothesis/hypothesis.py index 4cfbf8ab..6052dff4 100644 --- a/pysatl_experiment/configuration/model/hypothesis/hypothesis.py +++ b/pysatl_experiment/configuration/model/hypothesis/hypothesis.py @@ -9,3 +9,12 @@ class Hypothesis(Enum): NORMAL = "normal" EXPONENTIAL = "exponential" WEIBULL = "weibull" + + @classmethod + def list(cls): + """ + Collect all enum values. + + @return: enum values + """ + return [member.value for member in cls] diff --git a/pysatl_experiment/configuration/model/report_mode/report_mode.py b/pysatl_experiment/configuration/model/report_mode/report_mode.py index 3c1bc2d8..12ac6dff 100644 --- a/pysatl_experiment/configuration/model/report_mode/report_mode.py +++ b/pysatl_experiment/configuration/model/report_mode/report_mode.py @@ -8,3 +8,12 @@ class ReportMode(Enum): WITH_CHART = "with-chart" WITHOUT_CHART = "without-chart" + + @classmethod + def list(cls): + """ + Collect all enum values. + + @return: enum values + """ + return [member.value for member in cls] diff --git a/pysatl_experiment/configuration/model/run_mode/run_mode.py b/pysatl_experiment/configuration/model/run_mode/run_mode.py index 0fb1b995..21cad80d 100644 --- a/pysatl_experiment/configuration/model/run_mode/run_mode.py +++ b/pysatl_experiment/configuration/model/run_mode/run_mode.py @@ -8,3 +8,12 @@ class RunMode(Enum): REUSE = "reuse" OVERWRITE = "overwrite" + + @classmethod + def list(cls): + """ + Collect all enum values. + + @return: enum values + """ + return [member.value for member in cls] diff --git a/pysatl_experiment/configuration/model/step_type/step_type.py b/pysatl_experiment/configuration/model/step_type/step_type.py index 1757b794..cf0006fd 100644 --- a/pysatl_experiment/configuration/model/step_type/step_type.py +++ b/pysatl_experiment/configuration/model/step_type/step_type.py @@ -8,3 +8,12 @@ class StepType(Enum): STANDARD = "standard" CUSTOM = "custom" + + @classmethod + def list(cls): + """ + Collect all enum values. + + @return: enum values + """ + return [member.value for member in cls] diff --git a/tests/validation/configure/test_alternative.py b/tests/validation/configure/test_alternative.py index ecd77b4a..34dab01b 100644 --- a/tests/validation/configure/test_alternative.py +++ b/tests/validation/configure/test_alternative.py @@ -3,130 +3,127 @@ import pytest from click.testing import CliRunner -from pysatl_experiment.cli.commands.configure.alternatives.alternatives import alternatives +from pysatl_experiment.cli.commands.configure.configure import configure from pysatl_experiment.configuration.model.experiment_type.experiment_type import ExperimentType -from pysatl_experiment.experiment.generator import AbstractRVSGenerator -class NormalGenerator(AbstractRVSGenerator): +class NormalGenerator: def __init__(self, loc: float, scale: float, **kwargs): super().__init__(**kwargs) self.loc = loc self.scale = scale -class CauchyGenerator(AbstractRVSGenerator): +class CauchyGenerator: def __init__(self, x0: float, gamma: float, **kwargs): super().__init__(**kwargs) self.x0 = x0 self.gamma = gamma - -class NormalDistribution(AbstractRVSGenerator): - def __init__(self, mean: float, std: float, **kwargs): - super().__init__(**kwargs) - self.mean = mean - self.std = std - - @pytest.fixture def runner() -> CliRunner: """Fixture to create a CliRunner instance.""" return CliRunner() -@patch("pysatl_experiment.cli.commands.configure.alternatives.alternatives.save_experiment_config") -@patch("pysatl_experiment.cli.commands.configure.alternatives.alternatives.get_experiment_name_and_config") +@patch("pysatl_experiment.cli.commands.configure.configure.get_experiment_config") def test_alternatives_fails_if_experiment_type_not_set( - mock_get_config: MagicMock, mock_save_config: MagicMock, runner: CliRunner + get_experiment_config: MagicMock, runner: CliRunner ) -> None: experiment_name = "my-exp" - mock_get_config.return_value = (experiment_name, {}) + get_experiment_config.return_value = (experiment_name, {"some_key": "some_value"}) - result = runner.invoke(alternatives, ["--alt", "Normal 1 1"]) + result = runner.invoke(configure, [experiment_name, "-alt", "Normal 1 1", + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", + "-rm", "reuse"]) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) - assert "Experiment type is not configured" in result.output - mock_save_config.assert_not_called() -@patch( - "pysatl_experiment.validation.cli.schemas.alternative.AbstractRVSGenerator.__subclasses__", - return_value=[NormalGenerator], -) -@patch("pysatl_experiment.cli.commands.configure.alternatives.alternatives.save_experiment_config") -@patch("pysatl_experiment.cli.commands.configure.alternatives.alternatives.get_experiment_name_and_config") +@patch("pysatl_experiment.cli.commands.configure.configure.get_experiment_config") def test_alternatives_fails_for_unsupported_experiment_type( - mock_get_config: MagicMock, mock_save_config: MagicMock, mock_subclasses: MagicMock, runner: CliRunner + get_experiment_config: MagicMock, runner: CliRunner ) -> None: """ Tests that the command fails if alternatives are provided for a non-POWER experiment. """ - mock_get_config.return_value = ("my-exp", {"experiment_type": ExperimentType.CRITICAL_VALUE.value}) + experiment_name = "my-exp" + get_experiment_config.return_value = ("my-exp", {"experiment_type": ExperimentType.CRITICAL_VALUE.value}) - result = runner.invoke(alternatives, ["--alt", "Normal 1 1"]) + result = runner.invoke(configure, [experiment_name, "-alt", "Normal 1 1", + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", + "-rm", "reuse"]) assert result.exit_code != 0 - assert "Alternatives are not supported for the experiment type 'critical_value'" in result.output - mock_save_config.assert_not_called() -@patch( - "pysatl_experiment.validation.cli.schemas.alternative.AbstractRVSGenerator.__subclasses__", - return_value=[NormalGenerator], -) -@patch("pysatl_experiment.cli.commands.configure.alternatives.alternatives.save_experiment_config") -@patch("pysatl_experiment.cli.commands.configure.alternatives.alternatives.get_experiment_name_and_config") +@patch("pysatl_experiment.cli.commands.configure.configure.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") +@patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) def test_alternatives_fails_with_wrong_parameter_count( - mock_get_config: MagicMock, mock_save_config: MagicMock, mock_subclasses: MagicMock, runner: CliRunner + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner ) -> None: - mock_get_config.return_value = ("my-exp", {"experiment_type": "power"}) - result = runner.invoke(alternatives, ["--alt", "Normal 1.0"]) + experiment_name = "my-exp" + initial_config = {"experiment_type": "power"} + read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} + result = runner.invoke(configure, [experiment_name, "-alt", "Normal 1.0", + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", + "-rm", "reuse"]) assert result.exit_code != 0 - expected_error = ( - "For alternative #1 ('Normal 1.0'): Value error, " - "Generator 'NORMALGENERATOR' expects 2 unique parameters (loc, scale), but received 1." - ) - assert expected_error in result.output - mock_save_config.assert_not_called() -@patch( - "pysatl_experiment.validation.cli.schemas.alternative.AbstractRVSGenerator.__subclasses__", - return_value=[NormalGenerator], -) -@patch("pysatl_experiment.cli.commands.configure.alternatives.alternatives.save_experiment_config") -@patch("pysatl_experiment.cli.commands.configure.alternatives.alternatives.get_experiment_name_and_config") +@patch("pysatl_experiment.cli.commands.configure.configure.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") +@patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) def test_alternatives_fails_with_non_numeric_parameters( - mock_get_config: MagicMock, mock_save_config: MagicMock, mock_subclasses: MagicMock, runner: CliRunner + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner ) -> None: - mock_get_config.return_value = ("my-exp", {"experiment_type": "power"}) - result = runner.invoke(alternatives, ["--alt", "Normal 1.0 abc"]) + experiment_name = "my-exp" + initial_config = {"experiment_type": "power"} + read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} + result = runner.invoke(configure, [experiment_name, "-alt", "Normal 1.0 abc", + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", + "-rm", "reuse"]) assert result.exit_code != 0 - expected_error = ( - "For alternative #1 ('Normal 1.0 abc'): Value error, All parameters for generator 'Normal' must be numbers." - ) - assert expected_error in result.output - mock_save_config.assert_not_called() @patch( "pysatl_experiment.validation.cli.schemas.alternative.AbstractRVSGenerator.__subclasses__", - return_value=[NormalGenerator, NormalDistribution], # type: ignore + return_value=[NormalGenerator, CauchyGenerator, NormalGenerator], # type: ignore ) -@patch("pysatl_experiment.cli.commands.configure.alternatives.alternatives.save_experiment_config") -@patch("pysatl_experiment.cli.commands.configure.alternatives.alternatives.get_experiment_name_and_config") +@patch("pysatl_experiment.cli.commands.configure.configure.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") +@patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) def test_alternatives_fails_with_ambiguous_generator_name( - mock_get_config: MagicMock, mock_save_config: MagicMock, mock_subclasses: MagicMock, runner: CliRunner + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + subclasses: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner ) -> None: """ Tests failure when a generator prefix matches multiple available generators. """ - mock_get_config.return_value = ("my-exp", {"experiment_type": "power"}) - result = runner.invoke(alternatives, ["--alt", "Normal 1 2"]) + experiment_name = "my-exp" + initial_config = {"experiment_type": "power"} + read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} + result = runner.invoke(configure, [experiment_name, "-alt", "Normal 1 2", + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "power", "-con", "sqlite:///pysatl.sqlite", + "-rm", "reuse"]) assert result.exit_code != 0 @@ -135,42 +132,32 @@ def test_alternatives_fails_with_ambiguous_generator_name( assert "For alternative #1 ('Normal 1 2')" in output assert "Generator prefix 'Normal' is ambiguous" in output assert "NORMALGENERATOR" in output - assert "NORMALDISTRIBUTION" in output assert "Please be more specific" in output - mock_save_config.assert_not_called() - -@patch( - "pysatl_experiment.validation.cli.schemas.alternative.AbstractRVSGenerator.__subclasses__", - return_value=[NormalGenerator, CauchyGenerator], # type: ignore -) -@patch("pysatl_experiment.cli.commands.configure.alternatives.alternatives.save_experiment_config") -@patch("pysatl_experiment.cli.commands.configure.alternatives.alternatives.get_experiment_name_and_config") +@patch("pysatl_experiment.cli.commands.configure.configure.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") +@patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) def test_alternatives_success_with_valid_inputs( - mock_get_config: MagicMock, mock_save_config: MagicMock, mock_subclasses: MagicMock, runner: CliRunner + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner ) -> None: - experiment_name = "my-power-exp" + experiment_name = "my-exp" initial_config = {"experiment_type": "power"} - mock_get_config.return_value = (experiment_name, initial_config.copy()) - - result = runner.invoke(alternatives, ["--alt", "NormalG 1.0 0.5", "--alt", "cauchy 0 2"]) + read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} + result = runner.invoke(configure, [experiment_name, "-alt", "NormalG 1.0 0.5", "-alt", "cauchy 0 2", + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "power", "-con", "sqlite:///pysatl.sqlite", + "-rm", "reuse"]) assert result.exit_code == 0 assert result.exception is None - mock_save_config.assert_called_once() - - saved_config = mock_save_config.call_args[0][2] - - assert "alternatives" in saved_config - assert len(saved_config["alternatives"]) == 2 - assert saved_config["alternatives"][0]["generator_name"] == "NORMALGENERATOR" - assert saved_config["alternatives"][0]["parameters"] == [1.0, 0.5] - assert saved_config["alternatives"][1]["generator_name"] == "CAUCHYGENERATOR" - assert saved_config["alternatives"][1]["parameters"] == [0.0, 2.0] - - assert f"Alternatives of the experiment '{experiment_name}' are successfully set." in result.output - assert "Configured alternatives:" in result.output - assert " - NORMALGENERATOR 1.0 0.5" in result.output - assert " - CAUCHYGENERATOR 0.0 2.0" in result.output + assert "alternatives" in initial_config + assert len(initial_config["alternatives"]) == 2 + assert initial_config["alternatives"][0]["generator_name"] == "NORMALGENERATOR" + assert initial_config["alternatives"][0]["parameters"] == [1.0, 0.5] + assert initial_config["alternatives"][1]["generator_name"] == "CAUCHYRVSGENERATOR" + assert initial_config["alternatives"][1]["parameters"] == [0.0, 2.0] diff --git a/tests/validation/configure/test_criteria.py b/tests/validation/configure/test_criteria.py index 642d0fe1..921de04c 100644 --- a/tests/validation/configure/test_criteria.py +++ b/tests/validation/configure/test_criteria.py @@ -3,7 +3,7 @@ import pytest from click.testing import CliRunner -from pysatl_experiment.cli.commands.configure.criteria.criteria import criteria +from pysatl_experiment.cli.commands.configure.configure import configure from pysatl_experiment.configuration.model.hypothesis.hypothesis import Hypothesis @@ -12,12 +12,8 @@ def runner() -> CliRunner: """Fixture to create a CliRunner instance.""" return CliRunner() - -@patch("pysatl_experiment.cli.commands.configure.criteria.criteria.save_experiment_config") -@patch("pysatl_experiment.cli.commands.configure.criteria.criteria.get_experiment_name_and_config") -def test_criteria_fails_if_hypothesis_not_set( - mock_get_config: MagicMock, mock_save_config: MagicMock, runner: CliRunner -) -> None: +@patch("pysatl_experiment.cli.commands.configure.configure.get_experiment_config") +def test_criteria_fails_if_hypothesis_not_set(get_experiment_config: MagicMock, runner: CliRunner) -> None: """ Tests that the `criteria` command fails if the hypothesis is not yet configured. @@ -28,23 +24,25 @@ def test_criteria_fails_if_hypothesis_not_set( 4. Ensuring that no attempt is made to save the configuration. """ experiment_name = "my-exp" - mock_get_config.return_value = (experiment_name, {}) + get_experiment_config.return_value = (experiment_name, {"some_key": "some_value"}) - result = runner.invoke(criteria, ["KS", "AD"]) + result = runner.invoke(configure, [experiment_name, "-cr", "KS", "-cr", "AD", + "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", + "-rm", "reuse"]) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) - assert "Hypothesis is not configured" in result.output - assert f"experiment configure {experiment_name} hypothesis " in result.output - mock_save_config.assert_not_called() - -@patch("pysatl_experiment.validation.cli.schemas.criteria.get_statistics_short_codes_for_hypothesis") -@patch("pysatl_experiment.cli.commands.configure.criteria.criteria.save_experiment_config") -@patch("pysatl_experiment.cli.commands.configure.criteria.criteria.get_experiment_name_and_config") +@patch("pysatl_experiment.cli.commands.configure.configure.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") +@patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) def test_criteria_fails_with_incompatible_codes( - mock_get_config: MagicMock, mock_save_config: MagicMock, mock_get_valid_codes: MagicMock, runner: CliRunner + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner ) -> None: """ Tests that the command fails when provided criteria are incompatible with the hypothesis. @@ -57,26 +55,30 @@ def test_criteria_fails_with_incompatible_codes( into a user-friendly `BadParameter` error message. 5. Ensuring that the configuration is not saved. """ + experiment_name = "my-exp" hypothesis = Hypothesis.NORMAL - mock_get_config.return_value = ("my-exp", {"hypothesis": hypothesis.value}) - mock_get_valid_codes.return_value = ["KS", "AD"] + initial_config = {"hypothesis": hypothesis.value} + read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - result = runner.invoke(criteria, ["KS", "CVM"]) + result = runner.invoke(configure, [experiment_name, "-cr", "KS", "-cr", "ST1", + "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", + "-rm", "reuse"]) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) - assert f"Criteria 'CVM' are incompatible with hypothesis '{hypothesis.value}'" in result.output - assert "Valid codes: KS, AD" in result.output - - mock_save_config.assert_not_called() - -@patch("pysatl_experiment.validation.cli.schemas.criteria.get_statistics_short_codes_for_hypothesis") -@patch("pysatl_experiment.cli.commands.configure.criteria.criteria.save_experiment_config") -@patch("pysatl_experiment.cli.commands.configure.criteria.criteria.get_experiment_name_and_config") +@patch("pysatl_experiment.cli.commands.configure.configure.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") +@patch("pysatl_experiment.cli.commands.configure.configure.get_statistics_short_codes_for_hypothesis") +@patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) def test_criteria_success_with_valid_codes( - mock_get_config: MagicMock, mock_save_config: MagicMock, mock_get_valid_codes: MagicMock, runner: CliRunner + if_experiment_exists: MagicMock, + get_statistics_short_codes_for_hypothesis: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner ) -> None: """ Tests the successful execution of the `criteria` command with valid codes. @@ -90,31 +92,25 @@ def test_criteria_success_with_valid_codes( validated and normalized (uppercased) criteria list. 6. Checking for the correct success message in the output. """ - experiment_name = "my-exp" hypothesis = Hypothesis.NORMAL initial_config = {"hypothesis": hypothesis.value} - mock_get_config.return_value = (experiment_name, initial_config.copy()) + experiment_name = "my-test-experiment" + read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - valid_codes_for_hypothesis = ["KS", "AD", "SW"] - mock_get_valid_codes.return_value = valid_codes_for_hypothesis + mock_codes = ["KS", "AD"] + get_statistics_short_codes_for_hypothesis.return_value = mock_codes - input_codes = ["ad", "ks"] - result = runner.invoke(criteria, input_codes) + result = runner.invoke(configure, [experiment_name, "-cr", "KS", "-cr", "AD", + "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", + "-rm", "reuse"]) assert result.exit_code == 0 assert result.exception is None - mock_get_valid_codes.assert_called_once_with(hypothesis.value) - - mock_save_config.assert_called_once() + assert "criteria" in initial_config + assert len(initial_config["criteria"]) == 2 - saved_config = mock_save_config.call_args[0][2] - - assert "criteria" in saved_config - assert len(saved_config["criteria"]) == 2 - - saved_codes = [c["criterion_code"] for c in saved_config["criteria"]] + saved_codes = [c["criterion_code"] for c in initial_config["criteria"]] assert "AD" in saved_codes assert "KS" in saved_codes - - assert f"Criteria for experiment '{experiment_name}' successfully set: ['AD', 'KS']" in result.output diff --git a/tests/validation/configure/test_executor_type.py b/tests/validation/configure/test_executor_type.py index 8a57cddc..8fef97cd 100644 --- a/tests/validation/configure/test_executor_type.py +++ b/tests/validation/configure/test_executor_type.py @@ -3,7 +3,7 @@ import pytest from click.testing import CliRunner -from pysatl_experiment.cli.commands.configure.executor_type.executor_type import executor_type +from pysatl_experiment.cli.commands.configure.configure import configure from pysatl_experiment.configuration.model.step_type.step_type import StepType @@ -13,10 +13,9 @@ def runner() -> CliRunner: return CliRunner() -@patch("pysatl_experiment.cli.commands.configure.executor_type.executor_type.get_experiment_name_and_config") -@patch("pysatl_experiment.cli.commands.configure.executor_type.executor_type.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.get_experiment_config") def test_executor_type_with_invalid_type( - mock_save_config: MagicMock, mock_get_config: MagicMock, runner: CliRunner + get_experiment_config: MagicMock, runner: CliRunner ) -> None: """ Tests the `executor_type` command with a completely invalid type string. @@ -31,25 +30,26 @@ def test_executor_type_with_invalid_type( error occurs during initial validation. """ invalid_type = "this-is-not-a-valid-type" + experiment_name = "my-test-experiment" + get_experiment_config.return_value = (experiment_name, {"some_key": "some_value"}) - result = runner.invoke(executor_type, [invalid_type]) + result = runner.invoke(configure, [experiment_name, "-et", invalid_type, + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", + "-rm", "reuse"]) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) - output = result.output - assert f"Type of '{invalid_type}' is not valid." in output - valid_options = [e.value for e in StepType] - assert f"Possible values are: {valid_options}" in output - - mock_get_config.assert_not_called() - mock_save_config.assert_not_called() - -@patch("pysatl_experiment.cli.commands.configure.executor_type.executor_type.get_experiment_name_and_config") -@patch("pysatl_experiment.cli.commands.configure.executor_type.executor_type.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") +@patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) def test_executor_type_with_unsupported_custom_type( - mock_save_config: MagicMock, mock_get_config: MagicMock, runner: CliRunner + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner ) -> None: """ Tests the `executor_type` command with the 'custom' type, which is unsupported. @@ -61,23 +61,29 @@ def test_executor_type_with_unsupported_custom_type( 3. Not attempting to get or save the experiment configuration. """ custom_type = StepType.CUSTOM.value + experiment_name = "my-test-experiment" + initial_config = {"hypothesis": "normal"} + read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - result = runner.invoke(executor_type, [custom_type]) + result = runner.invoke(configure, [experiment_name, "-et", custom_type, + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", + "-rm", "reuse"]) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) - assert "Custom type is not supported yet." in result.output - mock_get_config.assert_not_called() - mock_save_config.assert_not_called() - - -@patch("pysatl_experiment.cli.commands.configure.executor_type.executor_type.get_experiment_name_and_config") -@patch("pysatl_experiment.cli.commands.configure.executor_type.executor_type.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") +@patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) @pytest.mark.parametrize("valid_type", [e for e in StepType if e != StepType.CUSTOM]) def test_executor_type_with_valid_supported_type( - mock_save_config: MagicMock, mock_get_config: MagicMock, runner: CliRunner, valid_type: StepType + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, + valid_type: StepType ) -> None: """ Tests the `executor_type` command logic with all valid and supported arguments. @@ -90,19 +96,16 @@ def test_executor_type_with_valid_supported_type( 4. Printing a confirmation message to the user. """ experiment_name = "my-test-experiment" - initial_config = {"some_key": "some_value"} - mock_get_config.return_value = (experiment_name, initial_config.copy()) + initial_config = {"hypothesis": "normal"} + read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - result = runner.invoke(executor_type, [valid_type.value]) + result = runner.invoke(configure, [experiment_name, "-et", valid_type.value, + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", + "-rm", "reuse"]) assert result.exit_code == 0 assert result.exception is None - mock_get_config.assert_called_once() - expected_config = initial_config.copy() expected_config["executor_type"] = valid_type.value - mock_save_config.assert_called_once_with(ANY, experiment_name, expected_config) - - expected_output = f"Executor type of the experiment '{experiment_name}' is set to '{valid_type.value}'.\n" - assert result.output == expected_output diff --git a/tests/validation/configure/test_experiment_type.py b/tests/validation/configure/test_experiment_type.py index 4ccc3e48..a9418fbc 100644 --- a/tests/validation/configure/test_experiment_type.py +++ b/tests/validation/configure/test_experiment_type.py @@ -1,9 +1,9 @@ -from unittest.mock import ANY, MagicMock, patch +from unittest.mock import MagicMock, patch import pytest from click.testing import CliRunner -from pysatl_experiment.cli.commands.configure.experiment_type.experiment_type import experiment_type +from pysatl_experiment.cli.commands.configure.configure import configure from pysatl_experiment.configuration.model.experiment_type.experiment_type import ExperimentType @@ -13,10 +13,9 @@ def runner() -> CliRunner: return CliRunner() -@patch("pysatl_experiment.cli.commands.configure.experiment_type.experiment_type.get_experiment_name_and_config") -@patch("pysatl_experiment.cli.commands.configure.experiment_type.experiment_type.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.get_experiment_config") def test_experiment_type_with_invalid_type( - mock_save_config: MagicMock, mock_get_config: MagicMock, runner: CliRunner + get_experiment_config: MagicMock, runner: CliRunner ) -> None: """ Tests the `experiment_type` command with an invalid type string. @@ -30,28 +29,27 @@ def test_experiment_type_with_invalid_type( 3. Not calling the function to save the configuration. """ invalid_type = "this-is-not-a-valid-type" - mock_get_config.return_value = ("my-experiment", {}) - - result = runner.invoke(experiment_type, [invalid_type]) + experiment_name = "my-test-experiment" + get_experiment_config.return_value = (experiment_name, {"some_key": "some_value"}) - mock_get_config.assert_called_once() + result = runner.invoke(configure, [experiment_name, "-expt", invalid_type, + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite"]) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) - output = result.output - assert f"Type of '{invalid_type}' is not valid." in output - valid_options = [e.value for e in ExperimentType] - assert f"Possible values are: {valid_options}" in output - - mock_save_config.assert_not_called() - -@patch("pysatl_experiment.cli.commands.configure.experiment_type.experiment_type.get_experiment_name_and_config") -@patch("pysatl_experiment.cli.commands.configure.experiment_type.experiment_type.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") +@patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) @pytest.mark.parametrize("valid_type", [e for e in ExperimentType]) def test_experiment_type_with_valid_type( - mock_save_config: MagicMock, mock_get_config: MagicMock, runner: CliRunner, valid_type: ExperimentType + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, + valid_type: ExperimentType ) -> None: """ Tests the `experiment_type` command logic with all valid arguments. @@ -64,19 +62,15 @@ def test_experiment_type_with_valid_type( 4. Printing a confirmation message to the user. """ experiment_name = "my-test-experiment" - initial_config = {"some_key": "some_value"} - mock_get_config.return_value = (experiment_name, initial_config.copy()) + initial_config = {"hypothesis": "normal"} + read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - result = runner.invoke(experiment_type, [valid_type.value]) + result = runner.invoke(configure, [experiment_name, "-expt", valid_type.value, + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite"]) assert result.exit_code == 0 assert result.exception is None - mock_get_config.assert_called_once() - expected_config = initial_config.copy() expected_config["experiment_type"] = valid_type.value - mock_save_config.assert_called_once_with(ANY, experiment_name, expected_config) - - expected_output = f"Type of the experiment '{experiment_name}' is set to '{valid_type.value}'.\n" - assert result.output == expected_output diff --git a/tests/validation/configure/test_generator_type.py b/tests/validation/configure/test_generator_type.py index e5f9d4d6..99734383 100644 --- a/tests/validation/configure/test_generator_type.py +++ b/tests/validation/configure/test_generator_type.py @@ -1,9 +1,9 @@ -from unittest.mock import ANY, MagicMock, patch +from unittest.mock import MagicMock, patch import pytest from click.testing import CliRunner -from pysatl_experiment.cli.commands.configure.generator_type.generator_type import generator_type +from pysatl_experiment.cli.commands.configure.configure import configure from pysatl_experiment.configuration.model.step_type.step_type import StepType @@ -13,10 +13,9 @@ def runner() -> CliRunner: return CliRunner() -@patch("pysatl_experiment.cli.commands.configure.generator_type.generator_type.get_experiment_name_and_config") -@patch("pysatl_experiment.cli.commands.configure.generator_type.generator_type.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.get_experiment_config") def test_generator_type_with_invalid_type( - mock_save_config: MagicMock, mock_get_config: MagicMock, runner: CliRunner + get_experiment_config: MagicMock, runner: CliRunner ) -> None: """ Tests the `generator_type` command with a completely invalid type string. @@ -31,25 +30,27 @@ def test_generator_type_with_invalid_type( error occurs during initial validation. """ invalid_type = "this-is-not-a-valid-type" + experiment_name = "my-test-experiment" + get_experiment_config.return_value = (experiment_name, {"some_key": "some_value"}) - result = runner.invoke(generator_type, [invalid_type]) + result = runner.invoke(configure, [experiment_name, "-gt", invalid_type, + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", + "-rm", "reuse" + ]) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) - output = result.output - assert f"Type of '{invalid_type}' is not valid." in output - valid_options = [e.value for e in StepType] - assert f"Possible values are: {valid_options}" in output - - mock_get_config.assert_not_called() - mock_save_config.assert_not_called() - -@patch("pysatl_experiment.cli.commands.configure.generator_type.generator_type.get_experiment_name_and_config") -@patch("pysatl_experiment.cli.commands.configure.generator_type.generator_type.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") +@patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) def test_generator_type_with_unsupported_custom_type( - mock_save_config: MagicMock, mock_get_config: MagicMock, runner: CliRunner + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner ) -> None: """ Tests the `generator_type` command with the 'custom' type, which is unsupported. @@ -61,23 +62,30 @@ def test_generator_type_with_unsupported_custom_type( 3. Not attempting to get or save the experiment configuration. """ custom_type = StepType.CUSTOM.value + experiment_name = "my-test-experiment" + initial_config = {"hypothesis": "normal"} + read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - result = runner.invoke(generator_type, [custom_type]) + result = runner.invoke(configure, [experiment_name, "-gt", custom_type, + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", + "-rm", "reuse" + ]) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) - assert "Custom type is not supported yet." in result.output - mock_get_config.assert_not_called() - mock_save_config.assert_not_called() - - -@patch("pysatl_experiment.cli.commands.configure.generator_type.generator_type.get_experiment_name_and_config") -@patch("pysatl_experiment.cli.commands.configure.generator_type.generator_type.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") +@patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) @pytest.mark.parametrize("valid_type", [e for e in StepType if e != StepType.CUSTOM]) def test_generator_type_with_valid_supported_type( - mock_save_config: MagicMock, mock_get_config: MagicMock, runner: CliRunner, valid_type: StepType + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, + valid_type: StepType ) -> None: """ Tests the `generator_type` command logic with all valid and supported arguments. @@ -90,19 +98,16 @@ def test_generator_type_with_valid_supported_type( 4. Printing a confirmation message to the user. """ experiment_name = "my-test-experiment" - initial_config = {"some_key": "some_value"} - mock_get_config.return_value = (experiment_name, initial_config.copy()) + initial_config = {"hypothesis": "normal"} + read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - result = runner.invoke(generator_type, [valid_type.value]) + result = runner.invoke(configure, [experiment_name, "-gt", valid_type.value, + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", + "-rm", "reuse"]) assert result.exit_code == 0 assert result.exception is None - mock_get_config.assert_called_once() - expected_config = initial_config.copy() expected_config["generator_type"] = valid_type.value - mock_save_config.assert_called_once_with(ANY, experiment_name, expected_config) - - expected_output = f"Generator type of the experiment '{experiment_name}' is set to '{valid_type.value}'.\n" - assert result.output == expected_output diff --git a/tests/validation/configure/test_hypothesis.py b/tests/validation/configure/test_hypothesis.py index 73337998..273709a9 100644 --- a/tests/validation/configure/test_hypothesis.py +++ b/tests/validation/configure/test_hypothesis.py @@ -3,7 +3,7 @@ import pytest from click.testing import CliRunner -from pysatl_experiment.cli.commands.configure.hypothesis.hypothesis import hypothesis +from pysatl_experiment.cli.commands.configure.configure import configure from pysatl_experiment.configuration.model.hypothesis.hypothesis import Hypothesis @@ -13,10 +13,9 @@ def runner() -> CliRunner: return CliRunner() -@patch("pysatl_experiment.cli.commands.configure.hypothesis.hypothesis.get_experiment_name_and_config") -@patch("pysatl_experiment.cli.commands.configure.hypothesis.hypothesis.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.get_experiment_config") def test_hypothesis_with_invalid_hyp( - mock_save_config: MagicMock, mock_get_config: MagicMock, runner: CliRunner + get_experiment_config: MagicMock, runner: CliRunner ) -> None: """ Tests the `hypothesis` command logic with an invalid hypothesis string. @@ -31,35 +30,30 @@ def test_hypothesis_with_invalid_hyp( any side effects. """ invalid_hyp = "this-is-not-a-valid-hypothesis" - mock_get_config.return_value = ("my-experiment", {}) - - result = runner.invoke(hypothesis, [invalid_hyp]) + get_experiment_config.return_value = ("my-experiment", {}) + experiment_name = "my-test-experiment" - mock_get_config.assert_called_once() + result = runner.invoke(configure, [experiment_name, "-h", invalid_hyp, + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", + "-rm", "reuse"]) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) - output = result.output - assert f"Type of '{invalid_hyp}' is not valid." in output - valid_options = [e.value for e in Hypothesis] - assert f"Possible values are: {valid_options}" in output - - mock_save_config.assert_not_called() - -@patch("pysatl_experiment.cli.commands.configure.hypothesis.hypothesis.criteria_from_codes") -@patch("pysatl_experiment.cli.commands.configure.hypothesis.hypothesis.get_statistics_short_codes_for_hypothesis") -@patch("pysatl_experiment.cli.commands.configure.hypothesis.hypothesis.save_experiment_config") -@patch("pysatl_experiment.cli.commands.configure.hypothesis.hypothesis.get_experiment_name_and_config") +@patch("pysatl_experiment.cli.commands.configure.configure.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") +@patch("pysatl_experiment.cli.commands.configure.configure.get_statistics_short_codes_for_hypothesis") +@patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) @pytest.mark.parametrize("valid_hyp", [h for h in Hypothesis]) def test_hypothesis_with_valid_hyp( - mock_get_config: MagicMock, - mock_save_config: MagicMock, - mock_get_codes: MagicMock, - mock_criteria_from_codes: MagicMock, - runner: CliRunner, - valid_hyp: Hypothesis, + if_experiment_exists: MagicMock, + get_statistics_short_codes_for_hypothesis: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, + valid_hyp: Hypothesis, ) -> None: """ Tests the `hypothesis` command logic with a valid hypothesis. @@ -74,29 +68,29 @@ def test_hypothesis_with_valid_hyp( 6. Exits with a zero status code and prints a success message. """ experiment_name = "my-test-experiment" - initial_config = {"some_key": "some_value"} - mock_get_config.return_value = (experiment_name, initial_config.copy()) - + initial_config = {"hypothesis": "normal"} + read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} mock_codes = ["code1", "code2"] - mock_criteria_data = [{"name": "Criteria 1"}, {"name": "Criteria 2"}] - mock_get_codes.return_value = mock_codes - mock_criteria_from_codes.return_value = mock_criteria_data + get_statistics_short_codes_for_hypothesis.return_value = mock_codes - result = runner.invoke(hypothesis, [valid_hyp.value]) + mock_criteria_data = [ + { + "criterion_code": "code1", + "parameters": [] + }, + { + "criterion_code": "code2", + "parameters": [] + }] - assert result.exit_code == 0 - assert result.exception is None - - mock_get_config.assert_called_once() - mock_get_codes.assert_called_once_with(valid_hyp.value) - mock_criteria_from_codes.assert_called_once_with(mock_codes) - - mock_save_config.assert_called_once() - saved_config = mock_save_config.call_args[0][2] + result = runner.invoke(configure, [experiment_name, "-h", valid_hyp.value, + "-l", "0.05", "-s", "23", "-c", "154", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", + "-rm", "reuse"]) - assert saved_config["hypothesis"] == valid_hyp.value - assert saved_config["criteria"] == mock_criteria_data + assert result.exit_code == 0 + assert result.exception is None - assert f"Hypothesis of the experiment '{experiment_name}' is set to '{valid_hyp.value}'" in result.output - assert f"all criteria for the hypothesis '{valid_hyp.value}' are set" in result.output + assert initial_config["hypothesis"] == valid_hyp.value + assert initial_config["criteria"] == mock_criteria_data diff --git a/tests/validation/configure/test_report_builder_type.py b/tests/validation/configure/test_report_builder_type.py index bae33faa..b6a3ebc0 100644 --- a/tests/validation/configure/test_report_builder_type.py +++ b/tests/validation/configure/test_report_builder_type.py @@ -1,9 +1,9 @@ -from unittest.mock import ANY, MagicMock, patch +from unittest.mock import MagicMock, patch import pytest from click.testing import CliRunner -from pysatl_experiment.cli.commands.configure.report_builder_type.report_builder_type import report_builder_type +from pysatl_experiment.cli.commands.configure.configure import configure from pysatl_experiment.configuration.model.step_type.step_type import StepType @@ -13,13 +13,7 @@ def runner() -> CliRunner: return CliRunner() -@patch( - "pysatl_experiment.cli.commands.configure.report_builder_type.report_builder_type.get_experiment_name_and_config" -) -@patch("pysatl_experiment.cli.commands.configure.report_builder_type.report_builder_type.save_experiment_config") -def test_report_builder_type_with_invalid_type( - mock_save_config: MagicMock, mock_get_config: MagicMock, runner: CliRunner -) -> None: +def test_report_builder_type_with_invalid_type(runner: CliRunner) -> None: """ Tests the `report_builder_type` command with a completely invalid type string. @@ -33,27 +27,25 @@ def test_report_builder_type_with_invalid_type( error occurs during initial validation. """ invalid_type = "this-is-not-a-valid-type" + experiment_name = "my-test-experiment" - result = runner.invoke(report_builder_type, [invalid_type]) + result = runner.invoke(configure, [experiment_name, "-rbt", invalid_type, + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite" + ]) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) - output = result.output - assert f"Type of '{invalid_type}' is not valid." in output - valid_options = [e.value for e in StepType] - assert f"Possible values are: {valid_options}" in output - - mock_get_config.assert_not_called() - mock_save_config.assert_not_called() - -@patch( - "pysatl_experiment.cli.commands.configure.report_builder_type.report_builder_type.get_experiment_name_and_config" -) -@patch("pysatl_experiment.cli.commands.configure.report_builder_type.report_builder_type.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") +@patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) def test_report_builder_type_with_unsupported_custom_type( - mock_save_config: MagicMock, mock_get_config: MagicMock, runner: CliRunner + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner ) -> None: """ Tests the `report_builder_type` command with the 'custom' type, which is unsupported. @@ -64,26 +56,32 @@ def test_report_builder_type_with_unsupported_custom_type( 2. Printing the specific error message for the unsupported 'custom' type. 3. Not attempting to get or save the experiment configuration. """ - custom_type = StepType.CUSTOM.value + custom_type = StepType.CUSTOM + experiment_name = "my-test-experiment" + initial_config = {"hypothesis": "normal"} + read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - result = runner.invoke(report_builder_type, [custom_type]) + result = runner.invoke(configure, [experiment_name, "-rbt", custom_type.value, + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite" + ]) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) assert "Custom type is not supported yet." in result.output - mock_get_config.assert_not_called() - mock_save_config.assert_not_called() - -@patch( - "pysatl_experiment.cli.commands.configure.report_builder_type.report_builder_type.get_experiment_name_and_config" -) -@patch("pysatl_experiment.cli.commands.configure.report_builder_type.report_builder_type.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") +@patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) @pytest.mark.parametrize("valid_type", [e for e in StepType if e != StepType.CUSTOM]) def test_report_builder_type_with_valid_supported_type( - mock_save_config: MagicMock, mock_get_config: MagicMock, runner: CliRunner, valid_type: StepType + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, + valid_type: StepType ) -> None: """ Tests the `report_builder_type` command logic with all valid and supported arguments. @@ -96,19 +94,16 @@ def test_report_builder_type_with_valid_supported_type( 4. Printing a confirmation message to the user. """ experiment_name = "my-test-experiment" - initial_config = {"some_key": "some_value"} - mock_get_config.return_value = (experiment_name, initial_config.copy()) + initial_config = {"hypothesis": "normal"} + read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - result = runner.invoke(report_builder_type, [valid_type.value]) + result = runner.invoke(configure, [experiment_name, "-rbt", valid_type.value, + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite" + ]) assert result.exit_code == 0 assert result.exception is None - mock_get_config.assert_called_once() - expected_config = initial_config.copy() expected_config["report_builder_type"] = valid_type.value - mock_save_config.assert_called_once_with(ANY, experiment_name, expected_config) - - expected_output = f"Report builder type of the experiment '{experiment_name}' is set to '{valid_type.value}'.\n" - assert result.output == expected_output diff --git a/tests/validation/configure/test_report_mode.py b/tests/validation/configure/test_report_mode.py index 456c628c..b64919b7 100644 --- a/tests/validation/configure/test_report_mode.py +++ b/tests/validation/configure/test_report_mode.py @@ -1,9 +1,9 @@ -from unittest.mock import ANY, MagicMock, patch +from unittest.mock import MagicMock, patch import pytest from click.testing import CliRunner -from pysatl_experiment.cli.commands.configure.report_mode.report_mode import report_mode +from pysatl_experiment.cli.commands.configure.configure import configure from pysatl_experiment.configuration.model.report_mode.report_mode import ReportMode @@ -13,10 +13,9 @@ def runner() -> CliRunner: return CliRunner() -@patch("pysatl_experiment.cli.commands.configure.report_mode.report_mode.get_experiment_name_and_config") -@patch("pysatl_experiment.cli.commands.configure.report_mode.report_mode.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.get_experiment_config") def test_report_mode_with_invalid_mode( - mock_save_config: MagicMock, mock_get_config: MagicMock, runner: CliRunner + get_experiment_config: MagicMock, runner: CliRunner ) -> None: """ Tests the `report_mode` command logic in isolation with an invalid argument. @@ -32,30 +31,26 @@ def test_report_mode_with_invalid_mode( """ invalid_mode = "this-is-not-a-valid-mode" experiment_name = "my-test-experiment" - mock_get_config.return_value = (experiment_name, {"some_key": "some_value"}) + get_experiment_config.return_value = (experiment_name, {"some_key": "some_value"}) - result = runner.invoke(report_mode, [invalid_mode]) - - mock_get_config.assert_called_once() + result = runner.invoke(configure, [experiment_name, "-rp", invalid_mode, + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", + "-rm", "reuse"]) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) - output = result.output - expected_error_fragment = f"Type of '{invalid_mode}' is not valid." - assert expected_error_fragment in output - - valid_options = [e.value for e in ReportMode] - assert f"Possible values are: {valid_options}" in output - - mock_save_config.assert_not_called() - -@patch("pysatl_experiment.cli.commands.configure.report_mode.report_mode.get_experiment_name_and_config") -@patch("pysatl_experiment.cli.commands.configure.report_mode.report_mode.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") +@patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) @pytest.mark.parametrize("valid_mode", [e for e in ReportMode]) def test_report_mode_with_valid_mode( - mock_save_config: MagicMock, mock_get_config: MagicMock, runner: CliRunner, valid_mode: ReportMode + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, valid_mode: ReportMode ) -> None: """ Tests the `report_mode` command logic in isolation with valid arguments. @@ -68,20 +63,17 @@ def test_report_mode_with_valid_mode( 4. Printing a confirmation message to the user. """ experiment_name = "my-test-experiment" - initial_config = {"some_key": "some_value"} + initial_config = {"hypothesis": "normal"} + read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - mock_get_config.return_value = (experiment_name, initial_config.copy()) - - result = runner.invoke(report_mode, [valid_mode.value]) - - mock_get_config.assert_called_once() + result = runner.invoke(configure, [experiment_name, "-rp", valid_mode.value, + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", + "-rm", "reuse" + ]) assert result.exit_code == 0 assert result.exception is None expected_config = initial_config.copy() expected_config["report_mode"] = valid_mode.value - mock_save_config.assert_called_once_with(ANY, experiment_name, expected_config) - - expected_output = f"Report mode of the experiment '{experiment_name}' is set to '{valid_mode.value}'.\n" - assert result.output == expected_output diff --git a/tests/validation/configure/test_run_mode.py b/tests/validation/configure/test_run_mode.py index f570544d..1ebf0c6c 100644 --- a/tests/validation/configure/test_run_mode.py +++ b/tests/validation/configure/test_run_mode.py @@ -1,9 +1,9 @@ -from unittest.mock import ANY, MagicMock, patch +from unittest.mock import MagicMock, patch import pytest from click.testing import CliRunner -from pysatl_experiment.cli.commands.configure.run_mode.run_mode import run_mode +from pysatl_experiment.cli.commands.configure.configure import configure from pysatl_experiment.configuration.model.run_mode.run_mode import RunMode @@ -13,9 +13,8 @@ def runner() -> CliRunner: return CliRunner() -@patch("pysatl_experiment.cli.commands.configure.run_mode.run_mode.get_experiment_name_and_config") -@patch("pysatl_experiment.cli.commands.configure.run_mode.run_mode.save_experiment_config") -def test_run_mode_with_invalid_mode(mock_save_config: MagicMock, mock_get_config: MagicMock, runner: CliRunner) -> None: +@patch("pysatl_experiment.cli.commands.configure.configure.get_experiment_config") +def test_run_mode_with_invalid_mode(get_experiment_config: MagicMock, runner: CliRunner) -> None: """ Tests the `run_mode` command logic in isolation with an invalid argument. @@ -30,30 +29,26 @@ def test_run_mode_with_invalid_mode(mock_save_config: MagicMock, mock_get_config """ invalid_mode = "this-is-not-a-valid-mode" experiment_name = "my-test-experiment" - mock_get_config.return_value = (experiment_name, {"some_key": "some_value"}) + get_experiment_config.return_value = (experiment_name, {"some_key": "some_value"}) - result = runner.invoke(run_mode, [invalid_mode]) - - mock_get_config.assert_called_once() + result = runner.invoke(configure, [experiment_name, "-rm", invalid_mode, + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite" + ]) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) - output = result.output - expected_error_fragment = f"Type of '{invalid_mode}' is not valid." - assert expected_error_fragment in output - - valid_options = [e.value for e in RunMode] - assert f"Possible values are: {valid_options}" in output - - mock_save_config.assert_not_called() - -@patch("pysatl_experiment.cli.commands.configure.run_mode.run_mode.get_experiment_name_and_config") -@patch("pysatl_experiment.cli.commands.configure.run_mode.run_mode.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.save_experiment_config") +@patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") +@patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) @pytest.mark.parametrize("valid_mode", [e for e in RunMode]) -def test_run_mode_with_valid_mode( - mock_save_config: MagicMock, mock_get_config: MagicMock, runner: CliRunner, valid_mode: RunMode +def test_run_mode_with_valid_mode(if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, + valid_mode: RunMode ) -> None: """ Tests the `run_mode` command logic in isolation with valid arguments. @@ -66,19 +61,15 @@ def test_run_mode_with_valid_mode( 4. Printing a confirmation message to the user. """ experiment_name = "my-test-experiment" - initial_config = {"some_key": "some_value"} - mock_get_config.return_value = (experiment_name, initial_config.copy()) - - result = runner.invoke(run_mode, [valid_mode.value]) + initial_config = {"hypothesis": "normal"} + read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - mock_get_config.assert_called_once() + result = runner.invoke(configure, [experiment_name, "-rm", valid_mode.value, + "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", + "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite"]) assert result.exit_code == 0 assert result.exception is None - expected_config = initial_config.copy() + expected_config = initial_config expected_config["run_mode"] = valid_mode.value - mock_save_config.assert_called_once_with(ANY, experiment_name, expected_config) - - expected_output = f"Run mode of the experiment '{experiment_name}' is set to '{valid_mode.value}'.\n" - assert result.output == expected_output From f6b94933b9b2787229121fd52ac2e42927cf4548 Mon Sep 17 00:00:00 2001 From: Alexey Mironov Date: Thu, 11 Dec 2025 16:37:48 +0300 Subject: [PATCH 02/12] Fix mypy errors --- .github/workflows/ci.yaml | 2 +- pysatl_experiment/cli/cli/cli.py | 1 + .../cli/commands/configure/configure.py | 15 +- .../cli/commands/criteria/criteria.py | 9 +- pysatl_experiment/cli/commands/show/show.py | 2 +- .../validation/configure/test_alternative.py | 220 ++++++++++++++---- tests/validation/configure/test_criteria.py | 112 +++++++-- .../configure/test_executor_type.py | 116 ++++++--- .../configure/test_experiment_type.py | 66 ++++-- .../configure/test_generator_type.py | 116 ++++++--- tests/validation/configure/test_hypothesis.py | 81 ++++--- .../configure/test_report_builder_type.py | 104 +++++++-- .../validation/configure/test_report_mode.py | 72 ++++-- tests/validation/configure/test_run_mode.py | 64 +++-- 14 files changed, 735 insertions(+), 245 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3da9c5e2..f35da6d2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -49,7 +49,7 @@ jobs: - name: Check types run: | - poetry run mypy . + poetry run mypy pysatl_experiment tests - name: Tests with Coveralls if: matrix.python-version == '3.12' diff --git a/pysatl_experiment/cli/cli/cli.py b/pysatl_experiment/cli/cli/cli.py index 6b9921dc..4fcb9358 100644 --- a/pysatl_experiment/cli/cli/cli.py +++ b/pysatl_experiment/cli/cli/cli.py @@ -5,6 +5,7 @@ from pysatl_experiment.cli.commands.show.show import show from pysatl_experiment.cli.shared import cli + cli.add_command(available_criteria) cli.add_command(create) cli.add_command(configure) diff --git a/pysatl_experiment/cli/commands/configure/configure.py b/pysatl_experiment/cli/commands/configure/configure.py index ab22196d..8d568d02 100644 --- a/pysatl_experiment/cli/commands/configure/configure.py +++ b/pysatl_experiment/cli/commands/configure/configure.py @@ -1,14 +1,15 @@ import json -from click import ClickException, option, command, FloatRange, IntRange, argument, Choice, BadParameter, echo +from click import BadParameter, Choice, ClickException, FloatRange, IntRange, argument, command, echo, option +from pydantic import ValidationError from pysatl_experiment.cli.commands.common.common import ( - read_experiment_data, - get_experiment_config, create_storage_path, - save_experiment_config, - get_statistics_short_codes_for_hypothesis, criteria_from_codes, + get_experiment_config, + get_statistics_short_codes_for_hypothesis, + read_experiment_data, + save_experiment_config, ) from pysatl_experiment.configuration.model.experiment_type.experiment_type import ExperimentType from pysatl_experiment.configuration.model.hypothesis.hypothesis import Hypothesis @@ -16,8 +17,6 @@ from pysatl_experiment.configuration.model.run_mode.run_mode import RunMode from pysatl_experiment.configuration.model.step_type.step_type import StepType from pysatl_experiment.validation.cli.commands.common.common import if_experiment_exists -from pydantic import ValidationError - from pysatl_experiment.validation.cli.schemas.alternative import AlternativesConfig from pysatl_experiment.validation.cli.schemas.criteria import CriteriaConfig @@ -125,7 +124,7 @@ def __configure_monte_carlo_count(experiment_config: dict, count: int | None): experiment_config["monte_carlo_count"] = count -def __configure_storage_connection(experiment_config: dict, connection: str | None): +def __configure_storage_connection(experiment_config: dict, connection: str): storage_path = create_storage_path(connection) experiment_config["storage_connection"] = storage_path diff --git a/pysatl_experiment/cli/commands/criteria/criteria.py b/pysatl_experiment/cli/commands/criteria/criteria.py index 3bd7cdbd..d780a3c2 100644 --- a/pysatl_experiment/cli/commands/criteria/criteria.py +++ b/pysatl_experiment/cli/commands/criteria/criteria.py @@ -1,12 +1,15 @@ -from click import argument, command, echo, option, Choice +from click import Choice, argument, command, echo, option from pysatl_experiment.cli.commands.common.common import get_statistics_short_codes_for_hypothesis from pysatl_experiment.configuration.model.hypothesis.hypothesis import Hypothesis @command() -@argument("distribution", type=Choice(Hypothesis.list()),) -@option('--description/--no-description', '-y/-n', default=False, help="Show criteria description") +@argument( + "distribution", + type=Choice(Hypothesis.list()), +) +@option("--description/--no-description", "-y/-n", default=False, help="Show criteria description") def available_criteria(distribution: str, description: bool) -> None: """ Collect all existing criteria. diff --git a/pysatl_experiment/cli/commands/show/show.py b/pysatl_experiment/cli/commands/show/show.py index 99ada6f9..639b6600 100644 --- a/pysatl_experiment/cli/commands/show/show.py +++ b/pysatl_experiment/cli/commands/show/show.py @@ -1,6 +1,6 @@ import json -from click import echo, command, argument +from click import argument, command, echo from pysatl_experiment.cli.commands.common.common import read_experiment_data diff --git a/tests/validation/configure/test_alternative.py b/tests/validation/configure/test_alternative.py index 34dab01b..d3872133 100644 --- a/tests/validation/configure/test_alternative.py +++ b/tests/validation/configure/test_alternative.py @@ -1,3 +1,4 @@ +from typing import Any from unittest.mock import MagicMock, patch import pytest @@ -20,6 +21,7 @@ def __init__(self, x0: float, gamma: float, **kwargs): self.x0 = x0 self.gamma = gamma + @pytest.fixture def runner() -> CliRunner: """Fixture to create a CliRunner instance.""" @@ -27,16 +29,34 @@ def runner() -> CliRunner: @patch("pysatl_experiment.cli.commands.configure.configure.get_experiment_config") -def test_alternatives_fails_if_experiment_type_not_set( - get_experiment_config: MagicMock, runner: CliRunner -) -> None: +def test_alternatives_fails_if_experiment_type_not_set(get_experiment_config: MagicMock, runner: CliRunner) -> None: experiment_name = "my-exp" get_experiment_config.return_value = (experiment_name, {"some_key": "some_value"}) - result = runner.invoke(configure, [experiment_name, "-alt", "Normal 1 1", - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", - "-rm", "reuse"]) + result = runner.invoke( + configure, + [ + experiment_name, + "-alt", + "Normal 1 1", + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + "-rm", + "reuse", + ], + ) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) @@ -52,10 +72,30 @@ def test_alternatives_fails_for_unsupported_experiment_type( experiment_name = "my-exp" get_experiment_config.return_value = ("my-exp", {"experiment_type": ExperimentType.CRITICAL_VALUE.value}) - result = runner.invoke(configure, [experiment_name, "-alt", "Normal 1 1", - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", - "-rm", "reuse"]) + result = runner.invoke( + configure, + [ + experiment_name, + "-alt", + "Normal 1 1", + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + "-rm", + "reuse", + ], + ) assert result.exit_code != 0 @@ -64,18 +104,38 @@ def test_alternatives_fails_for_unsupported_experiment_type( @patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") @patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) def test_alternatives_fails_with_wrong_parameter_count( - if_experiment_exists: MagicMock, - read_experiment_data: MagicMock, - save_experiment_config: MagicMock, - runner: CliRunner + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, ) -> None: experiment_name = "my-exp" initial_config = {"experiment_type": "power"} - read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - result = runner.invoke(configure, [experiment_name, "-alt", "Normal 1.0", - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", - "-rm", "reuse"]) + read_experiment_data.return_value = {"name": experiment_name, "config": initial_config} + result = runner.invoke( + configure, + [ + experiment_name, + "-alt", + "Normal 1.0", + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + "-rm", + "reuse", + ], + ) assert result.exit_code != 0 @@ -84,18 +144,38 @@ def test_alternatives_fails_with_wrong_parameter_count( @patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") @patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) def test_alternatives_fails_with_non_numeric_parameters( - if_experiment_exists: MagicMock, - read_experiment_data: MagicMock, - save_experiment_config: MagicMock, - runner: CliRunner + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, ) -> None: experiment_name = "my-exp" initial_config = {"experiment_type": "power"} - read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - result = runner.invoke(configure, [experiment_name, "-alt", "Normal 1.0 abc", - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", - "-rm", "reuse"]) + read_experiment_data.return_value = {"name": experiment_name, "config": initial_config} + result = runner.invoke( + configure, + [ + experiment_name, + "-alt", + "Normal 1.0 abc", + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + "-rm", + "reuse", + ], + ) assert result.exit_code != 0 @@ -108,22 +188,42 @@ def test_alternatives_fails_with_non_numeric_parameters( @patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") @patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) def test_alternatives_fails_with_ambiguous_generator_name( - if_experiment_exists: MagicMock, - read_experiment_data: MagicMock, - subclasses: MagicMock, - save_experiment_config: MagicMock, - runner: CliRunner + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + subclasses: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, ) -> None: """ Tests failure when a generator prefix matches multiple available generators. """ experiment_name = "my-exp" initial_config = {"experiment_type": "power"} - read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - result = runner.invoke(configure, [experiment_name, "-alt", "Normal 1 2", - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "power", "-con", "sqlite:///pysatl.sqlite", - "-rm", "reuse"]) + read_experiment_data.return_value = {"name": experiment_name, "config": initial_config} + result = runner.invoke( + configure, + [ + experiment_name, + "-alt", + "Normal 1 2", + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "power", + "-con", + "sqlite:///pysatl.sqlite", + "-rm", + "reuse", + ], + ) assert result.exit_code != 0 @@ -139,18 +239,40 @@ def test_alternatives_fails_with_ambiguous_generator_name( @patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") @patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) def test_alternatives_success_with_valid_inputs( - if_experiment_exists: MagicMock, - read_experiment_data: MagicMock, - save_experiment_config: MagicMock, - runner: CliRunner + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, ) -> None: experiment_name = "my-exp" - initial_config = {"experiment_type": "power"} - read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - result = runner.invoke(configure, [experiment_name, "-alt", "NormalG 1.0 0.5", "-alt", "cauchy 0 2", - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "power", "-con", "sqlite:///pysatl.sqlite", - "-rm", "reuse"]) + initial_config: dict[str, Any] = {"experiment_type": "power"} + read_experiment_data.return_value = {"name": experiment_name, "config": initial_config} + result = runner.invoke( + configure, + [ + experiment_name, + "-alt", + "NormalG 1.0 0.5", + "-alt", + "cauchy 0 2", + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "power", + "-con", + "sqlite:///pysatl.sqlite", + "-rm", + "reuse", + ], + ) assert result.exit_code == 0 assert result.exception is None diff --git a/tests/validation/configure/test_criteria.py b/tests/validation/configure/test_criteria.py index 921de04c..2ebc89d4 100644 --- a/tests/validation/configure/test_criteria.py +++ b/tests/validation/configure/test_criteria.py @@ -1,3 +1,4 @@ +from typing import Any from unittest.mock import MagicMock, patch import pytest @@ -12,6 +13,7 @@ def runner() -> CliRunner: """Fixture to create a CliRunner instance.""" return CliRunner() + @patch("pysatl_experiment.cli.commands.configure.configure.get_experiment_config") def test_criteria_fails_if_hypothesis_not_set(get_experiment_config: MagicMock, runner: CliRunner) -> None: """ @@ -26,10 +28,30 @@ def test_criteria_fails_if_hypothesis_not_set(get_experiment_config: MagicMock, experiment_name = "my-exp" get_experiment_config.return_value = (experiment_name, {"some_key": "some_value"}) - result = runner.invoke(configure, [experiment_name, "-cr", "KS", "-cr", "AD", - "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", - "-rm", "reuse"]) + result = runner.invoke( + configure, + [ + experiment_name, + "-cr", + "KS", + "-cr", + "AD", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + "-rm", + "reuse", + ], + ) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) @@ -39,10 +61,10 @@ def test_criteria_fails_if_hypothesis_not_set(get_experiment_config: MagicMock, @patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") @patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) def test_criteria_fails_with_incompatible_codes( - if_experiment_exists: MagicMock, - read_experiment_data: MagicMock, - save_experiment_config: MagicMock, - runner: CliRunner + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, ) -> None: """ Tests that the command fails when provided criteria are incompatible with the hypothesis. @@ -58,12 +80,32 @@ def test_criteria_fails_with_incompatible_codes( experiment_name = "my-exp" hypothesis = Hypothesis.NORMAL initial_config = {"hypothesis": hypothesis.value} - read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - - result = runner.invoke(configure, [experiment_name, "-cr", "KS", "-cr", "ST1", - "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", - "-rm", "reuse"]) + read_experiment_data.return_value = {"name": experiment_name, "config": initial_config} + + result = runner.invoke( + configure, + [ + experiment_name, + "-cr", + "KS", + "-cr", + "ST1", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + "-rm", + "reuse", + ], + ) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) @@ -74,11 +116,11 @@ def test_criteria_fails_with_incompatible_codes( @patch("pysatl_experiment.cli.commands.configure.configure.get_statistics_short_codes_for_hypothesis") @patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) def test_criteria_success_with_valid_codes( - if_experiment_exists: MagicMock, - get_statistics_short_codes_for_hypothesis: MagicMock, - read_experiment_data: MagicMock, - save_experiment_config: MagicMock, - runner: CliRunner + if_experiment_exists: MagicMock, + get_statistics_short_codes_for_hypothesis: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, ) -> None: """ Tests the successful execution of the `criteria` command with valid codes. @@ -93,17 +135,37 @@ def test_criteria_success_with_valid_codes( 6. Checking for the correct success message in the output. """ hypothesis = Hypothesis.NORMAL - initial_config = {"hypothesis": hypothesis.value} + initial_config: dict[str, Any] = {"hypothesis": hypothesis.value} experiment_name = "my-test-experiment" - read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} + read_experiment_data.return_value = {"name": experiment_name, "config": initial_config} mock_codes = ["KS", "AD"] get_statistics_short_codes_for_hypothesis.return_value = mock_codes - result = runner.invoke(configure, [experiment_name, "-cr", "KS", "-cr", "AD", - "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", - "-rm", "reuse"]) + result = runner.invoke( + configure, + [ + experiment_name, + "-cr", + "KS", + "-cr", + "AD", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + "-rm", + "reuse", + ], + ) assert result.exit_code == 0 assert result.exception is None diff --git a/tests/validation/configure/test_executor_type.py b/tests/validation/configure/test_executor_type.py index 8fef97cd..9579d57d 100644 --- a/tests/validation/configure/test_executor_type.py +++ b/tests/validation/configure/test_executor_type.py @@ -1,4 +1,4 @@ -from unittest.mock import ANY, MagicMock, patch +from unittest.mock import MagicMock, patch import pytest from click.testing import CliRunner @@ -14,9 +14,7 @@ def runner() -> CliRunner: @patch("pysatl_experiment.cli.commands.configure.configure.get_experiment_config") -def test_executor_type_with_invalid_type( - get_experiment_config: MagicMock, runner: CliRunner -) -> None: +def test_executor_type_with_invalid_type(get_experiment_config: MagicMock, runner: CliRunner) -> None: """ Tests the `executor_type` command with a completely invalid type string. @@ -33,10 +31,30 @@ def test_executor_type_with_invalid_type( experiment_name = "my-test-experiment" get_experiment_config.return_value = (experiment_name, {"some_key": "some_value"}) - result = runner.invoke(configure, [experiment_name, "-et", invalid_type, - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", - "-rm", "reuse"]) + result = runner.invoke( + configure, + [ + experiment_name, + "-et", + invalid_type, + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + "-rm", + "reuse", + ], + ) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) @@ -46,10 +64,10 @@ def test_executor_type_with_invalid_type( @patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") @patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) def test_executor_type_with_unsupported_custom_type( - if_experiment_exists: MagicMock, - read_experiment_data: MagicMock, - save_experiment_config: MagicMock, - runner: CliRunner + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, ) -> None: """ Tests the `executor_type` command with the 'custom' type, which is unsupported. @@ -63,12 +81,32 @@ def test_executor_type_with_unsupported_custom_type( custom_type = StepType.CUSTOM.value experiment_name = "my-test-experiment" initial_config = {"hypothesis": "normal"} - read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - - result = runner.invoke(configure, [experiment_name, "-et", custom_type, - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", - "-rm", "reuse"]) + read_experiment_data.return_value = {"name": experiment_name, "config": initial_config} + + result = runner.invoke( + configure, + [ + experiment_name, + "-et", + custom_type, + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + "-rm", + "reuse", + ], + ) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) @@ -79,11 +117,11 @@ def test_executor_type_with_unsupported_custom_type( @patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) @pytest.mark.parametrize("valid_type", [e for e in StepType if e != StepType.CUSTOM]) def test_executor_type_with_valid_supported_type( - if_experiment_exists: MagicMock, - read_experiment_data: MagicMock, - save_experiment_config: MagicMock, - runner: CliRunner, - valid_type: StepType + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, + valid_type: StepType, ) -> None: """ Tests the `executor_type` command logic with all valid and supported arguments. @@ -97,12 +135,32 @@ def test_executor_type_with_valid_supported_type( """ experiment_name = "my-test-experiment" initial_config = {"hypothesis": "normal"} - read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - - result = runner.invoke(configure, [experiment_name, "-et", valid_type.value, - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", - "-rm", "reuse"]) + read_experiment_data.return_value = {"name": experiment_name, "config": initial_config} + + result = runner.invoke( + configure, + [ + experiment_name, + "-et", + valid_type.value, + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + "-rm", + "reuse", + ], + ) assert result.exit_code == 0 assert result.exception is None diff --git a/tests/validation/configure/test_experiment_type.py b/tests/validation/configure/test_experiment_type.py index a9418fbc..2842f827 100644 --- a/tests/validation/configure/test_experiment_type.py +++ b/tests/validation/configure/test_experiment_type.py @@ -14,9 +14,7 @@ def runner() -> CliRunner: @patch("pysatl_experiment.cli.commands.configure.configure.get_experiment_config") -def test_experiment_type_with_invalid_type( - get_experiment_config: MagicMock, runner: CliRunner -) -> None: +def test_experiment_type_with_invalid_type(get_experiment_config: MagicMock, runner: CliRunner) -> None: """ Tests the `experiment_type` command with an invalid type string. @@ -32,9 +30,28 @@ def test_experiment_type_with_invalid_type( experiment_name = "my-test-experiment" get_experiment_config.return_value = (experiment_name, {"some_key": "some_value"}) - result = runner.invoke(configure, [experiment_name, "-expt", invalid_type, - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite"]) + result = runner.invoke( + configure, + [ + experiment_name, + "-expt", + invalid_type, + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + ], + ) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) @@ -45,11 +62,11 @@ def test_experiment_type_with_invalid_type( @patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) @pytest.mark.parametrize("valid_type", [e for e in ExperimentType]) def test_experiment_type_with_valid_type( - if_experiment_exists: MagicMock, - read_experiment_data: MagicMock, - save_experiment_config: MagicMock, - runner: CliRunner, - valid_type: ExperimentType + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, + valid_type: ExperimentType, ) -> None: """ Tests the `experiment_type` command logic with all valid arguments. @@ -63,11 +80,30 @@ def test_experiment_type_with_valid_type( """ experiment_name = "my-test-experiment" initial_config = {"hypothesis": "normal"} - read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} + read_experiment_data.return_value = {"name": experiment_name, "config": initial_config} - result = runner.invoke(configure, [experiment_name, "-expt", valid_type.value, - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite"]) + result = runner.invoke( + configure, + [ + experiment_name, + "-expt", + valid_type.value, + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + ], + ) assert result.exit_code == 0 assert result.exception is None diff --git a/tests/validation/configure/test_generator_type.py b/tests/validation/configure/test_generator_type.py index 99734383..3f533125 100644 --- a/tests/validation/configure/test_generator_type.py +++ b/tests/validation/configure/test_generator_type.py @@ -14,9 +14,7 @@ def runner() -> CliRunner: @patch("pysatl_experiment.cli.commands.configure.configure.get_experiment_config") -def test_generator_type_with_invalid_type( - get_experiment_config: MagicMock, runner: CliRunner -) -> None: +def test_generator_type_with_invalid_type(get_experiment_config: MagicMock, runner: CliRunner) -> None: """ Tests the `generator_type` command with a completely invalid type string. @@ -33,11 +31,30 @@ def test_generator_type_with_invalid_type( experiment_name = "my-test-experiment" get_experiment_config.return_value = (experiment_name, {"some_key": "some_value"}) - result = runner.invoke(configure, [experiment_name, "-gt", invalid_type, - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", - "-rm", "reuse" - ]) + result = runner.invoke( + configure, + [ + experiment_name, + "-gt", + invalid_type, + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + "-rm", + "reuse", + ], + ) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) @@ -47,10 +64,10 @@ def test_generator_type_with_invalid_type( @patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") @patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) def test_generator_type_with_unsupported_custom_type( - if_experiment_exists: MagicMock, - read_experiment_data: MagicMock, - save_experiment_config: MagicMock, - runner: CliRunner + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, ) -> None: """ Tests the `generator_type` command with the 'custom' type, which is unsupported. @@ -64,13 +81,32 @@ def test_generator_type_with_unsupported_custom_type( custom_type = StepType.CUSTOM.value experiment_name = "my-test-experiment" initial_config = {"hypothesis": "normal"} - read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - - result = runner.invoke(configure, [experiment_name, "-gt", custom_type, - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", - "-rm", "reuse" - ]) + read_experiment_data.return_value = {"name": experiment_name, "config": initial_config} + + result = runner.invoke( + configure, + [ + experiment_name, + "-gt", + custom_type, + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + "-rm", + "reuse", + ], + ) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) @@ -81,11 +117,11 @@ def test_generator_type_with_unsupported_custom_type( @patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) @pytest.mark.parametrize("valid_type", [e for e in StepType if e != StepType.CUSTOM]) def test_generator_type_with_valid_supported_type( - if_experiment_exists: MagicMock, - read_experiment_data: MagicMock, - save_experiment_config: MagicMock, - runner: CliRunner, - valid_type: StepType + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, + valid_type: StepType, ) -> None: """ Tests the `generator_type` command logic with all valid and supported arguments. @@ -99,12 +135,32 @@ def test_generator_type_with_valid_supported_type( """ experiment_name = "my-test-experiment" initial_config = {"hypothesis": "normal"} - read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - - result = runner.invoke(configure, [experiment_name, "-gt", valid_type.value, - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", - "-rm", "reuse"]) + read_experiment_data.return_value = {"name": experiment_name, "config": initial_config} + + result = runner.invoke( + configure, + [ + experiment_name, + "-gt", + valid_type.value, + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + "-rm", + "reuse", + ], + ) assert result.exit_code == 0 assert result.exception is None diff --git a/tests/validation/configure/test_hypothesis.py b/tests/validation/configure/test_hypothesis.py index 273709a9..bec49e5e 100644 --- a/tests/validation/configure/test_hypothesis.py +++ b/tests/validation/configure/test_hypothesis.py @@ -14,9 +14,7 @@ def runner() -> CliRunner: @patch("pysatl_experiment.cli.commands.configure.configure.get_experiment_config") -def test_hypothesis_with_invalid_hyp( - get_experiment_config: MagicMock, runner: CliRunner -) -> None: +def test_hypothesis_with_invalid_hyp(get_experiment_config: MagicMock, runner: CliRunner) -> None: """ Tests the `hypothesis` command logic with an invalid hypothesis string. @@ -33,10 +31,28 @@ def test_hypothesis_with_invalid_hyp( get_experiment_config.return_value = ("my-experiment", {}) experiment_name = "my-test-experiment" - result = runner.invoke(configure, [experiment_name, "-h", invalid_hyp, - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", - "-rm", "reuse"]) + result = runner.invoke( + configure, + [ + experiment_name, + "-h", + invalid_hyp, + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + "-rm", + "reuse", + ], + ) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) @@ -48,12 +64,12 @@ def test_hypothesis_with_invalid_hyp( @patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) @pytest.mark.parametrize("valid_hyp", [h for h in Hypothesis]) def test_hypothesis_with_valid_hyp( - if_experiment_exists: MagicMock, - get_statistics_short_codes_for_hypothesis: MagicMock, - read_experiment_data: MagicMock, - save_experiment_config: MagicMock, - runner: CliRunner, - valid_hyp: Hypothesis, + if_experiment_exists: MagicMock, + get_statistics_short_codes_for_hypothesis: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, + valid_hyp: Hypothesis, ) -> None: """ Tests the `hypothesis` command logic with a valid hypothesis. @@ -69,25 +85,32 @@ def test_hypothesis_with_valid_hyp( """ experiment_name = "my-test-experiment" initial_config = {"hypothesis": "normal"} - read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} + read_experiment_data.return_value = {"name": experiment_name, "config": initial_config} mock_codes = ["code1", "code2"] get_statistics_short_codes_for_hypothesis.return_value = mock_codes - mock_criteria_data = [ - { - "criterion_code": "code1", - "parameters": [] - }, - { - "criterion_code": "code2", - "parameters": [] - }] - - - result = runner.invoke(configure, [experiment_name, "-h", valid_hyp.value, - "-l", "0.05", "-s", "23", "-c", "154", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", - "-rm", "reuse"]) + mock_criteria_data = [{"criterion_code": "code1", "parameters": []}, {"criterion_code": "code2", "parameters": []}] + + result = runner.invoke( + configure, + [ + experiment_name, + "-h", + valid_hyp.value, + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + "-rm", + "reuse", + ], + ) assert result.exit_code == 0 assert result.exception is None diff --git a/tests/validation/configure/test_report_builder_type.py b/tests/validation/configure/test_report_builder_type.py index b6a3ebc0..69afb995 100644 --- a/tests/validation/configure/test_report_builder_type.py +++ b/tests/validation/configure/test_report_builder_type.py @@ -29,10 +29,28 @@ def test_report_builder_type_with_invalid_type(runner: CliRunner) -> None: invalid_type = "this-is-not-a-valid-type" experiment_name = "my-test-experiment" - result = runner.invoke(configure, [experiment_name, "-rbt", invalid_type, - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite" - ]) + result = runner.invoke( + configure, + [ + experiment_name, + "-rbt", + invalid_type, + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + ], + ) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) @@ -42,10 +60,10 @@ def test_report_builder_type_with_invalid_type(runner: CliRunner) -> None: @patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") @patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) def test_report_builder_type_with_unsupported_custom_type( - if_experiment_exists: MagicMock, - read_experiment_data: MagicMock, - save_experiment_config: MagicMock, - runner: CliRunner + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, ) -> None: """ Tests the `report_builder_type` command with the 'custom' type, which is unsupported. @@ -59,12 +77,30 @@ def test_report_builder_type_with_unsupported_custom_type( custom_type = StepType.CUSTOM experiment_name = "my-test-experiment" initial_config = {"hypothesis": "normal"} - read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - - result = runner.invoke(configure, [experiment_name, "-rbt", custom_type.value, - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite" - ]) + read_experiment_data.return_value = {"name": experiment_name, "config": initial_config} + + result = runner.invoke( + configure, + [ + experiment_name, + "-rbt", + custom_type.value, + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + ], + ) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) @@ -77,11 +113,11 @@ def test_report_builder_type_with_unsupported_custom_type( @patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) @pytest.mark.parametrize("valid_type", [e for e in StepType if e != StepType.CUSTOM]) def test_report_builder_type_with_valid_supported_type( - if_experiment_exists: MagicMock, - read_experiment_data: MagicMock, - save_experiment_config: MagicMock, - runner: CliRunner, - valid_type: StepType + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, + valid_type: StepType, ) -> None: """ Tests the `report_builder_type` command logic with all valid and supported arguments. @@ -95,12 +131,30 @@ def test_report_builder_type_with_valid_supported_type( """ experiment_name = "my-test-experiment" initial_config = {"hypothesis": "normal"} - read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} - - result = runner.invoke(configure, [experiment_name, "-rbt", valid_type.value, - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite" - ]) + read_experiment_data.return_value = {"name": experiment_name, "config": initial_config} + + result = runner.invoke( + configure, + [ + experiment_name, + "-rbt", + valid_type.value, + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + ], + ) assert result.exit_code == 0 assert result.exception is None diff --git a/tests/validation/configure/test_report_mode.py b/tests/validation/configure/test_report_mode.py index b64919b7..14c3e519 100644 --- a/tests/validation/configure/test_report_mode.py +++ b/tests/validation/configure/test_report_mode.py @@ -14,9 +14,7 @@ def runner() -> CliRunner: @patch("pysatl_experiment.cli.commands.configure.configure.get_experiment_config") -def test_report_mode_with_invalid_mode( - get_experiment_config: MagicMock, runner: CliRunner -) -> None: +def test_report_mode_with_invalid_mode(get_experiment_config: MagicMock, runner: CliRunner) -> None: """ Tests the `report_mode` command logic in isolation with an invalid argument. @@ -33,10 +31,30 @@ def test_report_mode_with_invalid_mode( experiment_name = "my-test-experiment" get_experiment_config.return_value = (experiment_name, {"some_key": "some_value"}) - result = runner.invoke(configure, [experiment_name, "-rp", invalid_mode, - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", - "-rm", "reuse"]) + result = runner.invoke( + configure, + [ + experiment_name, + "-rp", + invalid_mode, + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + "-rm", + "reuse", + ], + ) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) @@ -47,10 +65,11 @@ def test_report_mode_with_invalid_mode( @patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) @pytest.mark.parametrize("valid_mode", [e for e in ReportMode]) def test_report_mode_with_valid_mode( - if_experiment_exists: MagicMock, - read_experiment_data: MagicMock, - save_experiment_config: MagicMock, - runner: CliRunner, valid_mode: ReportMode + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, + valid_mode: ReportMode, ) -> None: """ Tests the `report_mode` command logic in isolation with valid arguments. @@ -64,13 +83,32 @@ def test_report_mode_with_valid_mode( """ experiment_name = "my-test-experiment" initial_config = {"hypothesis": "normal"} - read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} + read_experiment_data.return_value = {"name": experiment_name, "config": initial_config} - result = runner.invoke(configure, [experiment_name, "-rp", valid_mode.value, - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite", - "-rm", "reuse" - ]) + result = runner.invoke( + configure, + [ + experiment_name, + "-rp", + valid_mode.value, + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + "-rm", + "reuse", + ], + ) assert result.exit_code == 0 assert result.exception is None diff --git a/tests/validation/configure/test_run_mode.py b/tests/validation/configure/test_run_mode.py index 1ebf0c6c..7b43f26f 100644 --- a/tests/validation/configure/test_run_mode.py +++ b/tests/validation/configure/test_run_mode.py @@ -31,10 +31,28 @@ def test_run_mode_with_invalid_mode(get_experiment_config: MagicMock, runner: Cl experiment_name = "my-test-experiment" get_experiment_config.return_value = (experiment_name, {"some_key": "some_value"}) - result = runner.invoke(configure, [experiment_name, "-rm", invalid_mode, - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite" - ]) + result = runner.invoke( + configure, + [ + experiment_name, + "-rm", + invalid_mode, + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + ], + ) assert result.exit_code != 0 assert isinstance(result.exception, SystemExit) @@ -44,11 +62,12 @@ def test_run_mode_with_invalid_mode(get_experiment_config: MagicMock, runner: Cl @patch("pysatl_experiment.cli.commands.configure.configure.read_experiment_data") @patch("pysatl_experiment.cli.commands.configure.configure.if_experiment_exists", return_value=True) @pytest.mark.parametrize("valid_mode", [e for e in RunMode]) -def test_run_mode_with_valid_mode(if_experiment_exists: MagicMock, - read_experiment_data: MagicMock, - save_experiment_config: MagicMock, - runner: CliRunner, - valid_mode: RunMode +def test_run_mode_with_valid_mode( + if_experiment_exists: MagicMock, + read_experiment_data: MagicMock, + save_experiment_config: MagicMock, + runner: CliRunner, + valid_mode: RunMode, ) -> None: """ Tests the `run_mode` command logic in isolation with valid arguments. @@ -62,11 +81,30 @@ def test_run_mode_with_valid_mode(if_experiment_exists: MagicMock, """ experiment_name = "my-test-experiment" initial_config = {"hypothesis": "normal"} - read_experiment_data.return_value = {'name': experiment_name, 'config': initial_config} + read_experiment_data.return_value = {"name": experiment_name, "config": initial_config} - result = runner.invoke(configure, [experiment_name, "-rm", valid_mode.value, - "-cr", "KS", "-l", "0.05", "-s", "23", "-c", "154", "-h", "normal", - "-expt", "critical_value", "-con", "sqlite:///pysatl.sqlite"]) + result = runner.invoke( + configure, + [ + experiment_name, + "-rm", + valid_mode.value, + "-cr", + "KS", + "-l", + "0.05", + "-s", + "23", + "-c", + "154", + "-h", + "normal", + "-expt", + "critical_value", + "-con", + "sqlite:///pysatl.sqlite", + ], + ) assert result.exit_code == 0 assert result.exception is None From aea77bbc2bd9d4df54c538c6a20e3b6ef7562539 Mon Sep 17 00:00:00 2001 From: Alexey Mironov Date: Fri, 12 Dec 2025 18:19:00 +0300 Subject: [PATCH 03/12] Update pysatl_criterion --- pysatl_criterion | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pysatl_criterion b/pysatl_criterion index 721fcc38..b2cd252a 160000 --- a/pysatl_criterion +++ b/pysatl_criterion @@ -1 +1 @@ -Subproject commit 721fcc384bd46c1f25d8c9274fb951e030b40f47 +Subproject commit b2cd252a24c12bde62db9a651c5568625f044d05 From a39a7453caf924cfa0c2d73f6c783f2f4e608ff2 Mon Sep 17 00:00:00 2001 From: Alexey Mironov Date: Fri, 12 Dec 2025 18:24:04 +0300 Subject: [PATCH 04/12] Fix README.md --- README.md | 2 +- pysatl_experiment/cli/commands/configure/configure.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6e96bb37..0e52017c 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ poetry run experiment create NAME ```shell poetry run experiment configure NAME \ -cr KS \ --l 0.05 0.01 \ +-l 0.05 -l 0.01 \ -s 23 \ -c 154 \ -h normal \ diff --git a/pysatl_experiment/cli/commands/configure/configure.py b/pysatl_experiment/cli/commands/configure/configure.py index 8d568d02..f73f5fe8 100644 --- a/pysatl_experiment/cli/commands/configure/configure.py +++ b/pysatl_experiment/cli/commands/configure/configure.py @@ -4,7 +4,6 @@ from pydantic import ValidationError from pysatl_experiment.cli.commands.common.common import ( - create_storage_path, criteria_from_codes, get_experiment_config, get_statistics_short_codes_for_hypothesis, @@ -125,8 +124,7 @@ def __configure_monte_carlo_count(experiment_config: dict, count: int | None): def __configure_storage_connection(experiment_config: dict, connection: str): - storage_path = create_storage_path(connection) - experiment_config["storage_connection"] = storage_path + experiment_config["storage_connection"] = connection def __configure_experiment_type(experiment_config: dict, experiment_type: str | None): From d9e724ec9d07b152373b0efe5d47d28b08aa5b50 Mon Sep 17 00:00:00 2001 From: Alexey Mironov Date: Mon, 22 Dec 2025 13:49:21 +0300 Subject: [PATCH 05/12] Fix merge --- pysatl_criterion | 2 +- .../cli/commands/configure/configure.py | 20 +++++++++++ .../parallel_workers/parallel_workers.py | 33 ------------------- .../storage_connection/storage_connection.py | 0 4 files changed, 21 insertions(+), 34 deletions(-) delete mode 100644 pysatl_experiment/cli/commands/configure/parallel_workers/parallel_workers.py delete mode 100644 pysatl_experiment/cli/commands/configure/storage_connection/storage_connection.py diff --git a/pysatl_criterion b/pysatl_criterion index b2cd252a..4970bfe7 160000 --- a/pysatl_criterion +++ b/pysatl_criterion @@ -1 +1 @@ -Subproject commit b2cd252a24c12bde62db9a651c5568625f044d05 +Subproject commit 4970bfe7032ba16a24cd1dc3e75c27e8584c7162 diff --git a/pysatl_experiment/cli/commands/configure/configure.py b/pysatl_experiment/cli/commands/configure/configure.py index f73f5fe8..0e6bdd54 100644 --- a/pysatl_experiment/cli/commands/configure/configure.py +++ b/pysatl_experiment/cli/commands/configure/configure.py @@ -1,4 +1,5 @@ import json +import multiprocessing as mp from click import BadParameter, Choice, ClickException, FloatRange, IntRange, argument, command, echo, option from pydantic import ValidationError @@ -135,6 +136,21 @@ def __configure_experiment_type(experiment_config: dict, experiment_type: str | experiment_config["experiment_type"] = validated_experiment_type.value +def __configure_workers(experiment_config: dict, workers: int | None): + if workers is None: + return + + max_possible = mp.cpu_count() + if workers > max_possible: + raise ClickException( + f"Cannot set parallel workers to {workers}. " + f"Your machine has only {max_possible} CPU cores. " + f"Please specify a value between 1 and {max_possible}." + ) + + experiment_config["parallel_workers"] = workers + + def __configure_alternatives(experiment_config: dict, alternative: tuple[str] | None): if alternative is None: return @@ -215,6 +231,7 @@ def __configure_significance_levels(experiment_config: dict, levels: tuple[float ) @option("-et", "--executor-type", type=Choice(StepType.list()), help="Executor type. Example: standard") @option("-cr", "--criteria", multiple=True, help="Criterion codes. Example: KS") +@option("-w", "--workers", type=IntRange(min=1), help="Criterion codes. Example: KS") def configure( name: str, alternative: tuple[str], @@ -230,11 +247,13 @@ def configure( experiment_type: str, executor_type: str, criteria: tuple[str, ...], + workers: int, ) -> None: """ Configure experiment parameters. :param name: name of the experiment. + :param workers: Number of parallel workers (1 <= workers). """ experiment_exists = if_experiment_exists(name) @@ -256,6 +275,7 @@ def configure( __configure_executor_type(experiment_config, executor_type) __configure_criteria(experiment_config, criteria) __configure_alternatives(experiment_config, alternative) + __configure_workers(experiment_config, workers) save_experiment_config(name, experiment_config) diff --git a/pysatl_experiment/cli/commands/configure/parallel_workers/parallel_workers.py b/pysatl_experiment/cli/commands/configure/parallel_workers/parallel_workers.py deleted file mode 100644 index b173f6b6..00000000 --- a/pysatl_experiment/cli/commands/configure/parallel_workers/parallel_workers.py +++ /dev/null @@ -1,33 +0,0 @@ -import multiprocessing as mp - -from click import ClickException, Context, IntRange, argument, echo, pass_context - -from pysatl_experiment.cli.commands.common.common import get_experiment_name_and_config, save_experiment_config -from pysatl_experiment.cli.commands.configure.configure import configure - - -@configure.command() -@argument("workers", type=IntRange(min=1)) -@pass_context -def parallel_workers(ctx: Context, workers: int) -> None: - """ - Configure number of parallel workers for execution step. - - :param ctx: Click context. - :param workers: Number of parallel workers (1 <= workers). - """ - - experiment_name, experiment_config = get_experiment_name_and_config(ctx) - - max_possible = mp.cpu_count() - if workers > max_possible: - raise ClickException( - f"Cannot set parallel workers to {workers}. " - f"Your machine has only {max_possible} CPU cores. " - f"Please specify a value between 1 and {max_possible}." - ) - - experiment_config["parallel_workers"] = workers - save_experiment_config(ctx, experiment_name, experiment_config) - - echo(f"Parallel workers for experiment '{experiment_name}' are set to {workers}.") diff --git a/pysatl_experiment/cli/commands/configure/storage_connection/storage_connection.py b/pysatl_experiment/cli/commands/configure/storage_connection/storage_connection.py deleted file mode 100644 index e69de29b..00000000 From 5d5bcc1f31602310a5163d3ebf43264d776e8bc6 Mon Sep 17 00:00:00 2001 From: Alexey Mironov Date: Mon, 22 Dec 2025 13:52:27 +0300 Subject: [PATCH 06/12] Fix merge --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f35da6d2..5f5a4a78 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -39,10 +39,10 @@ jobs: run: poetry install --with dev - name: Run Ruff lint - run: poetry run ruff check --output-format=github + run: poetry run ruff check pysatl_experiment tests --output-format=github - name: Check formatting with Ruff - run: poetry run ruff format --check + run: poetry run ruff format pysatl_experiment tests --check - name: Run tests run: poetry run pytest --random-order tests From e3f1f6410b7802876deb34fd8af90a9ad15c48f5 Mon Sep 17 00:00:00 2001 From: Alexey Mironov Date: Mon, 22 Dec 2025 13:54:34 +0300 Subject: [PATCH 07/12] Update pysatl_criterion hash --- pysatl_criterion | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pysatl_criterion b/pysatl_criterion index 4970bfe7..e4e94c12 160000 --- a/pysatl_criterion +++ b/pysatl_criterion @@ -1 +1 @@ -Subproject commit 4970bfe7032ba16a24cd1dc3e75c27e8584c7162 +Subproject commit e4e94c12473659a677d2b10e1cdc4bc182209541 From 12b492f784e5dc9cfcf236cf1b2e61606058cfc7 Mon Sep 17 00:00:00 2001 From: Alexey Mironov Date: Mon, 22 Dec 2025 14:16:41 +0300 Subject: [PATCH 08/12] Fix mypy --- .../model/experiment_step/experiment_step.py | 5 +++-- .../experiment_new/step/execution/power/power.py | 5 ++++- .../experiment_new/step/generation/generation.py | 5 ++++- .../critical_value/critical_value.py | 5 ++++- .../step/report_building/power/power.py | 5 ++++- .../time_complexity/time_complexity.py | 5 ++++- .../persistence/model/experiment/experiment.py | 8 ++++++-- pysatl_experiment/persistence/model/power/power.py | 4 ++-- .../model/random_values/random_values.py | 9 +++++++-- .../model/time_complexity/time_complexity.py | 4 ++-- .../abstract_report_builder.py | 5 +++-- .../worker/model/abstract_worker/abstract_worker.py | 6 ++++-- tests/factory/test_critical_value_factory.py | 13 ++++++++++--- tests/factory/test_power_factory.py | 9 ++++++--- tests/factory/test_time_complexity_factory.py | 13 ++++++++++--- 15 files changed, 73 insertions(+), 28 deletions(-) diff --git a/pysatl_experiment/experiment_new/model/experiment_step/experiment_step.py b/pysatl_experiment/experiment_new/model/experiment_step/experiment_step.py index 1b3aed3e..0a1c7226 100644 --- a/pysatl_experiment/experiment_new/model/experiment_step/experiment_step.py +++ b/pysatl_experiment/experiment_new/model/experiment_step/experiment_step.py @@ -1,10 +1,11 @@ -from typing import Protocol +from abc import ABC, abstractmethod -class IExperimentStep(Protocol): +class IExperimentStep(ABC): """ Interface for experiment step. """ + @abstractmethod def run(self) -> None: pass diff --git a/pysatl_experiment/experiment_new/step/execution/power/power.py b/pysatl_experiment/experiment_new/step/execution/power/power.py index 7128c741..ad2aa899 100644 --- a/pysatl_experiment/experiment_new/step/execution/power/power.py +++ b/pysatl_experiment/experiment_new/step/execution/power/power.py @@ -2,8 +2,10 @@ from dataclasses import dataclass from line_profiler import profile +from typing_extensions import override from pysatl_experiment.configuration.model.alternative.alternative import Alternative +from pysatl_experiment.experiment_new.model.experiment_step.experiment_step import IExperimentStep from pysatl_experiment.experiment_new.step.execution.common.execution_step_data.execution_step_data import ( ExecutionStepData, ) @@ -25,7 +27,7 @@ class PowerStepData(ExecutionStepData): significance_level: float -class PowerExecutionStep: +class PowerExecutionStep(IExperimentStep): """ Standard power experiment execution step. """ @@ -49,6 +51,7 @@ def __init__( self.parallel_workers = parallel_workers @profile + @override def run(self) -> None: """ Run power experiment in parallel with buffering. diff --git a/pysatl_experiment/experiment_new/step/generation/generation.py b/pysatl_experiment/experiment_new/step/generation/generation.py index e90fe315..4fd908dc 100644 --- a/pysatl_experiment/experiment_new/step/generation/generation.py +++ b/pysatl_experiment/experiment_new/step/generation/generation.py @@ -1,8 +1,10 @@ from dataclasses import dataclass from line_profiler import profile +from typing_extensions import override from pysatl_experiment.experiment.generator import AbstractRVSGenerator +from pysatl_experiment.experiment_new.model.experiment_step.experiment_step import IExperimentStep from pysatl_experiment.persistence.model.random_values.random_values import IRandomValuesStorage, RandomValuesModel @@ -19,7 +21,7 @@ class GenerationStepData: count: int -class GenerationStep: +class GenerationStep(IExperimentStep): """ Standard experiment generation step. """ @@ -33,6 +35,7 @@ def __init__( self.data_storage = data_storage @profile + @override def run(self) -> None: """ Run standard generation step. diff --git a/pysatl_experiment/experiment_new/step/report_building/critical_value/critical_value.py b/pysatl_experiment/experiment_new/step/report_building/critical_value/critical_value.py index 92c2dce0..9128f0a5 100644 --- a/pysatl_experiment/experiment_new/step/report_building/critical_value/critical_value.py +++ b/pysatl_experiment/experiment_new/step/report_building/critical_value/critical_value.py @@ -1,6 +1,7 @@ from pathlib import Path from line_profiler import profile +from typing_extensions import override from pysatl_criterion.cv_calculator.cv_calculator.cv_calculator import CVCalculator from pysatl_criterion.persistence.model.limit_distribution.limit_distribution import ( @@ -9,10 +10,11 @@ ) from pysatl_experiment.configuration.criteria_config.criteria_config import CriterionConfig from pysatl_experiment.configuration.model.report_mode.report_mode import ReportMode +from pysatl_experiment.experiment_new.model.experiment_step.experiment_step import IExperimentStep from pysatl_experiment.report.critical_value.critical_value import CriticalValueReportBuilder -class CriticalValueReportBuildingStep: +class CriticalValueReportBuildingStep(IExperimentStep): """ Standard critical value experiment report building step. """ @@ -36,6 +38,7 @@ def __init__( self.with_chart = with_chart @profile + @override def run(self) -> None: """ Run standard critical value report building step. diff --git a/pysatl_experiment/experiment_new/step/report_building/power/power.py b/pysatl_experiment/experiment_new/step/report_building/power/power.py index 272eeb00..622a0f06 100644 --- a/pysatl_experiment/experiment_new/step/report_building/power/power.py +++ b/pysatl_experiment/experiment_new/step/report_building/power/power.py @@ -1,15 +1,17 @@ from pathlib import Path from line_profiler import profile +from typing_extensions import override from pysatl_experiment.configuration.criteria_config.criteria_config import CriterionConfig from pysatl_experiment.configuration.model.alternative.alternative import Alternative from pysatl_experiment.configuration.model.report_mode.report_mode import ReportMode +from pysatl_experiment.experiment_new.model.experiment_step.experiment_step import IExperimentStep from pysatl_experiment.persistence.model.power.power import IPowerStorage, PowerQuery from pysatl_experiment.report.power.power import PowerReportBuilder -class PowerReportBuildingStep: +class PowerReportBuildingStep(IExperimentStep): """ Standard power experiment report building step. """ @@ -35,6 +37,7 @@ def __init__( self.with_chart = with_chart @profile + @override def run(self) -> None: """ Run standard power report building step. diff --git a/pysatl_experiment/experiment_new/step/report_building/time_complexity/time_complexity.py b/pysatl_experiment/experiment_new/step/report_building/time_complexity/time_complexity.py index c6b4c09a..0ec3304d 100644 --- a/pysatl_experiment/experiment_new/step/report_building/time_complexity/time_complexity.py +++ b/pysatl_experiment/experiment_new/step/report_building/time_complexity/time_complexity.py @@ -2,9 +2,11 @@ import numpy as np from line_profiler import profile +from typing_extensions import override from pysatl_experiment.configuration.criteria_config.criteria_config import CriterionConfig from pysatl_experiment.configuration.model.report_mode.report_mode import ReportMode +from pysatl_experiment.experiment_new.model.experiment_step.experiment_step import IExperimentStep from pysatl_experiment.persistence.model.time_complexity.time_complexity import ( ITimeComplexityStorage, TimeComplexityQuery, @@ -12,7 +14,7 @@ from pysatl_experiment.report.time_complexity.time_complexity import TimeComplexityReportBuilder -class TimeComplexityReportBuildingStep: +class TimeComplexityReportBuildingStep(IExperimentStep): """ Standard time complexity experiment report building step. """ @@ -34,6 +36,7 @@ def __init__( self.with_chart = with_chart @profile + @override def run(self) -> None: """ Run standard time complexity report building step. diff --git a/pysatl_experiment/persistence/model/experiment/experiment.py b/pysatl_experiment/persistence/model/experiment/experiment.py index 6d28b55c..85931c07 100644 --- a/pysatl_experiment/persistence/model/experiment/experiment.py +++ b/pysatl_experiment/persistence/model/experiment/experiment.py @@ -1,5 +1,5 @@ +from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Protocol from pysatl_criterion.persistence.model.common.data_storage.data_storage import DataModel, DataQuery, IDataStorage @@ -43,11 +43,12 @@ class ExperimentQuery(DataQuery): parallel_workers: int -class IExperimentStorage(IDataStorage[ExperimentModel, ExperimentQuery], Protocol): +class IExperimentStorage(IDataStorage[ExperimentModel, ExperimentQuery], ABC): """ Experiment configuration storage interface. """ + @abstractmethod def get_experiment_id(self, query: ExperimentQuery) -> int | None: """ Get experiment id. @@ -56,6 +57,7 @@ def get_experiment_id(self, query: ExperimentQuery) -> int | None: """ pass + @abstractmethod def set_generation_done(self, experiment_id: int) -> None: """ Set generation step as done. @@ -64,6 +66,7 @@ def set_generation_done(self, experiment_id: int) -> None: """ pass + @abstractmethod def set_execution_done(self, experiment_id: int) -> None: """ Set execution step as done. @@ -72,6 +75,7 @@ def set_execution_done(self, experiment_id: int) -> None: """ pass + @abstractmethod def set_report_building_done(self, experiment_id: int) -> None: """ Set report building step as done. diff --git a/pysatl_experiment/persistence/model/power/power.py b/pysatl_experiment/persistence/model/power/power.py index aa026755..bf49188e 100644 --- a/pysatl_experiment/persistence/model/power/power.py +++ b/pysatl_experiment/persistence/model/power/power.py @@ -1,5 +1,5 @@ +from abc import ABC from dataclasses import dataclass -from typing import Protocol from pysatl_criterion.persistence.model.common.data_storage.data_storage import DataModel, DataQuery, IDataStorage @@ -28,7 +28,7 @@ class PowerQuery(DataQuery): significance_level: float -class IPowerStorage(IDataStorage[PowerModel, PowerQuery], Protocol): +class IPowerStorage(IDataStorage[PowerModel, PowerQuery], ABC): """ Power storage interface. """ diff --git a/pysatl_experiment/persistence/model/random_values/random_values.py b/pysatl_experiment/persistence/model/random_values/random_values.py index 735f7dee..481e94ec 100644 --- a/pysatl_experiment/persistence/model/random_values/random_values.py +++ b/pysatl_experiment/persistence/model/random_values/random_values.py @@ -1,5 +1,5 @@ +from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Protocol from pysatl_criterion.persistence.model.common.data_storage.data_storage import DataModel, DataQuery, IDataStorage @@ -44,35 +44,40 @@ class RandomValuesAllModel(DataModel): data: list[list[float]] -class IRandomValuesStorage(IDataStorage[RandomValuesModel, RandomValuesQuery], Protocol): +class IRandomValuesStorage(IDataStorage[RandomValuesModel, RandomValuesQuery], ABC): """ Random values storage interface. """ + @abstractmethod def get_rvs_count(self, query: RandomValuesAllQuery) -> int: """ Get count of samples. """ pass + @abstractmethod def insert_all_data(self, query: RandomValuesAllModel) -> None: """ Insert all data based on hypothesis and sample size. """ pass + @abstractmethod def get_all_data(self, query: RandomValuesAllQuery) -> list[RandomValuesModel] | None: """ Get all data based on hypothesis and sample size. """ pass + @abstractmethod def delete_all_data(self, query: RandomValuesAllQuery) -> None: """ Delete all data based on hypothesis and sample size. """ pass + @abstractmethod def get_count_data(self, query: RandomValuesCountQuery) -> list[RandomValuesModel] | None: """ Get count data based on hypothesis and sample size. diff --git a/pysatl_experiment/persistence/model/time_complexity/time_complexity.py b/pysatl_experiment/persistence/model/time_complexity/time_complexity.py index 60acc813..d51ae39f 100644 --- a/pysatl_experiment/persistence/model/time_complexity/time_complexity.py +++ b/pysatl_experiment/persistence/model/time_complexity/time_complexity.py @@ -1,5 +1,5 @@ +from abc import ABC from dataclasses import dataclass -from typing import Protocol from pysatl_criterion.persistence.model.common.data_storage.data_storage import DataModel, DataQuery, IDataStorage @@ -22,7 +22,7 @@ class TimeComplexityQuery(DataQuery): monte_carlo_count: int -class ITimeComplexityStorage(IDataStorage[TimeComplexityModel, TimeComplexityQuery], Protocol): +class ITimeComplexityStorage(IDataStorage[TimeComplexityModel, TimeComplexityQuery], ABC): """ Time complexity storage interface. """ diff --git a/pysatl_experiment/report/model/abstract_report_builder/abstract_report_builder.py b/pysatl_experiment/report/model/abstract_report_builder/abstract_report_builder.py index be5594e7..b5493373 100644 --- a/pysatl_experiment/report/model/abstract_report_builder/abstract_report_builder.py +++ b/pysatl_experiment/report/model/abstract_report_builder/abstract_report_builder.py @@ -1,11 +1,12 @@ -from typing import Protocol +from abc import ABC, abstractmethod -class IReportBuilder(Protocol): +class IReportBuilder(ABC): """ Report builder interface. """ + @abstractmethod def build(self) -> None: """ Build file. diff --git a/pysatl_experiment/worker/model/abstract_worker/abstract_worker.py b/pysatl_experiment/worker/model/abstract_worker/abstract_worker.py index ea8d5bf0..d40a4b07 100644 --- a/pysatl_experiment/worker/model/abstract_worker/abstract_worker.py +++ b/pysatl_experiment/worker/model/abstract_worker/abstract_worker.py @@ -1,5 +1,6 @@ +from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Protocol, TypeVar +from typing import Generic, TypeVar @dataclass @@ -12,11 +13,12 @@ class WorkerResult: R = TypeVar("R", covariant=True, bound=WorkerResult) -class IWorker(Protocol[R]): +class IWorker(Generic[R], ABC): """ Worker interface. """ + @abstractmethod def execute(self) -> R: """ Execute worker. diff --git a/tests/factory/test_critical_value_factory.py b/tests/factory/test_critical_value_factory.py index b2a7d277..471746d1 100644 --- a/tests/factory/test_critical_value_factory.py +++ b/tests/factory/test_critical_value_factory.py @@ -5,7 +5,9 @@ from typing import Any import pytest +from numpy import float64 +from pysatl_criterion.statistics.goodness_of_fit import AbstractGoodnessOfFitStatistic from pysatl_experiment.configuration.criteria_config.criteria_config import CriterionConfig from pysatl_experiment.configuration.experiment_config.critical_value.critical_value import ( CriticalValueExperimentConfig, @@ -23,6 +25,8 @@ CriticalValueReportBuildingStep, ) from pysatl_experiment.factory.critical_value.critical_value import CriticalValueExperimentFactory +from pysatl_experiment.persistence.model.experiment.experiment import IExperimentStorage +from pysatl_experiment.persistence.model.random_values.random_values import IRandomValuesStorage # Provide a stub for line_profiler to avoid optional dependency during imports @@ -49,13 +53,16 @@ def generate(self, n): # pragma: no cover - we don't run steps in these tests return [0.0 for _ in range(n)] -class FakeStatistics: +class FakeStatistics(AbstractGoodnessOfFitStatistic): + def execute_statistic(self, rvs, **kwargs) -> float | float64: + return 0 + @staticmethod def code() -> str: return "FAKE_CODE" -class FakeRandomValuesStorage: +class FakeRandomValuesStorage(IRandomValuesStorage): def __init__(self, counts_by_size: dict[int, int]): self.counts_by_size = counts_by_size @@ -113,7 +120,7 @@ def get_data_for_cv(self, query): return None -class FakeExperimentStorage: +class FakeExperimentStorage(IExperimentStorage): def __init__(self, experiment_id: int): self._id = experiment_id diff --git a/tests/factory/test_power_factory.py b/tests/factory/test_power_factory.py index 87e57872..deb9a774 100644 --- a/tests/factory/test_power_factory.py +++ b/tests/factory/test_power_factory.py @@ -20,6 +20,9 @@ from pysatl_experiment.experiment_new.step.generation.generation import GenerationStep from pysatl_experiment.experiment_new.step.report_building.power.power import PowerReportBuildingStep from pysatl_experiment.factory.power.power import PowerExperimentFactory +from pysatl_experiment.persistence.model.experiment.experiment import IExperimentStorage +from pysatl_experiment.persistence.model.power.power import IPowerStorage +from pysatl_experiment.persistence.model.random_values.random_values import IRandomValuesStorage # Provide a stub for line_profiler to avoid optional dependency during imports @@ -52,7 +55,7 @@ def code() -> str: return "FAKE_CODE" -class FakeRandomValuesStorage: +class FakeRandomValuesStorage(IRandomValuesStorage): def __init__(self, counts_by_key: dict[tuple[str, tuple[float, ...], int], int]): # key: (generator_name, parameters_tuple, sample_size) self.counts_by_key = counts_by_key @@ -90,7 +93,7 @@ def get_count_data(self, query): # pragma: no cover return None -class FakePowerStorage: +class FakePowerStorage(IPowerStorage): def __init__(self, has_result: set[tuple[str, int, int, str, tuple[float, ...], float]]): # key: (criterion_code, sample_size, monte_carlo_count, alternative_code, alternative_parameters, # significance_level) @@ -117,7 +120,7 @@ def delete_data(self, query): # pragma: no cover pass -class FakeExperimentStorage: +class FakeExperimentStorage(IExperimentStorage): def __init__(self, experiment_id: int): self._id = experiment_id diff --git a/tests/factory/test_time_complexity_factory.py b/tests/factory/test_time_complexity_factory.py index 698b49e9..f053c9f7 100644 --- a/tests/factory/test_time_complexity_factory.py +++ b/tests/factory/test_time_complexity_factory.py @@ -5,7 +5,9 @@ from typing import Any, cast import pytest +from numpy import float64 +from pysatl_criterion.statistics.goodness_of_fit import AbstractGoodnessOfFitStatistic from pysatl_experiment.configuration.criteria_config.criteria_config import CriterionConfig from pysatl_experiment.configuration.experiment_config.time_complexity.time_complexity import ( TimeComplexityExperimentConfig, @@ -23,7 +25,9 @@ TimeComplexityReportBuildingStep, ) from pysatl_experiment.factory.time_complexity.time_complexity import TimeComplexityExperimentFactory +from pysatl_experiment.persistence.model.experiment.experiment import IExperimentStorage from pysatl_experiment.persistence.model.random_values.random_values import IRandomValuesStorage +from pysatl_experiment.persistence.model.time_complexity.time_complexity import ITimeComplexityStorage # Provide a stub for line_profiler to avoid optional dependency during imports @@ -51,7 +55,10 @@ def generate(self, n): return [0.0 for _ in range(n)] -class FakeStatistics: +class FakeStatistics(AbstractGoodnessOfFitStatistic): + def execute_statistic(self, rvs, **kwargs) -> float | float64: + return 0 + @staticmethod def code() -> str: return "FAKE_CODE" @@ -88,7 +95,7 @@ def delete_data(self, query): # pragma: no cover pass -class FakeTimeComplexityStorage: +class FakeTimeComplexityStorage(ITimeComplexityStorage): def __init__(self, has_result: set[tuple[str, int, int]]): # key: (criterion_code, sample_size, monte_carlo_count) self.has_result = has_result @@ -112,7 +119,7 @@ def insert_data(self, model): # pragma: no cover - steps aren't run pass -class FakeExperimentStorage: +class FakeExperimentStorage(IExperimentStorage): def __init__(self, experiment_id: int): self._id = experiment_id From 0a155b5a9d27fda14fb64026e9dd73bd18be94a8 Mon Sep 17 00:00:00 2001 From: Alexey Mironov Date: Mon, 22 Dec 2025 14:20:59 +0300 Subject: [PATCH 09/12] Fix tests --- tests/factory/test_abstract_experiment_factory.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/factory/test_abstract_experiment_factory.py b/tests/factory/test_abstract_experiment_factory.py index 91382708..07690ee2 100644 --- a/tests/factory/test_abstract_experiment_factory.py +++ b/tests/factory/test_abstract_experiment_factory.py @@ -84,6 +84,9 @@ def code() -> str: class DummyStep(IExperimentStep): + def run(self) -> None: + pass + def __init__(self, name: str): self.name = name From b01492017c830c5ae24b7b425d290f96713dbda3 Mon Sep 17 00:00:00 2001 From: Alexey Mironov Date: Mon, 22 Dec 2025 17:24:20 +0300 Subject: [PATCH 10/12] Fix tests --- .../step/report_building/critical_value/critical_value.py | 2 +- .../abstract_experiment_factory.py | 4 ++-- tests/factory/test_critical_value_factory.py | 4 ++++ tests/factory/test_time_complexity_factory.py | 4 ++++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pysatl_experiment/experiment_new/step/report_building/critical_value/critical_value.py b/pysatl_experiment/experiment_new/step/report_building/critical_value/critical_value.py index 9128f0a5..91f22965 100644 --- a/pysatl_experiment/experiment_new/step/report_building/critical_value/critical_value.py +++ b/pysatl_experiment/experiment_new/step/report_building/critical_value/critical_value.py @@ -3,7 +3,7 @@ from line_profiler import profile from typing_extensions import override -from pysatl_criterion.cv_calculator.cv_calculator.cv_calculator import CVCalculator +from pysatl_criterion.critical_value.cv_calculator.cv_calculator import CVCalculator from pysatl_criterion.persistence.model.limit_distribution.limit_distribution import ( ILimitDistributionStorage, LimitDistributionQuery, diff --git a/pysatl_experiment/factory/model/abstract_experiment_factory/abstract_experiment_factory.py b/pysatl_experiment/factory/model/abstract_experiment_factory/abstract_experiment_factory.py index 2a8a9b32..1ce89a1b 100644 --- a/pysatl_experiment/factory/model/abstract_experiment_factory/abstract_experiment_factory.py +++ b/pysatl_experiment/factory/model/abstract_experiment_factory/abstract_experiment_factory.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod from typing import Any, Generic, TypeVar, cast -from pysatl_criterion.persistence.limit_distribution.datastorage.datastorage import SQLAlchemyLimitDistributionStorage +from pysatl_criterion.persistence.limit_distribution.datastorage.datastorage import AlchemyLimitDistributionStorage from pysatl_criterion.persistence.model.common.data_storage.data_storage import IDataStorage from pysatl_criterion.persistence.model.limit_distribution.limit_distribution import LimitDistributionQuery from pysatl_criterion.statistics import ( @@ -463,7 +463,7 @@ def _init_result_storage(self) -> RS: experiment_type = self.experiment_data.config.experiment_type storage_connection = self.experiment_data.config.storage_connection if experiment_type == ExperimentType.CRITICAL_VALUE: - limit_distribution_storage = SQLAlchemyLimitDistributionStorage(storage_connection) + limit_distribution_storage = AlchemyLimitDistributionStorage(storage_connection) limit_distribution_storage.init() return cast(RS, limit_distribution_storage) elif experiment_type == ExperimentType.POWER: diff --git a/tests/factory/test_critical_value_factory.py b/tests/factory/test_critical_value_factory.py index 471746d1..c2c0419a 100644 --- a/tests/factory/test_critical_value_factory.py +++ b/tests/factory/test_critical_value_factory.py @@ -54,6 +54,10 @@ def generate(self, n): # pragma: no cover - we don't run steps in these tests class FakeStatistics(AbstractGoodnessOfFitStatistic): + @staticmethod + def short_code(): + return "FAKE_CODE" + def execute_statistic(self, rvs, **kwargs) -> float | float64: return 0 diff --git a/tests/factory/test_time_complexity_factory.py b/tests/factory/test_time_complexity_factory.py index f053c9f7..51b920f4 100644 --- a/tests/factory/test_time_complexity_factory.py +++ b/tests/factory/test_time_complexity_factory.py @@ -56,6 +56,10 @@ def generate(self, n): class FakeStatistics(AbstractGoodnessOfFitStatistic): + @staticmethod + def short_code(): + return "FAKE_CODE" + def execute_statistic(self, rvs, **kwargs) -> float | float64: return 0 From 10a9893527a6f62ddc182cda9917f6856765936c Mon Sep 17 00:00:00 2001 From: Alexey Mironov Date: Mon, 22 Dec 2025 17:30:58 +0300 Subject: [PATCH 11/12] Fix mypy --- pysatl_experiment/core/distribution/lo_con_norm.py | 2 +- pysatl_experiment/core/distribution/mix_con_norm.py | 2 +- pysatl_experiment/core/distribution/scale_con_norm.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pysatl_experiment/core/distribution/lo_con_norm.py b/pysatl_experiment/core/distribution/lo_con_norm.py index 82695179..0fec1b4e 100644 --- a/pysatl_experiment/core/distribution/lo_con_norm.py +++ b/pysatl_experiment/core/distribution/lo_con_norm.py @@ -23,4 +23,4 @@ def generate_lo_con_norm(size, p=0.5, a=0) -> list[float]: item = norm.rvs(size=1) result.append(item) - return np.concatenate(result, axis=0) + return np.concatenate(result, axis=0).tolist() diff --git a/pysatl_experiment/core/distribution/mix_con_norm.py b/pysatl_experiment/core/distribution/mix_con_norm.py index c3bf2cfb..8f3c354e 100644 --- a/pysatl_experiment/core/distribution/mix_con_norm.py +++ b/pysatl_experiment/core/distribution/mix_con_norm.py @@ -24,4 +24,4 @@ def generate_mix_con_norm(size, p=0.5, a=0, b=1) -> list[float]: item = norm.rvs(size=1) result.append(item) - return np.concatenate(result, axis=0) + return np.concatenate(result, axis=0).tolist() diff --git a/pysatl_experiment/core/distribution/scale_con_norm.py b/pysatl_experiment/core/distribution/scale_con_norm.py index f34a4601..201055e6 100644 --- a/pysatl_experiment/core/distribution/scale_con_norm.py +++ b/pysatl_experiment/core/distribution/scale_con_norm.py @@ -23,4 +23,4 @@ def generate_scale_con_norm(size, p=0.5, b=0) -> list[float]: item = norm.rvs(size=1) result.append(item) - return np.concatenate(result, axis=0) + return np.concatenate(result, axis=0).tolist() From 48aa3193a611bfa26ece44df494e4a7b79356435 Mon Sep 17 00:00:00 2001 From: Alexey Mironov Date: Wed, 8 Apr 2026 12:52:16 +0300 Subject: [PATCH 12/12] Fix mypy --- pysatl_criterion | 2 +- pysatl_experiment/resolvers/__init__.py | 0 .../resolvers/generator_resolver.py | 111 ------- .../resolvers/hypothesis_resolver.py | 93 ------ pysatl_experiment/resolvers/iresolver.py | 297 ------------------ 5 files changed, 1 insertion(+), 502 deletions(-) delete mode 100644 pysatl_experiment/resolvers/__init__.py delete mode 100644 pysatl_experiment/resolvers/generator_resolver.py delete mode 100644 pysatl_experiment/resolvers/hypothesis_resolver.py delete mode 100644 pysatl_experiment/resolvers/iresolver.py diff --git a/pysatl_criterion b/pysatl_criterion index e4e94c12..907d3c24 160000 --- a/pysatl_criterion +++ b/pysatl_criterion @@ -1 +1 @@ -Subproject commit e4e94c12473659a677d2b10e1cdc4bc182209541 +Subproject commit 907d3c247e9e449f9db48cfe1b1b323a7bcc8476 diff --git a/pysatl_experiment/resolvers/__init__.py b/pysatl_experiment/resolvers/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pysatl_experiment/resolvers/generator_resolver.py b/pysatl_experiment/resolvers/generator_resolver.py deleted file mode 100644 index 929108ff..00000000 --- a/pysatl_experiment/resolvers/generator_resolver.py +++ /dev/null @@ -1,111 +0,0 @@ -# pragma pylint: disable=attribute-defined-outside-init - -""" -This module load custom RVS generators -""" - -import logging -from typing import Any - -from pysatl_experiment.constants import USERPATH_GENERATORS, Config -from pysatl_experiment.exceptions import OperationalException -from pysatl_experiment.experiment.generator import AbstractRVSGenerator -from pysatl_experiment.resolvers.iresolver import IResolver - - -logger = logging.getLogger(__name__) - - -class GeneratorResolver(IResolver): - """ - This class contains the logic to load custom RVS generator class - """ - - object_type = AbstractRVSGenerator - object_type_str = "AbstractRVSGenerator" - user_subdir = USERPATH_GENERATORS - initial_search_path = None - extra_path = "generator_path" - module_names = ["pysatl_experiment.experiment.generator"] - - @staticmethod - def load_generators(config: Config | None) -> list[AbstractRVSGenerator]: - if not config: - raise OperationalException("No configuration set. Please specify configuration.") - - if not config.get("alternatives_configuration"): - raise OperationalException("No alternatives configuration set.") - - alternatives_configuration = config["alternatives_configuration"] - if not alternatives_configuration.get("alternatives"): - raise OperationalException("No alternatives set.") - - alternatives = alternatives_configuration["alternatives"] - generators = [] - for generator_conf in alternatives: - generator = GeneratorResolver.load_generator(generator_conf["name"], generator_conf["params"]) - generators.append(generator) - - return generators - - @staticmethod - def load_generator( - generator_name: str, path: str | None = None, params: dict[str, Any] | None = None - ) -> AbstractRVSGenerator: - """ - Load the custom class from config parameter - :param params: - :param path: - :param generator_name: - """ - - generator: AbstractRVSGenerator = GeneratorResolver._load_generator( - generator_name, params=params, extra_dir=path - ) - - return generator - - @staticmethod - def validate_generator(generator: AbstractRVSGenerator) -> AbstractRVSGenerator: - # Validation can be added - return generator - - @staticmethod - def _load_generator( - generator_name: str, - params: dict[str, Any] | None, - extra_dir: str | None = None, - ) -> AbstractRVSGenerator: - """ - Search and loads the specified strategy. - :param generator_name: name of the module to import - :param config: configuration for the strategy - :param extra_dir: additional directory to search for the given strategy - :return: Strategy instance or None - """ - extra_dirs = [] - - if extra_dir: - extra_dirs.append(extra_dir) - - abs_paths = GeneratorResolver.build_search_paths( - user_data_dir=None, user_subdir=USERPATH_GENERATORS, extra_dirs=extra_dirs - ) - - generator = GeneratorResolver._load_object( - paths=abs_paths, - object_name=generator_name, - add_source=True, - kwargs=params, - ) - - if not generator: - generator = GeneratorResolver._load_modules_object(object_name=generator_name, kwargs=params) - - if generator: - return GeneratorResolver.validate_generator(generator) - - raise OperationalException( - f"Impossible to load RVS generator '{generator_name}'. This class does not exist " - "or contains Python code errors." - ) diff --git a/pysatl_experiment/resolvers/hypothesis_resolver.py b/pysatl_experiment/resolvers/hypothesis_resolver.py deleted file mode 100644 index 44130c11..00000000 --- a/pysatl_experiment/resolvers/hypothesis_resolver.py +++ /dev/null @@ -1,93 +0,0 @@ -# pragma pylint: disable=attribute-defined-outside-init - -""" -This module load custom RVS generators -""" - -import logging -from typing import Any - -from pysatl_experiment.constants import USERPATH_GENERATORS -from pysatl_experiment.exceptions import OperationalException -from pysatl_experiment.experiment.hypothesis import AbstractHypothesis -from pysatl_experiment.resolvers.iresolver import IResolver - - -logger = logging.getLogger(__name__) - - -class HypothesisResolver(IResolver): - """ - This class contains the logic to load custom RVS generator class - """ - - object_type = AbstractHypothesis - object_type_str = "AbstractHypothesis" - user_subdir = USERPATH_GENERATORS - initial_search_path = None - extra_path = "hypothesis_path" - module_name = "pysatl_experiment.experiment.hypothesis" - - @staticmethod - def load_hypothesis( - hypothesis_name: str, path: str | None = None, params: dict[str, Any] | None = None - ) -> AbstractHypothesis: - """ - Load the custom class from config parameter - :param params: - :param path: - :param hypothesis_name: - """ - - hypothesis: AbstractHypothesis = HypothesisResolver._load_hypothesis( - hypothesis_name, params=params, extra_dir=path - ) - - return hypothesis - - @staticmethod - def validate_hypothesis(generator: AbstractHypothesis) -> AbstractHypothesis: - # Validation can be added - return generator - - @staticmethod - def _load_hypothesis( - hypothesis_name: str, - params: dict[str, Any] | None, - extra_dir: str | None = None, - ) -> AbstractHypothesis: - """ - Search and loads the specified strategy. - :param hypothesis_name: name of the module to import - :param config: configuration for the strategy - :param extra_dir: additional directory to search for the given strategy - :return: Strategy instance or None - """ - extra_dirs = [] - - if extra_dir: - extra_dirs.append(extra_dir) - - abs_paths = HypothesisResolver.build_search_paths( - user_data_dir=None, user_subdir=USERPATH_GENERATORS, extra_dirs=extra_dirs - ) - - hypothesis = HypothesisResolver._load_object( - paths=abs_paths, - object_name=hypothesis_name, - add_source=True, - kwargs=params, - ) - - if not hypothesis: - hypothesis = HypothesisResolver._load_module_object( - object_name=hypothesis_name, kwargs=params, module_name="" - ) - - if hypothesis: - return HypothesisResolver.validate_hypothesis(hypothesis) - - raise OperationalException( - f"Impossible to load RVS hypothesis '{hypothesis_name}'. This class does not exist " - "or contains Python code errors." - ) diff --git a/pysatl_experiment/resolvers/iresolver.py b/pysatl_experiment/resolvers/iresolver.py deleted file mode 100644 index 7a6f9e6c..00000000 --- a/pysatl_experiment/resolvers/iresolver.py +++ /dev/null @@ -1,297 +0,0 @@ -# pragma pylint: disable=attribute-defined-outside-init - -""" -This module load custom objects -""" - -import importlib.util -import inspect -import logging -import sys -from collections.abc import Iterator -from pathlib import Path -from typing import Any - -from pysatl_experiment.constants import Config -from pysatl_experiment.exceptions import OperationalException - - -logger = logging.getLogger(__name__) - - -class PathModifier: - def __init__(self, path: Path): - self.path = path - - def __enter__(self): - """Inject path to allow importing with relative imports.""" - sys.path.insert(0, str(self.path)) - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - """Undo insertion of local path.""" - str_path = str(self.path) - if str_path in sys.path: - sys.path.remove(str_path) - - -class IResolver: - """ - This class contains all the logic to load custom classes - """ - - # Child classes need to override this - object_type: type[Any] - object_type_str: str - user_subdir: str | None = None - initial_search_path: Path | None = None - # Optional a path (generator_path, report_generator_path) - extra_path: str | None = None - module_names: list[str] | None = None - - @classmethod - def build_search_paths( - cls, - user_data_dir: Path | None = None, - user_subdir: str | None = None, - extra_dirs: list[str] | None = None, - ) -> list[Path]: - abs_paths: list[Path] = [] - if cls.initial_search_path: - abs_paths.append(cls.initial_search_path) - - if user_subdir and user_data_dir: - abs_paths.insert(0, user_data_dir.joinpath(user_subdir)) - - # Add extra directory to the top of the search paths - if extra_dirs: - for directory in extra_dirs: - abs_paths.insert(0, Path(directory).resolve()) - - if cls.extra_path: - abs_paths.insert(0, Path(cls.extra_path).resolve()) - - return abs_paths - - @classmethod - def _get_valid_object(cls, module_path: Path, object_name: str | None, enum_failed: bool = False) -> Iterator[Any]: - """ - Generator returning objects with matching object_type and object_name in the path given. - :param module_path: absolute path to the module - :param object_name: Class name of the object - :param enum_failed: If True, will return None for modules which fail. - Otherwise, failing modules are skipped. - :return: generator containing tuple of matching objects - Tuple format: [Object, source] - """ - - # Generate spec based on absolute path - # Pass object_name as first argument to have logging print a reasonable name. - with PathModifier(module_path.parent): - module_name = module_path.stem or "" - spec = importlib.util.spec_from_file_location(module_name, str(module_path)) - if not spec: - return iter([None]) - - module = importlib.util.module_from_spec(spec) - try: - spec.loader.exec_module(module) # type: ignore # importlib does not use typehints - except ( - AttributeError, - ModuleNotFoundError, - SyntaxError, - ImportError, - NameError, - ) as err: - # Catch errors in case a specific module is not installed - logger.warning(f"Could not import {module_path} due to '{err}'") - if enum_failed: - return iter([None]) - - def is_valid_class(obj): - try: - return ( - inspect.isclass(obj) - and issubclass(obj, cls.object_type) - and obj is not cls.object_type - and obj.__module__ == module_name - ) - except TypeError: - return False - - valid_objects_gen = ( - (obj, inspect.getsource(module)) - for name, obj in inspect.getmembers(module, is_valid_class) - if (object_name is None or object_name == name) - ) - # The __module__ check ensures we only use strategies that are defined in this folder. - return valid_objects_gen - - @classmethod - def _search_object( - cls, directory: Path, *, object_name: str, add_source: bool = False - ) -> tuple[Any, Path] | tuple[None, None]: - """ - Search for the objectname in the given directory - :param directory: relative or absolute directory path - :param object_name: ClassName of the object to load - :return: object class - """ - logger.debug(f"Searching for {cls.object_type.__name__} {object_name} in '{directory}'") - for entry in directory.iterdir(): - # Only consider python files - if entry.suffix != ".py": - logger.debug("Ignoring %s", entry) - continue - if entry.is_symlink() and not entry.is_file(): - logger.debug("Ignoring broken symlink %s", entry) - continue - module_path = entry.resolve() - - obj = next(cls._get_valid_object(module_path, object_name), None) - - if obj: - obj[0].__file__ = str(entry) - if add_source: - obj[0].__source__ = obj[1] - return obj[0], module_path - return None, None - - @classmethod - def _load_object( - cls, - paths: list[Path], - *, - object_name: str, - add_source: bool = False, - kwargs: dict[str, Any] | None, - ) -> Any | None: - """ - Try to load object from path list. - """ - - for _path in paths: - try: - (module, module_path) = cls._search_object( - directory=_path, object_name=object_name, add_source=add_source - ) - if module: - logger.info( - f"Using resolved {cls.object_type.__name__.lower()[1:]} {object_name} from '{module_path}'..." - ) - return module(**kwargs) - except FileNotFoundError: - logger.warning('Path "%s" does not exist.', _path.resolve()) - - return None - - @classmethod - def _load_modules_object(cls, *, object_name: str, kwargs: dict[str, Any] | None) -> Any | None: - """ - Try to load object from path list. - """ - if cls.module_names is None: - return None - - for module_name in cls.module_names: - module_object = cls._load_module_object(object_name=object_name, kwargs=kwargs, module_name=module_name) - if module_object is not None: - return module_object - return None - - @classmethod - def _load_module_object(cls, *, object_name: str, kwargs: dict[str, Any] | None, module_name: str) -> Any | None: - """ - Try to load object from path list. - """ - - try: - module = getattr(importlib.import_module(module_name), object_name) - if module: - logger.info( - f"Using resolved {cls.object_type.__name__.lower()[1:]} {object_name} from '{module_name}'..." - ) - return module(**kwargs) - except FileNotFoundError: - logger.warning('Object "%s" does not exist.', object_name) - - return None - - @classmethod - def load_object( - cls, - object_name: str, - config: Config, - *, - kwargs: dict, - extra_dir: str | None = None, - ) -> Any: - """ - Search and loads the specified object as configured in the child class. - :param object_name: name of the module to import - :param config: configuration dictionary - :param extra_dir: additional directory to search for the given pairlist - :raises: OperationalException if the class is invalid or does not exist. - :return: Object instance or None - """ - - extra_dirs: list[str] = [] - if extra_dir: - extra_dirs.append(extra_dir) - - # TODO: fix - abs_paths = cls.build_search_paths(None, user_subdir=cls.user_subdir, extra_dirs=extra_dirs) - - found_object = cls._load_object(paths=abs_paths, object_name=object_name, kwargs=kwargs) - if found_object: - return found_object - raise OperationalException( - f"Impossible to load {cls.object_type_str} '{object_name}'. This class does not exist " - "or contains Python code errors." - ) - - @classmethod - def _build_rel_location(cls, directory: Path, entry: Path) -> str: - builtin = cls.initial_search_path == directory - return f"/{entry.relative_to(directory)}" if builtin else str(entry.relative_to(directory)) - - @classmethod - def _search_all_objects( - cls, - directory: Path, - enum_failed: bool, - recursive: bool = False, - basedir: Path | None = None, - ) -> list[dict[str, Any]]: - """ - Searches a directory for valid objects - :param directory: Path to search - :param enum_failed: If True, will return None for modules which fail. - Otherwise, failing modules are skipped. - :param recursive: Recursively walk directory tree searching for strategies - :return: List of dicts containing 'name', 'class' and 'location' entries - """ - logger.debug(f"Searching for {cls.object_type.__name__} '{directory}'") - objects: list[dict[str, Any]] = [] - if not directory.is_dir(): - logger.info(f"'{directory}' is not a directory, skipping.") - return objects - for entry in directory.iterdir(): - if recursive and entry.is_dir() and not entry.name.startswith("__") and not entry.name.startswith("."): - objects.extend(cls._search_all_objects(entry, enum_failed, recursive, basedir or directory)) - # Only consider python files - if entry.suffix != ".py": - logger.debug("Ignoring %s", entry) - continue - module_path = entry.resolve() - logger.debug(f"Path {module_path}") - for obj in cls._get_valid_object(module_path, object_name=None, enum_failed=enum_failed): - objects.append( - { - "name": obj[0].__name__ if obj is not None else "", - "class": obj[0] if obj is not None else None, - "location": entry, - "location_rel": cls._build_rel_location(basedir or directory, entry), - } - ) - return objects