diff --git a/docs/trials_table_mapping.md b/docs/trials_table_mapping.md index d53a059..81f45db 100644 --- a/docs/trials_table_mapping.md +++ b/docs/trials_table_mapping.md @@ -42,21 +42,6 @@ Columns are grouped by the raw source they map from. | `block_beta`, `block_duration`, `block_min`, `block_max` | `block_length` | | `delay_beta`, `delay_duration`, `delay_min`, `delay_max` | `quiescent_duration_key` (scalar distribution, so no beta/min/max) | -### From `task_logic_input` (under `task_parameters`) - -| Trials column | Source field | -| --- | --- | -| `reward_size_left` | `task_parameters.reward_size.left_value_volume` — the reward volume (uL) at the left port. | -| `reward_size_right` | `task_parameters.reward_size.right_value_volume` — the reward volume (uL) at the right port. | - -> **Note:** `reward_size` is read from the task parameters, not the trial -> generator, so it is populated even when no summarising generator is resolved. -> The acquisition system can in principle vary reward size per trial, but the -> current data format only exposes a single session-level value, so these -> columns are constant across trials. They are **required** (non-nullable): a -> missing `TaskLogic` stream raises rather than silently producing null reward -> sizes when there are trials to build. - ### From `TrialMetrics.json` (`SoftwareEvents` stream) | Trials column | Mapping | @@ -81,6 +66,8 @@ Columns are grouped by the raw source they map from. | `response_duration` | `response_deadline_duration`. | | `reward_consumption_duration` | `Trial -> reward_consumption_duration`. | | `reward_probabilityL` / `reward_probabilityR` | The **block** probability from `Trial -> metadata -> p_reward_left` / `p_reward_right`. The top-level `trial.p_reward_left` / `p_reward_right` is the per-trial probability, not the block probability, so it is not used here. `None` when the trial or its metadata is missing. | +| `reward_size_left` | `Trial -> reward_size.left` — the reward volume (uL) at the left port. Defaults to `2.0` when not set on the trial. `None` when the trial is missing. | +| `reward_size_right` | `Trial -> reward_size.right` — the reward volume (uL) at the right port. Defaults to `2.0` when not set on the trial. `None` when the trial is missing. | | `rewarded_historyL` / `rewarded_historyR` | Filter `is_rewarded == True`, then on `is_right_choice`. | ### From `TrialGeneratorSpec.json` (`SoftwareEvents` stream) @@ -88,7 +75,7 @@ Columns are grouped by the raw source they map from. | Trials column | Mapping | | --- | --- | | `base_reward_probability_sum` | If `type == "CoupledTrialGenerator"`, look at `reward_probability_parameters`. | -| `min_reward_each_block` | Present when `type == "CoupledTrialGenerator"`; otherwise `None`. | +| `min_reward_each_block` | Present when `type == "CoupledWarmupTrialGenerator"` (has `min_block_reward`); otherwise `None`. | ### From `QuiescentPeriod.json` (`SoftwareEvents` stream) @@ -155,3 +142,4 @@ These were mapped during exploration but are no longer in scope: | 2026-06-17 | `auto_waterL` / `auto_waterR` now encode no auto-response (`is_auto_reward_right` is `None`) and missing trials as `0` instead of `NULL`. The columns are non-nullable (`int`, default `0`). | | 2026-06-20 | Added `reward_size_left` / `reward_size_right` (reward volume in uL) from `task_parameters.reward_size`, and `side_bias` from the per-trial `TrialMetrics` event (`bias` field). | | 2026-06-20 | `reward_probabilityL` / `reward_probabilityR` now read the block probability from `trial.metadata.p_reward_left` / `p_reward_right` instead of the top-level per-trial `trial.p_reward_left` / `p_reward_right`. | +| 2026-07-24 | `reward_size_left` / `reward_size_right` moved from session-level `task_parameters.reward_size` to per-trial `Trial.reward_size` (fields `.left` / `.right`). The columns are now nullable — `None` when the trial is missing. A missing `TaskLogic` stream no longer raises; session distribution columns are simply null. `min_reward_each_block` moved from `CoupledTrialGenerator` to `CoupledWarmupTrialGenerator`. | diff --git a/pyproject.toml b/pyproject.toml index b57ebc2..decf827 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ readme = "README.md" version = "0.1.0" dependencies = [ - "aind-behavior-dynamic-foraging[data] @ git+https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git@baab12133b22f599c1ba0583260eca9eca216cc0", + "aind-behavior-dynamic-foraging[data] @ git+https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git@ac5ddbf909c9375b9e8875d6a5f90796cfa98653", "ipykernel", ] diff --git a/src/dynamic_foraging_processing/processing/_trial_table.py b/src/dynamic_foraging_processing/processing/_trial_table.py index b83fb63..622f1ed 100644 --- a/src/dynamic_foraging_processing/processing/_trial_table.py +++ b/src/dynamic_foraging_processing/processing/_trial_table.py @@ -236,7 +236,7 @@ def _animal_response(payload: t.Any) -> int: return 1 if bool(choice) else 0 @staticmethod - def _parse_outcome(payload: t.Any) -> t.Optional[TrialOutcome]: + def _parse_outcome(payload: t.Any) -> TrialOutcome: """Parse a ``TrialOutcome`` software-event payload into its domain model. Parameters @@ -247,11 +247,16 @@ def _parse_outcome(payload: t.Any) -> t.Optional[TrialOutcome]: Returns ------- - TrialOutcome or None - The parsed model, or ``None`` if ``payload`` is empty. + TrialOutcome + The parsed model. + + Raises + ------ + ValueError + If ``payload`` is ``None``. """ if payload is None: - return None + raise ValueError("TrialOutcome payload is required but received None.") if isinstance(payload, TrialOutcome): return payload if isinstance(payload, str): @@ -320,7 +325,7 @@ def _rewarded_history( return is_rewarded and (is_right_choice is is_right) @staticmethod - def _is_baited(trial: t.Optional[Trial], *, is_right: bool) -> bool: + def _is_baited(trial: Trial, *, is_right: bool) -> bool: """Return whether the requested lickport is baited on this trial. A port is "baited" when reward is guaranteed there (its reward @@ -343,16 +348,15 @@ def _is_baited(trial: t.Optional[Trial], *, is_right: bool) -> bool: Parameters ---------- - trial : Trial or None - The per-trial task-logic model, or ``None`` when unavailable. + trial : Trial + The per-trial task-logic model. is_right : bool ``True`` for the right port, ``False`` for the left port. Returns ------- bool - Whether the requested side is baited. A missing ``trial`` is treated - as not baited (``False``). + Whether the requested side is baited. Examples -------- @@ -374,8 +378,6 @@ def _is_baited(trial: t.Optional[Trial], *, is_right: bool) -> bool: >>> TrialTableBuilder._is_baited(trial, is_right=False) False """ - if trial is None: - return False auto = trial.is_auto_reward_right if is_right: # Right stays baited unless the animal was auto-responded right. @@ -384,20 +386,19 @@ def _is_baited(trial: t.Optional[Trial], *, is_right: bool) -> bool: return trial.p_reward_left == 1 and auto in (None, True) @staticmethod - def _auto_water(trial: t.Optional[Trial], *, is_right: bool) -> int: + def _auto_water(trial: Trial, *, is_right: bool) -> int: """Encode autowater for a side from ``is_auto_reward_right``. Returns ``1`` if the auto response was to the requested side, else ``0``. - A missing trial or no auto-response (``is_auto_reward_right`` is - ``None``) counts as no autowater (``0``). ``is_right`` is ``True`` for - right. + No auto-response (``is_auto_reward_right`` is ``None``) counts as no + autowater (``0``). ``is_right`` is ``True`` for right. """ - if trial is None or trial.is_auto_reward_right is None: + if trial.is_auto_reward_right is None: return 0 return int(trial.is_auto_reward_right is is_right) @staticmethod - def _block_reward_probability(trial: t.Optional[Trial], *, is_right: bool) -> t.Optional[float]: + def _block_reward_probability(trial: Trial, *, is_right: bool) -> t.Optional[float]: """Return the block reward probability for a side from the trial metadata. The top-level ``trial.p_reward_left/right`` is the *per-trial* probability; @@ -406,18 +407,17 @@ def _block_reward_probability(trial: t.Optional[Trial], *, is_right: bool) -> t. Parameters ---------- - trial : Trial or None - The per-trial task-logic model, or ``None`` when unavailable. + trial : Trial + The per-trial task-logic model. is_right : bool ``True`` for the right port, ``False`` for the left port. Returns ------- float or None - The block reward probability, or ``None`` when the trial or its - metadata is unavailable. + The block reward probability, or ``None`` when metadata is unavailable. """ - if trial is None or trial.metadata is None: + if trial.metadata is None: return None return trial.metadata.p_reward_right if is_right else trial.metadata.p_reward_left @@ -483,14 +483,6 @@ def _session_columns(self, task_logic: AindDynamicForagingTaskLogic) -> t.Dict[s if task_logic is None: return columns - # Reward volumes live on the task parameters, not the trial generator, so - # populate them before the generator resolution (which may bail out). - # Known limitation: the acquisition system can vary reward size per trial, - # but the current data format only exposes a single session-level value. - reward_size = task_logic.task_parameters.reward_size - columns["reward_size_left"] = reward_size.left_value_volume - columns["reward_size_right"] = reward_size.right_value_volume - generator = self._summary_generator(task_logic.task_parameters.trial_generator) if generator is None: return columns @@ -519,7 +511,7 @@ def _session_columns(self, task_logic: AindDynamicForagingTaskLogic) -> t.Dict[s delay_max=delay_max, base_reward_probability_sum=base_reward_sum, ) - # ``min_block_reward`` is coupled-only; uncoupled generators omit it. + # ``min_block_reward`` is warmup-generator-only; main coupled generators omit it. if hasattr(generator, "min_block_reward"): columns["min_reward_each_block"] = generator.min_block_reward return columns @@ -566,7 +558,7 @@ def _lickspout_columns( def _build_row( self, *, - outcome: t.Optional[TrialOutcome], + outcome: TrialOutcome, start: float, stop: float, response: t.Any, @@ -578,9 +570,9 @@ def _build_row( lickspout: t.Dict[str, t.Optional[float]], ) -> TrialConfig: """Assemble a single ``TrialConfig`` from aligned per-trial inputs.""" - trial = outcome.trial if outcome is not None else None - is_right_choice = outcome.is_right_choice if outcome is not None else None - is_rewarded = bool(outcome.is_rewarded) if outcome is not None else False + trial = outcome.trial + is_right_choice = outcome.is_right_choice + is_rewarded = bool(outcome.is_rewarded) return TrialConfig( start_time=start, @@ -596,13 +588,13 @@ def _build_row( bait_right=self._is_baited(trial, is_right=True), reward_probabilityL=self._block_reward_probability(trial, is_right=False), reward_probabilityR=self._block_reward_probability(trial, is_right=True), + reward_size_left=trial.reward_size.left, + reward_size_right=trial.reward_size.right, side_bias=side_bias, - response_duration=trial.response_deadline_duration if trial is not None else None, - reward_consumption_duration=( - trial.reward_consumption_duration if trial is not None else None - ), - ITI_duration=trial.inter_trial_interval_duration if trial is not None else None, - delay_duration=trial.quiescence_period_duration if trial is not None else None, + response_duration=trial.response_deadline_duration, + reward_consumption_duration=trial.reward_consumption_duration, + ITI_duration=trial.inter_trial_interval_duration, + delay_duration=trial.quiescence_period_duration, auto_waterL=self._auto_water(trial, is_right=False), auto_waterR=self._auto_water(trial, is_right=True), **session, @@ -669,9 +661,7 @@ def build(self) -> pd.DataFrame: Raises ------ ValueError - If the ``TaskLogic`` stream is missing while there are trials to - build (the required reward-size columns are sourced from it), or if - ``raise_on_error`` is ``True`` and a per-trial stream length + If ``raise_on_error`` is ``True`` and a per-trial stream length disagrees with the ``TrialOutcome`` trial count. """ outcomes = self._load("Behavior", "SoftwareEvents", "TrialOutcome") @@ -696,16 +686,6 @@ def build(self) -> pd.DataFrame: # Guard the positional alignment before we pair streams by index. n_trials = len(outcome_payloads) - # Reward size is sourced from the task logic and is a required column, so - # a missing TaskLogic stream cannot yield a valid table when there are - # trials to build. Surface that clearly rather than failing later with a - # cryptic per-row validation error. - if n_trials > 0 and task_logic is None: - raise ValueError( - "TaskLogic stream is required to build the trials table " - f"(reward sizes are sourced from it) but it failed to load for {n_trials} trials." - ) - warnings = self._check_aligned( n_trials, { diff --git a/tests/test_processing/test_trial_table.py b/tests/test_processing/test_trial_table.py index e4b91a6..3f71c26 100644 --- a/tests/test_processing/test_trial_table.py +++ b/tests/test_processing/test_trial_table.py @@ -6,7 +6,6 @@ from aind_behavior_dynamic_foraging.task_logic import ( AindDynamicForagingTaskLogic, AindDynamicForagingTaskParameters, - RewardSize, ) from aind_behavior_dynamic_foraging.task_logic.trial_generators import ( CoupledTrialGeneratorSpec, @@ -89,13 +88,16 @@ def _outcome( auto=None, block_p_left=None, block_p_right=None, + reward_size_left=None, + reward_size_right=None, ): """Build a serialized ``TrialOutcome`` payload (dict, as delivered by the reader). ``p_left`` / ``p_right`` are the per-trial probabilities (top-level ``trial`` fields); ``block_p_left`` / ``block_p_right``, when given, are the block probabilities stored under ``trial.metadata`` (the source of the - ``reward_probability`` columns). + ``reward_probability`` columns). ``reward_size_left`` / ``reward_size_right`` + override the default per-trial reward volumes (uL). """ trial = { "p_reward_left": p_left, @@ -106,6 +108,11 @@ def _outcome( "inter_trial_interval_duration": 4.0, "is_auto_reward_right": auto, } + if reward_size_left is not None or reward_size_right is not None: + trial["reward_size"] = { + "left": reward_size_left if reward_size_left is not None else 2.0, + "right": reward_size_right if reward_size_right is not None else 2.0, + } if block_p_left is not None or block_p_right is not None: trial["metadata"] = {"p_reward_left": block_p_left, "p_reward_right": block_p_right} return { @@ -138,10 +145,7 @@ def _task_logic(quiescent_scalar=True): min_block_reward=2, ) return AindDynamicForagingTaskLogic( - task_parameters=AindDynamicForagingTaskParameters( - trial_generator=spec, - reward_size=RewardSize(left_value_volume=2.0, right_value_volume=4.0), - ) + task_parameters=AindDynamicForagingTaskParameters(trial_generator=spec) ) @@ -172,6 +176,8 @@ def _full_dataset(): is_rewarded=True, block_p_left=0.7, block_p_right=0.1, + reward_size_left=2.0, + reward_size_right=4.0, ), _outcome(0.5, 0.5, is_right_choice=None, is_rewarded=False), ], @@ -275,30 +281,33 @@ def test_build_full_dataset(): assert first["ITI_min"] == 1.0 and first["ITI_max"] == 10.0 assert first["block_beta"] == pytest.approx(20.0) assert pd.isna(first["delay_beta"]) # scalar quiescent distribution - assert first["min_reward_each_block"] == 2 + assert pd.isna(first["min_reward_each_block"]) # removed from generator schema assert first["base_reward_probability_sum"] == pytest.approx(0.8) # Lickspout positions from InitialManipulatorPosition. assert first["lickspout_position_x"] == 1.0 assert first["lickspout_position_z"] == 4.0 - # Session-level reward volumes (uL) from task_parameters.reward_size. + # Per-trial reward volumes (uL) from trial.reward_size; second trial uses the default. assert first["reward_size_left"] == 2.0 assert first["reward_size_right"] == 4.0 assert second["reward_size_left"] == 2.0 + assert second["reward_size_right"] == 2.0 # Per-trial side bias from the TrialMetrics event; null when not recorded. assert first["side_bias"] == pytest.approx(0.3) assert pd.isna(second["side_bias"]) -def test_build_raises_when_task_logic_missing_with_trials(): - """A missing TaskLogic stream is an error when there are trials (reward size is required).""" +def test_build_missing_task_logic_leaves_session_columns_null(): + """A missing TaskLogic stream still builds; session distribution columns are null.""" dataset = _full_dataset() input_schemas = dataset.children["Behavior"].children["InputSchemas"] input_schemas.children["TaskLogic"] = _FailedStream() - with pytest.raises(ValueError, match="TaskLogic stream is required"): - TrialTableBuilder(dataset).build() + table = TrialTableBuilder(dataset).build() + assert len(table) == 2 + assert table["ITI_beta"].isna().all() + assert table["block_beta"].isna().all() def test_build_empty_dataset_returns_empty_frame(): @@ -348,7 +357,7 @@ def test_summary_generator_prefers_coupled_over_uncoupled(): composite = TrialGeneratorCompositeSpec(generators=[UncoupledTrialGeneratorSpec(), coupled]) resolved = TrialTableBuilder._summary_generator(composite) assert resolved.type == "CoupledTrialGenerator" - assert resolved.min_block_reward == coupled.min_block_reward + assert resolved is coupled def test_summary_generator_falls_back_to_uncoupled(): @@ -367,29 +376,16 @@ def test_summary_generator_none_when_no_coupled_or_uncoupled(caplog): assert "No coupled or uncoupled generator" in caplog.text -def test_session_columns_only_reward_size_when_no_summary_generator(): - """With no coupled/uncoupled generator, only the generator-independent reward size remains.""" +def test_session_columns_empty_when_no_summary_generator(): + """With no coupled/uncoupled generator, the session columns dict is empty.""" task_logic = AindDynamicForagingTaskLogic( task_parameters=AindDynamicForagingTaskParameters( trial_generator=TrialGeneratorCompositeSpec( generators=[CoupledWarmupTrialGeneratorSpec()] ), - reward_size=RewardSize(left_value_volume=2.0, right_value_volume=4.0), ) ) - # Reward size lives on the task parameters, not the generator, so it is the - # only column populated when no summarising generator is found. - assert TrialTableBuilder(_Node({}))._session_columns(task_logic) == { - "reward_size_left": 2.0, - "reward_size_right": 4.0, - } - - -def test_session_columns_include_reward_size(): - """Reward volumes are read from ``task_parameters.reward_size``.""" - columns = TrialTableBuilder(_Node({}))._session_columns(_task_logic()) - assert columns["reward_size_left"] == 2.0 - assert columns["reward_size_right"] == 4.0 + assert TrialTableBuilder(_Node({}))._session_columns(task_logic) == {} def test_side_bias_parses_dict_model_json_and_none(): @@ -414,6 +410,17 @@ def test_build_missing_trial_metrics_leaves_side_bias_null(): assert table["side_bias"].isna().all() +def test_session_columns_warmup_generator_populates_min_reward_each_block(): + """A warmup generator exposes ``min_block_reward``, populating ``min_reward_each_block``.""" + task_logic = AindDynamicForagingTaskLogic( + task_parameters=AindDynamicForagingTaskParameters( + trial_generator=CoupledWarmupTrialGeneratorSpec() + ) + ) + columns = TrialTableBuilder(_Node({}))._session_columns(task_logic) + assert columns["min_reward_each_block"] == CoupledWarmupTrialGeneratorSpec().min_block_reward + + def test_session_columns_uncoupled_has_null_reward_sum(): """An uncoupled generator populates distribution columns but no coupled-only fields.""" task_logic = AindDynamicForagingTaskLogic( @@ -516,12 +523,6 @@ def test_animal_response_encoding(): assert TrialTableBuilder._animal_response({"Item1": 1.0}) == 2 -def test_is_baited_none_trial_returns_false(): - """A missing trial yields ``False`` bait for both sides.""" - assert TrialTableBuilder._is_baited(None, is_right=True) is False - assert TrialTableBuilder._is_baited(None, is_right=False) is False - - def test_is_baited_forfeited_by_auto_response_on_same_side(): """A side with guaranteed reward stays baited unless auto-responded to that side.""" trial = TrialOutcome.model_validate( @@ -538,8 +539,7 @@ def test_auto_water_encodes_side_from_auto_response(): ).trial assert TrialTableBuilder._auto_water(trial, is_right=True) == 1 assert TrialTableBuilder._auto_water(trial, is_right=False) == 0 - # A missing trial or no auto-response counts as no autowater (0). - assert TrialTableBuilder._auto_water(None, is_right=True) == 0 + # No auto-response counts as no autowater (0). no_auto = TrialOutcome.model_validate( _outcome(1.0, 1.0, is_right_choice=True, is_rewarded=True, auto=None) ).trial @@ -557,9 +557,8 @@ def test_block_reward_probability_reads_metadata_not_trial(): assert TrialTableBuilder._block_reward_probability(trial, is_right=True) == pytest.approx(0.1) -def test_block_reward_probability_none_without_metadata_or_trial(): - """A missing trial or absent metadata yields ``None``.""" - assert TrialTableBuilder._block_reward_probability(None, is_right=True) is None +def test_block_reward_probability_none_without_metadata(): + """Absent metadata yields ``None``.""" no_meta = TrialOutcome.model_validate( _outcome(1.0, 0.2, is_right_choice=True, is_rewarded=True) ).trial @@ -570,15 +569,19 @@ def test_block_reward_probability_none_without_metadata_or_trial(): @pytest.mark.parametrize( "payload", [ - None, _outcome(1.0, 1.0, is_right_choice=True, is_rewarded=True), TrialOutcome.model_validate(_outcome(1.0, 1.0, is_right_choice=False, is_rewarded=False)), ], ) -def test_parse_outcome_accepts_dict_model_and_none(payload): - """``_parse_outcome`` accepts dicts, model instances, and ``None``.""" - result = TrialTableBuilder._parse_outcome(payload) - assert result is None or isinstance(result, TrialOutcome) +def test_parse_outcome_accepts_dict_and_model(payload): + """``_parse_outcome`` accepts dicts and model instances.""" + assert isinstance(TrialTableBuilder._parse_outcome(payload), TrialOutcome) + + +def test_parse_outcome_raises_on_none(): + """``_parse_outcome`` raises ``ValueError`` for a ``None`` payload.""" + with pytest.raises(ValueError, match="required"): + TrialTableBuilder._parse_outcome(None) def test_parse_outcome_accepts_json_string(): diff --git a/uv.lock b/uv.lock index 798d492..5b408f9 100644 --- a/uv.lock +++ b/uv.lock @@ -39,7 +39,7 @@ wheels = [ [[package]] name = "aind-behavior-dynamic-foraging" version = "0.0.2" -source = { git = "https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git?rev=baab12133b22f599c1ba0583260eca9eca216cc0#baab12133b22f599c1ba0583260eca9eca216cc0" } +source = { git = "https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git?rev=ac5ddbf909c9375b9e8875d6a5f90796cfa98653#ac5ddbf909c9375b9e8875d6a5f90796cfa98653" } dependencies = [ { name = "aind-behavior-services" }, { name = "pydantic-settings" }, @@ -363,7 +363,7 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -776,7 +776,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "aind-behavior-dynamic-foraging", extras = ["data"], git = "https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git?rev=baab12133b22f599c1ba0583260eca9eca216cc0" }, + { name = "aind-behavior-dynamic-foraging", extras = ["data"], git = "https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git?rev=ac5ddbf909c9375b9e8875d6a5f90796cfa98653" }, { name = "aind-data-schema", marker = "extra == 'qc'", specifier = ">=2.4.1" }, { name = "aind-nwb-utils", marker = "extra == 'nwb'" }, { name = "dynamic-foraging-processing", extras = ["qc", "nwb"], marker = "extra == 'full'" }, @@ -1804,7 +1804,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess" }, + { name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [