diff --git a/pyproject.toml b/pyproject.toml index b76c721..73cc7fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "cantok" -version = "0.0.41" +version = "0.0.42" authors = [{ name = "Evgeniy Blinov", email = "zheni-b@yandex.ru" }] description = 'Implementation of the "Cancellation Token" pattern' readme = "README.md" diff --git a/tests/examples/test_examples.py b/tests/examples/test_examples.py index fc64da2..d0bc76e 100644 --- a/tests/examples/test_examples.py +++ b/tests/examples/test_examples.py @@ -6,6 +6,13 @@ counter = 0 def test_cancel_simple_token_with_function_and_thread(): + """ + A quick-start Condition, Counter, and Timeout composition can stop a worker thread. + + The worker receives the composed token, performs some work while `.cancelled` + is false, and exits once the random condition, indirect counter poll, or + timeout cancels the shared token. + """ def function(token): global counter # noqa: PLW0603 while not token.cancelled: @@ -20,6 +27,12 @@ def function(token): def test_cancel_simple_token_with_function_and_thread_2(): + """ + A quick-start Condition, Counter, and Timeout composition can be a truthy loop guard. + + The loop makes progress while the composed token is active, then stops once the + random condition, indirect counter poll, or timeout cancels it. + """ token = ConditionToken(lambda: randint(1, 100_000) == 1984) + CounterToken(400_000, direct=False) + TimeoutToken(1) counter = 0 diff --git a/tests/units/test_errors.py b/tests/units/test_errors.py index 3968d00..bc91fc2 100644 --- a/tests/units/test_errors.py +++ b/tests/units/test_errors.py @@ -13,6 +13,7 @@ def test_exception_inheritance_hierarchy(): + """Specialized cancellation errors share the common cancellation base class.""" assert issubclass(ConditionCancellationError, CancellationError) assert issubclass(TimeoutCancellationError, CancellationError) assert issubclass(CounterCancellationError, CancellationError) @@ -20,6 +21,13 @@ def test_exception_inheritance_hierarchy(): def test_exception_inheritance_hierarchy_from_view_of_tokens_classes(): + """ + Concrete token classes expose their configured cancellation exception types. + + Specialized token exceptions remain compatible with the base error exposed by + SimpleToken, and DefaultToken exposes the error used by its impossible-cancel + paths. + """ assert issubclass(ConditionToken.exception, SimpleToken.exception) assert issubclass(TimeoutToken.exception, SimpleToken.exception) assert issubclass(CounterToken.exception, SimpleToken.exception) diff --git a/tests/units/tokens/abstract/test_abstract_token.py b/tests/units/tokens/abstract/test_abstract_token.py index 56f4716..70138c2 100644 --- a/tests/units/tokens/abstract/test_abstract_token.py +++ b/tests/units/tokens/abstract/test_abstract_token.py @@ -33,6 +33,7 @@ def test_cant_instantiate_abstract_token(): + """Keep the abstract base token non-instantiable.""" with pytest.raises(TypeError): AbstractToken() @@ -62,6 +63,12 @@ def test_doc_attribute_stores_optional_description_for_all_tokens(token_fabric): ALL_TOKENS_FABRICS, ) def test_cancelled_true_as_parameter(token_fabric, cancelled_flag): + """ + Honor the initial cancelled flag for every regular concrete token. + + Each regular token type should report the requested initial status through + cancelled, is_cancelled(), keep_on(), and check(), for both true and false flags. + """ token = token_fabric(cancelled=cancelled_flag) assert token.cancelled == cancelled_flag @@ -89,6 +96,7 @@ def test_cancelled_true_as_parameter(token_fabric, cancelled_flag): ALL_TOKENS_FABRICS, ) def test_change_attribute_cancelled(token_fabric, first_cancelled_flag, second_cancelled_flag, expected_value): + """Changing cancelled is a one-way manual cancellation control.""" token = token_fabric(cancelled=first_cancelled_flag) if expected_value is None: @@ -113,6 +121,7 @@ def test_change_attribute_cancelled(token_fabric, first_cancelled_flag, second_c ALL_TOKENS_FABRICS, ) def test_set_cancelled_false_if_this_token_is_not_cancelled_but_nested_token_is(token_fabric): + """Setting a parent token active cannot hide cancellation inherited from a nested token.""" token = token_fabric(SimpleToken(cancelled=True)) with pytest.raises(ValueError, match=match('You cannot restore a cancelled token.')): @@ -146,6 +155,7 @@ def test_repr(token_fabric): ALL_TOKENS_FABRICS, ) def test_repr_with_another_token(token_fabric): + """Nested tokens appear in repr as constructor-style positional arguments.""" nested_token = token_fabric() token = token_fabric(nested_token) @@ -188,6 +198,7 @@ def test_default_token_with_doc_remains_neutral_in_composition(first_token_fabri ALL_TOKENS_FABRICS, ) def test_str(token_fabric): + """Report each regular token class and its current cancellation state.""" token = token_fabric() assert str(token) == '<' + type(token).__name__ + ' (not cancelled)>' @@ -711,6 +722,11 @@ def test_non_cancellation_errors_do_not_include_doc(trigger_non_cancellation_err ALL_TOKENS_FABRICS, ) def test_add_bound_tokens(first_token_fabric, second_token_fabric): + """ + Adding two bound regular tokens creates a new wrapper around both originals. + + The wrapper preserves the left-to-right operand order. + """ first_token = first_token_fabric() second_token = second_token_fabric() @@ -733,6 +749,12 @@ def test_add_bound_tokens(first_token_fabric, second_token_fabric): ALL_TOKENS_FABRICS, ) def test_add_inline_tokens(first_token_fabric, second_token_fabric): + """ + Retain inline non-default operands when composing tokens. + + The resulting sum should preserve both operand token types in order, even + though the operands were created only for the addition expression. + """ tokens_sum = first_token_fabric() + second_token_fabric() assert isinstance(tokens_sum, SimpleToken) @@ -746,6 +768,12 @@ def test_add_inline_tokens(first_token_fabric, second_token_fabric): ALL_TOKENS_FABRICS, ) def test_add_default_token_and_inline_token(token_fabric): + """ + Treat an inline left DefaultToken as neutral in token composition. + + Only the inline non-default token remains nested, so the surviving operand is + verified by type rather than identity. + """ tokens_sum = DefaultToken() + token_fabric() assert isinstance(tokens_sum, SimpleToken) @@ -758,6 +786,11 @@ def test_add_default_token_and_inline_token(token_fabric): ALL_TOKENS_FABRICS, ) def test_add_inline_token_and_default_token(token_fabric): + """ + Treat a right-hand default token as neutral when adding an inline token. + + The sum is a simple token that embeds only the inline non-default token type. + """ tokens_sum = token_fabric() + DefaultToken() assert isinstance(tokens_sum, SimpleToken) @@ -778,6 +811,12 @@ def test_add_inline_token_and_default_token(token_fabric): ALL_TOKENS_FABRICS, ) def test_add_three_tokens_keeps_intermediate_sum(first_token_fabric, second_token_fabric, third_token_fabric): + """ + Chained addition keeps the left intermediate sum as an operand. + + The three-token chain remains left-associative instead of flattening all + original tokens into one direct child list. + """ first_token = first_token_fabric() second_token = second_token_fabric() third_token = third_token_fabric() @@ -797,6 +836,7 @@ def test_add_three_tokens_keeps_intermediate_sum(first_token_fabric, second_toke ALL_TOKENS_FABRICS, ) def test_add_empty_simple_token_intermediate_as_regular_operand(token_fabric): + """Ensure an empty SimpleToken intermediate remains a live operand in later sums.""" empty_sum = DefaultToken() + DefaultToken() another_token = token_fabric() @@ -818,6 +858,12 @@ def test_add_empty_simple_token_intermediate_as_regular_operand(token_fabric): ALL_TOKENS_FABRICS, ) def test_add_cancelled_first_operand_keeps_original_operand(token_fabric): + """ + Preserve a cancelled left operand when composing tokens. + + The new sum keeps both original operands in order and is cancelled because the + stored left operand was already cancelled. + """ cancelled_token = token_fabric(cancelled=True) another_token = SimpleToken() @@ -835,6 +881,12 @@ def test_add_cancelled_first_operand_keeps_original_operand(token_fabric): ALL_TOKENS_FABRICS, ) def test_add_cancelled_second_operand_keeps_original_operand(token_fabric): + """ + Ensure summation keeps a pre-cancelled right operand by identity. + + The resulting token should be cancelled through that nested operand without + replacing, flattening, or dropping it. + """ another_token = SimpleToken() cancelled_token = token_fabric(cancelled=True) @@ -852,6 +904,10 @@ def test_add_cancelled_second_operand_keeps_original_operand(token_fabric): ALL_TOKENS_FABRICS, ) def test_add_preserves_link_to_attribute_operand_on_right(stored_token_fabric): + """ + Composing a neutral left operand with an instance-attribute token keeps that token linked. + + Cancelling the attribute token after the sum is created must still cancel the sum.""" class Holder: def __init__(self, token): self.token = token @@ -875,6 +931,11 @@ def combine(self): ALL_TOKENS_FABRICS, ) def test_add_preserves_link_to_attribute_operand_on_left(stored_token_fabric): + """ + Preserve a live left-hand attribute operand when composing with a default token. + + Cancelling the holder's token after composition must be reflected by the composed token. + """ class Holder: def __init__(self, token): self.token = token @@ -902,6 +963,11 @@ def combine(self): ALL_TOKENS_FABRICS, ) def test_add_preserves_link_to_attribute_operand_with_extra_token(extra_token_fabric, stored_token_fabric): + """ + Attribute operands stay live when composed with an additional non-default token. + + Cancelling `holder.token` after composition must still cancel the already-created sum. + """ class Holder: def __init__(self, token): self.token = token @@ -925,6 +991,12 @@ def combine(self, extra_token): ALL_TOKENS_FABRICS, ) def test_add_inside_generator_preserves_link_to_attribute_operand(stored_token_fabric): + """ + Ensure generator-created sums keep live attribute-token cancellation links. + + Cancelling the original attribute token after the first yield must still cancel + the already-created combined token when the generator resumes. + """ class Crawler: def __init__(self, token): self.token = token @@ -951,6 +1023,12 @@ def go(self, token=DefaultToken()): # noqa: B008 ALL_TOKENS_FABRICS, ) def test_add_preserves_link_to_property_operand(stored_token_fabric): + """ + Property-returned operands remain live links in token sums. + + Ensure a stored token exposed and later cancelled through a property is still + observed after composition with neutral DefaultToken() on the left. + """ class Holder: def __init__(self, token): self._token = token @@ -977,6 +1055,13 @@ def combine(self): ALL_TOKENS_FABRICS, ) def test_add_preserves_link_to_list_item_operand(stored_token_fabric): + """ + Keep a list-indexed token operand linked into the composed token. + + Cancelling the stored list item after addition with neutral DefaultToken() must + still cancel the sum, guarding against treating the indexed operand as a + disposable temporary. + """ class Holder: def __init__(self, token): self.tokens = [token] @@ -999,6 +1084,12 @@ def combine(self): ALL_TOKENS_FABRICS, ) def test_add_preserves_link_to_dict_item_operand(stored_token_fabric): + """ + Dict-item operands must remain linked after token summation. + + After summation with neutral DefaultToken() on the left, the combined token + should observe cancellation of the token still stored under the dict key. + """ class Holder: def __init__(self, token): self.tokens = {'main': token} @@ -1021,6 +1112,12 @@ def combine(self): ALL_TOKENS_FABRICS, ) def test_add_preserves_link_to_nested_attribute_operand(stored_token_fabric): + """ + Nested-attribute operands remain live links in token sums. + + After summation with neutral DefaultToken(), the composed token must observe + later cancellation through the original child token. + """ class Child: def __init__(self, token): self.token = token @@ -1048,6 +1145,12 @@ def combine(self): ALL_TOKENS_FABRICS, ) def test_add_preserves_link_to_class_attribute_operand(stored_token_fabric): + """ + Ensure addition keeps the live class-attribute token read through an instance. + + After summation as `DefaultToken() + holder.token`, cancelling the token through + the class later must cancel the composed token. + """ class Holder: token: AbstractToken @@ -1070,6 +1173,13 @@ def combine(self): ALL_TOKENS_FABRICS_WITH_NOT_CANCELLING_SUPERPOWER, ) def test_add_another_token_and_bound_simple_token(first_token_fabric): + """ + Adding an inactive superpower token to an already-created SimpleToken keeps both operands. + + The resulting SimpleToken treats the right-hand SimpleToken as a real operand, + not as an inline temporary, and preserves the superpower token then the + SimpleToken by identity in left-to-right order. + """ simple_token = SimpleToken() first_token = first_token_fabric() @@ -1086,6 +1196,12 @@ def test_add_another_token_and_bound_simple_token(first_token_fabric): [x for x in ALL_TOKENS_FABRICS if x is not SimpleToken], ) def test_add_bound_simple_token_and_another_token(second_token_fabric): + """ + Preserve a bound left SimpleToken as the first nested operand. + + Combining it with another concrete token should create a SimpleToken sum that + keeps both original operand objects by identity and in left-to-right order. + """ simple_token = SimpleToken() second_token = second_token_fabric() @@ -1102,6 +1218,12 @@ def test_add_bound_simple_token_and_another_token(second_token_fabric): ALL_TOKENS_FABRICS, ) def test_add_tokens_and_first_is_default_token(second_token_fabric): + """ + Treat a left-hand DefaultToken as neutral when adding tokens. + + The result remains a SimpleToken, but only the right-hand operand is nested + and it is preserved by identity. + """ first_token = DefaultToken() second_token = second_token_fabric() @@ -1117,6 +1239,11 @@ def test_add_tokens_and_first_is_default_token(second_token_fabric): ALL_TOKENS_FABRICS, ) def test_add_tokens_and_second_one_is_default_token(first_token_fabric): + """ + Treat a right-hand DefaultToken as neutral in bound-token composition. + + The sum is still a fresh SimpleToken, with only the original left token nested. + """ first_token = first_token_fabric() second_token = DefaultToken() @@ -1141,6 +1268,12 @@ def test_add_tokens_and_second_one_is_default_token(first_token_fabric): ], ) def test_add_token_and_not_token(token_fabric, another_object): + """ + Reject non-token operands on either side of token summation. + + The token-left form owns the stable library error message, while reverse + addition only guarantees a TypeError because Python tries the non-token first. + """ with pytest.raises(TypeError, match=r'Cancellation Token can only be combined with another Cancellation Token\.'): token_fabric() + another_object @@ -1153,6 +1286,12 @@ def test_add_token_and_not_token(token_fabric, another_object): ALL_TOKENS_FABRICS, ) def test_check_cancelled_token(token_fabric): + """ + Manual cancellation makes every cancellable token raise the base error. + + Repeated `check()` calls must keep reporting the directly cancelled token with + the generic `CancellationError`, not a token-specific superpower exception. + """ token = token_fabric() token.cancel() @@ -1170,6 +1309,12 @@ def test_check_cancelled_token(token_fabric): [*ALL_TOKENS_FABRICS, DefaultToken], ) def test_check_superpower_not_raised(token_fabric): + """ + Fresh public tokens pass `check()` while they are still active. + + This covers regular tokens and `DefaultToken`, including token classes with no + superpower to trigger. + """ token = token_fabric() assert token.check() is None @@ -1184,6 +1329,12 @@ def test_check_superpower_not_raised(token_fabric): ALL_TOKENS_FABRICS, ) def test_check_superpower_not_raised_nested(token_fabric_1, token_fabric_2): + """ + Ensure parent check stays quiet when nested tokens are still active. + + Guards nested traversal across regular token combinations whose own + superpowers have not triggered. + """ token = token_fabric_1(token_fabric_2()) assert token.check() is None @@ -1198,6 +1349,12 @@ def test_check_superpower_not_raised_nested(token_fabric_1, token_fabric_2): ALL_TOKENS_FABRICS, ) def test_check_cancelled_token_nested(token_fabric_1, token_fabric_2): + """ + Parent check reports manual cancellation from the nested token. + + The raised generic cancellation error keeps the nested token as its source, + including after the parent has cached the nested cancellation report. + """ nested_token = token_fabric_1() token = token_fabric_2(nested_token) nested_token.cancel() @@ -1216,6 +1373,7 @@ def test_check_cancelled_token_nested(token_fabric_1, token_fabric_2): [*ALL_TOKENS_FABRICS, DefaultToken], ) def test_get_report_not_cancelled(token_fabric): + """Fresh top-level tokens return their own not-cancelled report.""" token = token_fabric() report = token._get_report() @@ -1229,6 +1387,11 @@ def test_get_report_not_cancelled(token_fabric): ALL_TOKENS_FABRICS, ) def test_get_report_not_cancelled_nested(token_fabric): + """ + A non-cancelled parent owns the report when its same-type child is also active. + + The returned report should be attributed to the parent, not to the nested token. + """ token = token_fabric(token_fabric()) report = token._get_report() @@ -1246,6 +1409,11 @@ def test_get_report_not_cancelled_nested(token_fabric): ALL_TOKENS_FABRICS, ) def test_get_report_cancelled(token_fabric_1, token_fabric_2): + """ + Nested manual cancellation owns the parent report. + + A live parent returns the cancelled child report with CANCELLED cause and child attribution. + """ nested_token = token_fabric_1() token = token_fabric_2(nested_token) nested_token.cancel() @@ -1261,6 +1429,13 @@ def test_get_report_cancelled(token_fabric_1, token_fabric_2): [*ALL_TOKENS_FABRICS, DefaultToken], ) def test_type_conversion_not_cancelled(token_fabric): + """ + Fresh tokens are truthy in Python boolean contexts. + + The shared boolean conversion follows the non-cancelled state, so every + fresh token, including the non-cancellable default token, supports direct + truth-value checks. + """ token = token_fabric() assert token @@ -1272,6 +1447,13 @@ def test_type_conversion_not_cancelled(token_fabric): ALL_TOKENS_FABRICS, ) def test_type_conversion_cancelled(token_fabric): + """ + Ensure pre-cancelled cancellable tokens are falsey. + + This preserves the public `while token:` and `bool(token)` contract while + checking both implicit truth-value use and explicit `bool(token)`. DefaultToken + is intentionally excluded because it cannot be constructed cancelled. + """ token = token_fabric(cancelled=True) assert not token @@ -1296,6 +1478,12 @@ def test_type_conversion_cancelled(token_fabric): ALL_TOKENS_FABRICS, ) def test_repr_if_nested_token_is_cancelled(token_fabric_1, token_fabric_2, cancelled_flag_nested_token, cancelled_flag_token): + """ + Keep parent and nested `cancelled` repr markers independent. + + The assertion strips the nested repr before checking the parent text, so each + parameterized flag is verified against the repr it belongs to. + """ nested_token = token_fabric_1(cancelled=cancelled_flag_nested_token) token = token_fabric_2(nested_token, cancelled=cancelled_flag_token) @@ -1320,6 +1508,12 @@ def test_repr_if_nested_token_is_cancelled(token_fabric_1, token_fabric_2, cance [*ALL_TOKENS_FABRICS, DefaultToken], ) def test_wait_wrong_parameters(token_fabric, parameters): + """ + Reject invalid wait timing parameters for every public token type. + + Negative step, negative timeout, and step greater than timeout must raise + ValueError immediately, without pinning exact diagnostics or later wait behavior. + """ token = token_fabric() with pytest.raises(ValueError, match=r'.'): @@ -1331,6 +1525,13 @@ def test_wait_wrong_parameters(token_fabric, parameters): ALL_TOKENS_FABRICS, ) def test_sync_wait_with_cancel(token_fabric): + """ + Unbounded synchronous wait returns normally after another thread cancels the token. + + The helper thread sleeps before calling cancel, so the elapsed-time check proves + `wait()` was called without a timeout and blocked until it observed that + cancellation instead of returning immediately. + """ timeout = 0.001 token = token_fabric() @@ -1421,6 +1622,7 @@ def test_wait_without_timeout_returns_none(token_fabric): ALL_TOKENS_FABRICS, ) def test_insert_default_token_to_another_tokens(token_fabric): + """Regular token constructors discard nested default tokens without changing type.""" token = token_fabric(DefaultToken()) assert not isinstance(token, DefaultToken) @@ -1447,6 +1649,12 @@ def test_insert_default_token_to_another_tokens(token_fabric): ], ) def test_report_cache_is_working_in_simple_case(first_token_fabric, second_token_fabric, action): + """ + Cache a nested manual-cancellation report after any status check. + + The cached report should describe the cancelled child and be reused by both + direct and indirect parent report reads. + """ token = first_token_fabric(second_token_fabric(cancelled=True)) assert token._cached_report is None @@ -1484,6 +1692,13 @@ def test_report_cache_is_working_in_simple_case(first_token_fabric, second_token ], ) def test_cache_is_using_after_self_flag(first_token_fabric, second_token_fabric, action): + """ + Ensure parent cancellation takes precedence over a warmed nested report cache. + + After the nested cache is populated, cancelling the parent should make direct + and indirect report checks return fresh manual-cancellation reports instead of + the cached nested report. + """ token = first_token_fabric(second_token_fabric(cancelled=True)) action(token) @@ -1520,6 +1735,13 @@ def test_cache_is_using_after_self_flag(first_token_fabric, second_token_fabric, ], ) def test_superpower_is_more_important_than_cache(first_token_fabric, second_token_fabric, action): + """ + Ensure parent superpower and manual cancellation outrank nested cancellation. + + The active parent superpower must be reported on both direct and indirect paths + despite the cancelled child. Later manual parent cancellation must then replace + that superpower report. + """ token = first_token_fabric(second_token_fabric(cancelled=True)) for report in token._get_report(True), token._get_report(False): @@ -1550,6 +1772,12 @@ def test_superpower_is_more_important_than_cache(first_token_fabric, second_toke ALL_TOKENS_FABRICS, ) def test_just_neste_simple_token_to_another_token(token_fabric): + """ + A fresh SimpleToken passed to any regular token constructor remains nested. + + The constructor should store exactly one nested token, that entry should be a + SimpleToken, and the parent should remain active. + """ token = token_fabric(SimpleToken()) assert len(token._tokens) == 1 diff --git a/tests/units/tokens/abstract/test_report.py b/tests/units/tokens/abstract/test_report.py index 11b668b..b44b3f5 100644 --- a/tests/units/tokens/abstract/test_report.py +++ b/tests/units/tokens/abstract/test_report.py @@ -8,6 +8,7 @@ def test_cant_change_cancellation_report(): + """Cancellation reports must reject post-construction field changes.""" report = CancellationReport( cause=CancelCause.NOT_CANCELLED, from_token=SimpleToken(), @@ -19,6 +20,12 @@ def test_cant_change_cancellation_report(): @pytest.mark.skipif(version_info < (3, 8), reason='There is no support of __slots__ for dataclasses in old pythons.') def test_size_of_report_is_not_so_big(): + """ + Protect the compact memory footprint of cancellation reports. + + Report objects are produced during cancellation checks, so a neutral report + must stay within the expected size threshold. + """ report = CancellationReport( cause=CancelCause.NOT_CANCELLED, from_token=SimpleToken(), diff --git a/tests/units/tokens/test_condition_token.py b/tests/units/tokens/test_condition_token.py index 0af1287..9d0231c 100644 --- a/tests/units/tokens/test_condition_token.py +++ b/tests/units/tokens/test_condition_token.py @@ -7,6 +7,7 @@ def test_condition_counter(): + """Verify repeated condition polling stops after exactly five false results.""" loop_size = 5 def condition(): for _number in range(loop_size): @@ -24,12 +25,18 @@ def condition(): def test_condition_false(): + """A false condition leaves a new condition token active across all status APIs.""" assert ConditionToken(lambda: False).cancelled == False assert ConditionToken(lambda: False).is_cancelled() == False assert ConditionToken(lambda: False).keep_on() == True def test_condition_true(): + """ + Treat a true condition result as immediate cancellation. + + The cancelled and is_cancelled status APIs report True, while keep_on returns False. + """ assert ConditionToken(lambda: True).cancelled == True assert ConditionToken(lambda: True).is_cancelled() == True assert ConditionToken(lambda: True).keep_on() == False @@ -51,17 +58,30 @@ def test_condition_true(): ([ConditionToken(lambda: False), ConditionToken(lambda: False), ConditionToken(lambda: False)], False), ]) def test_just_created_condition_token_with_arguments(arguments, expected_cancelled_status): + """ + Propagate embedded token cancellation even when the condition is false. + + Verify that constructor-provided tokens determine the wrapper status through + cancelled, is_cancelled(), and keep_on(). + """ assert ConditionToken(lambda: False, *arguments).cancelled == expected_cancelled_status assert ConditionToken(lambda: False, *arguments).is_cancelled() == expected_cancelled_status assert ConditionToken(lambda: False, *arguments).keep_on() == (not expected_cancelled_status) def test_raise_without_first_argument(): + """Omitting the required condition callable is rejected by the condition-token constructor.""" with pytest.raises(TypeError): ConditionToken() def test_suppress_exception_false(): + """ + Disabled exception suppression propagates condition callable failures. + + Reading the cancellation state should raise the original ValueError from the + condition instead of converting it into a cancellation result. + """ def condition(): raise ValueError('error') @@ -72,6 +92,7 @@ def condition(): def test_suppress_exception_true(): + """Keep an explicitly suppressing condition token active when its condition raises.""" def condition(): raise ValueError @@ -81,6 +102,7 @@ def condition(): def test_suppress_exception_default_true(): + """Omitting `suppress_exceptions` suppresses condition errors and leaves the token active by default.""" def condition(): raise ValueError @@ -90,6 +112,12 @@ def condition(): def test_condition_function_returning_not_bool_value(): + """ + Non-bool condition results obey suppression instead of truthiness. + + Suppressed mode falls back to default=False, while strict mode raises TypeError + when cancellation state is read. + """ assert ConditionToken(lambda: 'kek', suppress_exceptions=True).cancelled == False assert ConditionToken(lambda: 'kek').cancelled == False @@ -102,6 +130,7 @@ def test_condition_function_returning_not_bool_value(): [True, False], ) def test_default_if_exception(default): + """Suppressed condition exceptions use the configured default as the cancellation state.""" def condition(): raise ValueError @@ -115,6 +144,7 @@ def condition(): [True, False], ) def test_default_if_not_bool(default): + """Use the configured default for suppressed non-bool condition results.""" def condition(): return 'kek' @@ -124,6 +154,7 @@ def condition(): def test_check_superpower_raised(): + """`check()` raises `ConditionCancellationError` for a directly satisfied condition.""" token = ConditionToken(lambda: True) with pytest.raises(ConditionCancellationError): @@ -136,6 +167,12 @@ def test_check_superpower_raised(): def test_check_superpower_raised_nested(): + """ + Nested condition superpower exceptions come from the nested token. + + A neutral SimpleToken wrapper must surface the nested ConditionToken's exception + type and token owner when the condition is already satisfied. + """ nested_token = ConditionToken(lambda: True) token = SimpleToken(nested_token) @@ -150,6 +187,7 @@ def test_check_superpower_raised_nested(): def test_get_report_cancelled(): + """Report a satisfied condition as this token's own CancellationReport superpower.""" token = ConditionToken(lambda: True) report = token._get_report() @@ -168,6 +206,13 @@ def test_get_report_cancelled(): ], ) def test_get_report_cancelled_nested(cancelled, cancelled_nested, from_token_is_nested): + """ + Report which condition token cancels a nested chain as a CancellationReport. + + Parent conditions take precedence, and nested SUPERPOWER reports propagate only + when the parent condition does not cancel. The checked cases are parent-only, + nested-only, and both conditions true. + """ nested_token = ConditionToken(lambda: cancelled_nested) token = ConditionToken(lambda: cancelled, nested_token) @@ -190,6 +235,11 @@ def test_get_report_cancelled_nested(cancelled, cancelled_nested, from_token_is_ ], ) def test_order_of_callbacks(options): + """ + Verify check calls before, condition, and after in order. + + This holds across suppression modes when the condition does not cancel. + """ lst = [] token = ConditionToken(lambda: lst.append(2) is not None, before=lambda: lst.append(1), after=lambda: lst.append(3), **options) @@ -206,6 +256,13 @@ def test_order_of_callbacks(options): ], ) def test_raise_suppressed_exception_in_before_callback(options): + """ + Ensure a suppressed before-callback exception does not abort the check. + + Through `check()`, both default suppression and explicit `suppress_exceptions=True` + must still run a non-cancelling condition and the after callback in order, and + `check()` should return normally. + """ lst = [] def before_callback(): @@ -227,6 +284,12 @@ def before_callback(): ], ) def test_raise_suppressed_exception_in_after_callback(options): + """ + Confirm suppressed after-callback exceptions do not make check fail. + + Through `check()`, both default suppression and explicit `suppress_exceptions=True` + must still observe the before, condition, and after side effects in order. + """ lst = [] def after_callback(): @@ -241,6 +304,12 @@ def after_callback(): def test_raise_not_suppressed_exception_in_before_callback(): + """ + Propagate unsuppressed before-callback exceptions from check(). + + Ensure the failed before callback short-circuits polling before the condition + callback can run. + """ lst = [] token = ConditionToken(lambda: lst.append(2) is not None, before=lambda: 1 / 0, suppress_exceptions=False) @@ -252,6 +321,12 @@ def test_raise_not_suppressed_exception_in_before_callback(): def test_raise_not_suppressed_exception_in_after_callback(): + """ + Re-raise an unsuppressed after-callback exception from check(). + + The before callback and condition callable have already run, preserving their + side effects before the original exception escapes. + """ lst = [] token = ConditionToken(lambda: lst.append(2) is not None, before=lambda: lst.append(1), after=lambda: 1 / 0, suppress_exceptions=False) @@ -271,6 +346,12 @@ def test_raise_not_suppressed_exception_in_after_callback(): ], ) def test_cached_condition_cancelling(options): + """ + Verify only cached condition cancellation stays sticky after wait observes it. + + Default and explicit caching keep the condition counter at 3 across later + public checks, while caching=False re-polls past the one true result. + """ counter = 0 def condition(): @@ -302,6 +383,12 @@ def condition(): def test_condition_token_plus_simple_token(): + """ + Verify that ConditionToken + SimpleToken preserves structural composition. + + The sum is a fresh SimpleToken wrapper that stores the original operands by + identity in left-to-right order, with the condition token first. + """ simple_token = SimpleToken() condition_token = ConditionToken(lambda: False) token = condition_token + simple_token @@ -315,6 +402,7 @@ def test_condition_token_plus_simple_token(): def test_simple_token_plus_condition_token(): + """Create a fresh composition wrapper that preserves simple and condition operands in order.""" simple_token = SimpleToken() condition_token = ConditionToken(lambda: False) token = simple_token + condition_token @@ -328,6 +416,13 @@ def test_simple_token_plus_condition_token(): def test_condition_function_is_more_important_than_cache(): + """ + A newly true condition outranks a cached nested cancellation report. + + After a cancelled child has provided the initial direct and indirect reports, + flipping the parent condition to true should make both report paths attribute + SUPERPOWER cancellation to the condition token itself. + """ flag = False inner_token = SimpleToken(cancelled=True) token = ConditionToken(lambda: flag, inner_token) @@ -348,11 +443,18 @@ def test_condition_function_is_more_important_than_cache(): def test_zero_condition_token_report_is_about_superpower(): + """ + Classify an immediately true condition as superpower cancellation. + + Both direct and indirect raw report polls should expose the condition + superpower before nested-token or cache behavior can matter. + """ for report in ConditionToken(lambda: True)._get_report(True), ConditionToken(lambda: True)._get_report(False): assert report.cause == CancelCause.SUPERPOWER def test_creating_condition_token_with_no_suppress_exceptions_is_not_calling_condition(): + """Creating a non-suppressing condition token does not call its condition.""" calls = [] ConditionToken(lambda: calls.append(True) is None, suppress_exceptions=False) @@ -401,6 +503,12 @@ def function(): return False def test_repr_for_class_based_function(): + """ + Represent class-based callable conditions with their own repr. + + This guards the fallback for callables without __name__ and proves __repr__ + is used instead of __str__. + """ class SomeChecker: def __call__(self) -> bool: return True diff --git a/tests/units/tokens/test_counter_token.py b/tests/units/tokens/test_counter_token.py index af95441..5922a11 100644 --- a/tests/units/tokens/test_counter_token.py +++ b/tests/units/tokens/test_counter_token.py @@ -7,6 +7,12 @@ def test_counter_token_is_deprecated(): + """ + Ensure CounterToken construction emits the public deprecation warning. + + The test checks the warning category and stable message prefix only; counter + behavior is covered by dedicated tests. + """ with pytest.warns(DeprecationWarning, match='CounterToken is deprecated'): CounterToken(1) @@ -21,6 +27,11 @@ def test_counter_token_is_deprecated(): ], ) def test_counter(iterations): + """ + Stop a direct polling loop after exactly the configured number of iterations. + + The zero-count boundary cancels before the loop body runs. + """ token = CounterToken(iterations) counter = 0 @@ -31,12 +42,24 @@ def test_counter(iterations): def test_double_str(): + """ + Ensure rendering the same one-attempt counter token twice is stable. + + The test compares the two string results directly instead of pinning the exact + text, and this repeated rendering must not consume the counter. + """ token = CounterToken(1) assert str(token) == str(token) # noqa: PLR0124 def test_counter_less_than_zero(): + """ + Reject negative initial counters at construction. + + CounterToken only accepts non-negative iteration limits, with zero as the + valid lower boundary. + """ with pytest.raises(ValueError, match=r'.'): CounterToken(-1) @@ -58,6 +81,7 @@ def test_counter_less_than_zero(): ], ) def test_race_condition_for_counter(iterations, number_of_threads): + """Ensure concurrent direct polls consume exactly the configured counter.""" results = [] token = CounterToken(iterations) @@ -88,6 +112,13 @@ def decrementer(_number): ], ) def test_direct_default_counter(kwargs, expected_result): + """ + Verify indirect parent polling honors the CounterToken `direct` option. + + Polling through SimpleToken must not consume default/direct=True counters, must + consume direct=False counters, and direct polling the nested token still + decrements afterward. + """ nested_token = CounterToken(5, **kwargs) token = SimpleToken(nested_token) @@ -99,6 +130,11 @@ def test_direct_default_counter(kwargs, expected_result): def test_check_superpower_raised(): + """ + Check raises CounterCancellationError after a standalone five-attempt CounterToken exhausts. + + Repeated checks keep reporting the exhausted token with the original attempt limit in the message. + """ token = CounterToken(5) while not token.cancelled: @@ -114,6 +150,13 @@ def test_check_superpower_raised(): def test_check_superpower_raised_nested(): + """ + Parent check preserves a nested five-attempt CounterToken cancellation attribution. + + The nested counter is `direct=False`, so parent polling can exhaust it; repeated + parent checks then raise with the nested counter's exception type, message, and + token attribution. + """ nested_token = CounterToken(5, direct=False) token = SimpleToken(nested_token) @@ -131,6 +174,7 @@ def test_check_superpower_raised_nested(): def test_get_report_cancelled(): + """Verify that ordinary polling reports an exhausted standalone CounterToken as its own superpower cancellation.""" token = CounterToken(5) while not token.cancelled: @@ -152,6 +196,14 @@ def test_get_report_cancelled(): ], ) def test_get_report_cancelled_nested(counter, counter_nested, from_token_is_nested): + """ + Report the first counter superpower in parent-first nested order. + + When both counters are already exhausted the parent owns the report, but if + the parent starts at one and only reaches zero during this lookup, the + already-exhausted nested counter remains the reported source. The report is + always a CancellationReport with SUPERPOWER cause. + """ nested_token = CounterToken(counter_nested) token = CounterToken(counter, nested_token) @@ -184,6 +236,7 @@ def test_get_report_cancelled_nested(counter, counter_nested, from_token_is_nest ], ) def test_check_is_decrementing_counter(function, initial_counter, final_counter): + """Direct CounterToken checks consume one attempt without decrementing below zero.""" token = CounterToken(initial_counter) try: @@ -195,6 +248,12 @@ def test_check_is_decrementing_counter(function, initial_counter, final_counter) def test_check_is_decrementing_counter_when_nested_token_is_cancelled(): + """ + Check still consumes a two-attempt parent counter while nested cancellation is reported. + + The check that reaches zero keeps raising from the nested token; the next check + raises from the parent counter superpower. + """ nested_token = SimpleToken(cancelled=True) token = CounterToken(2, nested_token) @@ -222,6 +281,7 @@ def test_check_is_decrementing_counter_when_nested_token_is_cancelled(): def test_decrement_counter_after_zero(): + """Direct polling an already exhausted CounterToken leaves its counter at zero.""" token = CounterToken(0) token.is_cancelled() @@ -230,6 +290,12 @@ def test_decrement_counter_after_zero(): def test_counter_token_plus_simple_token(): + """ + Check that CounterToken + SimpleToken preserves operands in order. + + The composed token is a fresh SimpleToken wrapper containing the original + counter token first and the original simple token second. + """ simple_token = SimpleToken() counter_token = CounterToken(1) token = counter_token + simple_token @@ -243,6 +309,12 @@ def test_counter_token_plus_simple_token(): def test_simple_token_plus_counter_token(): + """ + Adding a counter token on the right creates an ordered simple composite. + + The composite preserves the original simple token and counter token identities + in left-to-right order. + """ simple_token = SimpleToken() counter_token = CounterToken(1) token = simple_token + counter_token @@ -256,6 +328,11 @@ def test_simple_token_plus_counter_token(): def test_zero_counter_token_report_is_about_superpower(): + """ + Report an initially exhausted counter token as cancelled by its own superpower. + + Both direct and indirect report checks should classify a zero-count token as superpower-cancelled despite indirect polling rollback behavior. + """ for report in CounterToken(0)._get_report(True), CounterToken(0)._get_report(False): assert report.cause == CancelCause.SUPERPOWER diff --git a/tests/units/tokens/test_default_token.py b/tests/units/tokens/test_default_token.py index 55409e9..8e77cec 100644 --- a/tests/units/tokens/test_default_token.py +++ b/tests/units/tokens/test_default_token.py @@ -7,6 +7,7 @@ def test_dafault_token_is_not_cancelled_by_default(): + """DefaultToken starts active across the public status API.""" token = DefaultToken() assert bool(token) @@ -18,6 +19,12 @@ def test_dafault_token_is_not_cancelled_by_default(): def test_you_can_set_cancelled_attribute_as_false(): + """ + Allow assigning False to DefaultToken.cancelled as a no-op. + + The token should still expose the never-cancelled state through every public + status API, and check() should not raise. + """ token = DefaultToken() token.cancelled = False @@ -31,6 +38,12 @@ def test_you_can_set_cancelled_attribute_as_false(): def test_you_cant_set_true_as_cancelled_attribute(): + """ + Reject cancellation attempts made through the DefaultToken.cancelled setter. + + Setting the attribute to True must raise ImpossibleCancelError and leave the + token uncancelled. + """ token = DefaultToken() with pytest.raises(ImpossibleCancelError, match=match('You cannot cancel a default token.')): @@ -40,6 +53,7 @@ def test_you_cant_set_true_as_cancelled_attribute(): def test_you_cannot_cancel_default_token_by_standard_way(): + """`DefaultToken.cancel()` raises without changing its permanent non-cancelled state.""" token = DefaultToken() with pytest.raises(ImpossibleCancelError, match=match('You cannot cancel a default token.')): @@ -49,6 +63,12 @@ def test_you_cannot_cancel_default_token_by_standard_way(): def test_str_for_default_token(): + """ + `DefaultToken` stringifies as a regular, never-cancelled token. + + Unlike the shared string test, this pins only the not-cancelled spelling because + a default token cannot transition to the cancelled state. + """ assert str(DefaultToken()) == '' @@ -76,17 +96,29 @@ def test_repr_for_default_token(): @pytest.mark.skipif(sys.version_info >= (3, 10), reason='Format of this exception messages was changed.') def test_you_cannot_neste_another_token_to_default_one_old_pythons(): + """ + DefaultToken rejects nested-token positional arguments with only the old TypeError text. + + This old-Python compatibility case pins the unqualified `__init__()` wording; + the qualified new-Python wording is covered separately. + """ with pytest.raises(TypeError, match=match('__init__() takes 1 positional argument but 2 were given')): DefaultToken(SimpleToken()) @pytest.mark.skipif(sys.version_info < (3, 10), reason='Format of this exception messages was changed.') def test_you_cannot_neste_another_token_to_default_one_new_pythons(): + """ + Guard the Python 3.10+ TypeError for passing a nested token to DefaultToken. + + DefaultToken only accepts keyword-only constructor arguments, so a positional nested token must be rejected with the newer qualified error message. + """ with pytest.raises(TypeError, match=match('DefaultToken.__init__() takes 1 positional argument but 2 were given')): DefaultToken(SimpleToken()) def test_default_plus_default(): + """Two neutral DefaultToken operands produce an empty SimpleToken sum.""" empty_sum = DefaultToken() + DefaultToken() assert isinstance(empty_sum, SimpleToken) @@ -94,6 +126,7 @@ def test_default_plus_default(): def test_default_plus_default_plus_default(): + """Preserve the left-associative empty intermediate token in an inline all-default sum.""" empty_sum = DefaultToken() + DefaultToken() + DefaultToken() assert isinstance(empty_sum, SimpleToken) @@ -103,6 +136,12 @@ def test_default_plus_default_plus_default(): def test_default_plus_default_plus_default_preserves_intermediate_simple_token(): + """ + A default-only SimpleToken intermediate remains a live operand in later sums. + + Direct defaults are filtered from the first sum, but the bound intermediate must + be preserved by identity in the second sum and propagate its later cancellation. + """ inner_sum = DefaultToken() + DefaultToken() total = inner_sum + DefaultToken() @@ -116,6 +155,12 @@ def test_default_plus_default_plus_default_preserves_intermediate_simple_token() def test_default_token_plus_inline_simple_token(): + """ + DefaultToken is neutral when added to an inline SimpleToken. + + The resulting sum is a SimpleToken with exactly one nested operand, and that + operand is the temporary non-default SimpleToken rather than the neutral default. + """ total = DefaultToken() + SimpleToken() assert isinstance(total, SimpleToken) @@ -124,6 +169,11 @@ def test_default_token_plus_inline_simple_token(): def test_default_token_plus_bound_simple_token(): + """ + Treat a left-hand default token as neutral while preserving a bound simple token. + + The sum is a fresh wrapper whose sole live operand is the preexisting simple token. + """ simple_token = SimpleToken() total = DefaultToken() + simple_token diff --git a/tests/units/tokens/test_simple_token.py b/tests/units/tokens/test_simple_token.py index 626108f..610e103 100644 --- a/tests/units/tokens/test_simple_token.py +++ b/tests/units/tokens/test_simple_token.py @@ -5,12 +5,14 @@ def test_just_created_token_without_arguments(): + """A bare SimpleToken starts live across all public status APIs.""" assert SimpleToken().cancelled == False assert SimpleToken().is_cancelled() == False assert SimpleToken().keep_on() == True def test_just_created_token_with_argument_cancelled(): + """Verify a SimpleToken initialized as cancelled is immediately cancelled across status APIs.""" assert SimpleToken(cancelled=True).cancelled == True assert SimpleToken(cancelled=True).is_cancelled() == True assert SimpleToken(cancelled=True).keep_on() == False @@ -41,12 +43,24 @@ def test_repr_with_doc(): ([SimpleToken(), SimpleToken(), SimpleToken(), SimpleToken()], False), ]) def test_just_created_token_with_arguments(arguments, expected_cancelled_status): + """ + Verify constructor-time embedding of positional SimpleToken arguments. + + A new token should be cancelled when any nested argument is already cancelled, + and should stay active when all nested arguments are active, consistently across + the status APIs. + """ assert SimpleToken(*arguments).cancelled == expected_cancelled_status assert SimpleToken(*arguments).is_cancelled() == expected_cancelled_status assert SimpleToken(*arguments).keep_on() == (not expected_cancelled_status) def test_stopped_token_is_not_going_on(): + """ + Confirm that manual cancellation stops a standalone SimpleToken. + + After cancel() is called, all status APIs report the stopped state consistently. + """ token = SimpleToken() token.cancel() @@ -56,12 +70,24 @@ def test_stopped_token_is_not_going_on(): def test_chain_with_simple_tokens(): + """ + Verify SimpleToken cancellation propagates through nested wrapper chains. + + The chain reports cancelled for a leaf created with `cancelled=True` or with + `cancel()` already called, while an equally deep active chain remains not cancelled. + """ assert SimpleToken(SimpleToken(SimpleToken(SimpleToken(SimpleToken(cancelled=True))))).cancelled == True assert SimpleToken(SimpleToken(SimpleToken(SimpleToken(SimpleToken().cancel())))).cancelled == True assert SimpleToken(SimpleToken(SimpleToken(SimpleToken(SimpleToken())))).cancelled == False def test_check_superpower_raised(): + """ + Check that manual SimpleToken cancellation raises the base cancellation error. + + The raised error keeps the cancellation message and points back to the token + that was checked. + """ token = SimpleToken() token.cancel() @@ -77,6 +103,12 @@ def test_check_superpower_raised(): def test_check_superpower_raised_nested(): + """ + Protect exception attribution for a manually cancelled nested SimpleToken. + + The parent check must surface the nested token's generic cancellation error + without treating the parent as directly cancelled. + """ nested_token = SimpleToken() token = SimpleToken(nested_token) @@ -94,6 +126,7 @@ def test_check_superpower_raised_nested(): def test_get_report_cancelled(): + """A pre-cancelled SimpleToken reports itself as manually cancelled.""" token = SimpleToken(cancelled=True) report = token._get_report() @@ -112,6 +145,7 @@ def test_get_report_cancelled(): ], ) def test_get_report_cancelled_nested(cancelled_flag, cancelled_flag_nested, from_token_is_nested): + """Verify nested simple-token reports use parent-first manual cancellation attribution.""" nested_token = SimpleToken(cancelled=cancelled_flag_nested) token = SimpleToken(nested_token, cancelled=cancelled_flag) @@ -126,6 +160,11 @@ def test_get_report_cancelled_nested(cancelled_flag, cancelled_flag_nested, from def test_sum_of_2_bound_simple_tokens(): + """ + Keep two bound simple tokens as the sum's nested operands. + + The operands are preserved by identity in left-to-right order. + """ first_token = SimpleToken() second_token = SimpleToken() result = first_token + second_token diff --git a/tests/units/tokens/test_timeout_token.py b/tests/units/tokens/test_timeout_token.py index ef87df4..8cbca24 100644 --- a/tests/units/tokens/test_timeout_token.py +++ b/tests/units/tokens/test_timeout_token.py @@ -41,6 +41,12 @@ def test_timeout_token_deprecated_alias_points_to_timeout_token(): ], ) def test_zero_timeout(zero_timeout, options): + """ + Verify that a zero timeout is accepted and immediately expired. + + Checks integer and float zero across clock modes, with repeated status queries + remaining cancelled and keep_on() remaining false. + """ token = TimeoutToken(zero_timeout, **options) assert token.cancelled == True @@ -67,11 +73,17 @@ def test_zero_timeout(zero_timeout, options): ], ) def test_less_than_zero_timeout(options, timeout): + """ + Reject negative timeout values during construction. + + Pins the exact ValueError for integer and float negatives across all monotonic option forms. + """ with pytest.raises(ValueError, match=r'You cannot specify a timeout less than zero\.'): TimeoutToken(timeout, **options) def test_raise_without_first_argument(): + """Omitting the required timeout duration is rejected by the timeout-token constructor.""" with pytest.raises(TypeError): TimeoutToken() @@ -85,6 +97,7 @@ def test_raise_without_first_argument(): ], ) def test_timeout_expired(options): + """A positive timeout flips every status API after its deadline.""" timeout = 0.1 token = TimeoutToken(timeout, **options) @@ -134,6 +147,7 @@ def test_timeout_token_clock_can_be_patched_through_time_module(monkeypatch, mon def test_text_representaion_of_extra_kwargs(): + """Confirm timeout extra kwargs render only non-default clock options.""" assert TimeoutToken(5, monotonic=False)._text_representation_of_extra_kwargs() == '' assert TimeoutToken(5, monotonic=True)._text_representation_of_extra_kwargs() == 'monotonic=True' assert TimeoutToken(5)._text_representation_of_extra_kwargs() == '' @@ -170,6 +184,11 @@ def test_repr_of_timeout_token(): def test_check_superpower_raised(): + """ + Expired direct timeout checks raise the timeout-specific cancellation error. + + The raised error preserves the timeout message and points back to the checked token. + """ token = TimeoutToken(0.125) while not token.cancelled: @@ -192,6 +211,11 @@ def test_check_superpower_raised(): ], ) def test_check_superpower_raised_nested(timeout): + """ + Raise the nested timeout token's cancellation error through its parent. + + The parent must report the expired nested token as the cancellation source. + """ nested_token = TimeoutToken(timeout) token = SimpleToken(nested_token) @@ -209,6 +233,7 @@ def test_check_superpower_raised_nested(timeout): def test_get_report_cancelled(): + """Report an expired standalone timeout as its own superpower cancellation.""" token = TimeoutToken(0) while not token.cancelled: @@ -230,6 +255,14 @@ def test_get_report_cancelled(): ], ) def test_get_report_cancelled_nested(timeout, timeout_nested, from_token_is_nested): + """ + Report which timeout token causes nested cancellation across priority cases. + + The report is checked for three cases: both parent and nested timeout expired, + only the nested timeout expired, and only the parent timeout expired. The nested + token is reported only while the parent timeout is still active; otherwise the + parent reports its own timeout superpower. + """ nested_token = TimeoutToken(timeout_nested) token = TimeoutToken(timeout, nested_token) @@ -244,6 +277,11 @@ def test_get_report_cancelled_nested(timeout, timeout_nested, from_token_is_nest def test_timeout_wait(): + """ + Wait until the timeout token cancels itself. + + The synchronous wait should not return before the token's own timeout has elapsed. + """ sleep_duration = 1 token = TimeoutToken(sleep_duration) @@ -255,6 +293,12 @@ def test_timeout_wait(): def test_timeout_token_plus_simple_token(): + """ + Ensure TimeoutToken plus SimpleToken creates a new ordered composition. + + The resulting SimpleToken preserves both original operands by identity, with + the TimeoutToken first and the SimpleToken second. + """ simple_token = SimpleToken() timeout_token = TimeoutToken(1) token = timeout_token + simple_token @@ -268,6 +312,12 @@ def test_timeout_token_plus_simple_token(): def test_simple_token_plus_timeout_token(): + """ + Ensure SimpleToken plus TimeoutToken creates a new ordered container. + + The result preserves the original SimpleToken and TimeoutToken by identity in + left-to-right order. + """ simple_token = SimpleToken() timeout_token = TimeoutToken(1) token = simple_token + timeout_token @@ -281,6 +331,14 @@ def test_simple_token_plus_timeout_token(): def test_timeout_is_more_important_than_cache(): + """ + Ensure an expired timeout overrides a cached nested cancellation report. + + The parent report is read through direct and indirect calls while a nested + cancelled token is the cancellation cause. After the parent timeout expires, + both calls must ignore the cached nested report and return the parent timeout's + own superpower report. + """ sleep_time = 0.001 inner_token = SimpleToken(cancelled=True) token = TimeoutToken(sleep_time, inner_token) @@ -301,6 +359,12 @@ def test_timeout_is_more_important_than_cache(): def test_zero_timeout_token_report_is_about_superpower(): + """ + Ensure an expired zero-timeout token reports cancellation by superpower. + + Both direct and indirect report checks should classify the immediate timeout + as the token's own automatic cancellation cause. + """ for report in TimeoutToken(0)._get_report(True), TimeoutToken(0)._get_report(False): assert report.cause == CancelCause.SUPERPOWER @@ -314,6 +378,12 @@ def test_zero_timeout_token_report_is_about_superpower(): ], ) def test_bigger_timeout_token_plus_less_timeout_token_with_same_monotonic_flag(addictional_kwargs): + """ + Preserve larger-left and smaller-right timeout operands with the same monotonic setting. + + The resulting SimpleToken keeps both original plain TimeoutTokens in order, with + no nested tokens added to either operand. + """ left = TimeoutToken(2, **addictional_kwargs) right = TimeoutToken(1, **addictional_kwargs) token = left + right @@ -341,6 +411,12 @@ def test_bigger_timeout_token_plus_less_timeout_token_with_same_monotonic_flag(a ], ) def test_bigger_timeout_token_plus_less_timeout_token_with_not_same_monotonic_flag(left_addictional_kwargs, right_addictional_kwargs): + """ + Combine a bigger left timeout with a smaller right timeout without merging. + + Different monotonic flags keep the operands as two separate timeout tokens in + the resulting sum, with no nested tokens added to either operand. + """ left = TimeoutToken(2, **left_addictional_kwargs) right = TimeoutToken(1, **right_addictional_kwargs) token = left + right @@ -374,6 +450,14 @@ def test_bigger_timeout_token_plus_less_timeout_token_with_not_same_monotonic_fl ], ) def test_less_or_equal_not_monotonic_timeout_token_plus_bigger_or_equal_not_monotonic_timeout_token_with_same_monotonic_flag(timeout_for_equal_or_bigger_token, addictional_kwargs): + """ + Preserve two same-clock timeout operands without optimizing the sum. + + Cover a left timeout of 1 combined with a right timeout of 1 or 2, using + matching monotonic flags on both operands. With no nested tokens involved, the + sum must remain a SimpleToken containing the original left and right + TimeoutToken objects in order, with no merging or reordering. + """ left = TimeoutToken(1, **addictional_kwargs) right = TimeoutToken(timeout_for_equal_or_bigger_token, **addictional_kwargs) token = left + right @@ -408,6 +492,13 @@ def test_less_or_equal_not_monotonic_timeout_token_plus_bigger_or_equal_not_mono ], ) def test_less_or_equal_not_monotonic_timeout_token_plus_bigger_or_equal_not_monotonic_timeout_token_with_not_same_monotonic_flag(timeout_for_equal_or_bigger_token, left_addictional_kwargs, right_addictional_kwargs): + """ + Preserve two plain timeout operands with incompatible clock modes. + + Left timeout is 1 and right timeout is 1 or 2; the effective monotonic flags + differ, neither operand has nested tokens, and addition must not merge, + replace, reorder, or otherwise collapse the original operands. + """ left = TimeoutToken(1, **left_addictional_kwargs) right = TimeoutToken(timeout_for_equal_or_bigger_token, **right_addictional_kwargs) token = left + right @@ -434,6 +525,14 @@ def test_less_or_equal_not_monotonic_timeout_token_plus_bigger_or_equal_not_mono ], ) def test_bigger_timeout_token_plus_less_timeout_token_with_same_monotonic_flag_with_nested_condition_token_at_right(addictional_kwargs): + """ + Preserve original timeout operands when the right side has a nested condition. + + A larger left TimeoutToken plus a smaller right TimeoutToken with the same + monotonic flag should produce a SimpleToken containing those same two timeout + objects in order. The left timeout stays unnested, and the right timeout keeps + exactly its nested ConditionToken. + """ left = TimeoutToken(2, **addictional_kwargs) right = TimeoutToken(1, ConditionToken(lambda: False), **addictional_kwargs) token = left + right @@ -463,6 +562,14 @@ def test_bigger_timeout_token_plus_less_timeout_token_with_same_monotonic_flag_w ], ) def test_bigger_timeout_token_plus_less_timeout_token_with_not_same_monotonic_flag_with_nested_condition_token_at_right(left_addictional_kwargs, right_addictional_kwargs): + """ + Preserve original incompatible timeout operands when only the right side has a condition. + + A larger left TimeoutToken plus a smaller right TimeoutToken with different + monotonic flags should produce a SimpleToken containing those same two timeout + objects in order. The left timeout stays unnested, and the right timeout keeps + exactly its nested ConditionToken. + """ left = TimeoutToken(2, **left_addictional_kwargs) right = TimeoutToken(1, ConditionToken(lambda: False), **right_addictional_kwargs) token = left + right @@ -498,6 +605,14 @@ def test_bigger_timeout_token_plus_less_timeout_token_with_not_same_monotonic_fl ], ) def test_less_or_equal_not_monotonic_timeout_token_plus_bigger_or_equal_not_monotonic_timeout_token_with_same_monotonic_flag_with_nested_condition_token_at_right(timeout_for_equal_or_bigger_token, addictional_kwargs): + """ + Composing same-clock timeout tokens keeps the right nested condition attached. + + The left timeout is 1, the right timeout is 1 or 2, and both timeout tokens use + the same monotonic flag. Only the right timeout embeds a ConditionToken, so the + sum should preserve the original left and right timeout operands without + flattening or moving that nested condition. + """ left = TimeoutToken(1, **addictional_kwargs) right = TimeoutToken(timeout_for_equal_or_bigger_token, ConditionToken(lambda: False), **addictional_kwargs) token = left + right @@ -534,6 +649,14 @@ def test_less_or_equal_not_monotonic_timeout_token_plus_bigger_or_equal_not_mono ], ) def test_less_or_equal_not_monotonic_timeout_token_plus_bigger_or_equal_not_monotonic_timeout_token_with_not_same_monotonic_flag_with_nested_condition_token_at_right(timeout_for_equal_or_bigger_token, left_addictional_kwargs, right_addictional_kwargs): + """ + Preserve original incompatible timeout operands when the right timeout may be equal or larger. + + The left TimeoutToken has timeout 1, the right TimeoutToken has timeout 1 or 2, + and their effective monotonic flags differ. The sum should be a SimpleToken + containing those same timeout objects in order, with the left timeout unnested + and the right timeout keeping exactly its nested ConditionToken. + """ left = TimeoutToken(1, **left_addictional_kwargs) right = TimeoutToken(timeout_for_equal_or_bigger_token, ConditionToken(lambda: False), **right_addictional_kwargs) token = left + right @@ -562,6 +685,13 @@ def test_less_or_equal_not_monotonic_timeout_token_plus_bigger_or_equal_not_mono ], ) def test_bigger_timeout_token_plus_less_timeout_token_with_same_monotonic_flag_with_nested_condition_token_at_right_and_counter_token_at_left(addictional_kwargs): + """ + Verify adding a larger left timeout to a smaller right timeout preserves both operands. + + Both timeout tokens use the same monotonic flag configuration, while the left + timeout keeps its nested CounterToken and the right timeout keeps its nested + ConditionToken in the resulting composite structure. + """ left = TimeoutToken(2, CounterToken(5), **addictional_kwargs) right = TimeoutToken(1, ConditionToken(lambda: False), **addictional_kwargs) token = left + right @@ -593,6 +723,12 @@ def test_bigger_timeout_token_plus_less_timeout_token_with_same_monotonic_flag_w ], ) def test_bigger_timeout_token_plus_less_timeout_token_with_not_same_monotonic_flag_with_nested_condition_token_at_right_and_counter_token_at_left(left_addictional_kwargs, right_addictional_kwargs): + """ + Preserve larger-left and smaller-right timeout operands with different monotonic settings. + + The resulting SimpleToken keeps the left CounterToken and right ConditionToken + under their original TimeoutToken owners. + """ left = TimeoutToken(2, CounterToken(5), **left_addictional_kwargs) right = TimeoutToken(1, ConditionToken(lambda: False), **right_addictional_kwargs) token = left + right @@ -630,6 +766,14 @@ def test_bigger_timeout_token_plus_less_timeout_token_with_not_same_monotonic_fl ], ) def test_less_or_equal_not_monotonic_timeout_token_plus_bigger_or_equal_not_monotonic_timeout_token_with_same_monotonic_flag_with_nested_condition_token_at_right_and_counter_token_at_left(timeout_for_equal_or_bigger_token, addictional_kwargs): + """ + Preserve original same-clock timeout operands and their own nested tokens. + + The left TimeoutToken has timeout 1 and owns a CounterToken, while the right + TimeoutToken has timeout 1 or 2 and owns a ConditionToken. With matching + monotonic flags, the sum should be a SimpleToken containing those same timeout + objects in order, without moving, flattening, or copying either nested token. + """ left = TimeoutToken(1, CounterToken(5), **addictional_kwargs) right = TimeoutToken(timeout_for_equal_or_bigger_token, ConditionToken(lambda: False), **addictional_kwargs) token = left + right @@ -668,6 +812,15 @@ def test_less_or_equal_not_monotonic_timeout_token_plus_bigger_or_equal_not_mono ], ) def test_less_or_equal_not_monotonic_timeout_token_plus_bigger_or_equal_not_monotonic_timeout_token_with_not_same_monotonic_flag_with_nested_condition_token_at_right_and_counter_token_at_left(timeout_for_equal_or_bigger_token, left_addictional_kwargs, right_addictional_kwargs): + """ + Preserve two mismatched-monotonic timeout operands and their own nested tokens. + + Checks that adding a left TimeoutToken with timeout 1 to a right TimeoutToken + with timeout 1 or 2, using different monotonic flags, creates a SimpleToken + wrapper over the original operands. The CounterToken nested in the left timeout + and the ConditionToken nested in the right timeout must remain under those + owners without merging, reordering, flattening, or copying. + """ left = TimeoutToken(1, CounterToken(5), **left_addictional_kwargs) right = TimeoutToken(timeout_for_equal_or_bigger_token, ConditionToken(lambda: False), **right_addictional_kwargs) token = left + right @@ -698,6 +851,13 @@ def test_less_or_equal_not_monotonic_timeout_token_plus_bigger_or_equal_not_mono ], ) def test_bigger_timeout_token_plus_less_timeout_token_with_same_monotonic_flag_with_nested_counter_token_at_left(addictional_kwargs): + """ + Preserve a left CounterToken under larger-left/smaller-right timeout addition. + + With matching monotonic settings, the resulting SimpleToken keeps the original + TimeoutTokens in order, leaves the CounterToken under the left timeout, and + leaves the right timeout unnested. + """ left = TimeoutToken(2, CounterToken(5), **addictional_kwargs) right = TimeoutToken(1, **addictional_kwargs) token = left + right @@ -727,6 +887,13 @@ def test_bigger_timeout_token_plus_less_timeout_token_with_same_monotonic_flag_w ], ) def test_bigger_timeout_token_plus_less_timeout_token_with_not_same_monotonic_flag_with_nested_counter_token_at_left(left_addictional_kwargs, right_addictional_kwargs): + """ + Preserve a left CounterToken when unequal timeout operands use different monotonic settings. + + The resulting SimpleToken keeps the original larger-left and smaller-right + TimeoutTokens in order, with the CounterToken still nested only under the left + timeout. + """ left = TimeoutToken(2, CounterToken(5), **left_addictional_kwargs) right = TimeoutToken(1, **right_addictional_kwargs) token = left + right @@ -762,6 +929,15 @@ def test_bigger_timeout_token_plus_less_timeout_token_with_not_same_monotonic_fl ], ) def test_less_or_equal_not_monotonic_timeout_token_plus_bigger_or_equal_not_monotonic_timeout_token_with_same_monotonic_flag_with_nested_counter_token_at_left(timeout_for_equal_or_bigger_token, addictional_kwargs): + """ + Preserve both timeout operands when only the left timeout nests a counter. + + Left is TimeoutToken(1) with CounterToken nested inside it, while right is + TimeoutToken(1 or 2) without nested tokens. Both operands use the same monotonic + flag configuration. The sum should stay a SimpleToken containing the original + left and right timeout tokens in order, preserving the left-only CounterToken + and the right token's empty nested-token list. + """ left = TimeoutToken(1, CounterToken(5), **addictional_kwargs) right = TimeoutToken(timeout_for_equal_or_bigger_token, **addictional_kwargs) token = left + right @@ -798,6 +974,15 @@ def test_less_or_equal_not_monotonic_timeout_token_plus_bigger_or_equal_not_mono ], ) def test_less_or_equal_not_monotonic_timeout_token_plus_bigger_or_equal_not_monotonic_timeout_token_with_not_same_monotonic_flag_with_nested_counter_token_at_left(timeout_for_equal_or_bigger_token, left_addictional_kwargs, right_addictional_kwargs): + """ + Compose different-clock timeout siblings without merging nested left state. + + Left uses timeout 1 and owns the only nested CounterToken, while the right + timeout is 1 or 2 and has no nested tokens. Different effective monotonic flags + must keep both TimeoutToken operands as ordered SimpleToken children, preserving + their identities, timeout values, and the counter nested only inside the left + timeout. + """ left = TimeoutToken(1, CounterToken(5), **left_addictional_kwargs) right = TimeoutToken(timeout_for_equal_or_bigger_token, **right_addictional_kwargs) token = left + right